Add fullscreen player view (#27)
* Add store controls for fullscreen player * Normalize styles for playback config * Add fullscreen player component * Add option component * Update player controls to use option/popover components * Add esc hotkey to close player * Add usePlayerData hook
This commit is contained in:
parent
6cfdb8ff84
commit
e47fcfc62e
18 changed files with 780 additions and 62 deletions
|
@ -73,7 +73,7 @@ const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
||||||
border-radius: var(--dropdown-menu-border-radius);
|
border-radius: var(--dropdown-menu-border-radius);
|
||||||
filter: drop-shadow(0 0 5px rgb(0, 0, 0, 50%));
|
filter: drop-shadow(0 0 5px rgb(0, 0, 0, 50%));
|
||||||
|
|
||||||
*:first-child {
|
/* *:first-child {
|
||||||
border-top-left-radius: var(--dropdown-menu-border-radius);
|
border-top-left-radius: var(--dropdown-menu-border-radius);
|
||||||
border-top-right-radius: var(--dropdown-menu-border-radius);
|
border-top-right-radius: var(--dropdown-menu-border-radius);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
||||||
*:last-child {
|
*:last-child {
|
||||||
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
||||||
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
||||||
}
|
} */
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledMenuDivider = styled(MantineMenu.Divider)`
|
const StyledMenuDivider = styled(MantineMenu.Divider)`
|
||||||
|
|
|
@ -34,3 +34,4 @@ export * from './context-menu';
|
||||||
export * from './query-builder';
|
export * from './query-builder';
|
||||||
export * from './rating';
|
export * from './rating';
|
||||||
export * from './hover-card';
|
export * from './hover-card';
|
||||||
|
export * from './option';
|
||||||
|
|
32
src/renderer/components/option/index.tsx
Normal file
32
src/renderer/components/option/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { Flex, Group } from '@mantine/core';
|
||||||
|
|
||||||
|
export const Option = ({ children }: any) => {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
grow
|
||||||
|
p="0.5rem"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LabelProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Label = ({ children }: LabelProps) => {
|
||||||
|
return <Flex align="flex-start">{children}</Flex>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ControlProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Control = ({ children }: ControlProps) => {
|
||||||
|
return <Flex justify="flex-end">{children}</Flex>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Option.Label = Label;
|
||||||
|
Option.Control = Control;
|
|
@ -1,11 +1,10 @@
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { Divider, Stack } from '@mantine/core';
|
|
||||||
import { MultiSelect } from '/@/renderer/components/select';
|
import { MultiSelect } from '/@/renderer/components/select';
|
||||||
import { Slider } from '/@/renderer/components/slider';
|
import { Slider } from '/@/renderer/components/slider';
|
||||||
import { Switch } from '/@/renderer/components/switch';
|
import { Switch } from '/@/renderer/components/switch';
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/settings.store';
|
import { useSettingsStoreActions, useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import { TableColumn, TableType } from '/@/renderer/types';
|
import { TableColumn, TableType } from '/@/renderer/types';
|
||||||
|
import { Option } from '/@/renderer/components/option';
|
||||||
|
|
||||||
export const SONG_TABLE_COLUMNS = [
|
export const SONG_TABLE_COLUMNS = [
|
||||||
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
||||||
|
@ -168,42 +167,49 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<>
|
||||||
p="1rem"
|
<Option>
|
||||||
spacing="md"
|
<Option.Label>Auto-fit Columns</Option.Label>
|
||||||
>
|
<Option.Control>
|
||||||
<Stack spacing="xs">
|
<Switch
|
||||||
<Text>Table Columns</Text>
|
defaultChecked={tableConfig[type]?.autoFit}
|
||||||
<MultiSelect
|
onChange={handleUpdateAutoFit}
|
||||||
clearable
|
/>
|
||||||
data={SONG_TABLE_COLUMNS}
|
</Option.Control>
|
||||||
defaultValue={tableConfig[type]?.columns.map((column) => column.column)}
|
</Option>
|
||||||
dropdownPosition="top"
|
<Option>
|
||||||
width={300}
|
<Option.Label>Follow current song</Option.Label>
|
||||||
onChange={handleAddOrRemoveColumns}
|
<Option.Control>
|
||||||
/>
|
<Switch
|
||||||
</Stack>
|
defaultChecked={tableConfig[type]?.followCurrentSong}
|
||||||
<Stack spacing="xs">
|
onChange={handleUpdateFollow}
|
||||||
<Text>Row Height</Text>
|
/>
|
||||||
<Slider
|
</Option.Control>
|
||||||
defaultValue={tableConfig[type]?.rowHeight}
|
</Option>
|
||||||
max={100}
|
<Option>
|
||||||
min={25}
|
<Option.Control>
|
||||||
sx={{ width: 150 }}
|
<Slider
|
||||||
onChangeEnd={handleUpdateRowHeight}
|
defaultValue={tableConfig[type]?.rowHeight}
|
||||||
/>
|
label={(value) => `Item size: ${value}`}
|
||||||
</Stack>
|
max={100}
|
||||||
<Divider my="0.5rem" />
|
min={25}
|
||||||
<Switch
|
w="100%"
|
||||||
defaultChecked={tableConfig[type]?.autoFit}
|
onChangeEnd={handleUpdateRowHeight}
|
||||||
label="Auto-fit columns"
|
/>
|
||||||
onChange={handleUpdateAutoFit}
|
</Option.Control>
|
||||||
/>
|
</Option>
|
||||||
<Switch
|
<Option>
|
||||||
defaultChecked={tableConfig[type]?.followCurrentSong}
|
<Option.Control>
|
||||||
label="Follow current song"
|
<MultiSelect
|
||||||
onChange={handleUpdateFollow}
|
clearable
|
||||||
/>
|
data={SONG_TABLE_COLUMNS}
|
||||||
</Stack>
|
defaultValue={tableConfig[type]?.columns.map((column) => column.column)}
|
||||||
|
dropdownPosition="bottom"
|
||||||
|
width={300}
|
||||||
|
onChange={handleAddOrRemoveColumns}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
useCurrentSong,
|
useCurrentSong,
|
||||||
useDefaultQueue,
|
useDefaultQueue,
|
||||||
|
usePlayerControls,
|
||||||
usePreviousSong,
|
usePreviousSong,
|
||||||
useQueueControls,
|
useQueueControls,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
|
@ -53,6 +54,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
const tableConfig = useTableSettings(type);
|
const tableConfig = useTableSettings(type);
|
||||||
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
|
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
|
||||||
const playerType = usePlayerType();
|
const playerType = usePlayerType();
|
||||||
|
const { play } = usePlayerControls();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tableRef.current) {
|
if (tableRef.current) {
|
||||||
|
@ -79,6 +81,8 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playerType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer.setQueue(playerData);
|
mpvPlayer.setQueue(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
play();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = () => {
|
const handleDragStart = () => {
|
||||||
|
@ -160,7 +164,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowClassRules = useMemo<RowClassRules>(() => {
|
const rowClassRules = useMemo<RowClassRules | undefined>(() => {
|
||||||
return {
|
return {
|
||||||
'current-song': (params) => {
|
'current-song': (params) => {
|
||||||
return params.data.uniqueId === currentSong?.uniqueId;
|
return params.data.uniqueId === currentSong?.uniqueId;
|
||||||
|
@ -205,11 +209,13 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
rowDragMultiRow
|
rowDragMultiRow
|
||||||
autoFitColumns={tableConfig.autoFit}
|
autoFitColumns={tableConfig.autoFit}
|
||||||
columnDefs={columnDefs}
|
columnDefs={columnDefs}
|
||||||
|
deselectOnClickOutside={type === 'fullScreen'}
|
||||||
getRowId={(data) => data.data.uniqueId}
|
getRowId={(data) => data.data.uniqueId}
|
||||||
rowBuffer={50}
|
rowBuffer={50}
|
||||||
rowClassRules={rowClassRules}
|
rowClassRules={rowClassRules}
|
||||||
rowData={queue}
|
rowData={queue}
|
||||||
rowHeight={tableConfig.rowHeight || 40}
|
rowHeight={tableConfig.rowHeight || 40}
|
||||||
|
suppressCellFocus={type === 'fullScreen'}
|
||||||
onCellContextMenu={handleContextMenu}
|
onCellContextMenu={handleContextMenu}
|
||||||
onCellDoubleClicked={handleDoubleClick}
|
onCellDoubleClicked={handleDoubleClick}
|
||||||
onColumnMoved={handleColumnChange}
|
onColumnMoved={handleColumnChange}
|
||||||
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
import { Flex, Stack, Group, Center } from '@mantine/core';
|
||||||
|
import { useSetState } from '@mantine/hooks';
|
||||||
|
import { AnimatePresence, HTMLMotionProps, motion, Variants } from 'framer-motion';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { RiAlbumFill } from 'react-icons/ri';
|
||||||
|
import { generatePath } from 'react-router';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
import { Badge, Text, TextTitle } from '/@/renderer/components';
|
||||||
|
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { PlayerData, usePlayerData, usePlayerStore } from '/@/renderer/store';
|
||||||
|
|
||||||
|
const Image = styled(motion.img)`
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 40%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageContainer = styled(motion.div)`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 65%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const imageVariants: Variants = {
|
||||||
|
closed: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.8,
|
||||||
|
ease: 'linear',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
open: (custom) => {
|
||||||
|
const { isOpen } = custom;
|
||||||
|
return {
|
||||||
|
opacity: isOpen ? 1 : 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: 'linear',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const scaleImageUrl = (url?: string | null) => {
|
||||||
|
return url
|
||||||
|
?.replace(/&size=\d+/, '&size=800')
|
||||||
|
.replace(/\?width=\d+/, '?width=800')
|
||||||
|
.replace(/&height=\d+/, '&height=800');
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageWithPlaceholder = ({ ...props }: HTMLMotionProps<'img'>) => {
|
||||||
|
if (!props.src) {
|
||||||
|
return (
|
||||||
|
<Center
|
||||||
|
sx={{
|
||||||
|
background: 'var(--placeholder-bg)',
|
||||||
|
borderRadius: 'var(--card-default-radius)',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiAlbumFill
|
||||||
|
color="var(--placeholder-fg)"
|
||||||
|
size="25%"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Image {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullScreenPlayerImage = () => {
|
||||||
|
const { queue } = usePlayerData();
|
||||||
|
const currentSong = queue.current;
|
||||||
|
const background = useFastAverageColor(queue.current?.imageUrl, true, 'dominant');
|
||||||
|
const imageKey = `image-${background}`;
|
||||||
|
|
||||||
|
const [imageState, setImageState] = useSetState({
|
||||||
|
bottomImage: scaleImageUrl(queue.next?.imageUrl),
|
||||||
|
current: 0,
|
||||||
|
topImage: scaleImageUrl(queue.current?.imageUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubSongChange = usePlayerStore.subscribe(
|
||||||
|
(state) => [state.current.song, state.actions.getPlayerData().queue],
|
||||||
|
(state) => {
|
||||||
|
const isTop = imageState.current === 0;
|
||||||
|
const queue = state[1] as PlayerData['queue'];
|
||||||
|
|
||||||
|
const currentImageUrl = scaleImageUrl(queue.current?.imageUrl);
|
||||||
|
const nextImageUrl = scaleImageUrl(queue.next?.imageUrl);
|
||||||
|
|
||||||
|
setImageState({
|
||||||
|
bottomImage: isTop ? currentImageUrl : nextImageUrl,
|
||||||
|
current: isTop ? 1 : 0,
|
||||||
|
topImage: isTop ? nextImageUrl : currentImageUrl,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ equalityFn: (a, b) => (a[0] as QueueSong)?.id === (b[0] as QueueSong)?.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubSongChange();
|
||||||
|
};
|
||||||
|
}, [imageState, queue, setImageState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
className="full-screen-player-image-container"
|
||||||
|
direction="column"
|
||||||
|
justify="flex-start"
|
||||||
|
p="1rem"
|
||||||
|
sx={{ flex: 0.5, gap: '1rem' }}
|
||||||
|
>
|
||||||
|
<ImageContainer>
|
||||||
|
<AnimatePresence
|
||||||
|
initial={false}
|
||||||
|
mode="popLayout"
|
||||||
|
>
|
||||||
|
{imageState.current === 0 && (
|
||||||
|
<ImageWithPlaceholder
|
||||||
|
key={imageKey}
|
||||||
|
animate="open"
|
||||||
|
className="full-screen-player-image"
|
||||||
|
custom={{ isOpen: imageState.current === 0 }}
|
||||||
|
draggable={false}
|
||||||
|
exit="closed"
|
||||||
|
initial="closed"
|
||||||
|
placeholder="var(--placeholder-bg)"
|
||||||
|
src={imageState.topImage || ''}
|
||||||
|
variants={imageVariants}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<AnimatePresence
|
||||||
|
initial={false}
|
||||||
|
mode="popLayout"
|
||||||
|
>
|
||||||
|
{imageState.current === 1 && (
|
||||||
|
<ImageWithPlaceholder
|
||||||
|
key={imageKey}
|
||||||
|
animate="open"
|
||||||
|
className="full-screen-player-image"
|
||||||
|
custom={{ isOpen: imageState.current === 1 }}
|
||||||
|
draggable={false}
|
||||||
|
exit="closed"
|
||||||
|
initial="closed"
|
||||||
|
placeholder="var(--placeholder-bg)"
|
||||||
|
src={imageState.bottomImage || ''}
|
||||||
|
variants={imageVariants}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ImageContainer>
|
||||||
|
<Stack
|
||||||
|
className="full-screen-player-image-metadata"
|
||||||
|
spacing="sm"
|
||||||
|
>
|
||||||
|
<TextTitle
|
||||||
|
align="center"
|
||||||
|
order={1}
|
||||||
|
overflow="hidden"
|
||||||
|
w="100%"
|
||||||
|
weight={900}
|
||||||
|
>
|
||||||
|
{currentSong?.name}
|
||||||
|
</TextTitle>
|
||||||
|
<TextTitle
|
||||||
|
$link
|
||||||
|
align="center"
|
||||||
|
component={Link}
|
||||||
|
order={2}
|
||||||
|
overflow="hidden"
|
||||||
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
|
albumId: currentSong?.albumId || '',
|
||||||
|
})}
|
||||||
|
w="100%"
|
||||||
|
weight={600}
|
||||||
|
>
|
||||||
|
{currentSong?.album}{' '}
|
||||||
|
</TextTitle>
|
||||||
|
{currentSong?.artists?.map((artist, index) => (
|
||||||
|
<TextTitle
|
||||||
|
key={`fs-artist-${artist.id}`}
|
||||||
|
align="center"
|
||||||
|
order={4}
|
||||||
|
>
|
||||||
|
{index > 0 && (
|
||||||
|
<Text
|
||||||
|
sx={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '0 0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
$link
|
||||||
|
component={Link}
|
||||||
|
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
|
albumArtistId: artist.id,
|
||||||
|
})}
|
||||||
|
weight={600}
|
||||||
|
>
|
||||||
|
{artist.name}
|
||||||
|
</Text>
|
||||||
|
</TextTitle>
|
||||||
|
))}
|
||||||
|
<Group position="center">
|
||||||
|
{currentSong?.container && (
|
||||||
|
<Badge size="lg">
|
||||||
|
{currentSong?.container} {currentSong?.bitRate}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{currentSong?.releaseYear && <Badge size="lg">{currentSong?.releaseYear}</Badge>}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { Stack, Group, Center, Box } from '@mantine/core';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { HiOutlineQueueList } from 'react-icons/hi2';
|
||||||
|
import { RiFileMusicLine, RiFileTextLine, RiInformationFill } from 'react-icons/ri';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Button, TextTitle } from '/@/renderer/components';
|
||||||
|
import { PlayQueue } from '/@/renderer/features/now-playing';
|
||||||
|
import {
|
||||||
|
useFullScreenPlayerStore,
|
||||||
|
useFullScreenPlayerStoreActions,
|
||||||
|
} from '/@/renderer/store/full-screen-player.store';
|
||||||
|
|
||||||
|
const QueueContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.ag-theme-alpine-dark {
|
||||||
|
--ag-header-background-color: rgba(0, 0, 0, 0%) !important;
|
||||||
|
--ag-background-color: rgba(0, 0, 0, 0%) !important;
|
||||||
|
--ag-odd-row-background-color: rgba(0, 0, 0, 0%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-header {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ActiveTabIndicator = styled(motion.div)`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--main-fg);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FullScreenPlayerQueue = () => {
|
||||||
|
const { activeTab } = useFullScreenPlayerStore();
|
||||||
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
|
|
||||||
|
const headerItems = [
|
||||||
|
{
|
||||||
|
active: activeTab === 'queue',
|
||||||
|
icon: <RiFileMusicLine size="1.5rem" />,
|
||||||
|
label: 'Up Next',
|
||||||
|
onClick: () => setStore({ activeTab: 'queue' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: activeTab === 'related',
|
||||||
|
icon: <HiOutlineQueueList size="1.5rem" />,
|
||||||
|
label: 'Related',
|
||||||
|
onClick: () => setStore({ activeTab: 'related' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: activeTab === 'lyrics',
|
||||||
|
icon: <RiFileTextLine size="1.5rem" />,
|
||||||
|
label: 'Lyrics',
|
||||||
|
onClick: () => setStore({ activeTab: 'lyrics' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
className="full-screen-player-queue-container"
|
||||||
|
maw="100%"
|
||||||
|
sx={{ flex: 0.5 }}
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
grow
|
||||||
|
align="center"
|
||||||
|
position="center"
|
||||||
|
>
|
||||||
|
{headerItems.map((item) => (
|
||||||
|
<Box pos="relative">
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
uppercase
|
||||||
|
fw="600"
|
||||||
|
pos="relative"
|
||||||
|
size="lg"
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
color: item.active
|
||||||
|
? 'var(--main-fg) !important'
|
||||||
|
: 'var(--main-fg-secondary) !important',
|
||||||
|
letterSpacing: '1px',
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={item.onClick}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Button>
|
||||||
|
{item.active ? <ActiveTabIndicator layoutId="underline" /> : null}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
{activeTab === 'queue' ? (
|
||||||
|
<QueueContainer>
|
||||||
|
<PlayQueue type="fullScreen" />
|
||||||
|
</QueueContainer>
|
||||||
|
) : activeTab === 'related' ? (
|
||||||
|
<Center>
|
||||||
|
<Group>
|
||||||
|
<RiInformationFill size="2rem" />
|
||||||
|
<TextTitle
|
||||||
|
order={3}
|
||||||
|
weight={700}
|
||||||
|
>
|
||||||
|
COMING SOON
|
||||||
|
</TextTitle>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
) : activeTab === 'lyrics' ? (
|
||||||
|
<Center>
|
||||||
|
<Group>
|
||||||
|
<RiInformationFill size="2rem" />
|
||||||
|
<TextTitle
|
||||||
|
order={3}
|
||||||
|
weight={700}
|
||||||
|
>
|
||||||
|
COMING SOON
|
||||||
|
</TextTitle>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
194
src/renderer/features/player/components/full-screen-player.tsx
Normal file
194
src/renderer/features/player/components/full-screen-player.tsx
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
import { Group } from '@mantine/core';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
|
import { Variants, motion } from 'framer-motion';
|
||||||
|
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Button, Option, Popover, Switch, TableConfigDropdown } from '/@/renderer/components';
|
||||||
|
import {
|
||||||
|
useCurrentSong,
|
||||||
|
useFullScreenPlayerStore,
|
||||||
|
useFullScreenPlayerStoreActions,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { useFastAverageColor } from '../../../hooks/use-fast-average-color';
|
||||||
|
import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image';
|
||||||
|
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
|
||||||
|
|
||||||
|
const Container = styled(motion.div)`
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ResponsiveContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 2560px;
|
||||||
|
margin-top: 70px;
|
||||||
|
|
||||||
|
.full-screen-player-image {
|
||||||
|
max-height: calc(35vh - 90px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1080px) {
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.full-screen-player-image {
|
||||||
|
max-height: calc(70vh - 90px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-height: 800px) and (min-width: 1080px) {
|
||||||
|
.full-screen-player-image {
|
||||||
|
max-height: calc(50vh - 90px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BackgroundImageOverlay = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(180deg, rgba(20, 21, 23, 40%), var(--main-bg));
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Controls = () => {
|
||||||
|
const { dynamicBackground, expanded } = useFullScreenPlayerStore();
|
||||||
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
|
|
||||||
|
const handleToggleFullScreenPlayer = () => {
|
||||||
|
setStore({ expanded: !expanded });
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys([['Escape', handleToggleFullScreenPlayer]]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group
|
||||||
|
p="1rem"
|
||||||
|
pos="absolute"
|
||||||
|
spacing="sm"
|
||||||
|
sx={{
|
||||||
|
left: 0,
|
||||||
|
top: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Minimize' }}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleToggleFullScreenPlayer}
|
||||||
|
>
|
||||||
|
<RiArrowDownSLine size="2rem" />
|
||||||
|
</Button>
|
||||||
|
<Popover position="bottom-start">
|
||||||
|
<Popover.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Configure' }}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<RiSettings3Line size="1.5rem" />
|
||||||
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Option>
|
||||||
|
<Option.Label>Dynamic Background</Option.Label>
|
||||||
|
<Option.Control>
|
||||||
|
<Switch
|
||||||
|
defaultChecked={dynamicBackground}
|
||||||
|
onChange={(e) =>
|
||||||
|
setStore({
|
||||||
|
dynamicBackground: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Option.Control>
|
||||||
|
</Option>
|
||||||
|
<TableConfigDropdown type="fullScreen" />
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
closed: {
|
||||||
|
height: 'calc(100vh - 90px)',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '100vh',
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
},
|
||||||
|
width: '100vw',
|
||||||
|
y: -100,
|
||||||
|
},
|
||||||
|
open: (custom) => {
|
||||||
|
const { dynamicBackground, background } = custom;
|
||||||
|
return {
|
||||||
|
background: dynamicBackground ? background : 'var(--main-bg)',
|
||||||
|
height: 'calc(100vh - 90px)',
|
||||||
|
left: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
transition: {
|
||||||
|
background: {
|
||||||
|
duration: 1,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
},
|
||||||
|
delay: 0.1,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
},
|
||||||
|
width: '100vw',
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullScreenPlayer = () => {
|
||||||
|
const { dynamicBackground } = useFullScreenPlayerStore();
|
||||||
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const isOpenedRef = useRef<boolean | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (isOpenedRef.current !== null) {
|
||||||
|
setStore({ expanded: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpenedRef.current = true;
|
||||||
|
}, [location, setStore]);
|
||||||
|
|
||||||
|
const currentSong = useCurrentSong();
|
||||||
|
const background = useFastAverageColor(currentSong?.imageUrl, true, 'dominant');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
animate="open"
|
||||||
|
custom={{ background, dynamicBackground }}
|
||||||
|
exit="closed"
|
||||||
|
initial="closed"
|
||||||
|
variants={containerVariants}
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
{dynamicBackground && <BackgroundImageOverlay />}
|
||||||
|
<ResponsiveContainer>
|
||||||
|
<FullScreenPlayerImage />
|
||||||
|
<FullScreenPlayerQueue />
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { MouseEvent } from 'react';
|
||||||
import { Center, Group } from '@mantine/core';
|
import { Center, Group } from '@mantine/core';
|
||||||
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
|
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||||
import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri';
|
import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri';
|
||||||
|
@ -6,7 +6,13 @@ import { generatePath, Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, Text } from '/@/renderer/components';
|
import { Button, Text } from '/@/renderer/components';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAppStoreActions, useAppStore, useCurrentSong } from '/@/renderer/store';
|
import {
|
||||||
|
useAppStoreActions,
|
||||||
|
useAppStore,
|
||||||
|
useCurrentSong,
|
||||||
|
useSetFullScreenPlayerStore,
|
||||||
|
useFullScreenPlayerStore,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { fadeIn } from '/@/renderer/styles';
|
import { fadeIn } from '/@/renderer/styles';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
|
@ -35,7 +41,7 @@ const MetadataStack = styled(motion.div)`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled(motion(Link))`
|
const Image = styled(motion.div)`
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
filter: drop-shadow(0 0 5px rgb(0, 0, 0, 100%));
|
filter: drop-shadow(0 0 5px rgb(0, 0, 0, 100%));
|
||||||
|
@ -75,6 +81,8 @@ const LineItem = styled.div<{ $secondary?: boolean }>`
|
||||||
|
|
||||||
export const LeftControls = () => {
|
export const LeftControls = () => {
|
||||||
const { setSidebar } = useAppStoreActions();
|
const { setSidebar } = useAppStoreActions();
|
||||||
|
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
|
||||||
|
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
||||||
const hideImage = useAppStore((state) => state.sidebar.image);
|
const hideImage = useAppStore((state) => state.sidebar.image);
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const title = currentSong?.name;
|
const title = currentSong?.name;
|
||||||
|
@ -87,6 +95,16 @@ export const LeftControls = () => {
|
||||||
SONG_CONTEXT_MENU_ITEMS,
|
SONG_CONTEXT_MENU_ITEMS,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleToggleFullScreenPlayer = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleSidebarImage = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSidebar({ image: true });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftControlsContainer>
|
<LeftControlsContainer>
|
||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
|
@ -101,8 +119,9 @@ export const LeftControls = () => {
|
||||||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: -50 }}
|
exit={{ opacity: 0, x: -50 }}
|
||||||
initial={{ opacity: 0, x: -50 }}
|
initial={{ opacity: 0, x: -50 }}
|
||||||
to={AppRoute.NOW_PLAYING}
|
role="button"
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
onClick={handleToggleFullScreenPlayer}
|
||||||
>
|
>
|
||||||
{currentSong?.imageUrl ? (
|
{currentSong?.imageUrl ? (
|
||||||
<PlayerbarImage
|
<PlayerbarImage
|
||||||
|
@ -133,10 +152,7 @@ export const LeftControls = () => {
|
||||||
sx={{ position: 'absolute', right: 2, top: 2 }}
|
sx={{ position: 'absolute', right: 2, top: 2 }}
|
||||||
tooltip={{ label: 'Expand', openDelay: 500 }}
|
tooltip={{ label: 'Expand', openDelay: 500 }}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={(e) => {
|
onClick={handleToggleSidebarImage}
|
||||||
e.preventDefault();
|
|
||||||
setSidebar({ image: true });
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RiArrowUpSLine
|
<RiArrowUpSLine
|
||||||
color="white"
|
color="white"
|
||||||
|
|
|
@ -34,6 +34,8 @@ import {
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
useCurrentSong,
|
useCurrentSong,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
|
useSetFullScreenPlayerStore,
|
||||||
|
useFullScreenPlayerStore,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { fadeIn } from '/@/renderer/styles';
|
import { fadeIn } from '/@/renderer/styles';
|
||||||
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
|
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
|
||||||
|
@ -48,7 +50,7 @@ const SidebarContainer = styled.div`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageContainer = styled(motion(Link))<{ height: string }>`
|
const ImageContainer = styled(motion.div)<{ height: string }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
height: ${(props) => props.height};
|
height: ${(props) => props.height};
|
||||||
|
|
||||||
|
@ -112,6 +114,12 @@ export const Sidebar = () => {
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
||||||
|
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
|
||||||
|
const expandFullScreenPlayer = () => {
|
||||||
|
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });
|
||||||
|
};
|
||||||
|
|
||||||
const cq = useContainerQuery({ sm: 300 });
|
const cq = useContainerQuery({ sm: 300 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -327,8 +335,9 @@ export const Sidebar = () => {
|
||||||
exit={{ opacity: 0, y: 200 }}
|
exit={{ opacity: 0, y: 200 }}
|
||||||
height={sidebar.leftWidth}
|
height={sidebar.leftWidth}
|
||||||
initial={{ opacity: 0, y: 200 }}
|
initial={{ opacity: 0, y: 200 }}
|
||||||
to={AppRoute.NOW_PLAYING}
|
role="button"
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
onClick={expandFullScreenPlayer}
|
||||||
>
|
>
|
||||||
{upsizedImageUrl ? (
|
{upsizedImageUrl ? (
|
||||||
<SidebarImage
|
<SidebarImage
|
||||||
|
@ -352,7 +361,7 @@ export const Sidebar = () => {
|
||||||
tooltip={{ label: 'Collapse', openDelay: 500 }}
|
tooltip={{ label: 'Collapse', openDelay: 500 }}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.stopPropagation();
|
||||||
setSidebar({ image: false });
|
setSidebar({ image: false });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,10 +11,11 @@ import { DrawerPlayQueue, SidebarPlayQueue } from '/@/renderer/features/now-play
|
||||||
import { Playerbar } from '/@/renderer/features/player';
|
import { Playerbar } from '/@/renderer/features/player';
|
||||||
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
|
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAppStore, useAppStoreActions } from '/@/renderer/store';
|
import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store';
|
||||||
import { useSettingsStore, useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useSettingsStore, useGeneralSettings } from '/@/renderer/store/settings.store';
|
||||||
import { PlaybackType } from '/@/renderer/types';
|
import { PlaybackType } from '/@/renderer/types';
|
||||||
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
|
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({
|
||||||
|
@ -84,7 +85,7 @@ const ResizeHandle = styled.div<{
|
||||||
right: ${(props) => props.placement === 'right' && 0};
|
right: ${(props) => props.placement === 'right' && 0};
|
||||||
bottom: ${(props) => props.placement === 'bottom' && 0};
|
bottom: ${(props) => props.placement === 'bottom' && 0};
|
||||||
left: ${(props) => props.placement === 'left' && 0};
|
left: ${(props) => props.placement === 'left' && 0};
|
||||||
z-index: 100;
|
z-index: 90;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--sidebar-handle-bg);
|
background-color: var(--sidebar-handle-bg);
|
||||||
|
@ -147,6 +148,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
||||||
location.pathname !== AppRoute.NOW_PLAYING;
|
location.pathname !== AppRoute.NOW_PLAYING;
|
||||||
|
|
||||||
const showSideQueue = sidebar.rightExpanded && location.pathname !== AppRoute.NOW_PLAYING;
|
const showSideQueue = sidebar.rightExpanded && location.pathname !== AppRoute.NOW_PLAYING;
|
||||||
|
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
|
||||||
|
|
||||||
const queueDrawerButtonVariants: Variants = {
|
const queueDrawerButtonVariants: Variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
|
@ -259,6 +261,12 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
||||||
>
|
>
|
||||||
{!shell && (
|
{!shell && (
|
||||||
<>
|
<>
|
||||||
|
<AnimatePresence
|
||||||
|
initial={false}
|
||||||
|
mode="wait"
|
||||||
|
>
|
||||||
|
{isFullScreenPlayerExpanded && <FullScreenPlayer />}
|
||||||
|
</AnimatePresence>
|
||||||
<SidebarContainer id="sidebar">
|
<SidebarContainer id="sidebar">
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
|
|
46
src/renderer/store/full-screen-player.store.ts
Normal file
46
src/renderer/store/full-screen-player.store.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import create from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
import { immer } from 'zustand/middleware/immer';
|
||||||
|
|
||||||
|
interface FullScreenPlayerState {
|
||||||
|
activeTab: string | 'queue' | 'related' | 'lyrics';
|
||||||
|
dynamicBackground?: boolean;
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullScreenPlayerSlice extends FullScreenPlayerState {
|
||||||
|
actions: {
|
||||||
|
setStore: (data: Partial<FullScreenPlayerSlice>) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFullScreenPlayerStore = create<FullScreenPlayerSlice>()(
|
||||||
|
persist(
|
||||||
|
devtools(
|
||||||
|
immer((set, get) => ({
|
||||||
|
actions: {
|
||||||
|
setStore: (data) => {
|
||||||
|
set({ ...get(), ...data });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activeTab: 'queue',
|
||||||
|
expanded: false,
|
||||||
|
})),
|
||||||
|
{ name: 'store_full_screen_player' },
|
||||||
|
),
|
||||||
|
{
|
||||||
|
merge: (persistedState, currentState) => {
|
||||||
|
return merge(currentState, persistedState);
|
||||||
|
},
|
||||||
|
name: 'store_full_screen_player',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useFullScreenPlayerStoreActions = () =>
|
||||||
|
useFullScreenPlayerStore((state) => state.actions);
|
||||||
|
|
||||||
|
export const useSetFullScreenPlayerStore = () =>
|
||||||
|
useFullScreenPlayerStore((state) => state.actions.setStore);
|
|
@ -5,3 +5,4 @@ export * from './list.store';
|
||||||
export * from './playlist.store';
|
export * from './playlist.store';
|
||||||
export * from './album-list-data.store';
|
export * from './album-list-data.store';
|
||||||
export * from './album-artist-list-data.store';
|
export * from './album-artist-list-data.store';
|
||||||
|
export * from './full-screen-player.store';
|
||||||
|
|
|
@ -892,6 +892,12 @@ export const useDefaultQueue = () => usePlayerStore((state) => state.queue.defau
|
||||||
|
|
||||||
export const useCurrentSong = () => usePlayerStore((state) => state.current.song);
|
export const useCurrentSong = () => usePlayerStore((state) => state.current.song);
|
||||||
|
|
||||||
|
export const usePlayerData = () =>
|
||||||
|
usePlayerStore(
|
||||||
|
(state) => state.actions.getPlayerData(),
|
||||||
|
(a, b) => a.current.nextIndex === b.current.nextIndex,
|
||||||
|
);
|
||||||
|
|
||||||
export const useCurrentPlayer = () => usePlayerStore((state) => state.current.player);
|
export const useCurrentPlayer = () => usePlayerStore((state) => state.current.player);
|
||||||
|
|
||||||
export const useCurrentStatus = () => usePlayerStore((state) => state.current.status);
|
export const useCurrentStatus = () => usePlayerStore((state) => state.current.status);
|
||||||
|
|
|
@ -61,6 +61,7 @@ export interface SettingsState {
|
||||||
};
|
};
|
||||||
tab: 'general' | 'playback' | 'view' | string;
|
tab: 'general' | 'playback' | 'view' | string;
|
||||||
tables: {
|
tables: {
|
||||||
|
fullScreen: DataTableProps;
|
||||||
nowPlaying: DataTableProps;
|
nowPlaying: DataTableProps;
|
||||||
sideDrawerQueue: DataTableProps;
|
sideDrawerQueue: DataTableProps;
|
||||||
sideQueue: DataTableProps;
|
sideQueue: DataTableProps;
|
||||||
|
@ -116,6 +117,25 @@ export const useSettingsStore = create<SettingsSlice>()(
|
||||||
|
|
||||||
tab: 'general',
|
tab: 'general',
|
||||||
tables: {
|
tables: {
|
||||||
|
fullScreen: {
|
||||||
|
autoFit: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
column: TableColumn.TITLE_COMBINED,
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.DURATION,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.USER_FAVORITE,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
followCurrentSong: true,
|
||||||
|
rowHeight: 60,
|
||||||
|
},
|
||||||
nowPlaying: {
|
nowPlaying: {
|
||||||
autoFit: true,
|
autoFit: true,
|
||||||
columns: [
|
columns: [
|
||||||
|
|
|
@ -48,21 +48,21 @@
|
||||||
--btn-primary-fg: #ffffff;
|
--btn-primary-fg: #ffffff;
|
||||||
--btn-primary-fg-hover: #ffffff;
|
--btn-primary-fg-hover: #ffffff;
|
||||||
--btn-primary-border: none;
|
--btn-primary-border: none;
|
||||||
--btn-primary-radius: 0;
|
--btn-primary-radius: 4px;
|
||||||
|
|
||||||
--btn-default-bg: rgb(31, 31, 32);
|
--btn-default-bg: rgb(31, 31, 32);
|
||||||
--btn-default-bg-hover: rgb(63, 63, 63);
|
--btn-default-bg-hover: rgb(63, 63, 63);
|
||||||
--btn-default-fg: rgb(193, 193, 193);
|
--btn-default-fg: rgb(193, 193, 193);
|
||||||
--btn-default-fg-hover: rgb(193, 193, 193);
|
--btn-default-fg-hover: rgb(193, 193, 193);
|
||||||
--btn-default-border: none;
|
--btn-default-border: none;
|
||||||
--btn-default-radius: 0;
|
--btn-default-radius: 2px;
|
||||||
|
|
||||||
--btn-subtle-bg: transparent;
|
--btn-subtle-bg: transparent;
|
||||||
--btn-subtle-bg-hover: transparent;
|
--btn-subtle-bg-hover: transparent;
|
||||||
--btn-subtle-fg: rgb(220, 220, 220);
|
--btn-subtle-fg: rgb(220, 220, 220);
|
||||||
--btn-subtle-fg-hover: rgb(255, 255, 255);
|
--btn-subtle-fg-hover: rgb(255, 255, 255);
|
||||||
--btn-subtle-border: none;
|
--btn-subtle-border: none;
|
||||||
--btn-subtle-radius: 0;
|
--btn-subtle-radius: 4px;
|
||||||
|
|
||||||
--btn-outline-bg: transparent;
|
--btn-outline-bg: transparent;
|
||||||
--btn-outline-bg-hover: transparent;
|
--btn-outline-bg-hover: transparent;
|
||||||
|
@ -77,13 +77,13 @@
|
||||||
--input-active-fg: rgb(193, 193, 193);
|
--input-active-fg: rgb(193, 193, 193);
|
||||||
--input-active-bg: rgba(255, 255, 255, 0.1);
|
--input-active-bg: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
--dropdown-menu-bg: rgb(45, 45, 45);
|
--dropdown-menu-bg: rgb(32, 32, 32);
|
||||||
--dropdown-menu-fg: rgb(235, 235, 235);
|
--dropdown-menu-fg: rgb(235, 235, 235);
|
||||||
--dropdown-menu-item-padding: 0.8rem;
|
--dropdown-menu-item-padding: 0.8rem;
|
||||||
--dropdown-menu-item-font-size: 1rem;
|
--dropdown-menu-item-font-size: 1rem;
|
||||||
--dropdown-menu-bg-hover: rgb(62, 62, 62);
|
--dropdown-menu-bg-hover: rgb(62, 62, 62);
|
||||||
--dropdown-menu-border: 1px var(--generic-border-color) solid;
|
--dropdown-menu-border: 1px var(--generic-border-color) solid;
|
||||||
--dropdown-menu-border-radius: 0;
|
--dropdown-menu-border-radius: 4px;
|
||||||
|
|
||||||
--switch-track-bg: rgb(50, 50, 50);
|
--switch-track-bg: rgb(50, 50, 50);
|
||||||
--switch-track-enabled-bg: var(--primary-color);
|
--switch-track-enabled-bg: var(--primary-color);
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
|
|
||||||
--paper-bg: rgb(20, 20, 20);
|
--paper-bg: rgb(20, 20, 20);
|
||||||
|
|
||||||
--placeholder-bg: rgba(53, 53, 53, 0.5);
|
--placeholder-bg: rgba(53, 53, 53, 1);
|
||||||
--placeholder-fg: rgba(126, 126, 126);
|
--placeholder-fg: rgba(126, 126, 126);
|
||||||
|
|
||||||
--card-default-bg: rgb(32, 32, 32);
|
--card-default-bg: rgb(32, 32, 32);
|
||||||
|
|
|
@ -52,6 +52,13 @@ body[data-theme='defaultLight'] {
|
||||||
--btn-subtle-fg: rgb(80, 80, 80);
|
--btn-subtle-fg: rgb(80, 80, 80);
|
||||||
--btn-subtle-fg-hover: rgb(0, 0, 0);
|
--btn-subtle-fg-hover: rgb(0, 0, 0);
|
||||||
|
|
||||||
|
--btn-outline-bg: transparent;
|
||||||
|
--btn-outline-bg-hover: transparent;
|
||||||
|
--btn-outline-fg: rgb(60, 60, 60);
|
||||||
|
--btn-outline-fg-hover: rgb(0, 0, 0);
|
||||||
|
--btn-outline-border: 1px rgba(140, 140, 140, 0.5) solid;
|
||||||
|
--btn-outline-radius: 1px;
|
||||||
|
|
||||||
--input-bg: rgb(240, 241, 242);
|
--input-bg: rgb(240, 241, 242);
|
||||||
--input-fg: rgb(0, 0, 0);
|
--input-fg: rgb(0, 0, 0);
|
||||||
--input-placeholder-fg: rgb(119, 126, 139);
|
--input-placeholder-fg: rgb(119, 126, 139);
|
||||||
|
|
|
@ -18,7 +18,7 @@ export type CardRoute = {
|
||||||
slugs?: RouteSlug[];
|
slugs?: RouteSlug[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TableType = 'nowPlaying' | 'sideQueue' | 'sideDrawerQueue' | 'songs';
|
export type TableType = 'nowPlaying' | 'sideQueue' | 'sideDrawerQueue' | 'songs' | 'fullScreen';
|
||||||
|
|
||||||
export type CardRow<T> = {
|
export type CardRow<T> = {
|
||||||
arrayProperty?: string;
|
arrayProperty?: string;
|
||||||
|
|
Reference in a new issue