From ffa9d165f293e70338d37d4dc7a9a8d184f00911 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 26 Nov 2025 14:58:10 -0800 Subject: [PATCH] refactor shuffle all modal for styles and loading state --- src/i18n/locales/en.json | 11 + .../player/components/center-controls.tsx | 14 +- .../mobile-fullscreen-player-controls.tsx | 4 +- .../player/components/mobile-playerbar.tsx | 6 +- .../player/components/player-button.tsx | 2 +- .../player/components/shuffle-all-modal.tsx | 365 ++++++++++-------- .../settings/components/settings-modal.tsx | 2 +- src/renderer/router/app-router.tsx | 6 +- 8 files changed, 229 insertions(+), 181 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 676b2e67e..3550cbf89 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -311,6 +311,17 @@ "expireInvalid": "expiration must be in the future", "createFailed": "failed to create share (is sharing enabled?)" }, + "shuffleAll": { + "title": "play random", + "input_genre": "$t(entity.genre_one)", + "input_limit": "how many songs?", + "input_minYear": "from year", + "input_maxYear": "to year", + "input_played": "play filter", + "input_played_optionAll": "all tracks", + "input_played_optionUnplayed": "only unplayed tracks", + "input_played_optionPlayed": "only played tracks" + }, "updateServer": { "success": "server updated successfully", "title": "update server" diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index a859820d1..34eddc9cf 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -1,9 +1,8 @@ -import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import styles from './center-controls.module.css'; -import { PlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button'; +import { MainPlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; import { openShuffleAllModal } from '/@/renderer/features/player/components/shuffle-all-modal'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; @@ -135,7 +134,7 @@ const CenterPlayButton = () => { const { mediaTogglePlayPause } = usePlayer(); return ( - { const ShuffleAllButton = () => { const { t } = useTranslation(); - const queryClient = useQueryClient(); const buttonSize = useSettingsStore((state) => state.general.buttonSize); return ( } - onClick={() => - openShuffleAllModal({ - queryClient, - }) - } + onClick={() => openShuffleAllModal()} tooltip={{ - label: t('player.playRandom', { postProcess: 'sentenceCase' }), + label: t('form.shuffleAll.title', { postProcess: 'sentenceCase' }), openDelay: 0, }} variant="tertiary" diff --git a/src/renderer/features/player/components/mobile-fullscreen-player-controls.tsx b/src/renderer/features/player/components/mobile-fullscreen-player-controls.tsx index 449377fb2..67689601d 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player-controls.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player-controls.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import styles from './mobile-fullscreen-player.module.css'; -import { PlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button'; +import { MainPlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayerStatus } from '/@/renderer/store'; import { Icon } from '/@/shared/components/icon/icon'; @@ -50,7 +50,7 @@ export const MobileFullscreenPlayerControls = memo( }} variant="tertiary" /> - { const { setStore } = useFullScreenPlayerStoreActions(); const currentSong = usePlayerSong(); const status = usePlayerStatus(); - const { mediaTogglePlayPause, mediaNext, mediaPrevious } = usePlayer(); + const { mediaNext, mediaPrevious, mediaTogglePlayPause } = usePlayer(); const title = currentSong?.name; const artists = currentSong?.artists; const isSongDefined = Boolean(currentSong?.id); @@ -203,7 +203,7 @@ export const MobilePlayerbar = () => { }} variant="tertiary" /> - { diff --git a/src/renderer/features/player/components/player-button.tsx b/src/renderer/features/player/components/player-button.tsx index e35bea3f1..a8246feef 100644 --- a/src/renderer/features/player/components/player-button.tsx +++ b/src/renderer/features/player/components/player-button.tsx @@ -61,7 +61,7 @@ interface PlayButtonProps extends Omit { isPaused?: boolean; } -export const PlayButton = forwardRef( +export const MainPlayButton = forwardRef( ({ isPaused, onClick, ...props }: PlayButtonProps, ref) => { const playerStateClass = isPaused ? PlaybackSelectors.playerStatePaused diff --git a/src/renderer/features/player/components/shuffle-all-modal.tsx b/src/renderer/features/player/components/shuffle-all-modal.tsx index 2b2269aaf..fe5b9abdb 100644 --- a/src/renderer/features/player/components/shuffle-all-modal.tsx +++ b/src/renderer/features/player/components/shuffle-all-modal.tsx @@ -1,7 +1,7 @@ -import { closeAllModals, openModal } from '@mantine/modals'; -import { QueryClient } from '@tanstack/react-query'; +import { closeAllModals, openContextModal } from '@mantine/modals'; +import { queryOptions, useQuery } from '@tanstack/react-query'; import merge from 'lodash/merge'; -import { useMemo } from 'react'; +import { Suspense, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; @@ -10,25 +10,17 @@ import { createWithEqualityFn } from 'zustand/traditional'; import i18n from '/@/i18n/i18n'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; +import { useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; -import { useAuthStore } from '/@/renderer/store'; -import { Button } from '/@/shared/components/button/button'; +import { PlayButton } from '/@/renderer/features/shared/components/play-button'; +import { useCurrentServer } from '/@/renderer/store'; import { Checkbox } from '/@/shared/components/checkbox/checkbox'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; -import { Icon } from '/@/shared/components/icon/icon'; import { NumberInput } from '/@/shared/components/number-input/number-input'; import { Select } from '/@/shared/components/select/select'; import { Stack } from '/@/shared/components/stack/stack'; -import { - GenreListResponse, - GenreListSort, - Played, - RandomSongListQuery, - ServerListItem, - ServerType, - SortOrder, -} from '/@/shared/types/domain-types'; +import { Played, RandomSongListQuery, ServerType } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; interface ShuffleAllSlice extends RandomSongListQuery { @@ -72,46 +64,204 @@ const PLAYED_DATA: { label: string; value: Played }[] = [ export const useShuffleAllStoreActions = () => useShuffleAllStore((state) => state.actions); -interface ShuffleAllModalProps { - genres: GenreListResponse | undefined; - queryClient: QueryClient; - server: null | ServerListItem; -} - -export const ShuffleAllModal = ({ genres, queryClient, server }: ShuffleAllModalProps) => { +export const ShuffleAllContextModal = () => { + const server = useCurrentServer(); const { addToQueueByData } = usePlayer(); const { t } = useTranslation(); const { enableMaxYear, enableMinYear, genre, limit, maxYear, minYear, musicFolderId, played } = useShuffleAllStore(); const { setStore } = useShuffleAllStoreActions(); - const handlePlay = async (playType: Play) => { - const res = await queryClient.fetchQuery({ - gcTime: 0, - queryFn: ({ signal }) => - api.controller.getRandomSongList({ - apiClientProps: { - serverId: server?.id || '', - signal, - }, - query: { - genre: genre || undefined, - limit, - maxYear: enableMaxYear ? maxYear || undefined : undefined, - minYear: enableMinYear ? minYear || undefined : undefined, - musicFolderId: musicFolderId || undefined, - played, - }, - }), - queryKey: queryKeys.songs.randomSongList(server?.id), - staleTime: 0, - }); + const { isFetching, refetch } = useQuery({ + ...randomFetchQuery({ + query: { + genre: genre || undefined, + limit: limit || 100, + maxYear: enableMaxYear ? maxYear || undefined : undefined, + minYear: enableMinYear ? minYear || undefined : undefined, + musicFolderId: musicFolderId || undefined, + played, + }, + serverId: server.id, + }), + enabled: false, + gcTime: 0, + staleTime: 0, + }); - addToQueueByData(res?.items || [], playType); + const fetchTypeRef = useRef(null); + + const handlePlay = async (playType: Play) => { + fetchTypeRef.current = playType; + + const { data } = await refetch(); + + addToQueueByData(data?.items || [], playType); closeAllModals(); }; + const isLoadingNext = + isFetching && + (fetchTypeRef.current === Play.NEXT || fetchTypeRef.current === Play.NEXT_SHUFFLE); + + const isLoadingLast = + isFetching && + (fetchTypeRef.current === Play.LAST || fetchTypeRef.current === Play.LAST_SHUFFLE); + + const isLoadingNow = isFetching && fetchTypeRef.current === Play.NOW; + + return ( + + setStore({ limit: e ? Number(e) : 500 })} + required + value={limit} + /> + + setStore({ minYear: e ? Number(e) : 0 })} + rightSection={ + setStore({ enableMinYear: e.currentTarget.checked })} + style={{ marginRight: '0.5rem' }} + /> + } + value={minYear} + /> + setStore({ maxYear: e ? Number(e) : 0 })} + rightSection={ + setStore({ enableMaxYear: e.currentTarget.checked })} + style={{ marginRight: '0.5rem' }} + /> + } + value={maxYear} + /> + + }> + + + {server?.type === ServerType.JELLYFIN && ( + setStore({ genre: e || '' })} - value={genre} - /> - {server?.type === ServerType.JELLYFIN && ( - setStore({ genre: e || '' })} + searchable + value={genre} + /> ); }; - -export const openShuffleAllModal = async (props: Pick) => { - const server = useAuthStore.getState().currentServer; - - const genres = await props.queryClient.fetchQuery({ - gcTime: 1000 * 60 * 5, - queryFn: ({ signal }) => - api.controller.getGenreList({ - apiClientProps: { - serverId: server?.id || '', - signal, - }, - query: { - sortBy: GenreListSort.NAME, - sortOrder: SortOrder.ASC, - startIndex: 0, - }, - }), - queryKey: queryKeys.genres.list(server?.id), - staleTime: 1000 * 60 * 60 * 4, - }); - - openModal({ - children: , - size: 'sm', - title: i18n.t('player.playRandom', { postProcess: 'sentenceCase' }) as string, - }); -}; diff --git a/src/renderer/features/settings/components/settings-modal.tsx b/src/renderer/features/settings/components/settings-modal.tsx index cd8ce04cd..6dae672af 100644 --- a/src/renderer/features/settings/components/settings-modal.tsx +++ b/src/renderer/features/settings/components/settings-modal.tsx @@ -4,7 +4,7 @@ import { SettingsContent } from '/@/renderer/features/settings/components/settin import { SettingsHeader } from '/@/renderer/features/settings/components/settings-header'; import { SettingSearchContext } from '/@/renderer/features/settings/context/search-context'; -export const SettingsModal = () => { +export const SettingsContextModal = () => { const [search, setSearch] = useState(''); return ( diff --git a/src/renderer/router/app-router.tsx b/src/renderer/router/app-router.tsx index 1ae57fc7c..22cf52301 100644 --- a/src/renderer/router/app-router.tsx +++ b/src/renderer/router/app-router.tsx @@ -1,8 +1,9 @@ import { lazy, Suspense } from 'react'; import { HashRouter, Route, Routes } from 'react-router'; +import { ShuffleAllContextModal } from '/@/renderer/features/player/components/shuffle-all-modal'; import { AddToPlaylistContextModal } from '/@/renderer/features/playlists/components/add-to-playlist-context-modal'; -import { SettingsModal } from '/@/renderer/features/settings/components/settings-modal'; +import { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal'; import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary'; import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal'; import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout'; @@ -76,8 +77,9 @@ export const AppRouter = () => { modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal, - settings: SettingsModal, + settings: SettingsContextModal, shareItem: ShareItemContextModal, + shuffleAll: ShuffleAllContextModal, }} >