Add configurable sidebar

This commit is contained in:
jeffvli 2023-06-03 05:36:38 -07:00
parent e7bc29a8f1
commit c8a0df4759
5 changed files with 207 additions and 188 deletions

View file

@ -1,9 +1,10 @@
import { UnstyledButton } from '@mantine/core'; import { UnstyledButton } from '@mantine/core';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useMemo } from 'react';
import { IconType } from 'react-icons';
import { import {
RiUserVoiceLine, RiUserVoiceLine,
RiMenuFill, RiMenuFill,
RiFlag2Line,
RiFolder3Line, RiFolder3Line,
RiPlayListLine, RiPlayListLine,
RiAlbumLine, RiAlbumLine,
@ -18,14 +19,20 @@ import {
RiPlayListFill, RiPlayListFill,
RiSearchLine, RiSearchLine,
RiSearchFill, RiSearchFill,
RiPlayFill,
RiPlayLine,
RiSettings2Fill,
RiSettings2Line,
RiFlag2Line,
} from 'react-icons/ri'; } from 'react-icons/ri';
import { NavLink } from 'react-router-dom'; import { generatePath, NavLink } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { LibraryItem } from '/@/renderer/api/types';
import { DropdownMenu, ScrollArea } from '/@/renderer/components'; import { DropdownMenu, ScrollArea } from '/@/renderer/components';
import { CollapsedSidebarItem } from '/@/renderer/features/sidebar/components/collapsed-sidebar-item'; import { CollapsedSidebarItem } from '/@/renderer/features/sidebar/components/collapsed-sidebar-item';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu'; import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCommandPalette, useWindowSettings } from '/@/renderer/store'; import { SidebarItemType, useGeneralSettings, useWindowSettings } from '/@/renderer/store';
import { Platform } from '/@/renderer/types'; import { Platform } from '/@/renderer/types';
const SidebarContainer = styled(motion.div)<{ windowBarStyle: Platform }>` const SidebarContainer = styled(motion.div)<{ windowBarStyle: Platform }>`
@ -39,9 +46,68 @@ const SidebarContainer = styled(motion.div)<{ windowBarStyle: Platform }>`
user-select: none; user-select: none;
`; `;
const sidebarItemMap = {
[AppRoute.HOME]: {
activeIcon: RiHome6Fill,
icon: RiHome6Line,
},
[AppRoute.LIBRARY_ALBUMS]: {
activeIcon: RiAlbumFill,
icon: RiAlbumLine,
},
[AppRoute.LIBRARY_ALBUM_ARTISTS]: {
activeIcon: RiUserVoiceFill,
icon: RiUserVoiceLine,
},
[AppRoute.PLAYLISTS]: {
activeIcon: RiPlayListFill,
icon: RiPlayListLine,
},
[AppRoute.LIBRARY_SONGS]: {
activeIcon: RiMusic2Fill,
icon: RiMusic2Line,
},
[AppRoute.LIBRARY_FOLDERS]: {
activeIcon: RiFolder3Fill,
icon: RiFolder3Line,
},
[AppRoute.LIBRARY_GENRES]: {
activeIcon: RiFlag2Fill,
icon: RiFlag2Line,
},
[generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG })]: {
activeIcon: RiSearchFill,
icon: RiSearchLine,
},
[AppRoute.SETTINGS]: {
activeIcon: RiSettings2Fill,
icon: RiSettings2Line,
},
[AppRoute.NOW_PLAYING]: {
activeIcon: RiPlayFill,
icon: RiPlayLine,
},
};
export const CollapsedSidebar = () => { export const CollapsedSidebar = () => {
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const { open } = useCommandPalette(); const { sidebarItems } = useGeneralSettings();
const sidebarItemsWithRoute: (SidebarItemType & {
activeIcon: IconType;
icon: IconType;
})[] = useMemo(() => {
if (!sidebarItems) return [];
const items = sidebarItems
.filter((item) => !item.disabled)
.map((item) => ({
...item,
...sidebarItemMap[item.route as keyof typeof sidebarItemMap],
}));
return items;
}, [sidebarItems]);
return ( return (
<SidebarContainer windowBarStyle={windowBarStyle}> <SidebarContainer windowBarStyle={windowBarStyle}>
@ -62,68 +128,16 @@ export const CollapsedSidebar = () => {
<AppMenu /> <AppMenu />
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
{sidebarItemsWithRoute.map((item) => (
<CollapsedSidebarItem <CollapsedSidebarItem
activeIcon={<RiSearchFill size="25" />} activeIcon={<item.activeIcon size="25" />}
icon={<RiSearchLine size="25" />}
label="Search"
onClick={open}
/>
<CollapsedSidebarItem
activeIcon={<RiHome6Fill size="25" />}
component={NavLink} component={NavLink}
icon={<RiHome6Line size="25" />} icon={<item.icon size="25" />}
label="Home" label={item.label}
route={AppRoute.HOME} route={item.route}
to={AppRoute.HOME} to={item.route}
/>
<CollapsedSidebarItem
activeIcon={<RiAlbumFill size="25" />}
component={NavLink}
icon={<RiAlbumLine size="25" />}
label="Albums"
route={AppRoute.LIBRARY_ALBUMS}
to={AppRoute.LIBRARY_ALBUMS}
/>
<CollapsedSidebarItem
activeIcon={<RiMusic2Fill size="25" />}
component={NavLink}
icon={<RiMusic2Line size="25" />}
label="Tracks"
route={AppRoute.LIBRARY_SONGS}
to={AppRoute.LIBRARY_SONGS}
/>
<CollapsedSidebarItem
activeIcon={<RiUserVoiceFill size="25" />}
component={NavLink}
icon={<RiUserVoiceLine size="25" />}
label="Artists"
route={AppRoute.LIBRARY_ALBUM_ARTISTS}
to={AppRoute.LIBRARY_ALBUM_ARTISTS}
/>
<CollapsedSidebarItem
disabled
activeIcon={<RiFlag2Fill size="25" />}
component={NavLink}
icon={<RiFlag2Line size="25" />}
label="Genres"
to={AppRoute.LIBRARY_GENRES}
/>
<CollapsedSidebarItem
disabled
activeIcon={<RiFolder3Fill size="25" />}
component={NavLink}
icon={<RiFolder3Line size="25" />}
label="Folders"
to={AppRoute.LIBRARY_FOLDERS}
/>
<CollapsedSidebarItem
activeIcon={<RiPlayListFill size="25" />}
component={NavLink}
icon={<RiPlayListLine size="25" />}
label="Playlists"
route={AppRoute.PLAYLISTS}
to={AppRoute.PLAYLISTS}
/> />
))}
</ScrollArea> </ScrollArea>
</SidebarContainer> </SidebarContainer>
); );

View file

@ -12,7 +12,7 @@ interface ListItemProps extends FlexProps {
const StyledItem = styled(Flex)` const StyledItem = styled(Flex)`
width: 100%; width: 100%;
font-weight: 600; font-weight: 700;
font-family: var(--content-font-family); font-family: var(--content-font-family);
&:focus-visible { &:focus-visible {

View file

@ -1,46 +1,59 @@
import { MouseEvent } from 'react'; import { MouseEvent, useMemo } from 'react';
import { Stack, Accordion, Center, Group, Divider, Box } from '@mantine/core'; import { Box, Center, Divider, Group, Stack } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components'; import { IconType } from 'react-icons';
import { import {
RiAddFill, RiAddFill,
RiAlbumFill, RiAlbumFill,
RiAlbumLine, RiAlbumLine,
RiArrowDownSLine, RiArrowDownSLine,
RiDatabaseFill,
RiDatabaseLine,
RiDiscLine, RiDiscLine,
RiFlag2Line, RiFlag2Fill,
RiFlagLine,
RiFolder3Fill,
RiFolder3Line, RiFolder3Line,
RiHome6Fill, RiHome6Fill,
RiHome6Line, RiHome6Line,
RiListUnordered, RiListUnordered,
RiMusic2Fill, RiMusic2Fill,
RiMusic2Line, RiMusic2Line,
RiPlayLine,
RiSearchFill,
RiUserVoiceFill, RiUserVoiceFill,
RiUserVoiceLine, RiUserVoiceLine,
RiSearchLine,
RiPlayFill,
RiSettings2Line,
RiSettings2Fill,
RiPlayListLine,
RiPlayListFill,
} from 'react-icons/ri'; } from 'react-icons/ri';
import { Link, useLocation } from 'react-router-dom'; import { generatePath, Link, useLocation } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { AppRoute } from '/@/renderer/router/routes';
import { import {
useSidebarStore, SidebarItemType,
useAppStoreActions, useGeneralSettings,
useCurrentSong, useWindowSettings,
useCurrentServer, } from '../../../store/settings.store';
useSetFullScreenPlayerStore, import { LibraryItem, PlaylistListSort, ServerType, SortOrder } from '/@/renderer/api/types';
useFullScreenPlayerStore, import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components';
} from '/@/renderer/store';
import { fadeIn } from '/@/renderer/styles';
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists'; import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
import { PlaylistListSort, ServerType, SortOrder } from '/@/renderer/api/types'; import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { SidebarPlaylistList } from '/@/renderer/features/sidebar/components/sidebar-playlist-list'; import { SidebarPlaylistList } from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar'; import { AppRoute } from '/@/renderer/router/routes';
import {
useAppStoreActions,
useCurrentServer,
useCurrentSong,
useFullScreenPlayerStore,
useSetFullScreenPlayerStore,
useSidebarStore,
} from '/@/renderer/store';
import { fadeIn } from '/@/renderer/styles';
import { Platform } from '/@/renderer/types'; import { Platform } from '/@/renderer/types';
import { useWindowSettings } from '../../../store/settings.store';
const SidebarContainer = styled.div<{ windowBarStyle: Platform }>` const SidebarContainer = styled.div<{ windowBarStyle: Platform }>`
height: 100%; height: 100%;
@ -75,6 +88,49 @@ const SidebarImage = styled.img`
background: var(--placeholder-bg); background: var(--placeholder-bg);
`; `;
const sidebarItemMap = {
[AppRoute.HOME]: {
activeIcon: RiHome6Fill,
icon: RiHome6Line,
},
[AppRoute.LIBRARY_ALBUMS]: {
activeIcon: RiAlbumFill,
icon: RiAlbumLine,
},
[AppRoute.LIBRARY_ALBUM_ARTISTS]: {
activeIcon: RiUserVoiceFill,
icon: RiUserVoiceLine,
},
[AppRoute.PLAYLISTS]: {
activeIcon: RiPlayListFill,
icon: RiPlayListLine,
},
[AppRoute.LIBRARY_SONGS]: {
activeIcon: RiMusic2Fill,
icon: RiMusic2Line,
},
[AppRoute.LIBRARY_FOLDERS]: {
activeIcon: RiFolder3Fill,
icon: RiFolder3Line,
},
[AppRoute.LIBRARY_GENRES]: {
activeIcon: RiFlag2Fill,
icon: RiFlagLine,
},
[generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG })]: {
activeIcon: RiSearchFill,
icon: RiSearchLine,
},
[AppRoute.SETTINGS]: {
activeIcon: RiSettings2Fill,
icon: RiSettings2Line,
},
[AppRoute.NOW_PLAYING]: {
activeIcon: RiPlayFill,
icon: RiPlayLine,
},
};
export const Sidebar = () => { export const Sidebar = () => {
const location = useLocation(); const location = useLocation();
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
@ -117,6 +173,24 @@ export const Sidebar = () => {
const cq = useContainerQuery({ sm: 300 }); const cq = useContainerQuery({ sm: 300 });
const { sidebarItems } = useGeneralSettings();
const sidebarItemsWithRoute: (SidebarItemType & {
activeIcon: IconType;
icon: IconType;
})[] = useMemo(() => {
if (!sidebarItems) return [];
const items = sidebarItems
.filter((item) => !item.disabled)
.map((item) => ({
...item,
...sidebarItemMap[item.route as keyof typeof sidebarItemMap],
}));
return items;
}, [sidebarItems]);
return ( return (
<SidebarContainer <SidebarContainer
ref={cq.ref} ref={cq.ref}
@ -135,100 +209,21 @@ export const Sidebar = () => {
sx={{ maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%' }} sx={{ maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%' }}
> >
<Stack spacing={0}> <Stack spacing={0}>
{sidebarItemsWithRoute.map((item) => (
<SidebarItem <SidebarItem
px="1rem" key={`sidebar-${item.route}`}
py="0.5rem" to={item.route}
to={AppRoute.HOME}
> >
<Group spacing="sm"> <Group spacing="sm">
{location.pathname === AppRoute.HOME ? ( {location.pathname === item.route ? (
<RiHome6Fill size="1.3em" /> <item.activeIcon size="1.1em" />
) : ( ) : (
<RiHome6Line size="1.3em" /> <item.icon size="1.1em" />
)} )}
Home {item.label}
</Group> </Group>
</SidebarItem> </SidebarItem>
<Accordion ))}
multiple
styles={{
content: { padding: '0 1rem' },
control: {
'&:hover': { background: 'none', color: 'var(--sidebar-fg-hover)' },
color: 'var(--sidebar-fg)',
transition: 'color 0.2s ease-in-out',
},
item: { borderBottom: 'none', color: 'var(--sidebar-fg)' },
itemTitle: { color: 'var(--sidebar-fg)' },
label: { fontWeight: 600 },
panel: { padding: '0 1rem' },
}}
value={sidebar.expanded}
onChange={(e) => setSideBar({ expanded: e })}
>
<Accordion.Item value="library">
<Accordion.Control>
<Group spacing="sm">
{location.pathname.includes('/library/') ? (
<RiDatabaseFill size="1.3em" />
) : (
<RiDatabaseLine size="1.3em" />
)}
Library
</Group>
</Accordion.Control>
<Accordion.Panel>
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
<Group spacing="sm">
{location.pathname === AppRoute.LIBRARY_ALBUMS ? (
<RiAlbumFill size="1.1em" />
) : (
<RiAlbumLine size="1.1em" />
)}
Albums
</Group>
</SidebarItem>
<SidebarItem to={AppRoute.LIBRARY_SONGS}>
<Group spacing="sm">
{location.pathname === AppRoute.LIBRARY_SONGS ? (
<RiMusic2Fill size="1.1em" />
) : (
<RiMusic2Line size="1.1em" />
)}
Tracks
</Group>
</SidebarItem>
<SidebarItem to={AppRoute.LIBRARY_ALBUM_ARTISTS}>
<Group spacing="sm">
{location.pathname === AppRoute.LIBRARY_ALBUM_ARTISTS ? (
<RiUserVoiceFill size="1.1em" />
) : (
<RiUserVoiceLine size="1.1em" />
)}
Album Artists
</Group>
</SidebarItem>
<SidebarItem
disabled
to={AppRoute.LIBRARY_FOLDERS}
>
<Group spacing="sm">
<RiFlag2Line size="1.1em" />
Genres
</Group>
</SidebarItem>
<SidebarItem
disabled
to={AppRoute.LIBRARY_FOLDERS}
>
<Group spacing="sm">
<RiFolder3Line size="1.1em" />
Folders
</Group>
</SidebarItem>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack> </Stack>
<Divider <Divider
mx="1rem" mx="1rem"

View file

@ -3,10 +3,12 @@
import { ColDef } from '@ag-grid-community/core'; import { ColDef } from '@ag-grid-community/core';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import { generatePath } from 'react-router';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { LibraryItem } from '/@/renderer/api/types';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { AppTheme } from '/@/renderer/themes/types'; import { AppTheme } from '/@/renderer/themes/types';
import { import {
@ -23,22 +25,30 @@ export type SidebarItemType = {
disabled: boolean; disabled: boolean;
id: string; id: string;
label: string; label: string;
route: AppRoute; route: AppRoute | string;
}; };
export const sidebarItems = [ export const sidebarItems = [
{ disabled: true, id: 'Now Playing', label: 'Now Playing', route: AppRoute.NOW_PLAYING },
{
disabled: true,
id: 'Search',
label: 'Search',
route: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
},
{ disabled: false, id: 'Home', label: 'Home', route: AppRoute.HOME }, { disabled: false, id: 'Home', label: 'Home', route: AppRoute.HOME },
{ disabled: false, id: 'Albums', label: 'Albums', route: AppRoute.LIBRARY_ALBUMS }, { disabled: false, id: 'Albums', label: 'Albums', route: AppRoute.LIBRARY_ALBUMS },
{ disabled: false, id: 'Tracks', label: 'Tracks', route: AppRoute.LIBRARY_SONGS }, { disabled: false, id: 'Tracks', label: 'Tracks', route: AppRoute.LIBRARY_SONGS },
{ {
disabled: false, disabled: false,
id: 'Album Artists', id: 'Artists',
label: 'Album Artists', label: 'Artists',
route: AppRoute.LIBRARY_ALBUM_ARTISTS, route: AppRoute.LIBRARY_ALBUM_ARTISTS,
}, },
{ disabled: false, id: 'Genres', label: 'Genres', route: AppRoute.LIBRARY_GENRES }, { disabled: false, id: 'Genres', label: 'Genres', route: AppRoute.LIBRARY_GENRES },
{ disabled: false, id: 'Folders', label: 'Folders', route: AppRoute.LIBRARY_FOLDERS }, { disabled: true, id: 'Folders', label: 'Folders', route: AppRoute.LIBRARY_FOLDERS },
{ disabled: false, id: 'Playlists', label: 'Playlists', route: AppRoute.PLAYLISTS }, { disabled: true, id: 'Playlists', label: 'Playlists', route: AppRoute.PLAYLISTS },
{ disabled: true, id: 'Settings', label: 'Settings', route: AppRoute.SETTINGS },
]; ];
export type PersistedTableColumn = { export type PersistedTableColumn = {
@ -381,7 +391,7 @@ export const useSettingsStore = create<SettingsSlice>()(
return merge(currentState, persistedState); return merge(currentState, persistedState);
}, },
name: 'store_settings', name: 'store_settings',
version: 5, version: 6,
}, },
), ),
); );

View file

@ -22,7 +22,7 @@
--sidebar-bg: rgb(0, 0, 0); --sidebar-bg: rgb(0, 0, 0);
--sidebar-bg-hover: rgb(50, 50, 50); --sidebar-bg-hover: rgb(50, 50, 50);
--sidebar-fg: rgb(210, 210, 210); --sidebar-fg: rgb(190, 190, 190);
--sidebar-fg-hover: rgb(255, 255, 255); --sidebar-fg-hover: rgb(255, 255, 255);
--sidebar-handle-bg: #4d4d4d; --sidebar-handle-bg: #4d4d4d;
--sidebar-border: 2px rgba(18, 18, 18, 0.7) solid; --sidebar-border: 2px rgba(18, 18, 18, 0.7) solid;