378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } 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 throttle from 'lodash/throttle';
|
|
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 { Playerbar } from '/@/renderer/features/player';
|
|
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
|
|
import { AppRoute } from '/@/renderer/router/routes';
|
|
import { useAppStore, useAppStoreActions } from '/@/renderer/store';
|
|
import { useSettingsStore, useGeneralSettings } from '/@/renderer/store/settings.store';
|
|
import { PlaybackType } from '/@/renderer/types';
|
|
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
|
|
|
|
if (!isElectron()) {
|
|
useSettingsStore.getState().actions.setSettings({
|
|
player: {
|
|
...useSettingsStore.getState().player,
|
|
type: PlaybackType.WEB,
|
|
},
|
|
});
|
|
}
|
|
|
|
const Layout = styled.div`
|
|
display: grid;
|
|
grid-template-areas:
|
|
'main'
|
|
'player';
|
|
grid-template-rows: calc(100vh - 85px) 85px;
|
|
grid-template-columns: 1fr;
|
|
gap: 0;
|
|
height: 100%;
|
|
`;
|
|
|
|
const MainContainer = styled.main<{
|
|
leftSidebarWidth: string;
|
|
rightExpanded?: boolean;
|
|
rightSidebarWidth?: string;
|
|
shell?: boolean;
|
|
}>`
|
|
position: relative;
|
|
display: ${(props) => (props.shell ? 'flex' : 'grid')};
|
|
grid-area: main;
|
|
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.div`
|
|
position: relative;
|
|
grid-area: sidebar;
|
|
background: var(--sidebar-bg);
|
|
border-right: var(--sidebar-border);
|
|
`;
|
|
|
|
const RightSidebarContainer = styled(motion.div)`
|
|
position: relative;
|
|
grid-area: right-sidebar;
|
|
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: 100;
|
|
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(--sidebar-bg);
|
|
border-left: var(--sidebar-border);
|
|
`;
|
|
|
|
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 {
|
|
shell?: boolean;
|
|
}
|
|
|
|
export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
|
const sidebar = useAppStore((state) => state.sidebar);
|
|
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 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 - 170px)',
|
|
minWidth: '400px',
|
|
position: 'absolute',
|
|
right: 0,
|
|
top: '75px',
|
|
transition: {
|
|
duration: 0.5,
|
|
ease: 'anticipate',
|
|
},
|
|
width: '30vw',
|
|
x: '50vw',
|
|
},
|
|
open: {
|
|
borderRadius: '10px',
|
|
boxShadow: '1px 1px 10px 5px rgba(0, 0, 0, 0.3)',
|
|
height: 'calc(100vh - 170px)',
|
|
minWidth: '400px',
|
|
position: 'absolute',
|
|
right: '20px',
|
|
top: '75px',
|
|
|
|
transition: {
|
|
damping: 10,
|
|
delay: 0,
|
|
duration: 0.8,
|
|
ease: 'anticipate',
|
|
mass: 0.5,
|
|
},
|
|
width: '30vw',
|
|
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 (
|
|
<>
|
|
<Layout>
|
|
<MainContainer
|
|
leftSidebarWidth={sidebar.leftWidth}
|
|
rightExpanded={showSideQueue && sideQueueType === 'sideQueue'}
|
|
rightSidebarWidth={sidebar.rightWidth}
|
|
shell={shell}
|
|
>
|
|
{!shell && (
|
|
<>
|
|
<SidebarContainer>
|
|
<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"
|
|
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"
|
|
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>
|
|
</MainContainer>
|
|
<PlayerbarContainer>
|
|
<Playerbar />
|
|
</PlayerbarContainer>
|
|
</Layout>
|
|
</>
|
|
);
|
|
};
|
|
|
|
DefaultLayout.defaultProps = {
|
|
shell: false,
|
|
};
|