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:
@@ -14,8 +14,13 @@ import { RiPlayListAddFill, RiPlayList2Fill } from 'react-icons/ri'
|
||||
import { playNext, addTracks, playTracks, shuffleTracks } from '../actions'
|
||||
import subsonic from '../subsonic'
|
||||
import { formatBytes } from '../utils'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import { useMediaQuery, makeStyles } from '@material-ui/core'
|
||||
import config from '../config'
|
||||
import ToggleFieldsMenu from '../common/ToggleFieldsMenu'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
toolbar: { display: 'flex', justifyContent: 'space-between', width: '100%' },
|
||||
})
|
||||
|
||||
const AlbumActions = ({
|
||||
className,
|
||||
@@ -27,7 +32,9 @@ const AlbumActions = ({
|
||||
}) => {
|
||||
const dispatch = useDispatch()
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||
|
||||
const handlePlay = React.useCallback(() => {
|
||||
dispatch(playTracks(data, ids))
|
||||
@@ -51,41 +58,46 @@ const AlbumActions = ({
|
||||
|
||||
return (
|
||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||
<Button
|
||||
onClick={handlePlay}
|
||||
label={translate('resources.album.actions.playAll')}
|
||||
>
|
||||
<PlayArrowIcon />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShuffle}
|
||||
label={translate('resources.album.actions.shuffle')}
|
||||
>
|
||||
<ShuffleIcon />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePlayNext}
|
||||
label={translate('resources.album.actions.playNext')}
|
||||
>
|
||||
<RiPlayList2Fill />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePlayLater}
|
||||
label={translate('resources.album.actions.addToQueue')}
|
||||
>
|
||||
<RiPlayListAddFill />
|
||||
</Button>
|
||||
{config.enableDownloads && (
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
label={
|
||||
translate('resources.album.actions.download') +
|
||||
(isDesktop ? ` (${formatBytes(record.size)})` : '')
|
||||
}
|
||||
>
|
||||
<CloudDownloadOutlinedIcon />
|
||||
</Button>
|
||||
)}
|
||||
<div className={classes.toolbar}>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handlePlay}
|
||||
label={translate('resources.album.actions.playAll')}
|
||||
>
|
||||
<PlayArrowIcon />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShuffle}
|
||||
label={translate('resources.album.actions.shuffle')}
|
||||
>
|
||||
<ShuffleIcon />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePlayNext}
|
||||
label={translate('resources.album.actions.playNext')}
|
||||
>
|
||||
<RiPlayList2Fill />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePlayLater}
|
||||
label={translate('resources.album.actions.addToQueue')}
|
||||
>
|
||||
<RiPlayListAddFill />
|
||||
</Button>
|
||||
{config.enableDownloads && (
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
label={
|
||||
translate('resources.album.actions.download') +
|
||||
(isDesktop ? ` (${formatBytes(record.size)})` : '')
|
||||
}
|
||||
>
|
||||
<CloudDownloadOutlinedIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>{isNotSmall && <ToggleFieldsMenu resource="albumSong" />}</div>
|
||||
</div>
|
||||
</TopToolbar>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
Filter,
|
||||
NullableBooleanInput,
|
||||
NumberInput,
|
||||
Pagination,
|
||||
ReferenceInput,
|
||||
SearchInput,
|
||||
Pagination,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import FavoriteIcon from '@material-ui/icons/Favorite'
|
||||
@@ -20,6 +20,7 @@ import AlbumGridView from './AlbumGridView'
|
||||
import { AddToPlaylistDialog } from '../dialogs'
|
||||
import albumLists, { defaultAlbumList } from './albumLists'
|
||||
import config from '../config'
|
||||
import useSelectedFields from '../common/useSelectedFields'
|
||||
|
||||
const AlbumFilter = (props) => {
|
||||
const translate = useTranslate()
|
||||
@@ -70,6 +71,21 @@ const AlbumList = (props) => {
|
||||
.replace(/^\/album/, '')
|
||||
.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),
|
||||
// reload with correct filter/sort params
|
||||
if (!location.search) {
|
||||
|
||||
@@ -1,10 +1,71 @@
|
||||
import React, { cloneElement } from 'react'
|
||||
import { Button, sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||
import { ButtonGroup } from '@material-ui/core'
|
||||
import {
|
||||
Button,
|
||||
sanitizeListRestProps,
|
||||
TopToolbar,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import {
|
||||
ButtonGroup,
|
||||
useMediaQuery,
|
||||
Typography,
|
||||
makeStyles,
|
||||
} 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 { 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 = ({
|
||||
currentSort,
|
||||
@@ -24,9 +85,7 @@ const AlbumListActions = ({
|
||||
fullWidth,
|
||||
...rest
|
||||
}) => {
|
||||
const dispatch = useDispatch()
|
||||
const albumView = useSelector((state) => state.albumView)
|
||||
|
||||
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||
return (
|
||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||
{filters &&
|
||||
@@ -37,26 +96,11 @@ const AlbumListActions = ({
|
||||
filterValues,
|
||||
context: 'button',
|
||||
})}
|
||||
<ButtonGroup
|
||||
variant="text"
|
||||
color="primary"
|
||||
aria-label="text primary button group"
|
||||
>
|
||||
<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>
|
||||
{isNotSmall ? (
|
||||
<ToggleFieldsMenu resource="album" topbarComponent={AlbumViewToggler} />
|
||||
) : (
|
||||
<AlbumViewToggler showTitle={false} />
|
||||
)}
|
||||
</TopToolbar>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
RatingField,
|
||||
} from '../common'
|
||||
import config from '../config'
|
||||
import useSelectedFields from '../common/useSelectedFields'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
columnIcon: {
|
||||
@@ -109,6 +110,36 @@ const AlbumListView = ({
|
||||
const classes = useStyles()
|
||||
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
|
||||
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 ? (
|
||||
<SimpleList
|
||||
primaryText={(r) => r.name}
|
||||
@@ -147,19 +178,7 @@ const AlbumListView = ({
|
||||
{...rest}
|
||||
>
|
||||
<TextField source="name" />
|
||||
<ArtistLinkField source="artist" />
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
{columns}
|
||||
<AlbumContextMenu
|
||||
source={'starred'}
|
||||
sortBy={'starred ASC, starredAt ASC'}
|
||||
|
||||
@@ -12,7 +12,9 @@ import AlbumActions from './AlbumActions'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
albumActions: {},
|
||||
albumActions: {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'NDAlbumShow',
|
||||
|
||||
+43
-26
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
BulkActionsToolbar,
|
||||
ListToolbar,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '../common'
|
||||
import { AddToPlaylistDialog } from '../dialogs'
|
||||
import { QualityInfo } from '../common/QualityInfo'
|
||||
import useSelectedFields from '../common/useSelectedFields'
|
||||
import config from '../config'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
@@ -87,6 +88,46 @@ const AlbumSongs = (props) => {
|
||||
const classes = useStyles({ isDesktop })
|
||||
const dispatch = useDispatch()
|
||||
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 (
|
||||
<>
|
||||
<ListToolbar
|
||||
@@ -113,31 +154,7 @@ const AlbumSongs = (props) => {
|
||||
contextAlwaysVisible={!isDesktop}
|
||||
classes={{ row: classes.row }}
|
||||
>
|
||||
{isDesktop && (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
{columns}
|
||||
<SongContextMenu
|
||||
source={'starred'}
|
||||
sortable={false}
|
||||
|
||||
Reference in New Issue
Block a user