diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx
index ec65e5c0..53f2f5b4 100644
--- a/src/renderer/features/player/components/center-controls.tsx
+++ b/src/renderer/features/player/components/center-controls.tsx
@@ -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}
/>
+
+ }
+ tooltip={{
+ label: 'Shuffle all',
+ openDelay: 500,
+ }}
+ variant="tertiary"
+ onClick={() =>
+ openShuffleAllModal({
+ handlePlayQueueAdd,
+ queryClient,
+ })
+ }
+ />
diff --git a/src/renderer/features/player/components/player-button.tsx b/src/renderer/features/player/components/player-button.tsx
index 183a4972..60768d7e 100644
--- a/src/renderer/features/player/components/player-button.tsx
+++ b/src/renderer/features/player/components/player-button.tsx
@@ -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)`
: ButtonTertiaryVariant};
`;
-export const PlayerButton = ({ tooltip, variant, icon, ...rest }: PlayerButtonProps) => {
- if (tooltip) {
- return (
-
-
- (
+ ({ tooltip, variant, icon, ...rest }: PlayerButtonProps, ref) => {
+ if (tooltip) {
+ return (
+
+
- {icon}
-
-
-
- );
- }
+
+ {icon}
+
+
+
+ );
+ }
- return (
-
-
- {icon}
-
-
- );
-};
+
+ {icon}
+
+
+ );
+ },
+);
PlayerButton.defaultProps = {
$isActive: false,
diff --git a/src/renderer/features/player/components/shuffle-all-modal.tsx b/src/renderer/features/player/components/shuffle-all-modal.tsx
new file mode 100644
index 00000000..5c477bf9
--- /dev/null
+++ b/src/renderer/features/player/components/shuffle-all-modal.tsx
@@ -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) => void;
+ };
+ enableMaxYear: boolean;
+ enableMinYear: boolean;
+}
+
+const useShuffleAllStore = create()(
+ 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 (
+
+ setStore({ limit: e ? Number(e) : 0 })}
+ />
+
+ setStore({ enableMinYear: e.currentTarget.checked })}
+ />
+ }
+ value={minYear}
+ onChange={(e) => setStore({ minYear: e ? Number(e) : 0 })}
+ />
+
+ setStore({ enableMaxYear: e.currentTarget.checked })}
+ />
+ }
+ value={maxYear}
+ onChange={(e) => setStore({ maxYear: e ? Number(e) : 0 })}
+ />
+
+
+ );
+};
+
+export const openShuffleAllModal = async (
+ props: Pick,
+) => {
+ 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: (
+
+ ),
+ size: 'sm',
+ title: 'Shuffle all',
+ });
+};