Add sidebar image option

This commit is contained in:
jeffvli
2022-10-26 21:44:25 -07:00
parent 17258e950e
commit 88e716b970
4 changed files with 289 additions and 134 deletions
@@ -1,7 +1,10 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Text } from '../../../components'; import { Group } from '@mantine/core';
import { usePlayerStore } from '../../../store'; import { motion, AnimatePresence } from 'framer-motion';
import { Font } from '../../../styles'; import { RiArrowUpSLine } from 'react-icons/ri';
import { Button, Text } from '@/renderer/components';
import { useAppStore, usePlayerStore } from '@/renderer/store';
import { Font } from '@/renderer/styles';
const LeftControlsContainer = styled.div` const LeftControlsContainer = styled.div`
display: flex; display: flex;
@@ -25,7 +28,25 @@ const MetadataStack = styled.div`
overflow: hidden; overflow: hidden;
`; `;
const Image = styled(motion.div)<{ url: string }>`
width: 70px;
height: 70px;
background-image: url(${(props) => props.url});
background-repeat: no-repeat;
background-size: cover;
button {
display: none;
}
&:hover button {
display: block;
}
`;
export const LeftControls = () => { export const LeftControls = () => {
const hideImage = useAppStore((state) => state.sidebar.image);
const setSidebar = useAppStore((state) => state.setSidebar);
const song = usePlayerStore((state) => state.current.song); const song = usePlayerStore((state) => state.current.song);
const title = song?.name; const title = song?.name;
const artists = song?.artists?.map((artist) => artist?.name).join(', '); const artists = song?.artists?.map((artist) => artist?.name).join(', ');
@@ -34,9 +55,31 @@ export const LeftControls = () => {
return ( return (
<LeftControlsContainer> <LeftControlsContainer>
<ImageWrapper> <ImageWrapper>
{song?.imageUrl && ( <AnimatePresence>
<img alt="img" height={60} src={song?.imageUrl} width={60} /> {!hideImage && (
)} <Image
key="playerbar-image"
animate={{ opacity: 1, scale: 1, x: 0 }}
exit={{ opacity: 0, y: 50 }}
initial={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
url={song?.imageUrl}
>
<Group position="right">
<Button
compact
variant="subtle"
onClick={(e) => {
e.stopPropagation();
setSidebar({ image: true });
}}
>
<RiArrowUpSLine color="white" size={20} />
</Button>
</Group>
</Image>
)}
</AnimatePresence>
</ImageWrapper> </ImageWrapper>
<MetadataStack> <MetadataStack>
<Text <Text
@@ -1,8 +1,11 @@
import { useMemo } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Stack, Group, Grid, Accordion } from '@mantine/core'; import { Stack, Group, Grid, Accordion } from '@mantine/core';
import { SpotlightProvider, openSpotlight } from '@mantine/spotlight'; import { SpotlightProvider, openSpotlight } from '@mantine/spotlight';
import { AnimatePresence, motion } from 'framer-motion';
import { import {
RiAlbumLine, RiAlbumLine,
RiArrowDownSLine,
RiArrowLeftSLine, RiArrowLeftSLine,
RiArrowRightSLine, RiArrowRightSLine,
RiDatabaseLine, RiDatabaseLine,
@@ -16,120 +19,182 @@ import {
} from 'react-icons/ri'; } from 'react-icons/ri';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Button, TextInput } from '@/renderer/components'; import { Button, TextInput } from '@/renderer/components';
import { AppRoute } from '../../../router/routes'; import { AppRoute } from '@/renderer/router/routes';
import { useAppStore, usePlayerStore } from '@/renderer/store';
import { SidebarItem } from './sidebar-item'; import { SidebarItem } from './sidebar-item';
const StyledSidebar = styled.div``; const SidebarContainer = styled.div`
height: 100%;
max-height: calc(100vh - 120px); // Account for titlebar and playerbar
`;
const Image = styled(motion.div)<{ height: string; url: string }>`
height: ${(props) => props.height};
background-image: ${(props) => `url(${props.url})`};
background-repeat: no-repeat;
background-size: cover;
transition: background-image 0.5s linear 0.2s;
button {
display: none;
}
&:hover button {
display: block;
}
`;
export const Sidebar = () => { export const Sidebar = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const playerData = usePlayerStore((state) => state.getPlayerData());
const sidebar = useAppStore((state) => state.sidebar);
const setSidebar = useAppStore((state) => state.setSidebar);
const showImage = sidebar.image;
const backgroundImage = useMemo(() => {
return playerData.current.song.imageUrl;
}, [playerData]);
return ( return (
<StyledSidebar> <SidebarContainer>
<Stack p={10}> <Stack justify="space-between" spacing={0} sx={{ height: '100%' }}>
<Grid> <Stack
<Grid.Col span={8}> sx={{
<SpotlightProvider actions={[]}> maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%',
<TextInput }}
readOnly >
icon={<RiSearchLine />} <Grid p={10}>
placeholder="Search" <Grid.Col span={8}>
rightSectionWidth={90} <SpotlightProvider actions={[]}>
onClick={() => openSpotlight()} <TextInput
/> readOnly
</SpotlightProvider> icon={<RiSearchLine />}
</Grid.Col> placeholder="Search"
<Grid.Col span={4}> rightSectionWidth={90}
<Group grow spacing={5}> onClick={() => openSpotlight()}
<Button />
px={5} </SpotlightProvider>
sx={{ color: 'var(--titlebar-fg)' }} </Grid.Col>
variant="default" <Grid.Col span={4}>
onClick={() => navigate(-1)} <Group grow spacing={5}>
> <Button
<RiArrowLeftSLine size={20} /> px={5}
</Button> sx={{ color: 'var(--titlebar-fg)' }}
<Button variant="default"
px={5} onClick={() => navigate(-1)}
sx={{ color: 'var(--titlebar-fg)' }} >
variant="default" <RiArrowLeftSLine size={20} />
onClick={() => navigate(1)} </Button>
> <Button
<RiArrowRightSLine size={20} /> px={5}
</Button> sx={{ color: 'var(--titlebar-fg)' }}
</Group> variant="default"
</Grid.Col> onClick={() => navigate(1)}
</Grid> >
<RiArrowRightSLine size={20} />
</Button>
</Group>
</Grid.Col>
</Grid>
<Stack spacing={0} sx={{ overflowY: 'auto' }}>
<SidebarItem to={AppRoute.HOME}>
<Group>
<RiHome5Line size={15} />
Home
</Group>
</SidebarItem>
<SidebarItem>
<SidebarItem.Link disabled to={AppRoute.EXPLORE}>
<Group>
<RiEyeLine />
Explore
</Group>
</SidebarItem.Link>
</SidebarItem>
<Accordion disableChevronRotation multiple>
<Accordion.Item value="library">
<Accordion.Control p="1rem">
<Group>
<RiDatabaseLine size={15} />
Library
</Group>
</Accordion.Control>
<Accordion.Panel>
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
<Group>
<RiAlbumLine />
Albums
</Group>
</SidebarItem>
<SidebarItem disabled to={AppRoute.LIBRARY_SONGS}>
<Group>
<RiMusicLine />
Tracks
</Group>
</SidebarItem>
<SidebarItem disabled to={AppRoute.LIBRARY_ALBUMARTISTS}>
<Group>
<RiUserVoiceLine />
Artists
</Group>
</SidebarItem>
<SidebarItem disabled to={AppRoute.LIBRARY_FOLDERS}>
<Group>
<RiFolder3Line />
Folders
</Group>
</SidebarItem>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="collections">
<Accordion.Control disabled p="1rem">
<Group>
<RiPlayListLine size={20} />
Collections
</Group>
</Accordion.Control>
<Accordion.Panel />
</Accordion.Item>
<Accordion.Item value="playlists">
<Accordion.Control disabled p="1rem">
<Group>
<RiPlayListLine size={20} />
Playlists
</Group>
</Accordion.Control>
<Accordion.Panel />
</Accordion.Item>
</Accordion>
</Stack>
</Stack>
<AnimatePresence>
{showImage && (
<Image
key="sidebar-image"
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
height={sidebar.leftWidth}
initial={{ opacity: 0, y: 200 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
url={backgroundImage}
>
<Group position="right">
<Button
compact
variant="subtle"
onClick={(e) => {
e.stopPropagation();
setSidebar({ image: false });
}}
>
<RiArrowDownSLine color="white" size={20} />
</Button>
</Group>
</Image>
)}
</AnimatePresence>
</Stack> </Stack>
<SidebarItem to={AppRoute.HOME}> </SidebarContainer>
<Group>
<RiHome5Line size={15} />
Home
</Group>
</SidebarItem>
<SidebarItem>
<SidebarItem.Link disabled to={AppRoute.EXPLORE}>
<Group>
<RiEyeLine />
Explore
</Group>
</SidebarItem.Link>
</SidebarItem>
<Accordion disableChevronRotation multiple>
<Accordion.Item value="library">
<Accordion.Control p="1rem">
<Group>
<RiDatabaseLine size={15} />
Library
</Group>
</Accordion.Control>
<Accordion.Panel>
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
<Group>
<RiAlbumLine />
Albums
</Group>
</SidebarItem>
<SidebarItem disabled to={AppRoute.LIBRARY_SONGS}>
<Group>
<RiMusicLine />
Tracks
</Group>
</SidebarItem>
<SidebarItem disabled to={AppRoute.LIBRARY_ALBUMARTISTS}>
<Group>
<RiUserVoiceLine />
Artists
</Group>
</SidebarItem>
<SidebarItem disabled to={AppRoute.LIBRARY_FOLDERS}>
<Group>
<RiFolder3Line />
Folders
</Group>
</SidebarItem>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="collections">
<Accordion.Control disabled p="1rem">
<Group>
<RiPlayListLine size={20} />
Collections
</Group>
</Accordion.Control>
<Accordion.Panel />
</Accordion.Item>
<Accordion.Item value="playlists">
<Accordion.Control disabled p="1rem">
<Group>
<RiPlayListLine size={20} />
Playlists
</Group>
</Accordion.Control>
<Accordion.Panel />
</Accordion.Item>
</Accordion>
</StyledSidebar>
); );
}; };
+61 -21
View File
@@ -2,9 +2,13 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Menu, Button } from '@mantine/core'; import { Menu, Button } from '@mantine/core';
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import { SideQueue } from '@/renderer/features/side-queue/components/SideQueue';
import { Titlebar } from '@/renderer/features/titlebar/components/titlebar'; import { Titlebar } from '@/renderer/features/titlebar/components/titlebar';
import { useAppStore } from '@/renderer/store'; import { useAppStore } from '@/renderer/store';
import { constrainSidebarWidth } from '@/renderer/utils'; import {
constrainRightSidebarWidth,
constrainSidebarWidth,
} from '@/renderer/utils';
import { Playerbar } from '../features/player'; import { Playerbar } from '../features/player';
import { Sidebar } from '../features/sidebar/components/sidebar'; import { Sidebar } from '../features/sidebar/components/sidebar';
@@ -26,12 +30,17 @@ const TitlebarContainer = styled.header`
-webkit-app-region: drag; -webkit-app-region: drag;
`; `;
const MainContainer = styled.main<{ leftSidebarWidth: string }>` const MainContainer = styled.main<{
leftSidebarWidth: string;
rightExpanded?: boolean;
rightSidebarWidth?: string;
}>`
display: grid; display: grid;
grid-area: main; grid-area: main;
grid-template-areas: 'sidebar .'; grid-template-areas: 'sidebar . right-sidebar';
grid-template-rows: 1fr; grid-template-rows: 1fr;
grid-template-columns: ${(props) => props.leftSidebarWidth} 1fr; grid-template-columns: ${(props) => props.leftSidebarWidth} 1fr ${(props) =>
props.rightExpanded && props.rightSidebarWidth};
gap: 0; gap: 0;
background: var(--main-bg); background: var(--main-bg);
`; `;
@@ -42,7 +51,14 @@ const SidebarContainer = styled.div`
background: var(--sidebar-bg); background: var(--sidebar-bg);
`; `;
const RightSidebarContainer = styled.div`
position: relative;
grid-area: right-sidebar;
background: var(--sidebar-bg);
`;
const PlayerbarContainer = styled.footer` const PlayerbarContainer = styled.footer`
z-index: 50;
grid-area: player; grid-area: player;
background: var(--playerbar-bg); background: var(--playerbar-bg);
`; `;
@@ -52,20 +68,16 @@ const ResizeHandle = styled.div<{
placement: 'top' | 'left' | 'bottom' | 'right'; placement: 'top' | 'left' | 'bottom' | 'right';
}>` }>`
position: absolute; 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: 3px; width: 3px;
height: 100%; height: 100%;
right: 0;
background-color: var(--sidebar-handle-bg); background-color: var(--sidebar-handle-bg);
/* border-top: ${({ placement }) =>
placement === 'top' && '1px var(--sidebar-handle-bg) solid'};
border-right: ${({ placement }) =>
placement === 'right' && '1px var(--sidebar-handle-bg) solid'};
border-bottom: ${({ placement }) =>
placement === 'bottom' && '1px var(--sidebar-handle-bg) solid'};
border-left: ${({ placement }) =>
placement === 'left' && '1px var(--sidebar-handle-bg) solid'}; */
opacity: ${(props) => (props.isResizing ? 1 : 0)};
cursor: ew-resize; cursor: ew-resize;
opacity: ${(props) => (props.isResizing ? 1 : 0)};
&:hover { &:hover {
opacity: 1; opacity: 1;
@@ -76,15 +88,19 @@ export const DefaultLayout = () => {
const sidebar = useAppStore((state) => state.sidebar); const sidebar = useAppStore((state) => state.sidebar);
const setSidebar = useAppStore((state) => state.setSidebar); const setSidebar = useAppStore((state) => state.setSidebar);
const sidebarRef = useRef(null); const sidebarRef = useRef<HTMLDivElement | null>(null);
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const [isResizingRight, setIsResizingRight] = useState(false);
const startResizing = useCallback(() => { const startResizing = useCallback((position: 'left' | 'right') => {
setIsResizing(true); if (position === 'left') return setIsResizing(true);
return setIsResizingRight(true);
}, []); }, []);
const stopResizing = useCallback(() => { const stopResizing = useCallback(() => {
setIsResizing(false); setIsResizing(false);
setIsResizingRight(false);
}, []); }, []);
const resize = useCallback( const resize = useCallback(
@@ -93,8 +109,16 @@ export const DefaultLayout = () => {
const width = `${constrainSidebarWidth(mouseMoveEvent.clientX)}px`; const width = `${constrainSidebarWidth(mouseMoveEvent.clientX)}px`;
setSidebar({ leftWidth: width }); 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, setSidebar] [isResizing, isResizingRight, setSidebar, sidebar.rightWidth]
); );
useEffect(() => { useEffect(() => {
@@ -121,19 +145,35 @@ export const DefaultLayout = () => {
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
</TitlebarContainer> </TitlebarContainer>
<MainContainer leftSidebarWidth={sidebar.leftWidth}> <MainContainer
leftSidebarWidth={sidebar.leftWidth}
rightExpanded={sidebar.rightExpanded}
rightSidebarWidth={sidebar.rightWidth}
>
<SidebarContainer> <SidebarContainer>
<ResizeHandle <ResizeHandle
ref={sidebarRef} ref={sidebarRef}
isResizing={isResizing} isResizing={isResizing}
placement="left" placement="right"
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
startResizing(); startResizing('left');
}} }}
/> />
<Sidebar /> <Sidebar />
</SidebarContainer> </SidebarContainer>
<RightSidebarContainer>
<ResizeHandle
ref={rightSidebarRef}
isResizing={isResizingRight}
placement="left"
onMouseDown={(e) => {
e.preventDefault();
startResizing('right');
}}
/>
{sidebar.rightExpanded && <SideQueue />}
</RightSidebarContainer>
<Outlet /> <Outlet />
</MainContainer> </MainContainer>
<PlayerbarContainer> <PlayerbarContainer>
+7
View File
@@ -5,14 +5,19 @@ import { Platform } from '@/renderer/types';
type SidebarProps = { type SidebarProps = {
expanded: string[]; expanded: string[];
image: boolean;
leftWidth: string; leftWidth: string;
rightExpanded: boolean;
rightWidth: string; rightWidth: string;
}; };
export interface AppState { export interface AppState {
platform: Platform; platform: Platform;
sidebar: { sidebar: {
expanded: string[]; expanded: string[];
image: boolean;
leftWidth: string; leftWidth: string;
rightExpanded: boolean;
rightWidth: string; rightWidth: string;
}; };
} }
@@ -37,7 +42,9 @@ export const useAppStore = create<AppSlice>()(
}, },
sidebar: { sidebar: {
expanded: [], expanded: [],
image: false,
leftWidth: '230px', leftWidth: '230px',
rightExpanded: false,
rightWidth: '230px', rightWidth: '230px',
}, },
})), })),