Added genre as a toggleable column in the playlist songs table. The Genre column displays genre information for each song in playlists and is available through the column toggle menu but disabled by default. Implements feature request from GitHub discussion #4400. Signed-off-by: Deluan <deluan@navidrome.org>
264 lines
6.8 KiB
JavaScript
264 lines
6.8 KiB
JavaScript
import React, { useCallback, useEffect, useMemo } from 'react'
|
|
import {
|
|
BulkActionsToolbar,
|
|
ListToolbar,
|
|
TextField,
|
|
NumberField,
|
|
useDataProvider,
|
|
useNotify,
|
|
useVersion,
|
|
useListContext,
|
|
FunctionField,
|
|
} from 'react-admin'
|
|
import clsx from 'clsx'
|
|
import { useDispatch } from 'react-redux'
|
|
import { Card, useMediaQuery } from '@material-ui/core'
|
|
import { makeStyles } from '@material-ui/core/styles'
|
|
import ReactDragListView from 'react-drag-listview'
|
|
import {
|
|
DurationField,
|
|
SongInfo,
|
|
SongContextMenu,
|
|
SongDatagrid,
|
|
SongTitleField,
|
|
QualityInfo,
|
|
useSelectedFields,
|
|
useResourceRefresh,
|
|
DateField,
|
|
ArtistLinkField,
|
|
RatingField,
|
|
} from '../common'
|
|
import { AlbumLinkField } from '../song/AlbumLinkField'
|
|
import { playTracks } from '../actions'
|
|
import PlaylistSongBulkActions from './PlaylistSongBulkActions'
|
|
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
|
|
import config from '../config'
|
|
|
|
const useStyles = makeStyles(
|
|
(theme) => ({
|
|
root: {},
|
|
main: {
|
|
display: 'flex',
|
|
},
|
|
content: {
|
|
marginTop: 0,
|
|
transition: theme.transitions.create('margin-top'),
|
|
position: 'relative',
|
|
flex: '1 1 auto',
|
|
[theme.breakpoints.down('xs')]: {
|
|
boxShadow: 'none',
|
|
},
|
|
},
|
|
bulkActionsDisplayed: {
|
|
marginTop: -theme.spacing(8),
|
|
transition: theme.transitions.create('margin-top'),
|
|
},
|
|
actions: {
|
|
zIndex: 2,
|
|
display: 'flex',
|
|
justifyContent: 'flex-end',
|
|
flexWrap: 'wrap',
|
|
},
|
|
noResults: { padding: 20 },
|
|
toolbar: {
|
|
justifyContent: 'flex-start',
|
|
},
|
|
row: {
|
|
'&:hover': {
|
|
'& $contextMenu': {
|
|
visibility: 'visible',
|
|
},
|
|
'& $ratingField': {
|
|
visibility: 'visible',
|
|
},
|
|
},
|
|
},
|
|
contextMenu: {
|
|
visibility: (props) => (props.isDesktop ? 'hidden' : 'visible'),
|
|
},
|
|
ratingField: {
|
|
visibility: 'hidden',
|
|
},
|
|
}),
|
|
{ name: 'RaList' },
|
|
)
|
|
|
|
const ReorderableList = ({ readOnly, children, ...rest }) => {
|
|
if (readOnly) {
|
|
return children
|
|
}
|
|
return <ReactDragListView {...rest}>{children}</ReactDragListView>
|
|
}
|
|
|
|
const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
|
|
const listContext = useListContext()
|
|
const { data, ids, selectedIds, onUnselectItems, refetch, setPage } =
|
|
listContext
|
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
|
const classes = useStyles({ isDesktop })
|
|
const dispatch = useDispatch()
|
|
const dataProvider = useDataProvider()
|
|
const notify = useNotify()
|
|
const version = useVersion()
|
|
useResourceRefresh('song', 'playlist')
|
|
|
|
useEffect(() => {
|
|
setPage(1)
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}, [playlistId, setPage])
|
|
|
|
const onAddToPlaylist = useCallback(
|
|
(pls) => {
|
|
if (pls.id === playlistId) {
|
|
refetch()
|
|
}
|
|
},
|
|
[playlistId, refetch],
|
|
)
|
|
|
|
const reorder = useCallback(
|
|
(playlistId, id, newPos) => {
|
|
dataProvider
|
|
.update('playlistTrack', {
|
|
id,
|
|
data: { insert_before: newPos },
|
|
filter: { playlist_id: playlistId },
|
|
})
|
|
.then(() => {
|
|
refetch()
|
|
})
|
|
.catch(() => {
|
|
notify('ra.page.error', 'warning')
|
|
})
|
|
},
|
|
[dataProvider, notify, refetch],
|
|
)
|
|
|
|
const handleDragEnd = useCallback(
|
|
(from, to) => {
|
|
const toId = ids[to]
|
|
const fromId = ids[from]
|
|
reorder(playlistId, fromId, toId)
|
|
},
|
|
[playlistId, reorder, ids],
|
|
)
|
|
|
|
const toggleableFields = useMemo(() => {
|
|
return {
|
|
trackNumber: isDesktop && <TextField source="id" label={'#'} />,
|
|
title: <SongTitleField source="title" showTrackNumbers={false} />,
|
|
album: isDesktop && <AlbumLinkField source="album" />,
|
|
artist: isDesktop && <ArtistLinkField source="artist" />,
|
|
albumArtist: isDesktop && <ArtistLinkField source="albumArtist" />,
|
|
duration: (
|
|
<DurationField source="duration" className={classes.draggable} />
|
|
),
|
|
year: isDesktop && (
|
|
<FunctionField
|
|
source="year"
|
|
render={(r) => r.year || ''}
|
|
sortByOrder={'DESC'}
|
|
/>
|
|
),
|
|
playCount: isDesktop && (
|
|
<NumberField source="playCount" sortByOrder={'DESC'} />
|
|
),
|
|
playDate: isDesktop && (
|
|
<DateField source="playDate" sortByOrder={'DESC'} showTime />
|
|
),
|
|
quality: isDesktop && <QualityInfo source="quality" sortable={false} />,
|
|
channels: isDesktop && <NumberField source="channels" />,
|
|
bpm: isDesktop && <NumberField source="bpm" />,
|
|
genre: <TextField source="genre" />,
|
|
rating: config.enableStarRating && (
|
|
<RatingField
|
|
source="rating"
|
|
sortByOrder={'DESC'}
|
|
resource={'song'}
|
|
className={classes.ratingField}
|
|
/>
|
|
),
|
|
}
|
|
}, [isDesktop, classes.draggable, classes.ratingField])
|
|
|
|
const columns = useSelectedFields({
|
|
resource: 'playlistTrack',
|
|
columns: toggleableFields,
|
|
defaultOff: [
|
|
'channels',
|
|
'bpm',
|
|
'year',
|
|
'playCount',
|
|
'playDate',
|
|
'albumArtist',
|
|
'genre',
|
|
'rating',
|
|
],
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<ListToolbar
|
|
classes={{ toolbar: classes.toolbar }}
|
|
filters={props.filters}
|
|
actions={actions}
|
|
/>
|
|
<div className={classes.main}>
|
|
<Card
|
|
className={clsx(classes.content, {
|
|
[classes.bulkActionsDisplayed]: selectedIds.length > 0,
|
|
})}
|
|
key={version}
|
|
>
|
|
<BulkActionsToolbar>
|
|
<PlaylistSongBulkActions
|
|
playlistId={playlistId}
|
|
onUnselectItems={onUnselectItems}
|
|
readOnly={readOnly}
|
|
/>
|
|
</BulkActionsToolbar>
|
|
<ReorderableList
|
|
readOnly={readOnly}
|
|
onDragEnd={handleDragEnd}
|
|
nodeSelector={'tr'}
|
|
>
|
|
<SongDatagrid
|
|
rowClick={(id) => dispatch(playTracks(data, ids, id))}
|
|
{...listContext}
|
|
hasBulkActions={!readOnly}
|
|
contextAlwaysVisible={!isDesktop}
|
|
classes={{ row: classes.row }}
|
|
>
|
|
{columns}
|
|
<SongContextMenu
|
|
onAddToPlaylist={onAddToPlaylist}
|
|
showLove={true}
|
|
className={classes.contextMenu}
|
|
/>
|
|
</SongDatagrid>
|
|
</ReorderableList>
|
|
</Card>
|
|
</div>
|
|
<ExpandInfoDialog content={<SongInfo />} />
|
|
{React.cloneElement(props.pagination, listContext)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const SanitizedPlaylistSongs = (props) => {
|
|
const { loaded, ...rest } = props
|
|
return (
|
|
<>
|
|
{loaded && (
|
|
<PlaylistSongs
|
|
playlistId={props.id}
|
|
actions={props.actions}
|
|
pagination={props.pagination}
|
|
{...rest}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default SanitizedPlaylistSongs
|