diff --git a/conf/configuration.go b/conf/configuration.go index 83b3dd39..79ce7da5 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -70,6 +70,7 @@ type configOptions struct { DevFastAccessCoverArt bool DevActivityPanel bool DevEnableShare bool + DevSidebarPlaylists bool DevEnableBufferedScrobble bool } @@ -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) { diff --git a/server/serve_index.go b/server/serve_index.go index 204633dc..e3fc15b2 100644 --- a/server/serve_index.go +++ b/server/serve_index.go @@ -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, } diff --git a/server/serve_index_test.go b/server/serve_index_test.go index 75ca0be3..72a81037 100644 --- a/server/serve_index_test.go +++ b/server/serve_index_test.go @@ -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() diff --git a/ui/src/App.js b/ui/src/App.js index e1a300bb..67d16359 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -91,7 +91,11 @@ const Admin = (props) => { , , , - , + , , { const dispatch = useDispatch() const translate = useTranslate() const notify = useNotify() + const refresh = useRefresh() const [value, setValue] = useState({}) const [check, setCheck] = useState(false) const dataProvider = useDataProvider() @@ -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') diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index e5148fa3..444b8206 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -325,6 +325,8 @@ } }, "albumList": "Albums", + "playlists": "Playlists", + "sharedPlaylists": "Shared Playlists", "about": "About" }, "player": { @@ -380,4 +382,4 @@ "toggle_love": "Add this track to favourites" } } -} \ No newline at end of file +} diff --git a/ui/src/layout/Menu.js b/ui/src/layout/Menu.js index 74515624..23adb8d4 100644 --- a/ui/src/layout/Menu.js +++ b/ui/src/layout/Menu.js @@ -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' @@ -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: { @@ -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) => { @@ -122,6 +124,19 @@ const Menu = ({ dense = false }) => { )} {resources.filter(subItems(undefined)).map(renderResourceMenuItemLink)} + {config.devSidebarPlaylists && open ? ( + <> + + + + ) : ( + resources.filter(subItems('playlist')).map(renderResourceMenuItemLink) + )} ) diff --git a/ui/src/layout/PlaylistsSubMenu.js b/ui/src/layout/PlaylistsSubMenu.js new file mode 100644 index 00000000..1b02c2e0 --- /dev/null +++ b/ui/src/layout/PlaylistsSubMenu.js @@ -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 ( + + {pls.name} + + } + 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 ( + <> + handleToggle('menuPlaylists')} + isOpen={state.menuPlaylists} + sidebarIsOpen={sidebarIsOpen} + name={'menu.playlists'} + icon={} + dense={dense} + actionIcon={} + onAction={onPlaylistConfig} + > + {myPlaylists.map(renderPlaylistMenuItemLink)} + + {sharedPlaylists?.length > 0 && ( + handleToggle('menuSharedPlaylists')} + isOpen={state.menuSharedPlaylists} + sidebarIsOpen={sidebarIsOpen} + name={'menu.sharedPlaylists'} + icon={} + dense={dense} + > + {sharedPlaylists.map(renderPlaylistMenuItemLink)} + + )} + + ) +} + +export default PlaylistsSubMenu diff --git a/ui/src/layout/SubMenu.js b/ui/src/layout/SubMenu.js index c62f0869..2d62cdc9 100644 --- a/ui/src/layout/SubMenu.js +++ b/ui/src/layout/SubMenu.js @@ -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) => ({ @@ -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', @@ -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 = ( - - - {isOpen ? : icon} - - - {translate(name)} - - +
+ + + {isOpen ? : icon} + + + {translate(name)} + + {onAction && sidebarIsOpen && ( + + {actionIcon} + + )} + +
) return ( @@ -74,10 +111,14 @@ const SubMenu = ({ > {children} - ) } +SubMenu.defaultProps = { + action: null, + actionIcon: , +} + export default SubMenu diff --git a/ui/src/playlist/PlaylistCreate.js b/ui/src/playlist/PlaylistCreate.js index f224669e..ef5e1acb 100644 --- a/ui/src/playlist/PlaylistCreate.js +++ b/ui/src/playlist/PlaylistCreate.js @@ -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 ( - } {...props}> + } {...props} onSuccess={onSuccess}>