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 { useEffect, useState } from 'react';
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import formatDuration from 'format-duration';
|
import formatDuration from 'format-duration';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { IoIosPause } from 'react-icons/io';
|
import { IoIosPause } from 'react-icons/io';
|
||||||
import {
|
import {
|
||||||
|
RiMenuAddFill,
|
||||||
RiPlayFill,
|
RiPlayFill,
|
||||||
RiRepeat2Line,
|
RiRepeat2Line,
|
||||||
RiRepeatOneLine,
|
RiRepeatOneLine,
|
||||||
|
@ -34,6 +36,8 @@ import {
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types';
|
import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types';
|
||||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
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 {
|
interface CenterControlsProps {
|
||||||
playersRef: any;
|
playersRef: any;
|
||||||
|
@ -72,6 +76,7 @@ const SliderWrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [isSeeking, setIsSeeking] = useState(false);
|
const [isSeeking, setIsSeeking] = useState(false);
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const songDuration = currentSong?.duration;
|
const songDuration = currentSong?.duration;
|
||||||
|
@ -99,6 +104,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
handlePause,
|
handlePause,
|
||||||
handlePlay,
|
handlePlay,
|
||||||
} = useCenterControls({ playersRef });
|
} = useCenterControls({ playersRef });
|
||||||
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
const currentTime = useCurrentTime();
|
const currentTime = useCurrentTime();
|
||||||
const currentPlayerRef = player === 1 ? player1 : player2;
|
const currentPlayerRef = player === 1 ? player1 : player2;
|
||||||
|
@ -237,6 +243,21 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
onClick={handleToggleRepeat}
|
onClick={handleToggleRepeat}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PlayerButton
|
||||||
|
icon={<RiMenuAddFill size={15} />}
|
||||||
|
tooltip={{
|
||||||
|
label: 'Shuffle all',
|
||||||
|
openDelay: 500,
|
||||||
|
}}
|
||||||
|
variant="tertiary"
|
||||||
|
onClick={() =>
|
||||||
|
openShuffleAllModal({
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
queryClient,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ButtonsContainer>
|
</ButtonsContainer>
|
||||||
</ControlsContainer>
|
</ControlsContainer>
|
||||||
<SliderContainer>
|
<SliderContainer>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* stylelint-disable no-descending-specificity */
|
/* 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 type { TooltipProps, UnstyledButtonProps } from '@mantine/core';
|
||||||
import { UnstyledButton } from '@mantine/core';
|
import { UnstyledButton } from '@mantine/core';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
@ -118,33 +118,41 @@ const StyledPlayerButton = styled(UnstyledButton)<StyledPlayerButtonProps>`
|
||||||
: ButtonTertiaryVariant};
|
: ButtonTertiaryVariant};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PlayerButton = ({ tooltip, variant, icon, ...rest }: PlayerButtonProps) => {
|
export const PlayerButton = forwardRef<HTMLDivElement, PlayerButtonProps>(
|
||||||
if (tooltip) {
|
({ tooltip, variant, icon, ...rest }: PlayerButtonProps, ref) => {
|
||||||
return (
|
if (tooltip) {
|
||||||
<Tooltip {...tooltip}>
|
return (
|
||||||
<MotionWrapper variant={variant}>
|
<Tooltip {...tooltip}>
|
||||||
<StyledPlayerButton
|
<MotionWrapper
|
||||||
|
ref={ref}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
{icon}
|
<StyledPlayerButton
|
||||||
</StyledPlayerButton>
|
variant={variant}
|
||||||
</MotionWrapper>
|
{...rest}
|
||||||
</Tooltip>
|
>
|
||||||
);
|
{icon}
|
||||||
}
|
</StyledPlayerButton>
|
||||||
|
</MotionWrapper>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MotionWrapper variant={variant}>
|
<MotionWrapper
|
||||||
<StyledPlayerButton
|
ref={ref}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
{icon}
|
<StyledPlayerButton
|
||||||
</StyledPlayerButton>
|
variant={variant}
|
||||||
</MotionWrapper>
|
{...rest}
|
||||||
);
|
>
|
||||||
};
|
{icon}
|
||||||
|
</StyledPlayerButton>
|
||||||
|
</MotionWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
PlayerButton.defaultProps = {
|
PlayerButton.defaultProps = {
|
||||||
$isActive: false,
|
$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