Add dedicated OS window bars (#22)
This commit is contained in:
parent
ececc394e2
commit
58c7370536
25 changed files with 823 additions and 462 deletions
|
@ -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();
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
BIN
src/renderer/layouts/assets/close-mac-hover.png
Normal file
BIN
src/renderer/layouts/assets/close-mac-hover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 507 B |
BIN
src/renderer/layouts/assets/close-mac.png
Normal file
BIN
src/renderer/layouts/assets/close-mac.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 553 B |
BIN
src/renderer/layouts/assets/max-mac-hover.png
Normal file
BIN
src/renderer/layouts/assets/max-mac-hover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 566 B |
BIN
src/renderer/layouts/assets/max-mac.png
Normal file
BIN
src/renderer/layouts/assets/max-mac.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 557 B |
BIN
src/renderer/layouts/assets/min-mac-hover.png
Normal file
BIN
src/renderer/layouts/assets/min-mac-hover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 532 B |
BIN
src/renderer/layouts/assets/min-mac.png
Normal file
BIN
src/renderer/layouts/assets/min-mac.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 556 B |
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
345
src/renderer/layouts/default-layout/main-content.tsx
Normal file
345
src/renderer/layouts/default-layout/main-content.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
17
src/renderer/layouts/default-layout/player-bar.tsx
Normal file
17
src/renderer/layouts/default-layout/player-bar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
251
src/renderer/layouts/window-bar.tsx
Normal file
251
src/renderer/layouts/window-bar.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Reference in a new issue