8e96dd0784
* feat(ui): Add composer field to datatables In order to make the UI a bit more useful for classical music, where the recorded artist isn't the composer of the work, add the composer field to the song and album datatables. To not affect existing users, the field is default off. * Fix typo * Remove composer field for albums Albums can have more than one composer. Showing all or just one of them in a list doesn't really make sense. * Format code
253 lines
6.8 KiB
React
253 lines
6.8 KiB
React
import { useMemo } from 'react'
|
|
import {
|
|
AutocompleteArrayInput,
|
|
Filter,
|
|
FunctionField,
|
|
NumberField,
|
|
ReferenceArrayInput,
|
|
SearchInput,
|
|
TextField,
|
|
useTranslate,
|
|
NullableBooleanInput,
|
|
usePermissions,
|
|
} from 'react-admin'
|
|
import { useMediaQuery } from '@material-ui/core'
|
|
import FavoriteIcon from '@material-ui/icons/Favorite'
|
|
import {
|
|
DateField,
|
|
DurationField,
|
|
List,
|
|
SongContextMenu,
|
|
SongDatagrid,
|
|
SongInfo,
|
|
QuickFilter,
|
|
SongTitleField,
|
|
SongSimpleList,
|
|
RatingField,
|
|
useResourceRefresh,
|
|
ArtistLinkField,
|
|
PathField,
|
|
} from '../common'
|
|
import { useDispatch } from 'react-redux'
|
|
import { makeStyles } from '@material-ui/core/styles'
|
|
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
|
import { setTrack } from '../actions'
|
|
import { SongListActions } from './SongListActions'
|
|
import { AlbumLinkField } from './AlbumLinkField'
|
|
import { SongBulkActions, QualityInfo, useSelectedFields } from '../common'
|
|
import config from '../config'
|
|
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
|
|
|
|
const useStyles = makeStyles({
|
|
contextHeader: {
|
|
marginLeft: '3px',
|
|
marginTop: '-2px',
|
|
verticalAlign: 'text-top',
|
|
},
|
|
row: {
|
|
'&:hover': {
|
|
'& $contextMenu': {
|
|
visibility: 'visible',
|
|
},
|
|
'& $ratingField': {
|
|
visibility: 'visible',
|
|
},
|
|
},
|
|
},
|
|
contextMenu: {
|
|
visibility: 'hidden',
|
|
},
|
|
ratingField: {
|
|
visibility: 'hidden',
|
|
},
|
|
chip: {
|
|
margin: 0,
|
|
height: '24px',
|
|
},
|
|
})
|
|
|
|
const SongFilter = (props) => {
|
|
const classes = useStyles()
|
|
const translate = useTranslate()
|
|
const { permissions } = usePermissions()
|
|
const isAdmin = permissions === 'admin'
|
|
return (
|
|
<Filter {...props} variant={'outlined'}>
|
|
<SearchInput source="title" alwaysOn />
|
|
<ReferenceArrayInput
|
|
label={translate('resources.song.fields.genre')}
|
|
source="genre_id"
|
|
reference="genre"
|
|
perPage={0}
|
|
sort={{ field: 'name', order: 'ASC' }}
|
|
filterToQuery={(searchText) => ({ name: [searchText] })}
|
|
>
|
|
<AutocompleteArrayInput emptyText="-- None --" classes={classes} />
|
|
</ReferenceArrayInput>
|
|
<ReferenceArrayInput
|
|
label={translate('resources.song.fields.grouping')}
|
|
source="grouping"
|
|
reference="tag"
|
|
perPage={0}
|
|
sort={{ field: 'tagValue', order: 'ASC' }}
|
|
filter={{ tag_name: 'grouping' }}
|
|
filterToQuery={(searchText) => ({
|
|
tag_value: [searchText],
|
|
})}
|
|
>
|
|
<AutocompleteArrayInput
|
|
emptyText="-- None --"
|
|
classes={classes}
|
|
optionText="tagValue"
|
|
/>
|
|
</ReferenceArrayInput>
|
|
<ReferenceArrayInput
|
|
label={translate('resources.song.fields.mood')}
|
|
source="mood"
|
|
reference="tag"
|
|
perPage={0}
|
|
sort={{ field: 'tagValue', order: 'ASC' }}
|
|
filter={{ tag_name: 'mood' }}
|
|
filterToQuery={(searchText) => ({
|
|
tag_value: [searchText],
|
|
})}
|
|
>
|
|
<AutocompleteArrayInput
|
|
emptyText="-- None --"
|
|
classes={classes}
|
|
optionText="tagValue"
|
|
/>
|
|
</ReferenceArrayInput>
|
|
{config.enableFavourites && (
|
|
<QuickFilter
|
|
source="starred"
|
|
label={<FavoriteIcon fontSize={'small'} />}
|
|
defaultValue={true}
|
|
/>
|
|
)}
|
|
{isAdmin && <NullableBooleanInput source="missing" />}
|
|
</Filter>
|
|
)
|
|
}
|
|
|
|
const SongList = (props) => {
|
|
const classes = useStyles()
|
|
const dispatch = useDispatch()
|
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
|
useResourceRefresh('song')
|
|
|
|
const handleRowClick = (id, basePath, record) => {
|
|
dispatch(setTrack(record))
|
|
}
|
|
|
|
const toggleableFields = useMemo(() => {
|
|
return {
|
|
album: isDesktop && <AlbumLinkField source="album" sortByOrder={'ASC'} />,
|
|
artist: <ArtistLinkField source="artist" />,
|
|
composer: <ArtistLinkField source="composer" />,
|
|
albumArtist: <ArtistLinkField source="albumArtist" />,
|
|
trackNumber: isDesktop && <NumberField source="trackNumber" />,
|
|
playCount: isDesktop && (
|
|
<NumberField source="playCount" sortByOrder={'DESC'} />
|
|
),
|
|
playDate: <DateField source="playDate" sortByOrder={'DESC'} showTime />,
|
|
year: isDesktop && (
|
|
<FunctionField
|
|
source="year"
|
|
render={(r) => r.year || ''}
|
|
sortByOrder={'DESC'}
|
|
/>
|
|
),
|
|
quality: isDesktop && <QualityInfo source="quality" sortable={false} />,
|
|
channels: isDesktop && (
|
|
<NumberField source="channels" sortByOrder={'ASC'} />
|
|
),
|
|
duration: <DurationField source="duration" />,
|
|
rating: config.enableStarRating && (
|
|
<RatingField
|
|
source="rating"
|
|
sortByOrder={'DESC'}
|
|
resource={'song'}
|
|
className={classes.ratingField}
|
|
/>
|
|
),
|
|
bpm: isDesktop && <NumberField source="bpm" />,
|
|
genre: <TextField source="genre" />,
|
|
mood: isDesktop && (
|
|
<FunctionField
|
|
source="mood"
|
|
render={(r) => r.tags?.mood?.[0] || ''}
|
|
sortable={false}
|
|
/>
|
|
),
|
|
comment: <TextField source="comment" />,
|
|
path: <PathField source="path" />,
|
|
createdAt: (
|
|
<DateField source="createdAt" sortBy="recently_added" showTime />
|
|
),
|
|
}
|
|
}, [isDesktop, classes.ratingField])
|
|
|
|
const columns = useSelectedFields({
|
|
resource: 'song',
|
|
columns: toggleableFields,
|
|
defaultOff: [
|
|
'composer',
|
|
'channels',
|
|
'bpm',
|
|
'playDate',
|
|
'albumArtist',
|
|
'genre',
|
|
'mood',
|
|
'comment',
|
|
'path',
|
|
'createdAt',
|
|
],
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<List
|
|
{...props}
|
|
sort={{ field: 'title', order: 'ASC' }}
|
|
exporter={false}
|
|
bulkActionButtons={<SongBulkActions />}
|
|
actions={<SongListActions />}
|
|
filters={<SongFilter />}
|
|
perPage={isXsmall ? 50 : 15}
|
|
>
|
|
{isXsmall ? (
|
|
<SongSimpleList />
|
|
) : (
|
|
<SongDatagrid
|
|
rowClick={handleRowClick}
|
|
contextAlwaysVisible={!isDesktop}
|
|
classes={{ row: classes.row }}
|
|
>
|
|
<SongTitleField source="title" showTrackNumbers={false} />
|
|
{columns}
|
|
<SongContextMenu
|
|
source={'starred_at'}
|
|
sortByOrder={'DESC'}
|
|
sortable={config.enableFavourites}
|
|
className={classes.contextMenu}
|
|
label={
|
|
config.enableFavourites && (
|
|
<FavoriteBorderIcon
|
|
fontSize={'small'}
|
|
className={classes.contextHeader}
|
|
/>
|
|
)
|
|
}
|
|
/>
|
|
</SongDatagrid>
|
|
)}
|
|
</List>
|
|
<ExpandInfoDialog content={<SongInfo />} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default SongList
|