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 { motion } from 'framer-motion';
import { useMemo } from 'react';
import { IconType } from 'react-icons';
import {
RiUserVoiceLine,
RiMenuFill,
RiFlag2Line,
RiFolder3Line,
RiPlayListLine,
RiAlbumLine,
@ -18,14 +19,20 @@ import {
RiPlayListFill,
RiSearchLine,
RiSearchFill,
RiPlayFill,
RiPlayLine,
RiSettings2Fill,
RiSettings2Line,
RiFlag2Line,
} from 'react-icons/ri';
import { NavLink } from 'react-router-dom';
import { generatePath, NavLink } from 'react-router-dom';
import styled from 'styled-components';
import { LibraryItem } from '/@/renderer/api/types';
import { DropdownMenu, ScrollArea } from '/@/renderer/components';
import { CollapsedSidebarItem } from '/@/renderer/features/sidebar/components/collapsed-sidebar-item';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
import { AppRoute } from '/@/renderer/router/routes';
import { useCommandPalette, useWindowSettings } from '/@/renderer/store';
import { SidebarItemType, useGeneralSettings, useWindowSettings } from '/@/renderer/store';
import { Platform } from '/@/renderer/types';
const SidebarContainer = styled(motion.div)<{ windowBarStyle: Platform }>`
@ -39,9 +46,68 @@ const SidebarContainer = styled(motion.div)<{ windowBarStyle: Platform }>`
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 = () => {
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 (
<SidebarContainer windowBarStyle={windowBarStyle}>
@ -62,68 +128,16 @@ export const CollapsedSidebar = () => {
<AppMenu />
</DropdownMenu.Dropdown>
</DropdownMenu>
<CollapsedSidebarItem
activeIcon={<RiSearchFill size="25" />}
icon={<RiSearchLine size="25" />}
label="Search"
onClick={open}
/>
<CollapsedSidebarItem
activeIcon={<RiHome6Fill size="25" />}
component={NavLink}
icon={<RiHome6Line size="25" />}
label="Home"
route={AppRoute.HOME}
to={AppRoute.HOME}
/>
<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}
/>
{sidebarItemsWithRoute.map((item) => (
<CollapsedSidebarItem
activeIcon={<item.activeIcon size="25" />}
component={NavLink}
icon={<item.icon size="25" />}
label={item.label}
route={item.route}
to={item.route}
/>
))}
</ScrollArea>
</SidebarContainer>
);

View File

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

View File

@ -1,46 +1,59 @@
import { MouseEvent } from 'react';
import { Stack, Accordion, Center, Group, Divider, Box } from '@mantine/core';
import { MouseEvent, useMemo } from 'react';
import { Box, Center, Divider, Group, Stack } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { AnimatePresence, motion } from 'framer-motion';
import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components';
import { IconType } from 'react-icons';
import {
RiAddFill,
RiAlbumFill,
RiAlbumLine,
RiArrowDownSLine,
RiDatabaseFill,
RiDatabaseLine,
RiDiscLine,
RiFlag2Line,
RiFlag2Fill,
RiFlagLine,
RiFolder3Fill,
RiFolder3Line,
RiHome6Fill,
RiHome6Line,
RiListUnordered,
RiMusic2Fill,
RiMusic2Line,
RiPlayLine,
RiSearchFill,
RiUserVoiceFill,
RiUserVoiceLine,
RiSearchLine,
RiPlayFill,
RiSettings2Line,
RiSettings2Fill,
RiPlayListLine,
RiPlayListFill,
} 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 { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { AppRoute } from '/@/renderer/router/routes';
import {
useSidebarStore,
useAppStoreActions,
useCurrentSong,
useCurrentServer,
useSetFullScreenPlayerStore,
useFullScreenPlayerStore,
} from '/@/renderer/store';
import { fadeIn } from '/@/renderer/styles';
SidebarItemType,
useGeneralSettings,
useWindowSettings,
} from '../../../store/settings.store';
import { LibraryItem, PlaylistListSort, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components';
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 { 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 { useWindowSettings } from '../../../store/settings.store';
const SidebarContainer = styled.div<{ windowBarStyle: Platform }>`
height: 100%;
@ -75,6 +88,49 @@ const SidebarImage = styled.img`
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 = () => {
const location = useLocation();
const sidebar = useSidebarStore();
@ -117,6 +173,24 @@ export const Sidebar = () => {
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 (
<SidebarContainer
ref={cq.ref}
@ -135,100 +209,21 @@ export const Sidebar = () => {
sx={{ maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%' }}
>
<Stack spacing={0}>
<SidebarItem
px="1rem"
py="0.5rem"
to={AppRoute.HOME}
>
<Group spacing="sm">
{location.pathname === AppRoute.HOME ? (
<RiHome6Fill size="1.3em" />
) : (
<RiHome6Line size="1.3em" />
)}
Home
</Group>
</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>
{sidebarItemsWithRoute.map((item) => (
<SidebarItem
key={`sidebar-${item.route}`}
to={item.route}
>
<Group spacing="sm">
{location.pathname === item.route ? (
<item.activeIcon size="1.1em" />
) : (
<item.icon size="1.1em" />
)}
{item.label}
</Group>
</SidebarItem>
))}
</Stack>
<Divider
mx="1rem"

View File

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

View File

@ -22,7 +22,7 @@
--sidebar-bg: rgb(0, 0, 0);
--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-handle-bg: #4d4d4d;
--sidebar-border: 2px rgba(18, 18, 18, 0.7) solid;