Add shuffle all feature

This commit is contained in:
jeffvli 2023-05-21 07:33:22 -07:00
parent ba6f2a1637
commit debdb92dcf
3 changed files with 314 additions and 23 deletions

View file

@ -1,9 +1,11 @@
import { useEffect, useState } from 'react';
import { useHotkeys } from '@mantine/hooks';
import { useQueryClient } from '@tanstack/react-query';
import formatDuration from 'format-duration';
import isElectron from 'is-electron';
import { IoIosPause } from 'react-icons/io';
import {
RiMenuAddFill,
RiPlayFill,
RiRepeat2Line,
RiRepeatOneLine,
@ -34,6 +36,8 @@ import {
} from '/@/renderer/store/settings.store';
import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types';
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
import { openShuffleAllModal } from './shuffle-all-modal';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
interface CenterControlsProps {
playersRef: any;
@ -72,6 +76,7 @@ const SliderWrapper = styled.div`
`;
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const queryClient = useQueryClient();
const [isSeeking, setIsSeeking] = useState(false);
const currentSong = useCurrentSong();
const songDuration = currentSong?.duration;
@ -99,6 +104,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
handlePause,
handlePlay,
} = useCenterControls({ playersRef });
const handlePlayQueueAdd = usePlayQueueAdd();
const currentTime = useCurrentTime();
const currentPlayerRef = player === 1 ? player1 : player2;
@ -237,6 +243,21 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
variant="tertiary"
onClick={handleToggleRepeat}
/>
<PlayerButton
icon={<RiMenuAddFill size={15} />}
tooltip={{
label: 'Shuffle all',
openDelay: 500,
}}
variant="tertiary"
onClick={() =>
openShuffleAllModal({
handlePlayQueueAdd,
queryClient,
})
}
/>
</ButtonsContainer>
</ControlsContainer>
<SliderContainer>

View file

@ -1,5 +1,5 @@
/* stylelint-disable no-descending-specificity */
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react';
import type { TooltipProps, UnstyledButtonProps } from '@mantine/core';
import { UnstyledButton } from '@mantine/core';
import { motion } from 'framer-motion';
@ -118,33 +118,41 @@ const StyledPlayerButton = styled(UnstyledButton)<StyledPlayerButtonProps>`
: ButtonTertiaryVariant};
`;
export const PlayerButton = ({ tooltip, variant, icon, ...rest }: PlayerButtonProps) => {
if (tooltip) {
return (
<Tooltip {...tooltip}>
<MotionWrapper variant={variant}>
<StyledPlayerButton
export const PlayerButton = forwardRef<HTMLDivElement, PlayerButtonProps>(
({ tooltip, variant, icon, ...rest }: PlayerButtonProps, ref) => {
if (tooltip) {
return (
<Tooltip {...tooltip}>
<MotionWrapper
ref={ref}
variant={variant}
{...rest}
>
{icon}
</StyledPlayerButton>
</MotionWrapper>
</Tooltip>
);
}
<StyledPlayerButton
variant={variant}
{...rest}
>
{icon}
</StyledPlayerButton>
</MotionWrapper>
</Tooltip>
);
}
return (
<MotionWrapper variant={variant}>
<StyledPlayerButton
return (
<MotionWrapper
ref={ref}
variant={variant}
{...rest}
>
{icon}
</StyledPlayerButton>
</MotionWrapper>
);
};
<StyledPlayerButton
variant={variant}
{...rest}
>
{icon}
</StyledPlayerButton>
</MotionWrapper>
);
},
);
PlayerButton.defaultProps = {
$isActive: false,

View file

@ -0,0 +1,262 @@
import { useMemo } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { QueryClient } from '@tanstack/react-query';
import merge from 'lodash/merge';
import { RiAddBoxFill, RiPlayFill, RiAddCircleFill } from 'react-icons/ri';
import { Button, Checkbox, NumberInput, Select } from '/@/renderer/components';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import {
GenreListResponse,
RandomSongListQuery,
MusicFolderListResponse,
ServerType,
} from '/@/renderer/api/types';
import { api } from '/@/renderer/api';
import { useAuthStore } from '/@/renderer/store';
import { queryKeys } from '/@/renderer/api/query-keys';
import { Play, PlayQueueAddOptions, ServerListItem } from '/@/renderer/types';
interface ShuffleAllSlice extends RandomSongListQuery {
actions: {
setStore: (data: Partial<ShuffleAllSlice>) => void;
};
enableMaxYear: boolean;
enableMinYear: boolean;
}
const useShuffleAllStore = create<ShuffleAllSlice>()(
persist(
immer((set, get) => ({
actions: {
setStore: (data) => {
set({ ...get(), ...data });
},
},
enableMaxYear: false,
enableMinYear: false,
genre: '',
maxYear: 2020,
minYear: 2000,
musicFolder: '',
songCount: 100,
})),
{
merge: (persistedState, currentState) => merge(currentState, persistedState),
name: 'store_shuffle_all',
version: 1,
},
),
);
export const useShuffleAllStoreActions = () => useShuffleAllStore((state) => state.actions);
interface ShuffleAllModalProps {
genres: GenreListResponse | undefined;
handlePlayQueueAdd: ((options: PlayQueueAddOptions) => void) | undefined;
musicFolders: MusicFolderListResponse | undefined;
queryClient: QueryClient;
server: ServerListItem | null;
}
export const ShuffleAllModal = ({
handlePlayQueueAdd,
queryClient,
server,
genres,
musicFolders,
}: ShuffleAllModalProps) => {
const { genre, limit, maxYear, minYear, enableMaxYear, enableMinYear, musicFolderId } =
useShuffleAllStore();
const { setStore } = useShuffleAllStoreActions();
const handlePlay = async (playType: Play) => {
const res = await queryClient.fetchQuery({
cacheTime: 0,
queryFn: ({ signal }) =>
api.controller.getRandomSongList({
apiClientProps: {
server,
signal,
},
query: {
genre: genre || undefined,
limit,
maxYear: enableMaxYear ? maxYear || undefined : undefined,
minYear: enableMinYear ? minYear || undefined : undefined,
musicFolderId: musicFolderId || undefined,
},
}),
queryKey: queryKeys.songs.randomSongList(server?.id),
staleTime: 0,
});
handlePlayQueueAdd?.({
byData: res?.items || [],
playType,
});
closeAllModals();
};
const genreData = useMemo(() => {
if (!genres) return [];
return genres.items.map((genre) => {
const value =
server?.type === ServerType.NAVIDROME || server?.type === ServerType.SUBSONIC
? genre.name
: genre.id;
return {
label: genre.name,
value,
};
});
}, [genres, server?.type]);
const musicFolderData = useMemo(() => {
if (!musicFolders) return [];
return musicFolders.items.map((musicFolder) => ({
label: musicFolder.name,
value: String(musicFolder.id),
}));
}, [musicFolders]);
return (
<Stack spacing="md">
<NumberInput
required
label="How many tracks?"
max={500}
min={1}
value={limit}
onChange={(e) => setStore({ limit: e ? Number(e) : 0 })}
/>
<Group grow>
<NumberInput
disabled={!enableMinYear}
label="From year"
max={2050}
min={1850}
rightSection={
<Checkbox
checked={enableMinYear}
mr="0.5rem"
onChange={(e) => setStore({ enableMinYear: e.currentTarget.checked })}
/>
}
value={minYear}
onChange={(e) => setStore({ minYear: e ? Number(e) : 0 })}
/>
<NumberInput
disabled={!enableMaxYear}
label="To year"
max={2050}
min={1850}
rightSection={
<Checkbox
checked={enableMaxYear}
mr="0.5rem"
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
/>
}
value={maxYear}
onChange={(e) => setStore({ maxYear: e ? Number(e) : 0 })}
/>
</Group>
<Select
clearable
data={genreData}
label="Genre"
value={genre}
onChange={(e) => setStore({ genre: e || '' })}
/>
<Select
clearable
data={musicFolderData}
label="Music folder"
value={musicFolderId}
onChange={(e) => {
console.log('e :>> ', e);
setStore({ musicFolderId: e ? String(e) : '' });
}}
/>
<Divider />
<Group grow>
<Button
leftIcon={<RiAddBoxFill size="1rem" />}
type="submit"
variant="default"
onClick={() => handlePlay(Play.LAST)}
>
Add
</Button>
<Button
leftIcon={<RiAddCircleFill size="1rem" />}
type="submit"
variant="default"
onClick={() => handlePlay(Play.NEXT)}
>
Add next
</Button>
</Group>
<Button
leftIcon={<RiPlayFill size="1rem" />}
type="submit"
variant="filled"
onClick={() => handlePlay(Play.NOW)}
>
Play
</Button>
</Stack>
);
};
export const openShuffleAllModal = async (
props: Pick<ShuffleAllModalProps, 'handlePlayQueueAdd' | 'queryClient'>,
) => {
const server = useAuthStore.getState().currentServer;
const genres = await props.queryClient.fetchQuery({
cacheTime: 1000 * 60 * 60 * 4,
queryFn: ({ signal }) =>
api.controller.getGenreList({
apiClientProps: {
server,
signal,
},
query: null,
}),
queryKey: queryKeys.genres.list(server?.id),
staleTime: 1000 * 60 * 5,
});
const musicFolders = await props.queryClient.fetchQuery({
cacheTime: 1000 * 60 * 60 * 4,
queryFn: ({ signal }) =>
api.controller.getMusicFolderList({
apiClientProps: {
server,
signal,
},
}),
queryKey: queryKeys.musicFolders.list(server?.id),
staleTime: 1000 * 60 * 5,
});
openModal({
children: (
<ShuffleAllModal
genres={genres}
musicFolders={musicFolders}
server={server}
{...props}
/>
),
size: 'sm',
title: 'Shuffle all',
});
};