feat: option to display albums as a grid
This commit is contained in:
+5
-1
@@ -13,6 +13,7 @@ import album from './album'
|
|||||||
import artist from './artist'
|
import artist from './artist'
|
||||||
import { createMuiTheme } from '@material-ui/core/styles'
|
import { createMuiTheme } from '@material-ui/core/styles'
|
||||||
import { Player, playQueueReducer } from './audioplayer'
|
import { Player, playQueueReducer } from './audioplayer'
|
||||||
|
import { albumViewReducer } from './album/albumState'
|
||||||
|
|
||||||
const theme = createMuiTheme(DarkTheme)
|
const theme = createMuiTheme(DarkTheme)
|
||||||
|
|
||||||
@@ -34,7 +35,10 @@ const App = () => {
|
|||||||
return (
|
return (
|
||||||
<Admin
|
<Admin
|
||||||
theme={theme}
|
theme={theme}
|
||||||
customReducers={{ queue: playQueueReducer }}
|
customReducers={{
|
||||||
|
queue: playQueueReducer,
|
||||||
|
albumView: albumViewReducer
|
||||||
|
}}
|
||||||
dataProvider={dataProvider}
|
dataProvider={dataProvider}
|
||||||
authProvider={authProvider}
|
authProvider={authProvider}
|
||||||
i18nProvider={i18nProvider}
|
i18nProvider={i18nProvider}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { GridList, GridListTile, GridListTileBar } from '@material-ui/core'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import withWidth from '@material-ui/core/withWidth'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { linkToRecord } from 'ra-core'
|
||||||
|
import { Loading } from 'react-admin'
|
||||||
|
import { subsonicUrl } from '../subsonic'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
margin: '5px'
|
||||||
|
},
|
||||||
|
cover: {
|
||||||
|
display: 'inline-block',
|
||||||
|
maxWidth: '100%',
|
||||||
|
height: 'auto'
|
||||||
|
},
|
||||||
|
tileBar: {
|
||||||
|
textAlign: 'center',
|
||||||
|
background:
|
||||||
|
'linear-gradient(to top, rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)'
|
||||||
|
},
|
||||||
|
albumArtistName: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '1em'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const getColsForWidth = (width) => {
|
||||||
|
if (width === 'xs') return 2
|
||||||
|
if (width === 'sm') return 4
|
||||||
|
if (width === 'md') return 5
|
||||||
|
if (width === 'lg') return 6
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadedAlbumGrid = ({ ids, data, basePath, width }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<GridList
|
||||||
|
cellHeight={'auto'}
|
||||||
|
cols={getColsForWidth(width)}
|
||||||
|
className={classes.gridList}
|
||||||
|
spacing={20}
|
||||||
|
>
|
||||||
|
{ids.map((id) => (
|
||||||
|
<GridListTile
|
||||||
|
component={Link}
|
||||||
|
key={id}
|
||||||
|
to={linkToRecord(basePath, data[id].id, 'show')}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={subsonicUrl(
|
||||||
|
'getCoverArt',
|
||||||
|
data[id].coverArtId || 'not_found',
|
||||||
|
{ size: 300 }
|
||||||
|
)}
|
||||||
|
alt={data[id].album}
|
||||||
|
className={classes.cover}
|
||||||
|
/>
|
||||||
|
<GridListTileBar
|
||||||
|
className={classes.tileBar}
|
||||||
|
title={data[id].name}
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
<div className={classes.albumArtistName}>
|
||||||
|
{data[id].albumArtist}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</GridListTile>
|
||||||
|
))}
|
||||||
|
</GridList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumGridView = ({ loading, ...props }) =>
|
||||||
|
loading ? <Loading /> : <LoadedAlbumGrid {...props} />
|
||||||
|
|
||||||
|
export default withWidth()(AlbumGridView)
|
||||||
+41
-38
@@ -1,23 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import {
|
import {
|
||||||
BooleanField,
|
AutocompleteInput,
|
||||||
Datagrid,
|
|
||||||
DateField,
|
|
||||||
Filter,
|
Filter,
|
||||||
List,
|
List,
|
||||||
NumberField,
|
|
||||||
FunctionField,
|
|
||||||
SearchInput,
|
|
||||||
NumberInput,
|
|
||||||
NullableBooleanInput,
|
NullableBooleanInput,
|
||||||
Show,
|
NumberInput,
|
||||||
SimpleShowLayout,
|
|
||||||
ReferenceInput,
|
ReferenceInput,
|
||||||
AutocompleteInput,
|
SearchInput,
|
||||||
TextField
|
Pagination
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { DurationField, Pagination, Title, RangeField } from '../common'
|
import { Title } from '../common'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { withWidth } from '@material-ui/core'
|
||||||
|
import AlbumListActions from './AlbumListActions'
|
||||||
|
import AlbumListView from './AlbumListView'
|
||||||
|
import AlbumGridView from './AlbumGridView'
|
||||||
|
import { ALBUM_LIST_MODE } from './albumState'
|
||||||
|
|
||||||
const AlbumFilter = (props) => (
|
const AlbumFilter = (props) => (
|
||||||
<Filter {...props}>
|
<Filter {...props}>
|
||||||
@@ -35,21 +33,27 @@ const AlbumFilter = (props) => (
|
|||||||
</Filter>
|
</Filter>
|
||||||
)
|
)
|
||||||
|
|
||||||
const AlbumDetails = (props) => {
|
const getPerPage = (width) => {
|
||||||
return (
|
if (width === 'xs') return 12
|
||||||
<Show {...props} title=" ">
|
if (width === 'sm') return 12
|
||||||
<SimpleShowLayout>
|
if (width === 'md') return 15
|
||||||
<TextField source="albumArtist" />
|
if (width === 'lg') return 18
|
||||||
<TextField source="genre" />
|
return 21
|
||||||
<BooleanField source="compilation" />
|
}
|
||||||
<DateField source="updatedAt" showTime />
|
|
||||||
</SimpleShowLayout>
|
const getPerPageOptions = (width) => {
|
||||||
</Show>
|
const options = [3, 6, 12]
|
||||||
)
|
if (width === 'xs') return [12]
|
||||||
|
if (width === 'sm') return [12]
|
||||||
|
if (width === 'md') return options.map((v) => v * 5)
|
||||||
|
if (width === 'lg') return options.map((v) => v * 6)
|
||||||
|
return options.map((v) => v * 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumList = (props) => {
|
const AlbumList = (props) => {
|
||||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
const { width } = props
|
||||||
|
const albumView = useSelector((state) => state.albumView)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@@ -57,21 +61,20 @@ const AlbumList = (props) => {
|
|||||||
sort={{ field: 'name', order: 'ASC' }}
|
sort={{ field: 'name', order: 'ASC' }}
|
||||||
exporter={false}
|
exporter={false}
|
||||||
bulkActionButtons={false}
|
bulkActionButtons={false}
|
||||||
|
actions={<AlbumListActions />}
|
||||||
filters={<AlbumFilter />}
|
filters={<AlbumFilter />}
|
||||||
perPage={15}
|
perPage={getPerPage(width)}
|
||||||
pagination={<Pagination />}
|
pagination={
|
||||||
|
<Pagination rowsPerPageOptions={getPerPageOptions(width)} {...props} />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Datagrid expand={<AlbumDetails />} rowClick={'show'}>
|
{albumView.mode === ALBUM_LIST_MODE ? (
|
||||||
<TextField source="name" />
|
<AlbumListView {...props} />
|
||||||
<FunctionField
|
) : (
|
||||||
source="artist"
|
<AlbumGridView {...props} />
|
||||||
render={(r) => (r.albumArtist ? r.albumArtist : r.artist)}
|
)}
|
||||||
/>
|
|
||||||
{isDesktop && <NumberField source="songCount" />}
|
|
||||||
<RangeField source={'year'} sortBy={'maxYear'} />
|
|
||||||
{isDesktop && <DurationField source="duration" />}
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
</List>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default AlbumList
|
|
||||||
|
export default withWidth()(AlbumList)
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { cloneElement } from 'react'
|
||||||
|
import { Button, sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||||
|
import { ButtonGroup } from '@material-ui/core'
|
||||||
|
import ViewHeadlineIcon from '@material-ui/icons/ViewHeadline'
|
||||||
|
import ViewModuleIcon from '@material-ui/icons/ViewModule'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { ALBUM_GRID_MODE, ALBUM_LIST_MODE, selectViewMode } from './albumState'
|
||||||
|
|
||||||
|
const AlbumListActions = ({
|
||||||
|
currentSort,
|
||||||
|
className,
|
||||||
|
resource,
|
||||||
|
filters,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
permanentFilter,
|
||||||
|
exporter,
|
||||||
|
basePath,
|
||||||
|
selectedIds,
|
||||||
|
onUnselectItems,
|
||||||
|
showFilter,
|
||||||
|
maxResults,
|
||||||
|
total,
|
||||||
|
fullWidth,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const albumView = useSelector((state) => state.albumView)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
{filters &&
|
||||||
|
cloneElement(filters, {
|
||||||
|
resource,
|
||||||
|
showFilter,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
context: 'button'
|
||||||
|
})}
|
||||||
|
<ButtonGroup
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
aria-label="text primary button group"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color={albumView.mode === ALBUM_LIST_MODE ? 'primary' : 'secondary'}
|
||||||
|
onClick={() => dispatch(selectViewMode(ALBUM_LIST_MODE))}
|
||||||
|
>
|
||||||
|
<ViewHeadlineIcon fontSize="inherit" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color={albumView.mode === ALBUM_GRID_MODE ? 'primary' : 'secondary'}
|
||||||
|
onClick={() => dispatch(selectViewMode(ALBUM_GRID_MODE))}
|
||||||
|
>
|
||||||
|
<ViewModuleIcon fontSize="inherit" />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</TopToolbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbumListActions.defaultProps = {
|
||||||
|
selectedIds: [],
|
||||||
|
onUnselectItems: () => null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlbumListActions
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
BooleanField,
|
||||||
|
Datagrid,
|
||||||
|
DateField,
|
||||||
|
NumberField,
|
||||||
|
FunctionField,
|
||||||
|
Show,
|
||||||
|
SimpleShowLayout,
|
||||||
|
TextField
|
||||||
|
} from 'react-admin'
|
||||||
|
import { DurationField, RangeField } from '../common'
|
||||||
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
|
|
||||||
|
const AlbumDetails = (props) => {
|
||||||
|
return (
|
||||||
|
<Show {...props} title=" ">
|
||||||
|
<SimpleShowLayout>
|
||||||
|
<TextField source="albumArtist" />
|
||||||
|
<TextField source="genre" />
|
||||||
|
<BooleanField source="compilation" />
|
||||||
|
<DateField source="updatedAt" showTime />
|
||||||
|
</SimpleShowLayout>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumListView = (props) => {
|
||||||
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||||
|
return (
|
||||||
|
<Datagrid {...props} expand={<AlbumDetails />} rowClick={'show'}>
|
||||||
|
<TextField source="name" />
|
||||||
|
<FunctionField
|
||||||
|
source="artist"
|
||||||
|
render={(r) => (r.albumArtist ? r.albumArtist : r.artist)}
|
||||||
|
/>
|
||||||
|
{isDesktop && <NumberField source="songCount" />}
|
||||||
|
<RangeField source={'year'} sortBy={'maxYear'} />
|
||||||
|
{isDesktop && <DurationField source="duration" />}
|
||||||
|
</Datagrid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default AlbumListView
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const ALBUM_GRID_MODE = 'ALBUM_GRID_MODE'
|
||||||
|
const ALBUM_LIST_MODE = 'ALBUM_LIST_MODE'
|
||||||
|
|
||||||
|
const selectViewMode = (mode) => ({ type: mode })
|
||||||
|
|
||||||
|
const albumViewReducer = (
|
||||||
|
previousState = {
|
||||||
|
mode: localStorage.getItem('albumViewMode') || ALBUM_LIST_MODE
|
||||||
|
},
|
||||||
|
payload
|
||||||
|
) => {
|
||||||
|
const { type } = payload
|
||||||
|
switch (type) {
|
||||||
|
case ALBUM_GRID_MODE:
|
||||||
|
case ALBUM_LIST_MODE:
|
||||||
|
localStorage.setItem('albumViewMode', type)
|
||||||
|
return { mode: type }
|
||||||
|
default:
|
||||||
|
return previousState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ALBUM_LIST_MODE, ALBUM_GRID_MODE, albumViewReducer, selectViewMode }
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react'
|
||||||
import { AppBar as RAAppBar, UserMenu, MenuItemLink } from 'react-admin'
|
import { AppBar as RAAppBar, UserMenu, MenuItemLink } from 'react-admin'
|
||||||
import InfoIcon from '@material-ui/icons/Info';
|
import InfoIcon from '@material-ui/icons/Info'
|
||||||
|
|
||||||
const ConfigurationMenu = forwardRef(({ onClick }, ref) => (
|
const ConfigurationMenu = forwardRef(({ onClick }, ref) => (
|
||||||
<MenuItemLink
|
<MenuItemLink
|
||||||
ref={ref}
|
ref={ref}
|
||||||
to=""
|
to=""
|
||||||
primaryText={"Version " + localStorage.getItem("version") }
|
primaryText={'Version ' + localStorage.getItem('version')}
|
||||||
leftIcon={<InfoIcon />}
|
leftIcon={<InfoIcon />}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user