Add dedicated OS window bars (#22)

This commit is contained in:
jeffvli 2023-03-28 23:59:51 -07:00
parent ececc394e2
commit 58c7370536
25 changed files with 823 additions and 462 deletions

View file

@ -5,6 +5,8 @@ import { useMergedRef, useTimeout } from '@mantine/hooks';
import { motion, useScroll } from 'framer-motion'; import { motion, useScroll } from 'framer-motion';
import styled from 'styled-components'; import styled from 'styled-components';
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header'; import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
interface ScrollAreaProps extends MantineScrollAreaProps { interface ScrollAreaProps extends MantineScrollAreaProps {
children: React.ReactNode; children: React.ReactNode;
@ -26,16 +28,18 @@ const StyledScrollArea = styled(MantineScrollArea)`
} }
`; `;
const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string }>` const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string; windowBarStyle?: Platform }>`
height: 100%; height: 100%;
overflow-y: overlay !important; overflow-y: overlay !important;
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
margin-top: ${(props) => props.scrollBarOffset || '65px'}; margin-top: ${(props) =>
props.windowBarStyle !== Platform.WEB ? '0px' : props.scrollBarOffset || '65px'};
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
margin-top: ${(props) => props.scrollBarOffset || '65px'}; margin-top: ${(props) =>
props.windowBarStyle !== Platform.WEB ? '0px' : props.scrollBarOffset || '65px'};
} }
`; `;
@ -74,6 +78,7 @@ export const NativeScrollArea = forwardRef(
}: NativeScrollAreaProps, }: NativeScrollAreaProps,
ref: Ref<HTMLDivElement>, ref: Ref<HTMLDivElement>,
) => { ) => {
const { windowBarStyle } = useGeneralSettings();
const [hideScrollbar, setHideScrollbar] = useState(false); const [hideScrollbar, setHideScrollbar] = useState(false);
const [hideHeader, setHideHeader] = useState(true); const [hideHeader, setHideHeader] = useState(true);
const { start, clear } = useTimeout( const { start, clear } = useTimeout(
@ -130,6 +135,7 @@ export const NativeScrollArea = forwardRef(
ref={mergedRef} ref={mergedRef}
className={hideScrollbar ? 'hide-scrollbar' : undefined} className={hideScrollbar ? 'hide-scrollbar' : undefined}
scrollBarOffset={scrollBarOffset} scrollBarOffset={scrollBarOffset}
windowBarStyle={windowBarStyle}
onMouseEnter={() => { onMouseEnter={() => {
setHideScrollbar(false); setHideScrollbar(false);
clear(); clear();

View file

@ -54,7 +54,7 @@ export const SearchInput = ({
padding: isOpened ? '10px' : 0, padding: isOpened ? '10px' : 0,
}, },
}} }}
width={isOpened ? openedWidth || 150 : initialWidth || 50} width={isOpened ? openedWidth || 150 : initialWidth || 35}
onChange={onChange} onChange={onChange}
onKeyDown={handleEscape} onKeyDown={handleEscape}
/> />

View file

@ -1,12 +1,15 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useInView } from 'framer-motion'; import { useInView } from 'framer-motion';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
export const useFixedTableHeader = () => { export const useFixedTableHeader = () => {
const intersectRef = useRef<HTMLDivElement | null>(null); const intersectRef = useRef<HTMLDivElement | null>(null);
const tableContainerRef = useRef<HTMLDivElement | null>(null); const tableContainerRef = useRef<HTMLDivElement | null>(null);
const { windowBarStyle } = useGeneralSettings();
const isNotPastTableIntersection = useInView(intersectRef, { const isNotPastTableIntersection = useInView(intersectRef, {
margin: '-68px 0px 0px 0px', margin: windowBarStyle === Platform.WEB ? '-68px 0px 0px 0px' : '-98px 0px 0px 0px',
}); });
const tableInView = useInView(tableContainerRef, { const tableInView = useInView(tableContainerRef, {
@ -18,13 +21,19 @@ export const useFixedTableHeader = () => {
const root = document.querySelector('main .ag-root'); const root = document.querySelector('main .ag-root');
if (isNotPastTableIntersection || !tableInView) { if (isNotPastTableIntersection || !tableInView) {
if (windowBarStyle !== Platform.WEB) {
header?.classList.remove('window-frame');
}
header?.classList.remove('ag-header-fixed'); header?.classList.remove('ag-header-fixed');
root?.classList.remove('ag-header-fixed-margin'); root?.classList.remove('ag-header-fixed-margin');
} else { } else {
if (windowBarStyle !== Platform.WEB) {
header?.classList.add('window-frame');
}
header?.classList.add('ag-header-fixed'); header?.classList.add('ag-header-fixed');
root?.classList.add('ag-header-fixed-margin'); root?.classList.add('ag-header-fixed-margin');
} }
}, [isNotPastTableIntersection, tableInView]); }, [isNotPastTableIntersection, tableInView, windowBarStyle]);
return { intersectRef, tableContainerRef }; return { intersectRef, tableContainerRef };
}; };

View file

@ -80,7 +80,7 @@ const LineItem = styled.div<{ $secondary?: boolean }>`
`; `;
export const LeftControls = () => { export const LeftControls = () => {
const { setSidebar } = useAppStoreActions(); const { setSideBar } = useAppStoreActions();
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore(); const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
const hideImage = useAppStore((state) => state.sidebar.image); const hideImage = useAppStore((state) => state.sidebar.image);
@ -102,7 +102,7 @@ export const LeftControls = () => {
const handleToggleSidebarImage = (e: MouseEvent<HTMLButtonElement>) => { const handleToggleSidebarImage = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
setSidebar({ image: true }); setSideBar({ image: true });
}; };
return ( return (

View file

@ -28,7 +28,7 @@ export const RightControls = () => {
const muted = useMuted(); const muted = useMuted();
const server = useCurrentServer(); const server = useCurrentServer();
const currentSong = useCurrentSong(); const currentSong = useCurrentSong();
const { setSidebar } = useAppStoreActions(); const { setSideBar } = useAppStoreActions();
const { rightExpanded: isQueueExpanded } = useSidebarStore(); const { rightExpanded: isQueueExpanded } = useSidebarStore();
const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls(); const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls();
@ -145,7 +145,7 @@ export const RightControls = () => {
icon={<HiOutlineQueueList size="1.1rem" />} icon={<HiOutlineQueueList size="1.1rem" />}
tooltip={{ label: 'View queue', openDelay: 500 }} tooltip={{ label: 'View queue', openDelay: 500 }}
variant="secondary" variant="secondary"
onClick={() => setSidebar({ rightExpanded: !isQueueExpanded })} onClick={() => setSideBar({ rightExpanded: !isQueueExpanded })}
/> />
<Group <Group
noWrap noWrap

View file

@ -9,6 +9,7 @@ import {
SideQueueType, SideQueueType,
} from '/@/renderer/store/settings.store'; } from '/@/renderer/store/settings.store';
import { AppTheme } from '/@/renderer/themes/types'; import { AppTheme } from '/@/renderer/themes/types';
import { Platform } from '/@/renderer/types';
const FONT_OPTIONS = [ const FONT_OPTIONS = [
{ label: 'Archivo', value: 'Archivo' }, { label: 'Archivo', value: 'Archivo' },
@ -26,6 +27,12 @@ const SIDE_QUEUE_OPTIONS = [
{ label: 'Floating', value: 'sideDrawerQueue' }, { label: 'Floating', value: 'sideDrawerQueue' },
]; ];
const TITLEBAR_OPTIONS = [
{ label: 'Web (hidden)', value: Platform.WEB },
{ label: 'Windows', value: Platform.WINDOWS },
{ label: 'macOS', value: Platform.MACOS },
];
export const GeneralTab = () => { export const GeneralTab = () => {
const settings = useGeneralSettings(); const settings = useGeneralSettings();
const { setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
@ -34,9 +41,18 @@ export const GeneralTab = () => {
{ {
control: ( control: (
<Select <Select
disabled data={TITLEBAR_OPTIONS}
data={['Windows', 'macOS']} disabled={!isElectron()}
defaultValue="Windows" value={settings.windowBarStyle}
onChange={(e) => {
if (!e) return;
setSettings({
general: {
...settings,
windowBarStyle: e as Platform,
},
});
}}
/> />
), ),
description: 'Adjust the style of the titlebar', description: 'Adjust the style of the titlebar',

View file

@ -17,7 +17,8 @@ interface SidebarPlaylistListProps {
data: ReturnType<typeof usePlaylistList>['data']; data: ReturnType<typeof usePlaylistList>['data'];
} }
const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => ( const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
return (
<div style={{ margin: '0.5rem 0', padding: '0 1.5rem', ...style }}> <div style={{ margin: '0.5rem 0', padding: '0 1.5rem', ...style }}>
<Group <Group
noWrap noWrap
@ -102,7 +103,8 @@ const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => (
</Group> </Group>
</Group> </Group>
</div> </div>
); );
};
export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => { export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0); const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0);

View file

@ -87,7 +87,7 @@ export const Sidebar = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
const { setSidebar } = useAppStoreActions(); const { setSideBar } = useAppStoreActions();
const imageUrl = useCurrentSong()?.imageUrl; const imageUrl = useCurrentSong()?.imageUrl;
const server = useCurrentServer(); const server = useCurrentServer();
@ -215,7 +215,7 @@ export const Sidebar = () => {
panel: { padding: '0 1rem' }, panel: { padding: '0 1rem' },
}} }}
value={sidebar.expanded} value={sidebar.expanded}
onChange={(e) => setSidebar({ expanded: e })} onChange={(e) => setSideBar({ expanded: e })}
> >
<Accordion.Item value="library"> <Accordion.Item value="library">
<Accordion.Control> <Accordion.Control>
@ -362,7 +362,7 @@ export const Sidebar = () => {
variant="default" variant="default"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setSidebar({ image: false }); setSideBar({ image: false });
}} }}
> >
<RiArrowDownSLine <RiArrowDownSLine

View file

@ -2,14 +2,20 @@ import { useLocation } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useSidebarRightExpanded } from '/@/renderer/store'; import { useSidebarRightExpanded } from '/@/renderer/store';
import { useGeneralSettings } from '/@/renderer/store/settings.store'; import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
export const useShouldPadTitlebar = () => { export const useShouldPadTitlebar = () => {
const location = useLocation(); const location = useLocation();
const isSidebarExpanded = useSidebarRightExpanded(); const isSidebarExpanded = useSidebarRightExpanded();
const isQueuePage = location.pathname === AppRoute.NOW_PLAYING; const isQueuePage = location.pathname === AppRoute.NOW_PLAYING;
const { sideQueueType } = useGeneralSettings(); const { sideQueueType, windowBarStyle } = useGeneralSettings();
// If the sidebar is expanded, the sidebar queue is enabled, and the user is not on the queue page const conditions = [
windowBarStyle === Platform.WEB,
!(isSidebarExpanded && sideQueueType === 'sideQueue' && !isQueuePage),
];
return !(isSidebarExpanded && sideQueueType === 'sideQueue' && !isQueuePage); const shouldPadTitlebar = conditions.every((condition) => condition);
return shouldPadTitlebar;
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

View file

@ -1,21 +1,10 @@
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { lazy } from 'react';
import { useDisclosure, useTimeout } from '@mantine/hooks';
import type { Variants } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import throttle from 'lodash/throttle';
import { TbArrowBarLeft } from 'react-icons/tb';
import { Outlet, useLocation } from 'react-router';
import styled from 'styled-components'; import styled from 'styled-components';
import { DrawerPlayQueue, SidebarPlayQueue } from '/@/renderer/features/now-playing'; import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
import { Playerbar } from '/@/renderer/features/player'; import { Platform, PlaybackType } from '/@/renderer/types';
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar'; import { MainContent } from '/@/renderer/layouts/default-layout/main-content';
import { AppRoute } from '/@/renderer/router/routes'; import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store';
import { useSettingsStore, useGeneralSettings } from '/@/renderer/store/settings.store';
import { PlaybackType } from '/@/renderer/types';
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
import { FullScreenPlayer } from '/@/renderer/features/player/components/full-screen-player';
if (!isElectron()) { if (!isElectron()) {
useSettingsStore.getState().actions.setSettings({ useSettingsStore.getState().actions.setSettings({
@ -26,357 +15,44 @@ if (!isElectron()) {
}); });
} }
const Layout = styled.div` const Layout = styled.div<{ windowBarStyle: Platform }>`
display: grid; display: grid;
grid-template-areas: grid-template-areas:
'window-bar'
'main-content' 'main-content'
'player'; 'player';
grid-template-rows: calc(100vh - 90px) 90px; grid-template-rows:
${(props) => (props.windowBarStyle !== Platform.WEB ? '30px' : '0px')} calc(100vh - 120px)
90px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 0; gap: 0;
height: 100%; height: 100%;
`; `;
const MainContentContainer = styled.div<{ const WindowBar = lazy(() =>
leftSidebarWidth: string; import('/@/renderer/layouts/window-bar').then((module) => ({
rightExpanded?: boolean; default: module.WindowBar,
rightSidebarWidth?: string; })),
shell?: 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};
gap: 0;
background: var(--main-bg);
`;
const SidebarContainer = styled.aside`
position: relative;
grid-area: sidebar;
background: var(--sidebar-bg);
border-right: var(--sidebar-border);
`;
const RightSidebarContainer = styled(motion.aside)`
position: relative;
grid-area: right-sidebar;
height: 100%;
background: var(--sidebar-bg);
border-left: var(--sidebar-border);
`;
const PlayerbarContainer = styled.footer`
z-index: 100;
grid-area: player;
background: var(--playerbar-bg);
filter: drop-shadow(0 -3px 1px rgba(0, 0, 0, 10%));
`;
const ResizeHandle = styled.div<{
isResizing: boolean;
placement: 'top' | 'left' | 'bottom' | 'right';
}>`
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};
z-index: 90;
width: 2px;
height: 100%;
background-color: var(--sidebar-handle-bg);
cursor: ew-resize;
opacity: ${(props) => (props.isResizing ? 1 : 0)};
&:hover {
opacity: 0.5;
}
`;
const QueueDrawer = styled(motion.div)`
background: var(--main-bg);
border: 3px solid var(--generic-border-color);
border-radius: 10px;
`;
const QueueDrawerArea = styled(motion.div)`
position: absolute;
top: 50%;
right: 25px;
z-index: 100;
display: flex;
align-items: center;
width: 20px;
height: 30px;
user-select: none;
`;
interface DefaultLayoutProps { interface DefaultLayoutProps {
shell?: boolean; shell?: boolean;
} }
export const DefaultLayout = ({ shell }: DefaultLayoutProps) => { export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
const sidebar = useAppStore((state) => state.sidebar); const { windowBarStyle } = useGeneralSettings();
const { setSidebar } = useAppStoreActions();
const [drawer, drawerHandler] = useDisclosure(false);
const location = useLocation();
const { sideQueueType, showQueueDrawerButton } = useGeneralSettings();
const sidebarRef = useRef<HTMLDivElement | null>(null);
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
const [isResizing, setIsResizing] = useState(false);
const [isResizingRight, setIsResizingRight] = useState(false);
const drawerTimeout = useTimeout(() => drawerHandler.open(), 500);
const handleEnterDrawerButton = useCallback(() => {
drawerTimeout.start();
}, [drawerTimeout]);
const handleLeaveDrawerButton = useCallback(() => {
drawerTimeout.clear();
}, [drawerTimeout]);
const isQueueDrawerButtonVisible =
showQueueDrawerButton &&
!sidebar.rightExpanded &&
!drawer &&
location.pathname !== AppRoute.NOW_PLAYING;
const showSideQueue = sidebar.rightExpanded && location.pathname !== AppRoute.NOW_PLAYING;
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const queueDrawerButtonVariants: Variants = {
hidden: {
opacity: 0,
transition: { duration: 0.2 },
x: 100,
},
visible: {
opacity: 0.5,
transition: { duration: 0.1, ease: 'anticipate' },
x: 0,
},
};
const queueDrawerVariants: Variants = {
closed: {
height: 'calc(100vh - 175px)',
position: 'absolute',
right: 0,
top: '75px',
transition: {
duration: 0.4,
ease: 'anticipate',
},
width: '450px',
x: '50vw',
},
open: {
boxShadow: '0px 0px 10px 0px rgba(0, 0, 0, 0.8)',
height: 'calc(100vh - 175px)',
position: 'absolute',
right: '20px',
top: '75px',
transition: {
damping: 10,
delay: 0,
duration: 0.4,
ease: 'anticipate',
mass: 0.5,
},
width: '450px',
x: 0,
zIndex: 120,
},
};
const queueSidebarVariants: Variants = {
closed: {
transition: { duration: 0.5 },
width: sidebar.rightWidth,
x: 1000,
zIndex: 120,
},
open: {
transition: {
duration: 0.5,
ease: 'anticipate',
},
width: sidebar.rightWidth,
x: 0,
zIndex: 120,
},
};
const startResizing = useCallback((position: 'left' | 'right') => {
if (position === 'left') return setIsResizing(true);
return setIsResizingRight(true);
}, []);
const stopResizing = useCallback(() => {
setIsResizing(false);
setIsResizingRight(false);
}, []);
const resize = useCallback(
(mouseMoveEvent: any) => {
if (isResizing) {
const width = `${constrainSidebarWidth(mouseMoveEvent.clientX)}px`;
setSidebar({ leftWidth: width });
}
if (isResizingRight) {
const start = Number(sidebar.rightWidth.split('px')[0]);
const { left } = rightSidebarRef!.current!.getBoundingClientRect();
const width = `${constrainRightSidebarWidth(start + left - mouseMoveEvent.clientX)}px`;
setSidebar({ rightWidth: width });
}
},
[isResizing, isResizingRight, setSidebar, sidebar.rightWidth],
);
const throttledResize = useMemo(() => throttle(resize, 50), [resize]);
useEffect(() => {
window.addEventListener('mousemove', throttledResize);
window.addEventListener('mouseup', stopResizing);
return () => {
window.removeEventListener('mousemove', throttledResize);
window.removeEventListener('mouseup', stopResizing);
};
}, [throttledResize, stopResizing]);
return ( return (
<Layout id="default-layout">
<MainContentContainer
id="main-content"
leftSidebarWidth={sidebar.leftWidth}
rightExpanded={showSideQueue && sideQueueType === 'sideQueue'}
rightSidebarWidth={sidebar.rightWidth}
shell={shell}
>
{!shell && (
<> <>
<AnimatePresence <Layout
initial={false} id="default-layout"
mode="wait" windowBarStyle={windowBarStyle}
> >
{isFullScreenPlayerExpanded && <FullScreenPlayer />} {windowBarStyle !== Platform.WEB && <WindowBar />}
</AnimatePresence> <MainContent shell={shell} />
<SidebarContainer id="sidebar"> <PlayerBar />
<ResizeHandle
ref={sidebarRef}
isResizing={isResizing}
placement="right"
onMouseDown={(e) => {
e.preventDefault();
startResizing('left');
}}
/>
<Sidebar />
</SidebarContainer>
<AnimatePresence
initial={false}
mode="wait"
>
{isQueueDrawerButtonVisible && (
<QueueDrawerArea
key="queue-drawer-button"
animate="visible"
exit="hidden"
initial="hidden"
variants={queueDrawerButtonVariants}
whileHover={{ opacity: 1, scale: 2, transition: { duration: 0.5 } }}
onMouseEnter={handleEnterDrawerButton}
onMouseLeave={handleLeaveDrawerButton}
>
<TbArrowBarLeft size={12} />
</QueueDrawerArea>
)}
{drawer && (
<QueueDrawer
key="queue-drawer"
animate="open"
exit="closed"
initial="closed"
variants={queueDrawerVariants}
onMouseLeave={() => {
// The drawer will close due to the delay when setting isReorderingQueue
setTimeout(() => {
if (useAppStore.getState().isReorderingQueue) return;
drawerHandler.close();
}, 50);
}}
>
<DrawerPlayQueue />
</QueueDrawer>
)}
</AnimatePresence>
<AnimatePresence
key="queue-sidebar"
presenceAffectsLayout
initial={false}
mode="wait"
>
{showSideQueue && (
<>
{sideQueueType === 'sideQueue' ? (
<RightSidebarContainer
key="queue-sidebar"
animate="open"
exit="closed"
id="sidebar-queue"
initial="closed"
variants={queueSidebarVariants}
>
<ResizeHandle
ref={rightSidebarRef}
isResizing={isResizingRight}
placement="left"
onMouseDown={(e) => {
e.preventDefault();
startResizing('right');
}}
/>
<SidebarPlayQueue />
</RightSidebarContainer>
) : (
<QueueDrawer
key="queue-drawer"
animate="open"
exit="closed"
id="drawer-queue"
initial="closed"
variants={queueDrawerVariants}
onMouseLeave={() => {
// The drawer will close due to the delay when setting isReorderingQueue
setTimeout(() => {
if (useAppStore.getState().isReorderingQueue) return;
drawerHandler.close();
}, 50);
}}
>
<DrawerPlayQueue />
</QueueDrawer>
)}
</>
)}
</AnimatePresence>
</>
)}
<Suspense fallback={<></>}>
<Outlet />
</Suspense>
</MainContentContainer>
<PlayerbarContainer id="player-bar">
<Playerbar />
</PlayerbarContainer>
</Layout> </Layout>
</>
); );
}; };

View file

@ -0,0 +1,345 @@
import { useDisclosure, useTimeout } from '@mantine/hooks';
import { motion, AnimatePresence, Variants } from 'framer-motion';
import { throttle } from 'lodash';
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TbArrowBarLeft } from 'react-icons/tb';
import { Outlet, useLocation } from 'react-router';
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 { AppRoute } from '/@/renderer/router/routes';
import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
const MainContentContainer = styled.div<{
leftSidebarWidth: string;
rightExpanded?: boolean;
rightSidebarWidth?: string;
shell?: 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};
gap: 0;
background: var(--main-bg);
`;
const SidebarContainer = styled.aside`
position: relative;
grid-area: sidebar;
background: var(--sidebar-bg);
border-right: var(--sidebar-border);
`;
const RightSidebarContainer = styled(motion.aside)`
position: relative;
grid-area: right-sidebar;
height: 100%;
background: var(--sidebar-bg);
border-left: var(--sidebar-border);
`;
const ResizeHandle = styled.div<{
isResizing: boolean;
placement: 'top' | 'left' | 'bottom' | 'right';
}>`
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};
z-index: 90;
width: 2px;
height: 100%;
background-color: var(--sidebar-handle-bg);
cursor: ew-resize;
opacity: ${(props) => (props.isResizing ? 1 : 0)};
&:hover {
opacity: 0.5;
}
`;
const QueueDrawer = styled(motion.div)`
background: var(--main-bg);
border: 3px solid var(--generic-border-color);
border-radius: 10px;
`;
const QueueDrawerArea = styled(motion.div)`
position: absolute;
top: 50%;
right: 25px;
z-index: 100;
display: flex;
align-items: center;
width: 20px;
height: 30px;
user-select: none;
`;
const queueDrawerVariants: Variants = {
closed: (windowBarStyle) => ({
height: windowBarStyle !== Platform.WEB ? 'calc(100vh - 205px)' : 'calc(100vh - 175px)',
position: 'absolute',
right: 0,
top: '75px',
transition: {
duration: 0.4,
ease: 'anticipate',
},
width: '450px',
x: '50vw',
}),
open: (windowBarStyle) => ({
boxShadow: '0px 0px 10px 0px rgba(0, 0, 0, 0.8)',
height: windowBarStyle !== Platform.WEB ? 'calc(100vh - 205px)' : 'calc(100vh - 175px)',
position: 'absolute',
right: '20px',
top: '75px',
transition: {
damping: 10,
delay: 0,
duration: 0.4,
ease: 'anticipate',
mass: 0.5,
},
width: '450px',
x: 0,
zIndex: 120,
}),
};
const queueDrawerButtonVariants: Variants = {
hidden: {
opacity: 0,
transition: { duration: 0.2 },
x: 100,
},
visible: {
opacity: 0.5,
transition: { duration: 0.1, ease: 'anticipate' },
x: 0,
},
};
const queueSidebarVariants: Variants = {
closed: (rightWidth) => ({
transition: { duration: 0.5 },
width: rightWidth,
x: 1000,
zIndex: 120,
}),
open: (rightWidth) => ({
transition: {
duration: 0.5,
ease: 'anticipate',
},
width: rightWidth,
x: 0,
zIndex: 120,
}),
};
export const MainContent = ({ shell }: { shell?: boolean }) => {
const sidebar = useAppStore((state) => state.sidebar);
const { setSideBar } = useAppStoreActions();
const [drawer, drawerHandler] = useDisclosure(false);
const location = useLocation();
const { sideQueueType, showQueueDrawerButton } = useGeneralSettings();
const { windowBarStyle } = useGeneralSettings();
const sidebarRef = useRef<HTMLDivElement | null>(null);
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
const [isResizing, setIsResizing] = useState(false);
const [isResizingRight, setIsResizingRight] = useState(false);
const drawerTimeout = useTimeout(() => drawerHandler.open(), 500);
const handleEnterDrawerButton = useCallback(() => {
drawerTimeout.start();
}, [drawerTimeout]);
const handleLeaveDrawerButton = useCallback(() => {
drawerTimeout.clear();
}, [drawerTimeout]);
const isQueueDrawerButtonVisible =
showQueueDrawerButton &&
!sidebar.rightExpanded &&
!drawer &&
location.pathname !== AppRoute.NOW_PLAYING;
const showSideQueue = sidebar.rightExpanded && location.pathname !== AppRoute.NOW_PLAYING;
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const startResizing = useCallback((position: 'left' | 'right') => {
if (position === 'left') return setIsResizing(true);
return setIsResizingRight(true);
}, []);
const stopResizing = useCallback(() => {
setIsResizing(false);
setIsResizingRight(false);
}, []);
const resize = useCallback(
(mouseMoveEvent: any) => {
if (isResizing) {
const width = `${constrainSidebarWidth(mouseMoveEvent.clientX)}px`;
setSideBar({ leftWidth: width });
}
if (isResizingRight) {
const start = Number(sidebar.rightWidth.split('px')[0]);
const { left } = rightSidebarRef!.current!.getBoundingClientRect();
const width = `${constrainRightSidebarWidth(start + left - mouseMoveEvent.clientX)}px`;
setSideBar({ rightWidth: width });
}
},
[isResizing, isResizingRight, setSideBar, sidebar.rightWidth],
);
const throttledResize = useMemo(() => throttle(resize, 50), [resize]);
useEffect(() => {
window.addEventListener('mousemove', throttledResize);
window.addEventListener('mouseup', stopResizing);
return () => {
window.removeEventListener('mousemove', throttledResize);
window.removeEventListener('mouseup', stopResizing);
};
}, [throttledResize, stopResizing]);
return (
<MainContentContainer
id="main-content"
leftSidebarWidth={sidebar.leftWidth}
rightExpanded={showSideQueue && sideQueueType === 'sideQueue'}
rightSidebarWidth={sidebar.rightWidth}
shell={shell}
>
{!shell && (
<>
<AnimatePresence
initial={false}
mode="wait"
>
{isFullScreenPlayerExpanded && <FullScreenPlayer />}
</AnimatePresence>
<SidebarContainer id="sidebar">
<ResizeHandle
ref={sidebarRef}
isResizing={isResizing}
placement="right"
onMouseDown={(e) => {
e.preventDefault();
startResizing('left');
}}
/>
<Sidebar />
</SidebarContainer>
<AnimatePresence
initial={false}
mode="wait"
>
{isQueueDrawerButtonVisible && (
<QueueDrawerArea
key="queue-drawer-button"
animate="visible"
exit="hidden"
initial="hidden"
variants={queueDrawerButtonVariants}
whileHover={{ opacity: 1, scale: 2, transition: { duration: 0.5 } }}
onMouseEnter={handleEnterDrawerButton}
onMouseLeave={handleLeaveDrawerButton}
>
<TbArrowBarLeft size={12} />
</QueueDrawerArea>
)}
{drawer && (
<QueueDrawer
key="queue-drawer"
animate="open"
exit="closed"
initial="closed"
variants={queueDrawerVariants}
onMouseLeave={() => {
// The drawer will close due to the delay when setting isReorderingQueue
setTimeout(() => {
if (useAppStore.getState().isReorderingQueue) return;
drawerHandler.close();
}, 50);
}}
>
<DrawerPlayQueue />
</QueueDrawer>
)}
</AnimatePresence>
<AnimatePresence
key="queue-sidebar"
presenceAffectsLayout
initial={false}
mode="wait"
>
{showSideQueue && (
<>
{sideQueueType === 'sideQueue' ? (
<RightSidebarContainer
key="queue-sidebar"
animate="open"
custom={sidebar.rightWidth}
exit="closed"
id="sidebar-queue"
initial="closed"
variants={queueSidebarVariants}
>
<ResizeHandle
ref={rightSidebarRef}
isResizing={isResizingRight}
placement="left"
onMouseDown={(e) => {
e.preventDefault();
startResizing('right');
}}
/>
<SidebarPlayQueue />
</RightSidebarContainer>
) : (
<QueueDrawer
key="queue-drawer"
animate="open"
custom={windowBarStyle}
exit="closed"
id="drawer-queue"
initial="closed"
variants={queueDrawerVariants}
onMouseLeave={() => {
// The drawer will close due to the delay when setting isReorderingQueue
setTimeout(() => {
if (useAppStore.getState().isReorderingQueue) return;
drawerHandler.close();
}, 50);
}}
>
<DrawerPlayQueue />
</QueueDrawer>
)}
</>
)}
</AnimatePresence>
</>
)}
<Suspense fallback={<></>}>
<Outlet />
</Suspense>
</MainContentContainer>
);
};

View file

@ -0,0 +1,17 @@
import styled from 'styled-components';
import { Playerbar } from '/@/renderer/features/player';
const PlayerbarContainer = styled.footer`
z-index: 100;
grid-area: player;
background: var(--playerbar-bg);
filter: drop-shadow(0 -3px 1px rgba(0, 0, 0, 10%));
`;
export const PlayerBar = () => {
return (
<PlayerbarContainer id="player-bar">
<Playerbar />
</PlayerbarContainer>
);
};

View file

@ -0,0 +1,251 @@
import { useCallback, useState } from 'react';
import isElectron from 'is-electron';
import { RiCheckboxBlankLine, RiCloseLine, RiSubtractLine } from 'react-icons/ri';
import styled from 'styled-components';
import { useCurrentStatus, useQueueStatus } from '/@/renderer/store';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform, PlayerStatus } from '/@/renderer/types';
import appIcon from '../../../assets/icon.svg';
import macCloseHover from './assets/close-mac-hover.png';
import macClose from './assets/close-mac.png';
import macMaxHover from './assets/max-mac-hover.png';
import macMax from './assets/max-mac.png';
import macMinHover from './assets/min-mac-hover.png';
import macMin from './assets/min-mac.png';
const WindowsContainer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
color: var(--window-bar-fg);
background-color: var(--window-bar-bg);
-webkit-app-region: drag;
`;
const WindowsButtonGroup = styled.div`
display: flex;
width: 130px;
height: 100%;
-webkit-app-region: no-drag;
`;
const WindowsButton = styled.div<{ $exit?: boolean }>`
display: flex;
flex: 1;
align-items: center;
justify-content: center;
-webkit-app-region: no-drag;
width: 50px;
height: 30px;
img {
width: 35%;
height: 50%;
}
&:hover {
background: ${({ $exit }) => ($exit ? 'var(--danger-color)' : 'rgba(125, 125, 125, 30%)')};
}
`;
const PlayerStatusContainer = styled.div`
display: flex;
gap: 0.5rem;
max-width: 45vw;
padding-left: 1rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
const browser = isElectron() ? window.electron.browser : null;
const close = () => browser.exit();
const minimize = () => browser.minimize();
const maximize = () => browser.maximize();
const unmaximize = () => browser.unmaximize();
interface WindowBarControlsProps {
controls: {
handleClose: () => void;
handleMaximize: () => void;
handleMinimize: () => void;
};
title: string;
}
const WindowsControls = ({ controls, title }: WindowBarControlsProps) => {
const { handleClose, handleMaximize, handleMinimize } = controls;
return (
<WindowsContainer>
<PlayerStatusContainer>
<img
alt=""
height={18}
src={appIcon}
width={18}
/>
{title}
</PlayerStatusContainer>
<WindowsButtonGroup>
<WindowsButton
role="button"
onClick={handleMinimize}
>
<RiSubtractLine size={19} />
</WindowsButton>
<WindowsButton
role="button"
onClick={handleMaximize}
>
<RiCheckboxBlankLine size={13} />
</WindowsButton>
<WindowsButton
$exit
role="button"
onClick={handleClose}
>
<RiCloseLine size={19} />
</WindowsButton>
</WindowsButtonGroup>
</WindowsContainer>
);
};
const MacOsContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
-webkit-app-region: drag;
`;
const MacOsButtonGroup = styled.div`
position: absolute;
top: 5px;
left: 0.5rem;
display: grid;
grid-template-columns: repeat(3, 20px);
height: 100%;
-webkit-app-region: no-drag;
`;
export const MacOsButton = styled.div<{
maxButton?: boolean;
minButton?: boolean;
restoreButton?: boolean;
}>`
grid-row: 1 / span 1;
grid-column: ${(props) => (props.minButton ? 2 : props.maxButton || props.restoreButton ? 3 : 1)};
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
user-select: none;
img {
width: 18px;
height: 18px;
}
`;
const MacOsControls = ({ controls, title }: WindowBarControlsProps) => {
const { handleClose, handleMaximize, handleMinimize } = controls;
const [hoverMin, setHoverMin] = useState(false);
const [hoverMax, setHoverMax] = useState(false);
const [hoverClose, setHoverClose] = useState(false);
return (
<MacOsContainer>
<MacOsButtonGroup>
<MacOsButton
minButton
className="button"
id="min-button"
onClick={handleMinimize}
onMouseLeave={() => setHoverMin(false)}
onMouseOver={() => setHoverMin(true)}
>
<img
alt=""
className="icon"
draggable="false"
src={hoverMin ? macMinHover : macMin}
/>
</MacOsButton>
<MacOsButton
maxButton
className="button"
id="max-button"
onClick={handleMaximize}
onMouseLeave={() => setHoverMax(false)}
onMouseOver={() => setHoverMax(true)}
>
<img
alt=""
className="icon"
draggable="false"
src={hoverMax ? macMaxHover : macMax}
/>
</MacOsButton>
<MacOsButton
className="button"
id="close-button"
onClick={handleClose}
onMouseLeave={() => setHoverClose(false)}
onMouseOver={() => setHoverClose(true)}
>
<img
alt=""
className="icon"
draggable="false"
src={hoverClose ? macCloseHover : macClose}
/>
</MacOsButton>
</MacOsButtonGroup>
<PlayerStatusContainer>{title}</PlayerStatusContainer>
</MacOsContainer>
);
};
export const WindowBar = () => {
const playerStatus = useCurrentStatus();
const { currentSong, index, length } = useQueueStatus();
const { windowBarStyle } = useGeneralSettings();
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
const queueString = length ? `(${index + 1} / ${length}) ` : '';
const title = length ? `${statusString}${queueString}${currentSong?.name}` : 'Feishin';
const [max, setMax] = useState(false);
const handleMinimize = () => minimize();
const handleMaximize = useCallback(() => {
if (max) {
unmaximize();
} else {
maximize();
}
setMax(!max);
}, [max]);
const handleClose = useCallback(() => close(), []);
return (
<>
{windowBarStyle === Platform.WINDOWS ? (
<WindowsControls
controls={{ handleClose, handleMaximize, handleMinimize }}
title={title}
/>
) : (
<MacOsControls
controls={{ handleClose, handleMaximize, handleMinimize }}
title={title}
/>
)}
</>
);
};

View file

@ -1,6 +1,8 @@
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import styled from 'styled-components'; import styled from 'styled-components';
import { Titlebar } from '/@/renderer/features/titlebar/components/titlebar'; import { Titlebar } from '/@/renderer/features/titlebar/components/titlebar';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
const TitlebarContainer = styled.header` const TitlebarContainer = styled.header`
position: absolute; position: absolute;
@ -13,11 +15,15 @@ const TitlebarContainer = styled.header`
`; `;
export const TitlebarOutlet = () => { export const TitlebarOutlet = () => {
const { windowBarStyle } = useGeneralSettings();
return ( return (
<> <>
{windowBarStyle === Platform.WEB && (
<TitlebarContainer> <TitlebarContainer>
<Titlebar /> <Titlebar />
</TitlebarContainer> </TitlebarContainer>
)}
<Outlet /> <Outlet />
</> </>
); );

View file

@ -33,8 +33,8 @@ export interface AppState {
export interface AppSlice extends AppState { export interface AppSlice extends AppState {
actions: { actions: {
setAppStore: (data: Partial<AppSlice>) => void; setAppStore: (data: Partial<AppSlice>) => void;
setSidebar: (options: Partial<SidebarProps>) => void; setSideBar: (options: Partial<SidebarProps>) => void;
setTitlebar: (options: Partial<TitlebarProps>) => void; setTitleBar: (options: Partial<TitlebarProps>) => void;
}; };
} }
@ -46,12 +46,12 @@ export const useAppStore = create<AppSlice>()(
setAppStore: (data) => { setAppStore: (data) => {
set({ ...get(), ...data }); set({ ...get(), ...data });
}, },
setSidebar: (options) => { setSideBar: (options) => {
set((state) => { set((state) => {
state.sidebar = { ...state.sidebar, ...options }; state.sidebar = { ...state.sidebar, ...options };
}); });
}, },
setTitlebar: (options) => { setTitleBar: (options) => {
set((state) => { set((state) => {
state.titlebar = { ...state.titlebar, ...options }; state.titlebar = { ...state.titlebar, ...options };
}); });
@ -89,6 +89,6 @@ export const useSidebarStore = () => useAppStore((state) => state.sidebar);
export const useSidebarRightExpanded = () => useAppStore((state) => state.sidebar.rightExpanded); export const useSidebarRightExpanded = () => useAppStore((state) => state.sidebar.rightExpanded);
export const useSetTitlebar = () => useAppStore((state) => state.actions.setTitlebar); export const useSetTitlebar = () => useAppStore((state) => state.actions.setTitleBar);
export const useTitlebarStore = () => useAppStore((state) => state.titlebar); export const useTitlebarStore = () => useAppStore((state) => state.titlebar);

View file

@ -49,6 +49,7 @@ export interface PlayerData {
export interface QueueData { export interface QueueData {
current?: QueueSong; current?: QueueSong;
length: number;
next?: QueueSong; next?: QueueSong;
previous?: QueueSong; previous?: QueueSong;
} }
@ -318,6 +319,7 @@ export const usePlayerStore = create<PlayerSlice>()(
player2, player2,
queue: { queue: {
current, current,
length: get().queue.default.length || 0,
next, next,
previous, previous,
}, },
@ -377,6 +379,7 @@ export const usePlayerStore = create<PlayerSlice>()(
player2, player2,
queue: { queue: {
current: queue[currentIndex], current: queue[currentIndex],
length: get().queue.default.length || 0,
next: nextSongIndex !== undefined ? queue[nextSongIndex] : undefined, next: nextSongIndex !== undefined ? queue[nextSongIndex] : undefined,
previous: queue[currentIndex - 1], previous: queue[currentIndex - 1],
}, },
@ -386,6 +389,7 @@ export const usePlayerStore = create<PlayerSlice>()(
const queue = get().queue.default; const queue = get().queue.default;
return { return {
current: queue[get().current.index], current: queue[get().current.index],
length: queue.length || 0,
next: queue[get().current.index + 1], next: queue[get().current.index + 1],
previous: queue[get().current.index - 1], previous: queue[get().current.index - 1],
}; };
@ -895,7 +899,9 @@ export const useCurrentSong = () => usePlayerStore((state) => state.current.song
export const usePlayerData = () => export const usePlayerData = () =>
usePlayerStore( usePlayerStore(
(state) => state.actions.getPlayerData(), (state) => state.actions.getPlayerData(),
(a, b) => a.current.nextIndex === b.current.nextIndex, (a, b) => {
return a.current.nextIndex === b.current.nextIndex;
},
); );
export const useCurrentPlayer = () => usePlayerStore((state) => state.current.player); export const useCurrentPlayer = () => usePlayerStore((state) => state.current.player);
@ -920,3 +926,13 @@ export const useSetQueueRating = () => usePlayerStore((state) => state.actions.s
export const useIncrementQueuePlayCount = () => export const useIncrementQueuePlayCount = () =>
usePlayerStore((state) => state.actions.incrementPlayCount); usePlayerStore((state) => state.actions.incrementPlayCount);
export const useQueueStatus = () =>
usePlayerStore(
(state) => ({
currentSong: state.current.song,
index: state.current.index,
length: state.queue.default.length,
}),
shallow,
);

View file

@ -4,6 +4,7 @@ import merge from 'lodash/merge';
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 { AppTheme } from '/@/renderer/themes/types'; import { AppTheme } from '/@/renderer/themes/types';
import { import {
TableColumn, TableColumn,
@ -12,6 +13,7 @@ import {
PlaybackStyle, PlaybackStyle,
PlaybackType, PlaybackType,
TableType, TableType,
Platform,
} from '/@/renderer/types'; } from '/@/renderer/types';
export type PersistedTableColumn = { export type PersistedTableColumn = {
@ -38,6 +40,7 @@ export interface SettingsState {
themeDark: AppTheme; themeDark: AppTheme;
themeLight: AppTheme; themeLight: AppTheme;
volumeWheelStep: number; volumeWheelStep: number;
windowBarStyle: Platform;
}; };
player: { player: {
audioDeviceId?: string | null; audioDeviceId?: string | null;
@ -93,6 +96,7 @@ export const useSettingsStore = create<SettingsSlice>()(
themeDark: AppTheme.DEFAULT_DARK, themeDark: AppTheme.DEFAULT_DARK,
themeLight: AppTheme.DEFAULT_LIGHT, themeLight: AppTheme.DEFAULT_LIGHT,
volumeWheelStep: 5, volumeWheelStep: 5,
windowBarStyle: Platform.WEB,
}, },
player: { player: {
audioDeviceId: undefined, audioDeviceId: undefined,
@ -244,21 +248,21 @@ export const useSettingsStore = create<SettingsSlice>()(
return merge(currentState, persistedState); return merge(currentState, persistedState);
}, },
name: 'store_settings', name: 'store_settings',
version: 3, version: 4,
}, },
), ),
); );
export const useSettingsStoreActions = () => useSettingsStore((state) => state.actions); export const useSettingsStoreActions = () => useSettingsStore((state) => state.actions);
export const usePlayerSettings = () => useSettingsStore((state) => state.player); export const usePlayerSettings = () => useSettingsStore((state) => state.player, shallow);
export const useTableSettings = (type: TableType) => export const useTableSettings = (type: TableType) =>
useSettingsStore((state) => state.tables[type]); useSettingsStore((state) => state.tables[type]);
export const useGeneralSettings = () => useSettingsStore((state) => state.general); export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
export const usePlayerType = () => useSettingsStore((state) => state.player.type); export const usePlayerType = () => useSettingsStore((state) => state.player.type, shallow);
export const usePlayButtonBehavior = () => export const usePlayButtonBehavior = () =>
useSettingsStore((state) => state.player.playButtonBehavior); useSettingsStore((state) => state.player.playButtonBehavior, shallow);

View file

@ -10,6 +10,10 @@
transition: position 0.2s ease-in-out; transition: position 0.2s ease-in-out;
} }
.window-frame {
top: 95px;
}
.ag-header-transparent { .ag-header-transparent {
--ag-header-background-color: rgba(0, 0, 0, 0%) !important; --ag-header-background-color: rgba(0, 0, 0, 0%) !important;
} }

View file

@ -13,15 +13,18 @@
--main-fg: rgb(245, 245, 245); --main-fg: rgb(245, 245, 245);
--main-fg-secondary: rgb(150, 150, 150); --main-fg-secondary: rgb(150, 150, 150);
--window-bar-bg: rgb(24, 24, 24);
--window-bar-fg: rgb(255, 255, 255);
--titlebar-fg: rgb(255, 255, 255); --titlebar-fg: rgb(255, 255, 255);
--titlebar-bg: rgb(24, 24, 24); --titlebar-bg: rgb(12, 12, 12);
--titlebar-controls-bg: rgba(0, 0, 0, 0); --titlebar-controls-bg: rgba(0, 0, 0, 0);
--sidebar-bg: rgb(0, 0, 0); --sidebar-bg: rgb(0, 0, 0);
--sidebar-fg: rgb(210, 210, 210); --sidebar-fg: rgb(210, 210, 210);
--sidebar-fg-hover: rgb(255, 255, 255); --sidebar-fg-hover: rgb(255, 255, 255);
--sidebar-handle-bg: #4d4d4d; --sidebar-handle-bg: #4d4d4d;
--sidebar-border: none; --sidebar-border: 2px rgba(18, 18, 18, 0.7) solid;
--playerbar-bg: rgb(24, 24, 24); --playerbar-bg: rgb(24, 24, 24);
--playerbar-btn-main-fg: rgb(0, 0, 0); --playerbar-btn-main-fg: rgb(0, 0, 0);