diff --git a/src/renderer/features/shared/components/resize-handle.tsx b/src/renderer/features/shared/components/resize-handle.tsx new file mode 100644 index 00000000..de1cd6fc --- /dev/null +++ b/src/renderer/features/shared/components/resize-handle.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +export 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: 4px; + height: 100%; + cursor: ew-resize; + opacity: ${(props) => (props.isResizing ? 1 : 0)}; + + &:hover { + opacity: 0.7; + } + + &::before { + position: absolute; + top: ${(props) => props.placement === 'top' && 0}; + right: ${(props) => props.placement === 'right' && 0}; + bottom: ${(props) => props.placement === 'bottom' && 0}; + left: ${(props) => props.placement === 'left' && 0}; + width: 1px; + height: 100%; + background-color: var(--sidebar-handle-bg); + content: ''; + } +`; diff --git a/src/renderer/features/shared/index.ts b/src/renderer/features/shared/index.ts index eaf5dc01..93f39eaa 100644 --- a/src/renderer/features/shared/index.ts +++ b/src/renderer/features/shared/index.ts @@ -8,3 +8,4 @@ export * from './mutations/create-favorite-mutation'; export * from './mutations/delete-favorite-mutation'; export * from './mutations/set-rating-mutation'; export * from './components/filter-bar'; +export * from './components/resize-handle'; diff --git a/src/renderer/layouts/default-layout/full-screen-overlay.tsx b/src/renderer/layouts/default-layout/full-screen-overlay.tsx new file mode 100644 index 00000000..85899947 --- /dev/null +++ b/src/renderer/layouts/default-layout/full-screen-overlay.tsx @@ -0,0 +1,18 @@ +import { AnimatePresence } from 'framer-motion'; +import { FullScreenPlayer } from '/@/renderer/features/player/components/full-screen-player'; +import { useFullScreenPlayerStore } from '/@/renderer/store'; + +export const FullScreenOverlay = () => { + const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore(); + + return ( + <> + + {isFullScreenPlayerExpanded && } + + + ); +}; diff --git a/src/renderer/layouts/default-layout/left-sidebar.tsx b/src/renderer/layouts/default-layout/left-sidebar.tsx new file mode 100644 index 00000000..d84b1241 --- /dev/null +++ b/src/renderer/layouts/default-layout/left-sidebar.tsx @@ -0,0 +1,38 @@ +import { useRef } from 'react'; +import styled from 'styled-components'; +import { ResizeHandle } from '/@/renderer/features/shared'; +import { CollapsedSidebar } from '/@/renderer/features/sidebar/components/collapsed-sidebar'; +import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar'; +import { useSidebarStore } from '/@/renderer/store'; + +const SidebarContainer = styled.aside` + position: relative; + grid-area: sidebar; + background: var(--sidebar-bg); + border-right: var(--sidebar-border); +`; + +interface LeftSidebarProps { + isResizing: boolean; + startResizing: (direction: 'left' | 'right') => void; +} + +export const LeftSidebar = ({ isResizing, startResizing }: LeftSidebarProps) => { + const sidebarRef = useRef(null); + const { collapsed } = useSidebarStore(); + + return ( + + { + e.preventDefault(); + startResizing('left'); + }} + /> + {collapsed ? : } + + ); +}; diff --git a/src/renderer/layouts/default-layout/main-content.tsx b/src/renderer/layouts/default-layout/main-content.tsx index 6acd1233..39f30ada 100644 --- a/src/renderer/layouts/default-layout/main-content.tsx +++ b/src/renderer/layouts/default-layout/main-content.tsx @@ -1,19 +1,20 @@ -import { useDisclosure, useTimeout } from '@mantine/hooks'; -import { motion, AnimatePresence, Variants } from 'framer-motion'; +import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 { CollapsedSidebar } from '../../features/sidebar/components/collapsed-sidebar'; import { AppRoute } from '/@/renderer/router/routes'; -import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store'; -import { useWindowSettings, useGeneralSettings } from '/@/renderer/store/settings.store'; -import { Platform } from '/@/renderer/types'; +import { useAppStoreActions, useSidebarStore } from '/@/renderer/store'; +import { useGeneralSettings } from '/@/renderer/store/settings.store'; import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils'; +import { LeftSidebar } from '/@/renderer/layouts/default-layout/left-sidebar'; +import { FullScreenOverlay } from '/@/renderer/layouts/default-layout/full-screen-overlay'; +import { RightSidebar } from '/@/renderer/layouts/default-layout/right-sidebar'; + +const SideDrawerQueue = lazy(() => + import('/@/renderer/layouts/default-layout/side-drawer-queue').then((module) => ({ + default: module.SideDrawerQueue, + })), +); const MINIMUM_SIDEBAR_WIDTH = 260; @@ -37,171 +38,17 @@ const MainContentContainer = styled.div<{ 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: 4px; - height: 100%; - cursor: ew-resize; - opacity: ${(props) => (props.isResizing ? 1 : 0)}; - - &:hover { - opacity: 0.7; - } - - &::before { - position: absolute; - top: ${(props) => props.placement === 'top' && 0}; - right: ${(props) => props.placement === 'right' && 0}; - bottom: ${(props) => props.placement === 'bottom' && 0}; - left: ${(props) => props.placement === 'left' && 0}; - width: 1px; - height: 100%; - background-color: var(--sidebar-handle-bg); - content: ''; - } -`; - -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.WINDOWS || Platform.MACOS - ? '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.WINDOWS || Platform.MACOS - ? '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 } = useWindowSettings(); - - const sidebarRef = useRef(null); - const rightSidebarRef = useRef(null); + const { collapsed, leftWidth, rightWidth, rightExpanded } = useSidebarStore(); + const { setSideBar } = useAppStoreActions(); + const { sideQueueType } = useGeneralSettings(); const [isResizing, setIsResizing] = useState(false); const [isResizingRight, setIsResizingRight] = useState(false); + const { showQueueDrawerButton } = useGeneralSettings(); - 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 showSideQueue = rightExpanded && location.pathname !== AppRoute.NOW_PLAYING; + const rightSidebarRef = useRef(null); const startResizing = useCallback((position: 'left' | 'right') => { if (position === 'left') return setIsResizing(true); @@ -225,13 +72,13 @@ export const MainContent = ({ shell }: { shell?: boolean }) => { setSideBar({ collapsed: false, leftWidth: constrainedWidth }); } } else if (isResizingRight) { - const start = Number(sidebar.rightWidth.split('px')[0]); + const start = Number(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], + [isResizing, isResizingRight, setSideBar, rightWidth], ); const throttledResize = useMemo(() => throttle(resize, 50), [resize]); @@ -248,123 +95,26 @@ export const MainContent = ({ shell }: { shell?: boolean }) => { return ( {!shell && ( - <> - - {isFullScreenPlayerExpanded && } - - - { - e.preventDefault(); - startResizing('left'); - }} - /> - {sidebar.collapsed ? : } - - - {isQueueDrawerButtonVisible && ( - - - - )} - - {drawer && ( - { - // The drawer will close due to the delay when setting isReorderingQueue - setTimeout(() => { - if (useAppStore.getState().isReorderingQueue) return; - drawerHandler.close(); - }, 50); - }} - > - - - )} - - - {showSideQueue && ( - <> - {sideQueueType === 'sideQueue' ? ( - - { - e.preventDefault(); - startResizing('right'); - }} - /> - - - ) : ( - { - // The drawer will close due to the delay when setting isReorderingQueue - setTimeout(() => { - if (useAppStore.getState().isReorderingQueue) return; - drawerHandler.close(); - }, 50); - }} - > - - - )} - - )} - - + }> + {showQueueDrawerButton && } + + + + )} }> diff --git a/src/renderer/layouts/default-layout/right-sidebar.tsx b/src/renderer/layouts/default-layout/right-sidebar.tsx new file mode 100644 index 00000000..49ceb827 --- /dev/null +++ b/src/renderer/layouts/default-layout/right-sidebar.tsx @@ -0,0 +1,142 @@ +import { AnimatePresence, motion, Variants } from 'framer-motion'; +import { forwardRef, Ref } from 'react'; +import { useLocation } from 'react-router'; +import styled from 'styled-components'; +import { SidebarPlayQueue, DrawerPlayQueue } from '/@/renderer/features/now-playing'; +import { ResizeHandle } from '/@/renderer/features/shared'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useGeneralSettings, useSidebarStore, useWindowSettings } from '/@/renderer/store'; +import { Platform } from '/@/renderer/types'; + +const RightSidebarContainer = styled(motion.aside)` + position: relative; + grid-area: right-sidebar; + height: 100%; + background: var(--sidebar-bg); + border-left: var(--sidebar-border); +`; + +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, + }), +}; + +const QueueDrawer = styled(motion.div)` + background: var(--main-bg); + border: 3px solid var(--generic-border-color); + border-radius: 10px; +`; + +const queueDrawerVariants: Variants = { + closed: (windowBarStyle) => ({ + height: + windowBarStyle === Platform.WINDOWS || Platform.MACOS + ? '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.WINDOWS || Platform.MACOS + ? '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, + }), +}; + +interface RightSidebarProps { + isResizing: boolean; + startResizing: (direction: 'left' | 'right') => void; +} + +export const RightSidebar = forwardRef( + ({ isResizing: isResizingRight, startResizing }: RightSidebarProps, ref: Ref) => { + const { windowBarStyle } = useWindowSettings(); + const { rightWidth, rightExpanded } = useSidebarStore(); + const { sideQueueType } = useGeneralSettings(); + const location = useLocation(); + const showSideQueue = rightExpanded && location.pathname !== AppRoute.NOW_PLAYING; + + return ( + + {showSideQueue && ( + <> + {sideQueueType === 'sideQueue' ? ( + + { + e.preventDefault(); + startResizing('right'); + }} + /> + + + ) : ( + + + + )} + + )} + + ); + }, +); diff --git a/src/renderer/layouts/default-layout/side-drawer-queue.tsx b/src/renderer/layouts/default-layout/side-drawer-queue.tsx new file mode 100644 index 00000000..84230218 --- /dev/null +++ b/src/renderer/layouts/default-layout/side-drawer-queue.tsx @@ -0,0 +1,141 @@ +import { useDisclosure, useTimeout } from '@mantine/hooks'; +import { AnimatePresence, motion, Variants } from 'framer-motion'; +import { useCallback } from 'react'; +import { TbArrowBarLeft } from 'react-icons/tb'; +import { useLocation } from 'react-router'; +import styled from 'styled-components'; +import { DrawerPlayQueue } from '/@/renderer/features/now-playing'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useAppStore, useSidebarStore } from '/@/renderer/store'; +import { Platform } from '/@/renderer/types'; + +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 QueueDrawer = styled(motion.div)` + background: var(--main-bg); + border: 3px solid var(--generic-border-color); + border-radius: 10px; +`; + +const queueDrawerVariants: Variants = { + closed: (windowBarStyle) => ({ + height: + windowBarStyle === Platform.WINDOWS || Platform.MACOS + ? '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.WINDOWS || Platform.MACOS + ? '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, + }, +}; + +export const SideDrawerQueue = () => { + const location = useLocation(); + const [drawer, drawerHandler] = useDisclosure(false); + const { rightExpanded } = useSidebarStore(); + + const drawerTimeout = useTimeout(() => drawerHandler.open(), 500); + + const handleEnterDrawerButton = useCallback(() => { + drawerTimeout.start(); + }, [drawerTimeout]); + + const handleLeaveDrawerButton = useCallback(() => { + drawerTimeout.clear(); + }, [drawerTimeout]); + + const isQueueDrawerButtonVisible = + !rightExpanded && !drawer && location.pathname !== AppRoute.NOW_PLAYING; + + return ( + <> + + {isQueueDrawerButtonVisible && ( + + + + )} + + {drawer && ( + { + // The drawer will close due to the delay when setting isReorderingQueue + setTimeout(() => { + if (useAppStore.getState().isReorderingQueue) return; + drawerHandler.close(); + }, 50); + }} + > + + + )} + + + ); +};