Option to toggle fields in songs, albums & artists (#923)
* Add toggleColumns - Add logic for toggling columns - Add MenuComponent + useSelectedFields hook * Refactoring * eslint-fixes * Typo * skip menu in albumGridView * add omittedFields * Add toggling for playlists and albumSong * Refactoring * defaultProps - fix * Add toggling for PlaylistSongs * remove accidental console log * Refactoring for future compatibility * Hide ToggleMenu in albumGridView * Add TopBarComponent in ToggleFieldsMenu * Add defaultOff for useSelectedFields * Fix edge case * eslint fix * Refactoring * Add propType for forwardRef * Fix issues * add translation for grid and table * add translation for grid and table * Ignore menuBtn for spotify-ish and Ligera themes * hide bpm by default in playlistSongs * Add memoization * Default album view must be Grid Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -1,6 +1,18 @@
|
|||||||
export const SET_NOTIFICATIONS_STATE = 'SET_NOTIFICATIONS_STATE'
|
export const SET_NOTIFICATIONS_STATE = 'SET_NOTIFICATIONS_STATE'
|
||||||
|
export const SET_TOGGLEABLE_FIELDS = 'SET_TOGGLEABLE_FIELDS'
|
||||||
|
export const SET_OMITTED_FIELDS = 'SET_OMITTED_FIELDS'
|
||||||
|
|
||||||
export const setNotificationsState = (enabled) => ({
|
export const setNotificationsState = (enabled) => ({
|
||||||
type: SET_NOTIFICATIONS_STATE,
|
type: SET_NOTIFICATIONS_STATE,
|
||||||
data: enabled,
|
data: enabled,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const setToggleableFields = (obj) => ({
|
||||||
|
type: SET_TOGGLEABLE_FIELDS,
|
||||||
|
data: obj,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setOmittedFields = (obj) => ({
|
||||||
|
type: SET_OMITTED_FIELDS,
|
||||||
|
data: obj,
|
||||||
|
})
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ import { RiPlayListAddFill, RiPlayList2Fill } from 'react-icons/ri'
|
|||||||
import { playNext, addTracks, playTracks, shuffleTracks } from '../actions'
|
import { playNext, addTracks, playTracks, shuffleTracks } from '../actions'
|
||||||
import subsonic from '../subsonic'
|
import subsonic from '../subsonic'
|
||||||
import { formatBytes } from '../utils'
|
import { formatBytes } from '../utils'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery, makeStyles } from '@material-ui/core'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
toolbar: { display: 'flex', justifyContent: 'space-between', width: '100%' },
|
||||||
|
})
|
||||||
|
|
||||||
const AlbumActions = ({
|
const AlbumActions = ({
|
||||||
className,
|
className,
|
||||||
@@ -27,7 +32,9 @@ const AlbumActions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const translate = useTranslate()
|
const translate = useTranslate()
|
||||||
|
const classes = useStyles()
|
||||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||||
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
|
|
||||||
const handlePlay = React.useCallback(() => {
|
const handlePlay = React.useCallback(() => {
|
||||||
dispatch(playTracks(data, ids))
|
dispatch(playTracks(data, ids))
|
||||||
@@ -51,6 +58,8 @@ const AlbumActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
<div className={classes.toolbar}>
|
||||||
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
label={translate('resources.album.actions.playAll')}
|
label={translate('resources.album.actions.playAll')}
|
||||||
@@ -86,6 +95,9 @@ const AlbumActions = ({
|
|||||||
<CloudDownloadOutlinedIcon />
|
<CloudDownloadOutlinedIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{isNotSmall && <ToggleFieldsMenu resource="albumSong" />}</div>
|
||||||
|
</div>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
NullableBooleanInput,
|
NullableBooleanInput,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
|
Pagination,
|
||||||
ReferenceInput,
|
ReferenceInput,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
Pagination,
|
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import FavoriteIcon from '@material-ui/icons/Favorite'
|
import FavoriteIcon from '@material-ui/icons/Favorite'
|
||||||
@@ -20,6 +20,7 @@ import AlbumGridView from './AlbumGridView'
|
|||||||
import { AddToPlaylistDialog } from '../dialogs'
|
import { AddToPlaylistDialog } from '../dialogs'
|
||||||
import albumLists, { defaultAlbumList } from './albumLists'
|
import albumLists, { defaultAlbumList } from './albumLists'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
|
|
||||||
const AlbumFilter = (props) => {
|
const AlbumFilter = (props) => {
|
||||||
const translate = useTranslate()
|
const translate = useTranslate()
|
||||||
@@ -70,6 +71,21 @@ const AlbumList = (props) => {
|
|||||||
.replace(/^\/album/, '')
|
.replace(/^\/album/, '')
|
||||||
.replace(/^\//, '')
|
.replace(/^\//, '')
|
||||||
|
|
||||||
|
// Workaround to force album columns to appear the first time.
|
||||||
|
// See https://github.com/navidrome/navidrome/pull/923#issuecomment-833004842
|
||||||
|
// TODO: Find a better solution
|
||||||
|
useSelectedFields({
|
||||||
|
resource: 'album',
|
||||||
|
columns: {
|
||||||
|
artist: 'artist',
|
||||||
|
songCount: 'songCount',
|
||||||
|
playCount: 'playCount',
|
||||||
|
year: 'year',
|
||||||
|
duration: 'duration',
|
||||||
|
rating: 'rating',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// If it does not have filter/sort params (usually coming from Menu),
|
// If it does not have filter/sort params (usually coming from Menu),
|
||||||
// reload with correct filter/sort params
|
// reload with correct filter/sort params
|
||||||
if (!location.search) {
|
if (!location.search) {
|
||||||
|
|||||||
@@ -1,10 +1,71 @@
|
|||||||
import React, { cloneElement } from 'react'
|
import React, { cloneElement } from 'react'
|
||||||
import { Button, sanitizeListRestProps, TopToolbar } from 'react-admin'
|
import {
|
||||||
import { ButtonGroup } from '@material-ui/core'
|
Button,
|
||||||
|
sanitizeListRestProps,
|
||||||
|
TopToolbar,
|
||||||
|
useTranslate,
|
||||||
|
} from 'react-admin'
|
||||||
|
import {
|
||||||
|
ButtonGroup,
|
||||||
|
useMediaQuery,
|
||||||
|
Typography,
|
||||||
|
makeStyles,
|
||||||
|
} from '@material-ui/core'
|
||||||
import ViewHeadlineIcon from '@material-ui/icons/ViewHeadline'
|
import ViewHeadlineIcon from '@material-ui/icons/ViewHeadline'
|
||||||
import ViewModuleIcon from '@material-ui/icons/ViewModule'
|
import ViewModuleIcon from '@material-ui/icons/ViewModule'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { albumViewGrid, albumViewList } from '../actions'
|
import { albumViewGrid, albumViewList } from '../actions'
|
||||||
|
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
title: { margin: '1rem' },
|
||||||
|
buttonGroup: { width: '100%', justifyContent: 'center' },
|
||||||
|
leftButton: { paddingRight: '0.5rem' },
|
||||||
|
rightButton: { paddingLeft: '0.5rem' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const AlbumViewToggler = React.forwardRef(
|
||||||
|
({ showTitle = true, disableElevation, fullWidth }, ref) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const albumView = useSelector((state) => state.albumView)
|
||||||
|
const classes = useStyles()
|
||||||
|
const translate = useTranslate()
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
{showTitle && (
|
||||||
|
<Typography className={classes.title}>
|
||||||
|
{translate('ra.toggleFieldsMenu.layout')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<ButtonGroup
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
aria-label="text primary button group"
|
||||||
|
className={classes.buttonGroup}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className={classes.leftButton}
|
||||||
|
label={translate('ra.toggleFieldsMenu.grid')}
|
||||||
|
color={albumView.grid ? 'primary' : 'secondary'}
|
||||||
|
onClick={() => dispatch(albumViewGrid())}
|
||||||
|
>
|
||||||
|
<ViewModuleIcon fontSize="inherit" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className={classes.rightButton}
|
||||||
|
label={translate('ra.toggleFieldsMenu.table')}
|
||||||
|
color={albumView.grid ? 'secondary' : 'primary'}
|
||||||
|
onClick={() => dispatch(albumViewList())}
|
||||||
|
>
|
||||||
|
<ViewHeadlineIcon fontSize="inherit" />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const AlbumListActions = ({
|
const AlbumListActions = ({
|
||||||
currentSort,
|
currentSort,
|
||||||
@@ -24,9 +85,7 @@ const AlbumListActions = ({
|
|||||||
fullWidth,
|
fullWidth,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch()
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
const albumView = useSelector((state) => state.albumView)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
{filters &&
|
{filters &&
|
||||||
@@ -37,26 +96,11 @@ const AlbumListActions = ({
|
|||||||
filterValues,
|
filterValues,
|
||||||
context: 'button',
|
context: 'button',
|
||||||
})}
|
})}
|
||||||
<ButtonGroup
|
{isNotSmall ? (
|
||||||
variant="text"
|
<ToggleFieldsMenu resource="album" topbarComponent={AlbumViewToggler} />
|
||||||
color="primary"
|
) : (
|
||||||
aria-label="text primary button group"
|
<AlbumViewToggler showTitle={false} />
|
||||||
>
|
)}
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
color={albumView.grid ? 'primary' : 'secondary'}
|
|
||||||
onClick={() => dispatch(albumViewGrid())}
|
|
||||||
>
|
|
||||||
<ViewModuleIcon fontSize="inherit" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
color={albumView.grid ? 'secondary' : 'primary'}
|
|
||||||
onClick={() => dispatch(albumViewList())}
|
|
||||||
>
|
|
||||||
<ViewHeadlineIcon fontSize="inherit" />
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import Paper from '@material-ui/core/Paper'
|
import Paper from '@material-ui/core/Paper'
|
||||||
import Table from '@material-ui/core/Table'
|
import Table from '@material-ui/core/Table'
|
||||||
import TableBody from '@material-ui/core/TableBody'
|
import TableBody from '@material-ui/core/TableBody'
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
RatingField,
|
RatingField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
columnIcon: {
|
columnIcon: {
|
||||||
@@ -109,6 +110,36 @@ const AlbumListView = ({
|
|||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||||
|
|
||||||
|
const toggleableFields = useMemo(() => {
|
||||||
|
return {
|
||||||
|
artist: <ArtistLinkField source="artist" />,
|
||||||
|
songCount: isDesktop && (
|
||||||
|
<NumberField source="songCount" sortByOrder={'DESC'} />
|
||||||
|
),
|
||||||
|
playCount: isDesktop && (
|
||||||
|
<NumberField source="playCount" sortByOrder={'DESC'} />
|
||||||
|
),
|
||||||
|
year: (
|
||||||
|
<RangeField source={'year'} sortBy={'maxYear'} sortByOrder={'DESC'} />
|
||||||
|
),
|
||||||
|
duration: isDesktop && <DurationField source="duration" />,
|
||||||
|
rating: config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source={'rating'}
|
||||||
|
resource={'album'}
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}, [classes.ratingField, isDesktop])
|
||||||
|
|
||||||
|
const columns = useSelectedFields({
|
||||||
|
resource: 'album',
|
||||||
|
columns: toggleableFields,
|
||||||
|
})
|
||||||
|
|
||||||
return isXsmall ? (
|
return isXsmall ? (
|
||||||
<SimpleList
|
<SimpleList
|
||||||
primaryText={(r) => r.name}
|
primaryText={(r) => r.name}
|
||||||
@@ -147,19 +178,7 @@ const AlbumListView = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<ArtistLinkField source="artist" />
|
{columns}
|
||||||
{isDesktop && <NumberField source="songCount" sortByOrder={'DESC'} />}
|
|
||||||
{isDesktop && <NumberField source="playCount" sortByOrder={'DESC'} />}
|
|
||||||
<RangeField source={'year'} sortBy={'maxYear'} sortByOrder={'DESC'} />
|
|
||||||
{isDesktop && <DurationField source="duration" />}
|
|
||||||
{config.enableStarRating && (
|
|
||||||
<RatingField
|
|
||||||
source={'rating'}
|
|
||||||
resource={'album'}
|
|
||||||
sortByOrder={'DESC'}
|
|
||||||
className={classes.ratingField}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AlbumContextMenu
|
<AlbumContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortBy={'starred ASC, starredAt ASC'}
|
sortBy={'starred ASC, starredAt ASC'}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import AlbumActions from './AlbumActions'
|
|||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
(theme) => ({
|
(theme) => ({
|
||||||
albumActions: {},
|
albumActions: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'NDAlbumShow',
|
name: 'NDAlbumShow',
|
||||||
|
|||||||
+43
-26
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
BulkActionsToolbar,
|
BulkActionsToolbar,
|
||||||
ListToolbar,
|
ListToolbar,
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '../common'
|
} from '../common'
|
||||||
import { AddToPlaylistDialog } from '../dialogs'
|
import { AddToPlaylistDialog } from '../dialogs'
|
||||||
import { QualityInfo } from '../common/QualityInfo'
|
import { QualityInfo } from '../common/QualityInfo'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
@@ -87,6 +88,46 @@ const AlbumSongs = (props) => {
|
|||||||
const classes = useStyles({ isDesktop })
|
const classes = useStyles({ isDesktop })
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const version = useVersion()
|
const version = useVersion()
|
||||||
|
|
||||||
|
const toggleableFields = useMemo(() => {
|
||||||
|
return {
|
||||||
|
trackNumber: isDesktop && (
|
||||||
|
<TextField
|
||||||
|
source="trackNumber"
|
||||||
|
sortBy="discNumber asc, trackNumber asc"
|
||||||
|
label="#"
|
||||||
|
sortable={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: (
|
||||||
|
<SongTitleField
|
||||||
|
source="title"
|
||||||
|
sortable={false}
|
||||||
|
showTrackNumbers={!isDesktop}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
artist: isDesktop && <TextField source="artist" sortable={false} />,
|
||||||
|
duration: <DurationField source="duration" sortable={false} />,
|
||||||
|
quality: isDesktop && <QualityInfo source="quality" sortable={false} />,
|
||||||
|
bpm: isDesktop && <NumberField source="bpm" sortable={false} />,
|
||||||
|
rating: isDesktop && config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source="rating"
|
||||||
|
resource={'albumSong'}
|
||||||
|
sortable={false}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}, [isDesktop, classes.ratingField])
|
||||||
|
|
||||||
|
const columns = useSelectedFields({
|
||||||
|
resource: 'albumSong',
|
||||||
|
columns: toggleableFields,
|
||||||
|
omittedColumns: ['title'],
|
||||||
|
defaultOff: ['bpm'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
@@ -113,31 +154,7 @@ const AlbumSongs = (props) => {
|
|||||||
contextAlwaysVisible={!isDesktop}
|
contextAlwaysVisible={!isDesktop}
|
||||||
classes={{ row: classes.row }}
|
classes={{ row: classes.row }}
|
||||||
>
|
>
|
||||||
{isDesktop && (
|
{columns}
|
||||||
<TextField
|
|
||||||
source="trackNumber"
|
|
||||||
sortBy="discNumber asc, trackNumber asc"
|
|
||||||
label="#"
|
|
||||||
sortable={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SongTitleField
|
|
||||||
source="title"
|
|
||||||
sortable={false}
|
|
||||||
showTrackNumbers={!isDesktop}
|
|
||||||
/>
|
|
||||||
{isDesktop && <TextField source="artist" sortable={false} />}
|
|
||||||
<DurationField source="duration" sortable={false} />
|
|
||||||
{isDesktop && <QualityInfo source="quality" sortable={false} />}
|
|
||||||
{isDesktop && <NumberField source="bpm" sortable={false} />}
|
|
||||||
{isDesktop && config.enableStarRating && (
|
|
||||||
<RatingField
|
|
||||||
source="rating"
|
|
||||||
resource={'albumSong'}
|
|
||||||
sortable={false}
|
|
||||||
className={classes.ratingField}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SongContextMenu
|
<SongContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortable={false}
|
sortable={false}
|
||||||
|
|||||||
+27
-12
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
} from '../common'
|
} from '../common'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
import ArtistListActions from './ArtistListActions'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
contextHeader: {
|
contextHeader: {
|
||||||
@@ -64,6 +66,28 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
|
|||||||
const handleArtistLink = useGetHandleArtistClick(width)
|
const handleArtistLink = useGetHandleArtistClick(width)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||||
|
|
||||||
|
const toggleableFields = useMemo(() => {
|
||||||
|
return {
|
||||||
|
albumCount: <NumberField source="albumCount" sortByOrder={'DESC'} />,
|
||||||
|
songCount: <NumberField source="songCount" sortByOrder={'DESC'} />,
|
||||||
|
playCount: <NumberField source="playCount" sortByOrder={'DESC'} />,
|
||||||
|
rating: config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source="rating"
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
resource={'artist'}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}, [classes.ratingField])
|
||||||
|
|
||||||
|
const columns = useSelectedFields({
|
||||||
|
resource: 'artist',
|
||||||
|
columns: toggleableFields,
|
||||||
|
})
|
||||||
|
|
||||||
return isXsmall ? (
|
return isXsmall ? (
|
||||||
<ArtistSimpleList
|
<ArtistSimpleList
|
||||||
linkType={(id) => history.push(handleArtistLink(id))}
|
linkType={(id) => history.push(handleArtistLink(id))}
|
||||||
@@ -72,17 +96,7 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
|
|||||||
) : (
|
) : (
|
||||||
<Datagrid rowClick={handleArtistLink} classes={{ row: classes.row }}>
|
<Datagrid rowClick={handleArtistLink} classes={{ row: classes.row }}>
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<NumberField source="albumCount" sortByOrder={'DESC'} />
|
{columns}
|
||||||
<NumberField source="songCount" sortByOrder={'DESC'} />
|
|
||||||
<NumberField source="playCount" sortByOrder={'DESC'} />
|
|
||||||
{config.enableStarRating && (
|
|
||||||
<RatingField
|
|
||||||
source="rating"
|
|
||||||
sortByOrder={'DESC'}
|
|
||||||
resource={'artist'}
|
|
||||||
className={classes.ratingField}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ArtistContextMenu
|
<ArtistContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortBy={'starred ASC, starredAt ASC'}
|
sortBy={'starred ASC, starredAt ASC'}
|
||||||
@@ -111,6 +125,7 @@ const ArtistList = (props) => {
|
|||||||
exporter={false}
|
exporter={false}
|
||||||
bulkActionButtons={false}
|
bulkActionButtons={false}
|
||||||
filters={<ArtistFilter />}
|
filters={<ArtistFilter />}
|
||||||
|
actions={<ArtistListActions />}
|
||||||
>
|
>
|
||||||
<ArtistListView {...props} />
|
<ArtistListView {...props} />
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||||
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
|
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||||
|
|
||||||
|
const ArtistListActions = ({ className, ...rest }) => {
|
||||||
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
{isNotSmall && <ToggleFieldsMenu resource="artist" />}
|
||||||
|
</TopToolbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArtistListActions
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Menu from '@material-ui/core/Menu'
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem'
|
||||||
|
import { makeStyles, Typography } from '@material-ui/core'
|
||||||
|
import MoreVertIcon from '@material-ui/icons/MoreVert'
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { useTranslate } from 'react-admin'
|
||||||
|
import { setToggleableFields } from '../actions'
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
menuIcon: {
|
||||||
|
position: 'relative',
|
||||||
|
top: '-0.5em',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
width: '24ch',
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
maxHeight: '21rem',
|
||||||
|
overflow: 'auto',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
margin: '1rem',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ToggleFieldsMenu = ({ resource, topbarComponent: TopBarComponent }) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const translate = useTranslate()
|
||||||
|
const toggleableColumns = useSelector(
|
||||||
|
(state) => state.settings.toggleableFields[resource]
|
||||||
|
)
|
||||||
|
const omittedColumns =
|
||||||
|
useSelector((state) => state.settings.omittedFields[resource]) || []
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
|
const open = Boolean(anchorEl)
|
||||||
|
|
||||||
|
const handleOpen = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (selectedColumn) => {
|
||||||
|
dispatch(
|
||||||
|
setToggleableFields({
|
||||||
|
[resource]: {
|
||||||
|
...toggleableColumns,
|
||||||
|
[selectedColumn]: !toggleableColumns[selectedColumn],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.menuIcon}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="more"
|
||||||
|
aria-controls="long-menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleOpen}
|
||||||
|
>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
id="long-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
keepMounted
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
classes={{
|
||||||
|
paper: classes.menu,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{TopBarComponent && <TopBarComponent />}
|
||||||
|
{toggleableColumns ? (
|
||||||
|
<div>
|
||||||
|
<Typography className={classes.title}>
|
||||||
|
{translate('ra.toggleFieldsMenu.columnsToDisplay')}
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.columns}>
|
||||||
|
{Object.entries(toggleableColumns).map(([key, val]) =>
|
||||||
|
!omittedColumns.includes(key) ? (
|
||||||
|
<MenuItem key={key} onClick={() => handleClick(key)}>
|
||||||
|
<Checkbox checked={val} />
|
||||||
|
{translate(`resources.${resource}.fields.${key}`)}
|
||||||
|
</MenuItem>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToggleFieldsMenu
|
||||||
|
|
||||||
|
ToggleFieldsMenu.propTypes = {
|
||||||
|
resource: PropTypes.string.isRequired,
|
||||||
|
topbarComponent: PropTypes.elementType,
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { setOmittedFields, setToggleableFields } from '../actions'
|
||||||
|
|
||||||
|
const useSelectedFields = ({
|
||||||
|
resource,
|
||||||
|
columns,
|
||||||
|
omittedColumns = [],
|
||||||
|
defaultOff = [],
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const resourceFields = useSelector(
|
||||||
|
(state) => state.settings.toggleableFields
|
||||||
|
)?.[resource]
|
||||||
|
const omittedFields = useSelector((state) => state.settings.omittedFields)?.[
|
||||||
|
resource
|
||||||
|
]
|
||||||
|
|
||||||
|
const [filteredComponents, setFilteredComponents] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!resourceFields ||
|
||||||
|
Object.keys(resourceFields).length !== Object.keys(columns).length
|
||||||
|
) {
|
||||||
|
const obj = {}
|
||||||
|
for (const key of Object.keys(columns)) {
|
||||||
|
obj[key] = !defaultOff.includes(key)
|
||||||
|
}
|
||||||
|
dispatch(setToggleableFields({ [resource]: obj }))
|
||||||
|
}
|
||||||
|
if (!omittedFields) {
|
||||||
|
dispatch(setOmittedFields({ [resource]: omittedColumns }))
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
columns,
|
||||||
|
defaultOff,
|
||||||
|
dispatch,
|
||||||
|
omittedColumns,
|
||||||
|
omittedFields,
|
||||||
|
resource,
|
||||||
|
resourceFields,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resourceFields) {
|
||||||
|
const filtered = []
|
||||||
|
const omitted = omittedColumns
|
||||||
|
for (const [key, val] of Object.entries(columns)) {
|
||||||
|
if (!val) omitted.push(key)
|
||||||
|
else if (resourceFields[key]) filtered.push(val)
|
||||||
|
}
|
||||||
|
if (filteredComponents.length !== filtered.length)
|
||||||
|
setFilteredComponents(filtered)
|
||||||
|
if (omittedFields.length !== omitted.length)
|
||||||
|
dispatch(setOmittedFields({ [resource]: omitted }))
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
resourceFields,
|
||||||
|
columns,
|
||||||
|
dispatch,
|
||||||
|
omittedColumns,
|
||||||
|
omittedFields,
|
||||||
|
resource,
|
||||||
|
filteredComponents.length,
|
||||||
|
])
|
||||||
|
|
||||||
|
return React.Children.toArray(filteredComponents)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSelectedFields
|
||||||
|
|
||||||
|
useSelectedFields.propTypes = {
|
||||||
|
resource: PropTypes.string,
|
||||||
|
columns: PropTypes.object,
|
||||||
|
omittedColumns: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
defaultOff: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
}
|
||||||
@@ -278,6 +278,12 @@
|
|||||||
"i18n_error": "Cannot load the translations for the specified language",
|
"i18n_error": "Cannot load the translations for the specified language",
|
||||||
"canceled": "Action cancelled",
|
"canceled": "Action cancelled",
|
||||||
"logged_out": "Your session has ended, please reconnect."
|
"logged_out": "Your session has ended, please reconnect."
|
||||||
|
},
|
||||||
|
"toggleFieldsMenu": {
|
||||||
|
"columnsToDisplay": "Columns To Display",
|
||||||
|
"layout": "Layout",
|
||||||
|
"grid": "Grid",
|
||||||
|
"table": "Table"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
|
|||||||
@@ -19,15 +19,22 @@ import { M3U_MIME_TYPE, REST_URL } from '../consts'
|
|||||||
import subsonic from '../subsonic'
|
import subsonic from '../subsonic'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { formatBytes } from '../utils'
|
import { formatBytes } from '../utils'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery, makeStyles } from '@material-ui/core'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
toolbar: { display: 'flex', justifyContent: 'space-between', width: '100%' },
|
||||||
|
})
|
||||||
|
|
||||||
const PlaylistActions = ({ className, ids, data, record, ...rest }) => {
|
const PlaylistActions = ({ className, ids, data, record, ...rest }) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const translate = useTranslate()
|
const translate = useTranslate()
|
||||||
|
const classes = useStyles()
|
||||||
const dataProvider = useDataProvider()
|
const dataProvider = useDataProvider()
|
||||||
const notify = useNotify()
|
const notify = useNotify()
|
||||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||||
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
|
|
||||||
const getAllSongsAndDispatch = React.useCallback(
|
const getAllSongsAndDispatch = React.useCallback(
|
||||||
(action) => {
|
(action) => {
|
||||||
@@ -94,6 +101,8 @@ const PlaylistActions = ({ className, ids, data, record, ...rest }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
<div className={classes.toolbar}>
|
||||||
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
label={translate('resources.album.actions.playAll')}
|
label={translate('resources.album.actions.playAll')}
|
||||||
@@ -135,6 +144,9 @@ const PlaylistActions = ({ className, ids, data, record, ...rest }) => {
|
|||||||
>
|
>
|
||||||
<QueueMusicIcon />
|
<QueueMusicIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>{isNotSmall && <ToggleFieldsMenu resource="playlistTrack" />}</div>
|
||||||
|
</div>
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
DateField,
|
DateField,
|
||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
useNotify,
|
useNotify,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import Switch from '@material-ui/core/Switch'
|
import Switch from '@material-ui/core/Switch'
|
||||||
import { DurationField, List, Writable, isWritable } from '../common'
|
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
|
import { DurationField, List, Writable, isWritable } from '../common'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
|
import PlaylistListActions from './PlaylistListActions'
|
||||||
|
|
||||||
const PlaylistFilter = (props) => (
|
const PlaylistFilter = (props) => (
|
||||||
<Filter {...props} variant={'outlined'}>
|
<Filter {...props} variant={'outlined'}>
|
||||||
@@ -60,24 +62,42 @@ const PlaylistList = ({ permissions, ...props }) => {
|
|||||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||||
|
|
||||||
return (
|
const toggleableFields = useMemo(() => {
|
||||||
<List {...props} exporter={false} filters={<PlaylistFilter />}>
|
return {
|
||||||
<Datagrid
|
owner: <TextField source="owner" />,
|
||||||
rowClick="show"
|
songCount: isDesktop && <NumberField source="songCount" />,
|
||||||
isRowSelectable={(r) => isWritable(r && r.owner)}
|
duration: isDesktop && <DurationField source="duration" />,
|
||||||
>
|
updatedAt: isDesktop && (
|
||||||
<TextField source="name" />
|
<DateField source="updatedAt" sortByOrder={'DESC'} />
|
||||||
<TextField source="owner" />
|
),
|
||||||
{isDesktop && <NumberField source="songCount" />}
|
public: !isXsmall && (
|
||||||
{isDesktop && <DurationField source="duration" />}
|
|
||||||
{isDesktop && <DateField source="updatedAt" sortByOrder={'DESC'} />}
|
|
||||||
{!isXsmall && (
|
|
||||||
<TogglePublicInput
|
<TogglePublicInput
|
||||||
source="public"
|
source="public"
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
sortByOrder={'DESC'}
|
sortByOrder={'DESC'}
|
||||||
/>
|
/>
|
||||||
)}
|
),
|
||||||
|
}
|
||||||
|
}, [isDesktop, isXsmall, permissions])
|
||||||
|
|
||||||
|
const columns = useSelectedFields({
|
||||||
|
resource: 'playlist',
|
||||||
|
columns: toggleableFields,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
{...props}
|
||||||
|
exporter={false}
|
||||||
|
filters={<PlaylistFilter />}
|
||||||
|
actions={<PlaylistListActions />}
|
||||||
|
>
|
||||||
|
<Datagrid
|
||||||
|
rowClick="show"
|
||||||
|
isRowSelectable={(r) => isWritable(r && r.owner)}
|
||||||
|
>
|
||||||
|
<TextField source="name" />
|
||||||
|
{columns}
|
||||||
<Writable>
|
<Writable>
|
||||||
<EditButton />
|
<EditButton />
|
||||||
</Writable>
|
</Writable>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
sanitizeListRestProps,
|
||||||
|
TopToolbar,
|
||||||
|
CreateButton,
|
||||||
|
useTranslate,
|
||||||
|
} from 'react-admin'
|
||||||
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
|
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||||
|
|
||||||
|
const PlaylistListActions = ({ className, ...rest }) => {
|
||||||
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
|
const translate = useTranslate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
<CreateButton basePath="/playlist">
|
||||||
|
{translate('ra.action.create')}
|
||||||
|
</CreateButton>
|
||||||
|
{isNotSmall && <ToggleFieldsMenu resource="playlist" />}
|
||||||
|
</TopToolbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlaylistListActions
|
||||||
@@ -13,7 +13,9 @@ import PlaylistActions from './PlaylistActions'
|
|||||||
import { Title, isReadOnly } from '../common'
|
import { Title, isReadOnly } from '../common'
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
(theme) => ({
|
(theme) => ({
|
||||||
playlistActions: {},
|
playlistActions: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'NDPlaylistShow',
|
name: 'NDPlaylistShow',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
BulkActionsToolbar,
|
BulkActionsToolbar,
|
||||||
ListToolbar,
|
ListToolbar,
|
||||||
@@ -28,6 +28,7 @@ import { AlbumLinkField } from '../song/AlbumLinkField'
|
|||||||
import { playTracks } from '../actions'
|
import { playTracks } from '../actions'
|
||||||
import PlaylistSongBulkActions from './PlaylistSongBulkActions'
|
import PlaylistSongBulkActions from './PlaylistSongBulkActions'
|
||||||
import { QualityInfo } from '../common/QualityInfo'
|
import { QualityInfo } from '../common/QualityInfo'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
(theme) => ({
|
(theme) => ({
|
||||||
@@ -127,6 +128,26 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
|
|||||||
[playlistId, reorder, ids]
|
[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 && <TextField source="artist" />,
|
||||||
|
duration: (
|
||||||
|
<DurationField source="duration" className={classes.draggable} />
|
||||||
|
),
|
||||||
|
quality: isDesktop && <QualityInfo source="quality" sortable={false} />,
|
||||||
|
bpm: isDesktop && <NumberField source="bpm" />,
|
||||||
|
}
|
||||||
|
}, [isDesktop, classes.draggable])
|
||||||
|
|
||||||
|
const columns = useSelectedFields({
|
||||||
|
resource: 'playlistTrack',
|
||||||
|
columns: toggleableFields,
|
||||||
|
defaultOff: ['bpm'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
@@ -161,13 +182,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
|
|||||||
contextAlwaysVisible={!isDesktop}
|
contextAlwaysVisible={!isDesktop}
|
||||||
classes={{ row: classes.row }}
|
classes={{ row: classes.row }}
|
||||||
>
|
>
|
||||||
{isDesktop && <TextField source="id" label={'#'} />}
|
{columns}
|
||||||
<SongTitleField source="title" showTrackNumbers={false} />
|
|
||||||
{isDesktop && <AlbumLinkField source="album" />}
|
|
||||||
{isDesktop && <TextField source="artist" />}
|
|
||||||
<DurationField source="duration" className={classes.draggable} />
|
|
||||||
{isDesktop && <QualityInfo source="quality" sortable={false} />}
|
|
||||||
{isDesktop && <NumberField source="bpm" />}
|
|
||||||
<SongContextMenu
|
<SongContextMenu
|
||||||
onAddToPlaylist={onAddToPlaylist}
|
onAddToPlaylist={onAddToPlaylist}
|
||||||
showLove={false}
|
showLove={false}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { SET_NOTIFICATIONS_STATE } from '../actions'
|
import {
|
||||||
|
SET_NOTIFICATIONS_STATE,
|
||||||
|
SET_OMITTED_FIELDS,
|
||||||
|
SET_TOGGLEABLE_FIELDS,
|
||||||
|
} from '../actions'
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
notifications: false,
|
notifications: false,
|
||||||
|
toggleableFields: {},
|
||||||
|
omittedFields: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsReducer = (previousState = initialState, payload) => {
|
export const settingsReducer = (previousState = initialState, payload) => {
|
||||||
@@ -12,6 +18,22 @@ export const settingsReducer = (previousState = initialState, payload) => {
|
|||||||
...previousState,
|
...previousState,
|
||||||
notifications: data,
|
notifications: data,
|
||||||
}
|
}
|
||||||
|
case SET_TOGGLEABLE_FIELDS:
|
||||||
|
return {
|
||||||
|
...previousState,
|
||||||
|
toggleableFields: {
|
||||||
|
...previousState.toggleableFields,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case SET_OMITTED_FIELDS:
|
||||||
|
return {
|
||||||
|
...previousState,
|
||||||
|
omittedFields: {
|
||||||
|
...previousState.omittedFields,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return previousState
|
return previousState
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-32
@@ -28,6 +28,7 @@ import { AddToPlaylistDialog } from '../dialogs'
|
|||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
import useSelectedFields from '../common/useSelectedFields'
|
||||||
import { QualityInfo } from '../common/QualityInfo'
|
import { QualityInfo } from '../common/QualityInfo'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -77,6 +78,49 @@ const SongList = (props) => {
|
|||||||
dispatch(setTrack(record))
|
dispatch(setTrack(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleableFields = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
album: isDesktop && (
|
||||||
|
<AlbumLinkField
|
||||||
|
source="album"
|
||||||
|
sortBy={
|
||||||
|
'album, order_album_artist_name, disc_number, track_number, title'
|
||||||
|
}
|
||||||
|
sortByOrder={'ASC'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
artist: <TextField source="artist" />,
|
||||||
|
trackNumber: isDesktop && <NumberField source="trackNumber" />,
|
||||||
|
playCount: isDesktop && (
|
||||||
|
<NumberField source="playCount" sortByOrder={'DESC'} />
|
||||||
|
),
|
||||||
|
year: isDesktop && (
|
||||||
|
<FunctionField
|
||||||
|
source="year"
|
||||||
|
render={(r) => r.year || ''}
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
quality: isDesktop && <QualityInfo source="quality" sortable={false} />,
|
||||||
|
duration: <DurationField source="duration" />,
|
||||||
|
rating: config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source="rating"
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
resource={'song'}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
bpm: isDesktop && <NumberField source="bpm" />,
|
||||||
|
}
|
||||||
|
}, [isDesktop, classes.ratingField])
|
||||||
|
|
||||||
|
const columns = useSelectedFields({
|
||||||
|
resource: 'song',
|
||||||
|
columns: toggleableFields,
|
||||||
|
defaultOff: ['bpm'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List
|
<List
|
||||||
@@ -98,38 +142,7 @@ const SongList = (props) => {
|
|||||||
classes={{ row: classes.row }}
|
classes={{ row: classes.row }}
|
||||||
>
|
>
|
||||||
<SongTitleField source="title" showTrackNumbers={false} />
|
<SongTitleField source="title" showTrackNumbers={false} />
|
||||||
{isDesktop && (
|
{columns}
|
||||||
<AlbumLinkField
|
|
||||||
source="album"
|
|
||||||
sortBy={
|
|
||||||
'album, order_album_artist_name, disc_number, track_number, title'
|
|
||||||
}
|
|
||||||
sortByOrder={'ASC'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextField source="artist" />
|
|
||||||
{isDesktop && <NumberField source="trackNumber" />}
|
|
||||||
{isDesktop && (
|
|
||||||
<NumberField source="playCount" sortByOrder={'DESC'} />
|
|
||||||
)}
|
|
||||||
{isDesktop && (
|
|
||||||
<FunctionField
|
|
||||||
source="year"
|
|
||||||
render={(r) => r.year || ''}
|
|
||||||
sortByOrder={'DESC'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isDesktop && <QualityInfo source="quality" sortable={false} />}
|
|
||||||
<DurationField source="duration" />
|
|
||||||
{isDesktop && <NumberField source="bpm" />}
|
|
||||||
{config.enableStarRating && (
|
|
||||||
<RatingField
|
|
||||||
source="rating"
|
|
||||||
sortByOrder={'DESC'}
|
|
||||||
resource={'song'}
|
|
||||||
className={classes.ratingField}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SongContextMenu
|
<SongContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortBy={'starred ASC, starredAt ASC'}
|
sortBy={'starred ASC, starredAt ASC'}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { cloneElement } from 'react'
|
import React, { cloneElement } from 'react'
|
||||||
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||||
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
import { ShuffleAllButton } from '../common'
|
import { ShuffleAllButton } from '../common'
|
||||||
|
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||||
|
|
||||||
export const SongListActions = ({
|
export const SongListActions = ({
|
||||||
currentSort,
|
currentSort,
|
||||||
@@ -20,6 +22,7 @@ export const SongListActions = ({
|
|||||||
ids,
|
ids,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
{filters &&
|
{filters &&
|
||||||
@@ -31,6 +34,7 @@ export const SongListActions = ({
|
|||||||
context: 'button',
|
context: 'button',
|
||||||
})}
|
})}
|
||||||
<ShuffleAllButton filters={filterValues} />
|
<ShuffleAllButton filters={filterValues} />
|
||||||
|
{isNotSmall && <ToggleFieldsMenu resource="song" />}
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const musicListActions = {
|
|||||||
backgroundColor: 'inherit !important',
|
backgroundColor: 'inherit !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'button:first-child': {
|
'button:first-child:not(:only-child)': {
|
||||||
'@media screen and (max-width: 720px)': {
|
'@media screen and (max-width: 720px)': {
|
||||||
transform: 'scale(1.5)',
|
transform: 'scale(1.5)',
|
||||||
margin: '1rem',
|
margin: '1rem',
|
||||||
@@ -40,6 +40,9 @@ const musicListActions = {
|
|||||||
boxShadow: '0px 0px 4px 0px #5656567d',
|
boxShadow: '0px 0px 4px 0px #5656567d',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'button:only-child': {
|
||||||
|
margin: '1.5rem',
|
||||||
|
},
|
||||||
'button:first-child>span:first-child': {
|
'button:first-child>span:first-child': {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
color: bLight['300'],
|
color: bLight['300'],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const musicListActions = {
|
|||||||
backgroundColor: 'inherit !important',
|
backgroundColor: 'inherit !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'button:first-child': {
|
'button:first-child:not(:only-child)': {
|
||||||
'@media screen and (max-width: 720px)': {
|
'@media screen and (max-width: 720px)': {
|
||||||
transform: 'scale(1.5)',
|
transform: 'scale(1.5)',
|
||||||
margin: '1rem',
|
margin: '1rem',
|
||||||
@@ -42,6 +42,9 @@ const musicListActions = {
|
|||||||
border: 0,
|
border: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'button:only-child': {
|
||||||
|
margin: '1.5rem',
|
||||||
|
},
|
||||||
'button:first-child>span:first-child': {
|
'button:first-child>span:first-child': {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user