diff --git a/src/renderer/features/player/components/left-controls.tsx b/src/renderer/features/player/components/left-controls.tsx
index e54f8db0..e1b7f422 100644
--- a/src/renderer/features/player/components/left-controls.tsx
+++ b/src/renderer/features/player/components/left-controls.tsx
@@ -8,10 +8,10 @@ import { Button, Text } from '/@/renderer/components';
import { AppRoute } from '/@/renderer/router/routes';
import {
useAppStoreActions,
- useAppStore,
useCurrentSong,
useSetFullScreenPlayerStore,
useFullScreenPlayerStore,
+ useSidebarStore,
} from '/@/renderer/store';
import { fadeIn } from '/@/renderer/styles';
import { LibraryItem } from '/@/renderer/api/types';
@@ -45,6 +45,7 @@ const Image = styled(motion.div)`
width: 60px;
height: 60px;
background-color: var(--placeholder-bg);
+ cursor: pointer;
filter: drop-shadow(0 5px 6px rgb(0, 0, 0, 50%));
${fadeIn};
@@ -84,7 +85,9 @@ export const LeftControls = () => {
const { setSideBar } = useAppStoreActions();
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
- const hideImage = useAppStore((state) => state.sidebar.image);
+ // const hideImage = useAppStore((state) => state.sidebar.image);
+ const { image, collapsed } = useSidebarStore();
+ const hideImage = image && !collapsed;
const currentSong = useCurrentSong();
const title = currentSong?.name;
const artists = currentSong?.artists;
@@ -144,22 +147,23 @@ export const LeftControls = () => {
>
)}
-
-
+ {!collapsed && (
+
+ )}
)}
diff --git a/src/renderer/features/sidebar/components/action-bar.tsx b/src/renderer/features/sidebar/components/action-bar.tsx
index 097ef4e2..2cd7e03c 100644
--- a/src/renderer/features/sidebar/components/action-bar.tsx
+++ b/src/renderer/features/sidebar/components/action-bar.tsx
@@ -27,7 +27,7 @@ export const ActionBar = () => {
ref={cq.ref}
gutter="sm"
>
-
+
{
size="md"
/>
-
+
`
+ position: relative;
+ width: 100%;
+ padding: 0.9rem 0.3rem;
+ border-right: var(--sidebar-border);
+ cursor: ${(props) => (props.$disabled ? 'default' : 'pointer')};
+ opacity: ${(props) => props.$disabled && 0.6};
+
+ svg {
+ fill: ${(props) => (props.$active ? 'var(--primary-color)' : 'var(--sidebar-fg)')};
+ }
+
+ &:focus-visible {
+ background-color: var(--sidebar-bg-hover);
+ outline: none;
+ }
+
+ ${(props) =>
+ !props.$disabled &&
+ `
+ &:hover {
+ background-color: var(--sidebar-bg-hover);
+
+ div {
+ color: var(--main-fg) !important;
+ }
+
+ svg {
+ fill: var(--primary-color);
+ }
+ }
+ `}
+`;
+
+const TextWrapper = styled.div`
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-align: center;
+ text-overflow: ellipsis;
+`;
+
+const ActiveTabIndicator = styled(motion.div)`
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 3px;
+ width: 2px;
+ height: 80%;
+ margin-top: auto;
+ margin-bottom: auto;
+ background: var(--primary-color);
+`;
+
+interface CollapsedSidebarItemProps {
+ active?: boolean;
+ activeIcon: React.ReactNode;
+ disabled?: boolean;
+ icon: React.ReactNode;
+ label: string;
+}
+
+const _CollapsedSidebarItem = forwardRef(
+ ({ active, activeIcon, icon, label, disabled, ...props }: CollapsedSidebarItemProps, ref) => {
+ return (
+
+ {active && }
+ {active ? activeIcon : icon}
+
+
+ {label}
+
+
+
+ );
+ },
+);
+
+export const CollapsedSidebarItem = createPolymorphicComponent<'button', CollapsedSidebarItemProps>(
+ _CollapsedSidebarItem,
+);
diff --git a/src/renderer/features/sidebar/components/collapsed-sidebar.tsx b/src/renderer/features/sidebar/components/collapsed-sidebar.tsx
new file mode 100644
index 00000000..2f7d6462
--- /dev/null
+++ b/src/renderer/features/sidebar/components/collapsed-sidebar.tsx
@@ -0,0 +1,123 @@
+import { UnstyledButton } from '@mantine/core';
+import { motion } from 'framer-motion';
+import {
+ RiUserVoiceLine,
+ RiMenuFill,
+ RiFlag2Line,
+ RiFolder3Line,
+ RiPlayListLine,
+ RiAlbumLine,
+ RiHome6Line,
+ RiMusic2Line,
+ RiHome6Fill,
+ RiAlbumFill,
+ RiMusic2Fill,
+ RiUserVoiceFill,
+ RiFlag2Fill,
+ RiFolder3Fill,
+ RiPlayListFill,
+} from 'react-icons/ri';
+import { useLocation } from 'react-router';
+import { Link } from 'react-router-dom';
+import styled from 'styled-components';
+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 { useWindowSettings } from '/@/renderer/store';
+import { Platform } from '/@/renderer/types';
+
+const SidebarContainer = styled(motion.div)<{ windowBarStyle: Platform }>`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-height: ${(props) =>
+ props.windowBarStyle === Platform.WEB
+ ? 'calc(100vh - 149px)'
+ : 'calc(100vh - 119px)'}; // Playerbar (90px), titlebar (65px), windowbar (30px)
+ user-select: none;
+`;
+
+export const CollapsedSidebar = () => {
+ const location = useLocation();
+ const { windowBarStyle } = useWindowSettings();
+
+ return (
+
+
+
+
+ }
+ component={UnstyledButton}
+ icon={}
+ label="Menu"
+ />
+
+
+
+
+
+ }
+ component={Link}
+ icon={}
+ label="Home"
+ to={AppRoute.HOME}
+ />
+ }
+ component={Link}
+ icon={}
+ label="Albums"
+ to={AppRoute.LIBRARY_ALBUMS}
+ />
+ }
+ component={Link}
+ icon={}
+ label="Tracks"
+ to={AppRoute.LIBRARY_SONGS}
+ />
+ }
+ component={Link}
+ icon={}
+ label="Artists"
+ to={AppRoute.LIBRARY_ALBUM_ARTISTS}
+ />
+ }
+ component={Link}
+ icon={}
+ label="Genres"
+ to={AppRoute.LIBRARY_GENRES}
+ />
+ }
+ component={Link}
+ icon={}
+ label="Folders"
+ to={AppRoute.LIBRARY_FOLDERS}
+ />
+ }
+ component={Link}
+ icon={}
+ label="Playlists"
+ to={AppRoute.PLAYLISTS}
+ />
+
+
+ );
+};
diff --git a/src/renderer/features/sidebar/components/sidebar.tsx b/src/renderer/features/sidebar/components/sidebar.tsx
index db6cf1a6..d3d1632a 100644
--- a/src/renderer/features/sidebar/components/sidebar.tsx
+++ b/src/renderer/features/sidebar/components/sidebar.tsx
@@ -13,8 +13,8 @@ import {
RiDiscLine,
RiFlag2Line,
RiFolder3Line,
- RiHome5Fill,
- RiHome5Line,
+ RiHome6Fill,
+ RiHome6Line,
RiListUnordered,
RiMusic2Fill,
RiMusic2Line,
@@ -54,6 +54,7 @@ const SidebarContainer = styled.div<{ windowBarStyle: Platform }>`
const ImageContainer = styled(motion.div)<{ height: string }>`
position: relative;
height: ${(props) => props.height};
+ cursor: pointer;
${fadeIn};
animation: fadein 0.2s ease-in-out;
@@ -141,9 +142,9 @@ export const Sidebar = () => {
>
{location.pathname === AppRoute.HOME ? (
-
+
) : (
-
+
)}
Home
@@ -305,7 +306,7 @@ export const Sidebar = () => {
opacity={0.8}
radius={100}
size="md"
- sx={{ position: 'absolute', right: 5, top: 5 }}
+ sx={{ cursor: 'default', position: 'absolute', right: 5, top: 5 }}
tooltip={{ label: 'Collapse', openDelay: 500 }}
variant="default"
onClick={(e) => {
diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx
index b908c4f5..30caa58d 100644
--- a/src/renderer/features/titlebar/components/app-menu.tsx
+++ b/src/renderer/features/titlebar/components/app-menu.tsx
@@ -3,10 +3,14 @@ import { openModal, closeAllModals } from '@mantine/modals';
import isElectron from 'is-electron';
import {
RiLockLine,
- RiServerFill,
- RiEdit2Fill,
- RiSettings3Fill,
RiWindowFill,
+ RiArrowLeftSLine,
+ RiArrowRightSLine,
+ RiLayoutRightLine,
+ RiLayoutLeftLine,
+ RiEdit2Line,
+ RiSettings3Line,
+ RiServerLine,
} from 'react-icons/ri';
import { useNavigate } from 'react-router';
import { Link } from 'react-router-dom';
@@ -14,7 +18,13 @@ import { DropdownMenu } from '/@/renderer/components';
import { ServerList } from '/@/renderer/features/servers';
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
import { AppRoute } from '/@/renderer/router/routes';
-import { useCurrentServer, useServerList, useAuthStoreActions } from '/@/renderer/store';
+import {
+ useCurrentServer,
+ useServerList,
+ useAuthStoreActions,
+ useSidebarStore,
+ useAppStoreActions,
+} from '/@/renderer/store';
import { ServerListItem, ServerType } from '/@/renderer/types';
const browser = isElectron() ? window.electron.browser : null;
@@ -24,6 +34,8 @@ export const AppMenu = () => {
const currentServer = useCurrentServer();
const serverList = useServerList();
const { setCurrentServer } = useAuthStoreActions();
+ const { collapsed } = useSidebarStore();
+ const { setSideBar } = useAppStoreActions();
const handleSetCurrentServer = (server: ServerListItem) => {
navigate(AppRoute.HOME);
@@ -55,6 +67,14 @@ export const AppMenu = () => {
browser?.devtools();
};
+ const handleCollapseSidebar = () => {
+ setSideBar({ collapsed: true });
+ };
+
+ const handleExpandSidebar = () => {
+ setSideBar({ collapsed: false });
+ };
+
const showBrowserDevToolsButton = isElectron();
return (
@@ -70,7 +90,7 @@ export const AppMenu = () => {
: }
+ icon={isSessionExpired ? : }
onClick={() => {
if (!isSessionExpired) return handleSetCurrentServer(server);
return handleCredentialsModal(server);
@@ -82,18 +102,46 @@ export const AppMenu = () => {
})}
}
+ icon={}
onClick={handleManageServersModal}
>
Manage servers
}
+ icon={}
to={AppRoute.SETTINGS}
>
Settings
+
+ }
+ onClick={() => navigate(-1)}
+ >
+ Go back
+
+ }
+ onClick={() => navigate(1)}
+ >
+ Go forward
+
+ {collapsed ? (
+ }
+ onClick={handleExpandSidebar}
+ >
+ Expand sidebar
+
+ ) : (
+ }
+ onClick={handleCollapseSidebar}
+ >
+ Collapse sidebar
+
+ )}
{showBrowserDevToolsButton && (
<>
diff --git a/src/renderer/layouts/default-layout/main-content.tsx b/src/renderer/layouts/default-layout/main-content.tsx
index 073b8723..8952bf89 100644
--- a/src/renderer/layouts/default-layout/main-content.tsx
+++ b/src/renderer/layouts/default-layout/main-content.tsx
@@ -8,25 +8,31 @@ import styled from 'styled-components';
import { DrawerPlayQueue, SidebarPlayQueue } from '/@/renderer/features/now-playing';
import { FullScreenPlayer } from '/@/renderer/features/player/components/full-screen-player';
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
+import { CollapsedSidebar } from '../../features/sidebar/components/collapsed-sidebar';
import { AppRoute } from '/@/renderer/router/routes';
import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store';
import { useWindowSettings, useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
+const MINIMUM_SIDEBAR_WIDTH = 260;
+
const MainContentContainer = styled.div<{
leftSidebarWidth: string;
rightExpanded?: boolean;
rightSidebarWidth?: string;
shell?: boolean;
+ sidebarCollapsed?: boolean;
}>`
position: relative;
display: ${(props) => (props.shell ? 'flex' : 'grid')};
grid-area: main-content;
grid-template-areas: 'sidebar . right-sidebar';
grid-template-rows: 1fr;
- grid-template-columns: ${(props) => props.leftSidebarWidth} 1fr ${(props) =>
- props.rightExpanded && props.rightSidebarWidth};
+ grid-template-columns: ${(props) => (props.sidebarCollapsed ? '80px' : props.leftSidebarWidth)} 1fr ${(
+ props,
+ ) => props.rightExpanded && props.rightSidebarWidth};
+
gap: 0;
background: var(--main-bg);
`;
@@ -56,14 +62,25 @@ const ResizeHandle = styled.div<{
bottom: ${(props) => props.placement === 'bottom' && 0};
left: ${(props) => props.placement === 'left' && 0};
z-index: 90;
- width: 2px;
+ width: 4px;
height: 100%;
- background-color: var(--sidebar-handle-bg);
cursor: ew-resize;
opacity: ${(props) => (props.isResizing ? 1 : 0)};
&:hover {
- opacity: 0.5;
+ opacity: 0.7;
+ }
+
+ &::before {
+ position: absolute;
+ top: ${(props) => props.placement === 'top' && 0};
+ right: ${(props) => props.placement === 'right' && 0};
+ bottom: ${(props) => props.placement === 'bottom' && 0};
+ left: ${(props) => props.placement === 'left' && 0};
+ width: 1px;
+ height: 100%;
+ background-color: var(--sidebar-handle-bg);
+ content: '';
}
`;
@@ -193,10 +210,15 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
const resize = useCallback(
(mouseMoveEvent: any) => {
if (isResizing) {
- const width = `${constrainSidebarWidth(mouseMoveEvent.clientX)}px`;
- setSideBar({ leftWidth: width });
- }
- if (isResizingRight) {
+ const width = mouseMoveEvent.clientX;
+ const constrainedWidth = `${constrainSidebarWidth(width)}px`;
+
+ if (width < MINIMUM_SIDEBAR_WIDTH - 100) {
+ setSideBar({ collapsed: true });
+ } else {
+ setSideBar({ collapsed: false, leftWidth: constrainedWidth });
+ }
+ } else if (isResizingRight) {
const start = Number(sidebar.rightWidth.split('px')[0]);
const { left } = rightSidebarRef!.current!.getBoundingClientRect();
const width = `${constrainRightSidebarWidth(start + left - mouseMoveEvent.clientX)}px`;
@@ -224,6 +246,7 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
rightExpanded={showSideQueue && sideQueueType === 'sideQueue'}
rightSidebarWidth={sidebar.rightWidth}
shell={shell}
+ sidebarCollapsed={sidebar.collapsed}
>
{!shell && (
<>
@@ -243,7 +266,7 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
startResizing('left');
}}
/>
-
+ {sidebar.collapsed ? : }
()(
isReorderingQueue: false,
platform: Platform.WINDOWS,
sidebar: {
+ collapsed: false,
expanded: [],
image: false,
leftWidth: '400px',
@@ -78,7 +74,7 @@ export const useAppStore = create()(
return merge(currentState, persistedState);
},
name: 'store_app',
- version: 1,
+ version: 2,
},
),
);
diff --git a/src/renderer/themes/default.scss b/src/renderer/themes/default.scss
index 18987894..4c479b6d 100644
--- a/src/renderer/themes/default.scss
+++ b/src/renderer/themes/default.scss
@@ -21,6 +21,7 @@
--titlebar-controls-bg: rgba(0, 0, 0, 0);
--sidebar-bg: rgb(0, 0, 0);
+ --sidebar-bg-hover: rgb(50, 50, 50);
--sidebar-fg: rgb(210, 210, 210);
--sidebar-fg-hover: rgb(255, 255, 255);
--sidebar-handle-bg: #4d4d4d;
diff --git a/src/renderer/themes/light.scss b/src/renderer/themes/light.scss
index f4f94d20..79009359 100644
--- a/src/renderer/themes/light.scss
+++ b/src/renderer/themes/light.scss
@@ -10,6 +10,7 @@ body[data-theme='defaultLight'] {
--titlebar-controls-bg: rgba(0, 0, 0, 0);
--sidebar-bg: rgb(240, 241, 242);
+ --sidebar-bg-hover: rgb(200, 200, 200);
--sidebar-fg: rgb(0, 0, 0);
--sidebar-fg-hover: rgb(85, 85, 85);
--sidebar-handle-bg: rgb(220, 220, 220);