Add shuffle all feature
This commit is contained in:
parent
ba6f2a1637
commit
debdb92dcf
3 changed files with 314 additions and 23 deletions
|
@ -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>
|
||||
|
|
|
@ -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,11 +118,15 @@ const StyledPlayerButton = styled(UnstyledButton)<StyledPlayerButtonProps>`
|
|||
: ButtonTertiaryVariant};
|
||||
`;
|
||||
|
||||
export const PlayerButton = ({ tooltip, variant, icon, ...rest }: PlayerButtonProps) => {
|
||||
export const PlayerButton = forwardRef<HTMLDivElement, PlayerButtonProps>(
|
||||
({ tooltip, variant, icon, ...rest }: PlayerButtonProps, ref) => {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip {...tooltip}>
|
||||
<MotionWrapper variant={variant}>
|
||||
<MotionWrapper
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
>
|
||||
<StyledPlayerButton
|
||||
variant={variant}
|
||||
{...rest}
|
||||
|
@ -135,7 +139,10 @@ export const PlayerButton = ({ tooltip, variant, icon, ...rest }: PlayerButtonPr
|
|||
}
|
||||
|
||||
return (
|
||||
<MotionWrapper variant={variant}>
|
||||
<MotionWrapper
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
>
|
||||
<StyledPlayerButton
|
||||
variant={variant}
|
||||
{...rest}
|
||||
|
@ -144,7 +151,8 @@ export const PlayerButton = ({ tooltip, variant, icon, ...rest }: PlayerButtonPr
|
|||
</StyledPlayerButton>
|
||||
</MotionWrapper>
|
||||
);
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
PlayerButton.defaultProps = {
|
||||
$isActive: false,
|
||||
|
|
262
src/renderer/features/player/components/shuffle-all-modal.tsx
Normal file
262
src/renderer/features/player/components/shuffle-all-modal.tsx
Normal 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',
|
||||
});
|
||||
};
|
Reference in a new issue