feat: implement AlbumShow using a Datagrid. WIP: still need to make it responsive

This commit is contained in:
Deluan
2020-02-12 20:35:35 -05:00
parent 8ebb85b0af
commit 9fa73e3b7b
10 changed files with 230 additions and 158 deletions
+64
View File
@@ -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 (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
<Button
color={'secondary'}
onClick={() => {
dispatch(playAlbum(ids[0], data))
}}
label={translation('resources.album.actions.playAll')}
>
<PlayArrowIcon />
</Button>
<Button
color={'secondary'}
onClick={() => {
const shuffled = shuffle(data)
const firstId = Object.keys(shuffled)[0]
dispatch(playAlbum(firstId, shuffled))
}}
label={translation('resources.album.actions.shuffle')}
>
<ShuffleIcon />
</Button>
</TopToolbar>
)
}
AlbumActions.defaultProps = {
selectedIds: [],
onUnselectItems: () => null
}
+10 -21
View File
@@ -1,26 +1,15 @@
import React from 'react' import React from 'react'
import { Loading, useGetOne } from 'react-admin'
import { Card, CardContent, CardMedia, Typography } from '@material-ui/core' import { Card, CardContent, CardMedia, Typography } from '@material-ui/core'
import { subsonicUrl } from '../subsonic' import { subsonicUrl } from '../subsonic'
const AlbumDetails = ({ id, classes }) => { const AlbumDetails = ({ classes, record }) => {
const { data, loading, error } = useGetOne('album', id) const genreYear = (record) => {
if (loading) {
return <Loading />
}
if (error) {
return <p>ERROR: {error}</p>
}
const genreYear = (data) => {
let genreDateLine = [] let genreDateLine = []
if (data.genre) { if (record.genre) {
genreDateLine.push(data.genre) genreDateLine.push(record.genre)
} }
if (data.year) { if (record.year) {
genreDateLine.push(data.year) genreDateLine.push(record.year)
} }
return genreDateLine.join(' - ') return genreDateLine.join(' - ')
} }
@@ -30,19 +19,19 @@ const AlbumDetails = ({ id, classes }) => {
<CardMedia <CardMedia
image={subsonicUrl( image={subsonicUrl(
'getCoverArt', 'getCoverArt',
data.coverArtId || 'not_found', record.coverArtId || 'not_found',
'size=500' 'size=500'
)} )}
className={classes.albumCover} className={classes.albumCover}
/> />
<CardContent className={classes.albumDetails}> <CardContent className={classes.albumDetails}>
<Typography variant="h5" className={classes.albumTitle}> <Typography variant="h5" className={classes.albumTitle}>
{data.name} {record.name}
</Typography> </Typography>
<Typography component="h6"> <Typography component="h6">
{data.albumArtist || data.artist} {record.albumArtist || record.artist}
</Typography> </Typography>
<Typography component="p">{genreYear(data)}</Typography> <Typography component="p">{genreYear(record)}</Typography>
</CardContent> </CardContent>
</Card> </Card>
) )
+65 -58
View File
@@ -1,68 +1,75 @@
import React from 'react' import React from 'react'
import { Show } from 'react-admin' import {
import { Title } from '../common' Datagrid,
import { makeStyles } from '@material-ui/core/styles' FunctionField,
import AlbumSongList from './AlbumSongList' List,
Loading,
TextField,
useGetOne
} from 'react-admin'
import AlbumDetails from './AlbumDetails' import AlbumDetails from './AlbumDetails'
import { DurationField, Title } from '../common'
const AlbumTitle = ({ record }) => { import { useStyles } from './styles'
return <Title subTitle={record ? record.name : ''} /> import { SongBulkActions } from '../song/SongBulkActions'
} import { AlbumActions } from './AlbumActions'
import { useMediaQuery } from '@material-ui/core'
const useStyles = makeStyles((theme) => ({ import { setTrack } from '../player'
container: { import { useDispatch } from 'react-redux'
[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'
}
}))
const AlbumShow = (props) => { const AlbumShow = (props) => {
const dispatch = useDispatch()
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
const classes = useStyles() 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 ( return (
<> <>
<AlbumDetails classes={classes} {...props} /> <AlbumDetails {...props} classes={classes} record={record} />
<Show title={<AlbumTitle />} {...props}> <List
<AlbumSongList {...props} /> {...props}
</Show> 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>
</> </>
) )
} }
-54
View File
@@ -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
+47
View File
@@ -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'
}
}))
+6
View File
@@ -17,6 +17,12 @@ export default deepmerge(englishMessages, {
fields: { fields: {
albumArtist: 'Album Artist', albumArtist: 'Album Artist',
duration: 'Time' duration: 'Time'
},
actions: {
playAll: 'Play',
playNext: 'Play Next',
addToQueue: 'Play Later',
shuffle: 'Shuffle'
} }
} }
}, },
+2 -2
View File
@@ -1,4 +1,4 @@
import Player from './Player' 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 }
+6 -9
View File
@@ -2,15 +2,13 @@ import React from 'react'
import { import {
Button, Button,
useDataProvider, useDataProvider,
useUnselectAll, useTranslate,
useTranslate useUnselectAll
} from 'react-admin' } from 'react-admin'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { addTrack } from '../player' import { addTrack } from '../player'
import AddToQueueIcon from '@material-ui/icons/AddToQueue' import AddToQueueIcon from '@material-ui/icons/AddToQueue'
import Tooltip from '@material-ui/core/Tooltip'
const AddToQueueButton = ({ selectedIds }) => { const AddToQueueButton = ({ selectedIds }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
const translate = useTranslate() const translate = useTranslate()
@@ -26,13 +24,12 @@ const AddToQueueButton = ({ selectedIds }) => {
} }
return ( return (
<Button color="secondary" onClick={addToQueue}> <Button
<Tooltip color="secondary"
title={translate('resources.song.bulk.addToQueue')} onClick={addToQueue}
placement="right" label={translate('resources.song.bulk.addToQueue')}
> >
<AddToQueueIcon /> <AddToQueueIcon />
</Tooltip>
</Button> </Button>
) )
} }
+16
View File
@@ -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>
)
}
+12 -12
View File
@@ -1,4 +1,4 @@
import React, { Fragment } from 'react' import React from 'react'
import { import {
BooleanField, BooleanField,
Datagrid, Datagrid,
@@ -13,12 +13,18 @@ import {
TextInput TextInput
} from 'react-admin' } from 'react-admin'
import { useMediaQuery } from '@material-ui/core' import { useMediaQuery } from '@material-ui/core'
import { BitrateField, DurationField, Pagination, Title } from '../common' import {
import AddToQueueButton from './AddToQueueButton' BitrateField,
import { PlayButton, SimpleList } from '../common' DurationField,
Pagination,
PlayButton,
SimpleList,
Title
} from '../common'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { setTrack, addTrack } from '../player' import { addTrack, setTrack } from '../player'
import AddIcon from '@material-ui/icons/Add' import AddIcon from '@material-ui/icons/Add'
import { SongBulkActions } from './SongBulkActions'
const SongFilter = (props) => ( const SongFilter = (props) => (
<Filter {...props}> <Filter {...props}>
@@ -28,12 +34,6 @@ const SongFilter = (props) => (
</Filter> </Filter>
) )
const SongBulkActionButtons = (props) => (
<Fragment>
<AddToQueueButton {...props} />
</Fragment>
)
const SongDetails = (props) => { const SongDetails = (props) => {
return ( return (
<Show {...props} title=" "> <Show {...props} title=" ">
@@ -59,7 +59,7 @@ const SongList = (props) => {
title={<Title subTitle={'Songs'} />} title={<Title subTitle={'Songs'} />}
sort={{ field: 'title', order: 'ASC' }} sort={{ field: 'title', order: 'ASC' }}
exporter={false} exporter={false}
bulkActionButtons={<SongBulkActionButtons />} bulkActionButtons={<SongBulkActions />}
filters={<SongFilter />} filters={<SongFilter />}
perPage={isXsmall ? 50 : 15} perPage={isXsmall ? 50 : 15}
pagination={<Pagination />} pagination={<Pagination />}