Skip to content

Commit

Permalink
Move Playlists to the sidebar menu (navidrome#1339)
Browse files Browse the repository at this point in the history
* Show playlists in sidebar menu

* Fix menu

* Refresh playlist submenu when adding new playlist

* Group shared playlists below user's playlists

* Fix text overflow in menu options

* Add button in playlist menu to go to Playlists list

* Add config option `DevSidebarPlaylists` to enable this feature (default false)
  • Loading branch information
deluan authored Sep 11, 2021
1 parent a7017e4 commit 79363d6
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 17 deletions.
2 changes: 2 additions & 0 deletions conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type configOptions struct {
DevFastAccessCoverArt bool
DevActivityPanel bool
DevEnableShare bool
DevSidebarPlaylists bool
DevEnableBufferedScrobble bool
}

Expand Down Expand Up @@ -234,6 +235,7 @@ func init() {
viper.SetDefault("devactivitypanel", true)
viper.SetDefault("devenableshare", false)
viper.SetDefault("devenablebufferedscrobble", true)
viper.SetDefault("devsidebarplaylists", false)
}

func InitConfig(cfgFile string) {
Expand Down
1 change: 1 addition & 0 deletions server/serve_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func serveIndex(ds model.DataStore, fs fs.FS) http.HandlerFunc {
"devFastAccessCoverArt": conf.Server.DevFastAccessCoverArt,
"enableUserEditing": conf.Server.EnableUserEditing,
"devEnableShare": conf.Server.DevEnableShare,
"devSidebarPlaylists": conf.Server.DevSidebarPlaylists,
"lastFMEnabled": conf.Server.LastFM.Enabled,
"lastFMApiKey": conf.Server.LastFM.ApiKey,
}
Expand Down
12 changes: 12 additions & 0 deletions server/serve_index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,18 @@ var _ = Describe("serveIndex", func() {
Expect(config).To(HaveKeyWithValue("devEnableShare", false))
})

It("sets the devSidebarPlaylists", func() {
conf.Server.DevSidebarPlaylists = true

r := httptest.NewRequest("GET", "/index.html", nil)
w := httptest.NewRecorder()

serveIndex(ds, fs)(w, r)

config := extractAppConfig(w.Body.String())
Expect(config).To(HaveKeyWithValue("devSidebarPlaylists", true))
})

It("sets the lastFMEnabled", func() {
r := httptest.NewRequest("GET", "/index.html", nil)
w := httptest.NewRecorder()
Expand Down
6 changes: 5 additions & 1 deletion ui/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ const Admin = (props) => {
<Resource name="album" {...album} options={{ subMenu: 'albumList' }} />,
<Resource name="artist" {...artist} />,
<Resource name="song" {...song} />,
<Resource name="playlist" {...playlist} />,
<Resource
name="playlist"
{...playlist}
options={{ subMenu: 'playlist' }}
/>,
<Resource name="user" {...user} options={{ subMenu: 'settings' }} />,
<Resource
name="player"
Expand Down
1 change: 1 addition & 0 deletions ui/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const defaultConfig = {
defaultTheme: 'Dark',
enableUserEditing: true,
devEnableShare: true,
devSidebarPlaylists: true,
lastFMEnabled: true,
lastFMApiKey: '9b94a5515ea66b2da3ec03c12300327e',
enableCoverAnimation: true,
Expand Down
9 changes: 8 additions & 1 deletion ui/src/dialogs/AddToPlaylistDialog.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useDataProvider, useNotify, useTranslate } from 'react-admin'
import {
useDataProvider,
useNotify,
useRefresh,
useTranslate,
} from 'react-admin'
import {
Button,
Dialog,
Expand All @@ -22,6 +27,7 @@ export const AddToPlaylistDialog = () => {
const dispatch = useDispatch()
const translate = useTranslate()
const notify = useNotify()
const refresh = useRefresh()
const [value, setValue] = useState({})
const [check, setCheck] = useState(false)
const dataProvider = useDataProvider()
Expand All @@ -47,6 +53,7 @@ export const AddToPlaylistDialog = () => {
const len = trackIds.length
notify('message.songsAddedToPlaylist', 'info', { smart_count: len })
onSuccess && onSuccess(value, len)
refresh()
})
.catch(() => {
notify('ra.page.error', 'warning')
Expand Down
4 changes: 3 additions & 1 deletion ui/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@
}
},
"albumList": "Albums",
"playlists": "Playlists",
"sharedPlaylists": "Shared Playlists",
"about": "About"
},
"player": {
Expand Down Expand Up @@ -380,4 +382,4 @@
"toggle_love": "Add this track to favourites"
}
}
}
}
21 changes: 18 additions & 3 deletions ui/src/layout/Menu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import { makeStyles } from '@material-ui/core'
import { Divider, makeStyles } from '@material-ui/core'
import clsx from 'clsx'
import { useTranslate, MenuItemLink, getResources } from 'react-admin'
import { withRouter } from 'react-router-dom'
Expand All @@ -10,6 +10,8 @@ import SubMenu from './SubMenu'
import inflection from 'inflection'
import albumLists from '../album/albumLists'
import { HelpDialog } from '../dialogs'
import PlaylistsSubMenu from './PlaylistsSubMenu'
import config from '../config'

const useStyles = makeStyles((theme) => ({
root: {
Expand Down Expand Up @@ -53,8 +55,8 @@ const Menu = ({ dense = false }) => {
// TODO State is not persisted in mobile when you close the sidebar menu. Move to redux?
const [state, setState] = useState({
menuAlbumList: true,
menuLibrary: true,
menuSettings: false,
menuPlaylists: true,
menuSharedPlaylists: true,
})

const handleToggle = (menu) => {
Expand Down Expand Up @@ -122,6 +124,19 @@ const Menu = ({ dense = false }) => {
)}
</SubMenu>
{resources.filter(subItems(undefined)).map(renderResourceMenuItemLink)}
{config.devSidebarPlaylists && open ? (
<>
<Divider />
<PlaylistsSubMenu
state={state}
setState={setState}
sidebarIsOpen={open}
dense={dense}
/>
</>
) : (
resources.filter(subItems('playlist')).map(renderResourceMenuItemLink)
)}
<HelpDialog />
</div>
)
Expand Down
95 changes: 95 additions & 0 deletions ui/src/layout/PlaylistsSubMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useCallback } from 'react'
import { MenuItemLink, useQueryWithStore } from 'react-admin'
import { useHistory } from 'react-router-dom'
import QueueMusicIcon from '@material-ui/icons/QueueMusic'
import { Typography } from '@material-ui/core'
import QueueMusicOutlinedIcon from '@material-ui/icons/QueueMusicOutlined'
import { BiCog } from 'react-icons/all'
import SubMenu from './SubMenu'

const PlaylistsSubMenu = ({ state, setState, sidebarIsOpen, dense }) => {
const history = useHistory()
const { data, loaded } = useQueryWithStore({
type: 'getList',
resource: 'playlist',
payload: {
pagination: {
page: 0,
perPage: 0,
},
sort: { field: 'name' },
},
})

const handleToggle = (menu) => {
setState((state) => ({ ...state, [menu]: !state[menu] }))
}

const renderPlaylistMenuItemLink = (pls) => {
return (
<MenuItemLink
key={pls.id}
to={`/playlist/${pls.id}/show`}
primaryText={
<Typography variant="inherit" noWrap>
{pls.name}
</Typography>
}
sidebarIsOpen={sidebarIsOpen}
dense={false}
/>
)
}

const user = localStorage.getItem('username')
const myPlaylists = []
const sharedPlaylists = []

if (loaded) {
const allPlaylists = Object.keys(data).map((id) => data[id])

allPlaylists.forEach((pls) => {
if (user === pls.owner) {
myPlaylists.push(pls)
} else {
sharedPlaylists.push(pls)
}
})
}

const onPlaylistConfig = useCallback(
() => history.push('/playlist'),
[history]
)

return (
<>
<SubMenu
handleToggle={() => handleToggle('menuPlaylists')}
isOpen={state.menuPlaylists}
sidebarIsOpen={sidebarIsOpen}
name={'menu.playlists'}
icon={<QueueMusicIcon />}
dense={dense}
actionIcon={<BiCog />}
onAction={onPlaylistConfig}
>
{myPlaylists.map(renderPlaylistMenuItemLink)}
</SubMenu>
{sharedPlaylists?.length > 0 && (
<SubMenu
handleToggle={() => handleToggle('menuSharedPlaylists')}
isOpen={state.menuSharedPlaylists}
sidebarIsOpen={sidebarIsOpen}
name={'menu.sharedPlaylists'}
icon={<QueueMusicOutlinedIcon />}
dense={dense}
>
{sharedPlaylists.map(renderPlaylistMenuItemLink)}
</SubMenu>
)}
</>
)
}

export default PlaylistsSubMenu
61 changes: 51 additions & 10 deletions ui/src/layout/SubMenu.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { Fragment } from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import ArrowRightOutlined from '@material-ui/icons/ArrowRightOutlined'
import List from '@material-ui/core/List'
import MenuItem from '@material-ui/core/MenuItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import Typography from '@material-ui/core/Typography'
import Divider from '@material-ui/core/Divider'
import Collapse from '@material-ui/core/Collapse'
import Tooltip from '@material-ui/core/Tooltip'
import { makeStyles } from '@material-ui/core/styles'
import { useTranslate } from 'react-admin'
import { IconButton, useMediaQuery } from '@material-ui/core'

const useStyles = makeStyles(
(theme) => ({
Expand All @@ -25,6 +26,18 @@ const useStyles = makeStyles(
paddingLeft: theme.spacing(2),
},
},
actionIcon: {
opacity: 0,
},
menuHeader: {
width: '100%',
},
headerWrapper: {
display: 'flex',
'&:hover $actionIcon': {
opacity: 1,
},
},
}),
{
name: 'NDSubMenu',
Expand All @@ -39,19 +52,43 @@ const SubMenu = ({
icon,
children,
dense,
onAction,
actionIcon,
}) => {
const translate = useTranslate()
const classes = useStyles()
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('sm'))

const handleOnClick = (e) => {
e.stopPropagation()
onAction(e)
}

const header = (
<MenuItem dense={dense} button onClick={handleToggle}>
<ListItemIcon className={classes.icon}>
{isOpen ? <ExpandMore /> : icon}
</ListItemIcon>
<Typography variant="inherit" color="textSecondary">
{translate(name)}
</Typography>
</MenuItem>
<div className={classes.headerWrapper}>
<MenuItem
dense={dense}
button
className={classes.menuHeader}
onClick={handleToggle}
>
<ListItemIcon className={classes.icon}>
{isOpen ? <ExpandMore /> : icon}
</ListItemIcon>
<Typography variant="inherit" color="textSecondary">
{translate(name)}
</Typography>
{onAction && sidebarIsOpen && (
<IconButton
size={'small'}
className={isDesktop ? classes.actionIcon : null}
onClick={handleOnClick}
>
{actionIcon}
</IconButton>
)}
</MenuItem>
</div>
)

return (
Expand All @@ -74,10 +111,14 @@ const SubMenu = ({
>
{children}
</List>
<Divider />
</Collapse>
</Fragment>
)
}

SubMenu.defaultProps = {
action: null,
actionIcon: <ArrowRightOutlined fontSize={'small'} />,
}

export default SubMenu
16 changes: 15 additions & 1 deletion ui/src/playlist/PlaylistCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,31 @@ import {
BooleanInput,
required,
useTranslate,
useRefresh,
useNotify,
useRedirect,
} from 'react-admin'
import { Title } from '../common'

const PlaylistCreate = (props) => {
const { basePath } = props
const refresh = useRefresh()
const notify = useNotify()
const redirect = useRedirect()
const translate = useTranslate()
const resourceName = translate('resources.playlist.name', { smart_count: 1 })
const title = translate('ra.page.create', {
name: `${resourceName}`,
})

const onSuccess = () => {
notify('ra.notification.created', 'info', { smart_count: 1 })
redirect('list', basePath)
refresh()
}

return (
<Create title={<Title subTitle={title} />} {...props}>
<Create title={<Title subTitle={title} />} {...props} onSuccess={onSuccess}>
<SimpleForm redirect="list" variant={'outlined'}>
<TextInput source="name" validate={required()} />
<TextInput multiline source="comment" />
Expand Down

0 comments on commit 79363d6

Please sign in to comment.