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

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
}

View File

@ -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 <Loading />
}
if (error) {
return <p>ERROR: {error}</p>
}
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 }) => {
<CardMedia
image={subsonicUrl(
'getCoverArt',
data.coverArtId || 'not_found',
record.coverArtId || 'not_found',
'size=500'
)}
className={classes.albumCover}
/>
<CardContent className={classes.albumDetails}>
<Typography variant="h5" className={classes.albumTitle}>
{data.name}
{record.name}
</Typography>
<Typography component="h6">
{data.albumArtist || data.artist}
{record.albumArtist || record.artist}
</Typography>
<Typography component="p">{genreYear(data)}</Typography>
<Typography component="p">{genreYear(record)}</Typography>
</CardContent>
</Card>
)

View File

@ -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 <Title subTitle={record ? record.name : ''} />
}
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>
</>
)
}

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
ui/src/album/styles.js Normal file
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'
}
}))

View File

@ -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'
}
}
},

View File

@ -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 }

View File

@ -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>
)
}

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>
)
}

View File

@ -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 />}