Files
navidrome/ui/src/song/SongList.jsx
T
Alex Gustafsson 8e96dd0784 feat(ui): add composer field to table views (#4857)
* 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
2026-01-18 13:15:53 -05:00

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