diff --git a/ui/src/album/AlbumActions.js b/ui/src/album/AlbumActions.js new file mode 100644 index 000000000..518ff19f2 --- /dev/null +++ b/ui/src/album/AlbumActions.js @@ -0,0 +1,64 @@ +import { + Button, + sanitizeListRestProps, + TopToolbar, + useTranslate +} from 'react-admin' +import PlayArrowIcon from '@material-ui/icons/PlayArrow' +import ShuffleIcon from '@material-ui/icons/Shuffle' +import React from 'react' +import { useDispatch } from 'react-redux' +import { playAlbum } from '../player' + +export const AlbumActions = ({ + className, + ids, + data, + exporter, + permanentFilter, + ...rest +}) => { + const dispatch = useDispatch() + const translation = useTranslate() + + const shuffle = (data) => { + const ids = Object.keys(data) + for (let i = ids.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)) + ;[ids[i], ids[j]] = [ids[j], ids[i]] + } + const shuffled = {} + ids.forEach((id) => (shuffled[id] = data[id])) + return shuffled + } + + return ( + + + + + ) +} + +AlbumActions.defaultProps = { + selectedIds: [], + onUnselectItems: () => null +} diff --git a/ui/src/album/AlbumDetails.js b/ui/src/album/AlbumDetails.js index 7b6b00164..7473f07a6 100644 --- a/ui/src/album/AlbumDetails.js +++ b/ui/src/album/AlbumDetails.js @@ -1,26 +1,15 @@ import React from 'react' -import { Loading, useGetOne } from 'react-admin' import { Card, CardContent, CardMedia, Typography } from '@material-ui/core' import { subsonicUrl } from '../subsonic' -const AlbumDetails = ({ id, classes }) => { - const { data, loading, error } = useGetOne('album', id) - - if (loading) { - return - } - - if (error) { - return

ERROR: {error}

- } - - const genreYear = (data) => { +const AlbumDetails = ({ classes, record }) => { + const genreYear = (record) => { let genreDateLine = [] - if (data.genre) { - genreDateLine.push(data.genre) + if (record.genre) { + genreDateLine.push(record.genre) } - if (data.year) { - genreDateLine.push(data.year) + if (record.year) { + genreDateLine.push(record.year) } return genreDateLine.join(' - ') } @@ -30,19 +19,19 @@ const AlbumDetails = ({ id, classes }) => { - {data.name} + {record.name} - {data.albumArtist || data.artist} + {record.albumArtist || record.artist} - {genreYear(data)} + {genreYear(record)} ) diff --git a/ui/src/album/AlbumShow.js b/ui/src/album/AlbumShow.js index 5485a76af..095158e80 100644 --- a/ui/src/album/AlbumShow.js +++ b/ui/src/album/AlbumShow.js @@ -1,68 +1,75 @@ import React from 'react' -import { Show } from 'react-admin' -import { Title } from '../common' -import { makeStyles } from '@material-ui/core/styles' -import AlbumSongList from './AlbumSongList' +import { + Datagrid, + FunctionField, + List, + Loading, + TextField, + useGetOne +} from 'react-admin' import AlbumDetails from './AlbumDetails' - -const AlbumTitle = ({ record }) => { - return -} - -const useStyles = makeStyles((theme) => ({ - container: { - [theme.breakpoints.down('xs')]: { - padding: '0.7em', - minWidth: '24em' - }, - [theme.breakpoints.up('sm')]: { - padding: '1em', - minWidth: '32em' - } - }, - albumCover: { - display: 'inline-block', - [theme.breakpoints.down('xs')]: { - height: '8em', - width: '8em' - }, - [theme.breakpoints.up('sm')]: { - height: '15em', - width: '15em' - }, - [theme.breakpoints.up('lg')]: { - height: '20em', - width: '20em' - } - }, - albumDetails: { - display: 'inline-block', - verticalAlign: 'top', - [theme.breakpoints.down('xs')]: { - width: '14em' - }, - [theme.breakpoints.up('sm')]: { - width: '26em' - }, - [theme.breakpoints.up('lg')]: { - width: '38em' - } - }, - albumTitle: { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis' - } -})) +import { DurationField, Title } from '../common' +import { useStyles } from './styles' +import { SongBulkActions } from '../song/SongBulkActions' +import { AlbumActions } from './AlbumActions' +import { useMediaQuery } from '@material-ui/core' +import { setTrack } from '../player' +import { useDispatch } from 'react-redux' const AlbumShow = (props) => { + const dispatch = useDispatch() + const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) const classes = useStyles() + const { data: record, loading, error } = useGetOne('album', props.id) + + if (loading) { + return <Loading /> + } + + if (error) { + return <p>ERROR: {error}</p> + } + + const trackName = (r) => { + const name = r.title + if (r.trackNumber) { + return r.trackNumber.toString().padStart(2, '0') + ' ' + name + } + return name + } + return ( <> - <AlbumDetails classes={classes} {...props} /> - <Show title={<AlbumTitle />} {...props}> - <AlbumSongList {...props} /> - </Show> + <AlbumDetails {...props} classes={classes} record={record} /> + <List + {...props} + title={<Title subTitle={record.name} />} + actions={<AlbumActions />} + filter={{ album_id: props.id }} + resource={'song'} + exporter={false} + basePath={'/song'} + perPage={1000} + pagination={null} + sort={{ field: 'discNumber asc, trackNumber asc', order: 'ASC' }} + bulkActionButtons={<SongBulkActions />} + > + <Datagrid + rowClick={(id, basePath, record) => dispatch(setTrack(record))} + > + {isDesktop && ( + <TextField + source="trackNumber" + sortBy="discNumber asc, trackNumber asc" + label="#" + /> + )} + {isDesktop && <TextField source="title" />} + {!isDesktop && <FunctionField source="title" render={trackName} />} + {record.compilation && <TextField source="artist" />} + <DurationField source="duration" /> + </Datagrid> + </List> </> ) } diff --git a/ui/src/album/AlbumSongList.js b/ui/src/album/AlbumSongList.js deleted file mode 100644 index f227f5351..000000000 --- a/ui/src/album/AlbumSongList.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react' -import { useGetList } from 'react-admin' -import { DurationField, PlayButton, SimpleList } from '../common' -import { addTrack } from '../player' -import AddIcon from '@material-ui/icons/Add' -import { useDispatch } from 'react-redux' -import { playAlbum } from '../player/queue' - -const AlbumSongList = (props) => { - const dispatch = useDispatch() - const { record } = props - - const { data, total, loading, error } = useGetList( - 'song', - { page: 0, perPage: 100 }, - { field: 'album', order: 'ASC' }, - { album_id: record.id } - ) - - if (error) { - return <p>ERROR: {error}</p> - } - - const trackName = (r) => { - const name = r.title - if (r.trackNumber) { - return r.trackNumber.toString().padStart(2, '0') + ' ' + name - } - return name - } - - return ( - <SimpleList - data={data} - ids={Object.keys(data)} - loading={loading} - total={total} - primaryText={(r) => ( - <> - <PlayButton action={playAlbum(r.id, data)} /> - <PlayButton action={addTrack(r)} icon={<AddIcon />} /> - {trackName(r)} - </> - )} - secondaryText={(r) => - r.albumArtist && r.artist !== r.albumArtist ? r.artist : '' - } - tertiaryText={(r) => <DurationField record={r} source={'duration'} />} - linkType={(id) => dispatch(playAlbum(id, data))} - /> - ) -} - -export default AlbumSongList diff --git a/ui/src/album/styles.js b/ui/src/album/styles.js new file mode 100644 index 000000000..3046968a4 --- /dev/null +++ b/ui/src/album/styles.js @@ -0,0 +1,47 @@ +import { makeStyles } from '@material-ui/core/styles' + +export const useStyles = makeStyles((theme) => ({ + container: { + [theme.breakpoints.down('xs')]: { + padding: '0.7em', + minWidth: '24em' + }, + [theme.breakpoints.up('sm')]: { + padding: '1em', + minWidth: '32em' + } + }, + albumCover: { + display: 'inline-block', + [theme.breakpoints.down('xs')]: { + height: '8em', + width: '8em' + }, + [theme.breakpoints.up('sm')]: { + height: '10em', + width: '10em' + }, + [theme.breakpoints.up('lg')]: { + height: '15em', + width: '15em' + } + }, + albumDetails: { + display: 'inline-block', + verticalAlign: 'top', + [theme.breakpoints.down('xs')]: { + width: '14em' + }, + [theme.breakpoints.up('sm')]: { + width: '26em' + }, + [theme.breakpoints.up('lg')]: { + width: '38em' + } + }, + albumTitle: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } +})) diff --git a/ui/src/i18n/en.js b/ui/src/i18n/en.js index e67b12e34..4d85b9a35 100644 --- a/ui/src/i18n/en.js +++ b/ui/src/i18n/en.js @@ -17,6 +17,12 @@ export default deepmerge(englishMessages, { fields: { albumArtist: 'Album Artist', duration: 'Time' + }, + actions: { + playAll: 'Play', + playNext: 'Play Next', + addToQueue: 'Play Later', + shuffle: 'Shuffle' } } }, diff --git a/ui/src/player/index.js b/ui/src/player/index.js index 38bef76d5..754843c37 100644 --- a/ui/src/player/index.js +++ b/ui/src/player/index.js @@ -1,4 +1,4 @@ import Player from './Player' -import { addTrack, setTrack, playQueueReducer } from './queue' +import { addTrack, setTrack, playQueueReducer, playAlbum } from './queue' -export { Player, addTrack, setTrack, playQueueReducer } +export { Player, addTrack, setTrack, playAlbum, playQueueReducer } diff --git a/ui/src/song/AddToQueueButton.js b/ui/src/song/AddToQueueButton.js index 6574b75e9..2ee88cb7c 100644 --- a/ui/src/song/AddToQueueButton.js +++ b/ui/src/song/AddToQueueButton.js @@ -2,15 +2,13 @@ import React from 'react' import { Button, useDataProvider, - useUnselectAll, - useTranslate + useTranslate, + useUnselectAll } from 'react-admin' import { useDispatch } from 'react-redux' import { addTrack } from '../player' import AddToQueueIcon from '@material-ui/icons/AddToQueue' -import Tooltip from '@material-ui/core/Tooltip' - const AddToQueueButton = ({ selectedIds }) => { const dispatch = useDispatch() const translate = useTranslate() @@ -26,13 +24,12 @@ const AddToQueueButton = ({ selectedIds }) => { } return ( - <Button color="secondary" onClick={addToQueue}> - <Tooltip - title={translate('resources.song.bulk.addToQueue')} - placement="right" - > - <AddToQueueIcon /> - </Tooltip> + <Button + color="secondary" + onClick={addToQueue} + label={translate('resources.song.bulk.addToQueue')} + > + <AddToQueueIcon /> </Button> ) } diff --git a/ui/src/song/SongBulkActions.js b/ui/src/song/SongBulkActions.js new file mode 100644 index 000000000..95ae36cf9 --- /dev/null +++ b/ui/src/song/SongBulkActions.js @@ -0,0 +1,16 @@ +import React, { Fragment, useEffect } from 'react' +import { useUnselectAll } from 'react-admin' +import AddToQueueButton from './AddToQueueButton' + +export const SongBulkActions = (props) => { + const unselectAll = useUnselectAll() + useEffect(() => { + console.log('UNSELECT!') + unselectAll('song') + }, []) + return ( + <Fragment> + <AddToQueueButton {...props} /> + </Fragment> + ) +} diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js index cd58b53e8..d01ef539d 100644 --- a/ui/src/song/SongList.js +++ b/ui/src/song/SongList.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react' +import React from 'react' import { BooleanField, Datagrid, @@ -13,12 +13,18 @@ import { TextInput } from 'react-admin' import { useMediaQuery } from '@material-ui/core' -import { BitrateField, DurationField, Pagination, Title } from '../common' -import AddToQueueButton from './AddToQueueButton' -import { PlayButton, SimpleList } from '../common' +import { + BitrateField, + DurationField, + Pagination, + PlayButton, + SimpleList, + Title +} from '../common' import { useDispatch } from 'react-redux' -import { setTrack, addTrack } from '../player' +import { addTrack, setTrack } from '../player' import AddIcon from '@material-ui/icons/Add' +import { SongBulkActions } from './SongBulkActions' const SongFilter = (props) => ( <Filter {...props}> @@ -28,12 +34,6 @@ const SongFilter = (props) => ( </Filter> ) -const SongBulkActionButtons = (props) => ( - <Fragment> - <AddToQueueButton {...props} /> - </Fragment> -) - const SongDetails = (props) => { return ( <Show {...props} title=" "> @@ -59,7 +59,7 @@ const SongList = (props) => { title={<Title subTitle={'Songs'} />} sort={{ field: 'title', order: 'ASC' }} exporter={false} - bulkActionButtons={<SongBulkActionButtons />} + bulkActionButtons={<SongBulkActions />} filters={<SongFilter />} perPage={isXsmall ? 50 : 15} pagination={<Pagination />}