mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
refactor shuffle all modal for styles and loading state
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
<PlayButton
|
||||
<MainPlayButton
|
||||
disabled={currentSong?.id === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
onClick={mediaTogglePlayPause}
|
||||
@@ -229,19 +228,14 @@ const RepeatButton = () => {
|
||||
|
||||
const ShuffleAllButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
||||
|
||||
return (
|
||||
<PlayerButton
|
||||
icon={<Icon fill="default" icon="mediaRandom" size={buttonSize} />}
|
||||
onClick={() =>
|
||||
openShuffleAllModal({
|
||||
queryClient,
|
||||
})
|
||||
}
|
||||
onClick={() => openShuffleAllModal()}
|
||||
tooltip={{
|
||||
label: t('player.playRandom', { postProcess: 'sentenceCase' }),
|
||||
label: t('form.shuffleAll.title', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 0,
|
||||
}}
|
||||
variant="tertiary"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<PlayButton
|
||||
<MainPlayButton
|
||||
disabled={currentSongId === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
onClick={mediaTogglePlayPause}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { generatePath, Link } from 'react-router';
|
||||
import styles from './mobile-playerbar.module.css';
|
||||
|
||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
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 { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ export const MobilePlayerbar = () => {
|
||||
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"
|
||||
/>
|
||||
<PlayButton
|
||||
<MainPlayButton
|
||||
disabled={currentSong?.id === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -61,7 +61,7 @@ interface PlayButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>(
|
||||
export const MainPlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>(
|
||||
({ isPaused, onClick, ...props }: PlayButtonProps, ref) => {
|
||||
const playerStateClass = isPaused
|
||||
? PlaybackSelectors.playerStatePaused
|
||||
|
||||
@@ -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<Play>(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 (
|
||||
<Stack gap="md">
|
||||
<NumberInput
|
||||
label={t('form.shuffleAll.input_limit', { postProcess: 'sentenceCase' })}
|
||||
max={500}
|
||||
min={1}
|
||||
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
||||
required
|
||||
value={limit}
|
||||
/>
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
label={t('form.shuffleAll.input_minYear', { postProcess: 'sentenceCase' })}
|
||||
max={2050}
|
||||
min={1850}
|
||||
onChange={(e) => setStore({ minYear: e ? Number(e) : 0 })}
|
||||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMinYear}
|
||||
onChange={(e) => setStore({ enableMinYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
}
|
||||
value={minYear}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('form.shuffleAll.input_maxYear', { postProcess: 'sentenceCase' })}
|
||||
max={2050}
|
||||
min={1850}
|
||||
onChange={(e) => setStore({ maxYear: e ? Number(e) : 0 })}
|
||||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMaxYear}
|
||||
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
}
|
||||
value={maxYear}
|
||||
/>
|
||||
</Group>
|
||||
<Suspense fallback={<Select data={[]} />}>
|
||||
<GenreSelect />
|
||||
</Suspense>
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<Select
|
||||
clearable
|
||||
data={PLAYED_DATA}
|
||||
label={t('form.shuffleAll.input_played', { postProcess: 'sentenceCase' })}
|
||||
onChange={(e) => {
|
||||
setStore({ played: e as Played });
|
||||
}}
|
||||
value={played}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<Group align="center" gap="md" justify="center" w="100%">
|
||||
<PlayButton
|
||||
icon="mediaPlayNext"
|
||||
isSecondary
|
||||
loading={isLoadingNext}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
onLongPress={() => handlePlay(Play.NEXT_SHUFFLE)}
|
||||
/>
|
||||
<PlayButton
|
||||
fill
|
||||
loading={isLoadingNow}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
onLongPress={() => handlePlay(Play.SHUFFLE)}
|
||||
/>
|
||||
<PlayButton
|
||||
icon="mediaPlayLast"
|
||||
isSecondary
|
||||
loading={isLoadingLast}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
onLongPress={() => handlePlay(Play.LAST_SHUFFLE)}
|
||||
/>
|
||||
</Group>
|
||||
{/* <Group grow>
|
||||
<Button
|
||||
disabled={!limit || isFetching}
|
||||
leftSection={<Icon icon="mediaPlayNext" />}
|
||||
loading={fetchTypeRef.current === Play.NEXT && isFetching}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!limit || isFetching}
|
||||
leftSection={<Icon icon="mediaPlayLast" />}
|
||||
loading={fetchTypeRef.current === Play.LAST && isFetching}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
<Button
|
||||
disabled={!limit || isFetching}
|
||||
leftSection={<Icon icon="mediaPlay" />}
|
||||
loading={fetchTypeRef.current === Play.NOW && isFetching}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</Button> */}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const randomFetchQuery = (args: {
|
||||
query: {
|
||||
genre?: string;
|
||||
limit: number;
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
musicFolderId?: string | string[];
|
||||
played: Played;
|
||||
};
|
||||
serverId: string;
|
||||
}) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ signal }) => {
|
||||
return api.controller.getRandomSongList({
|
||||
apiClientProps: { serverId: args.serverId, signal },
|
||||
query: args.query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.player.fetch(),
|
||||
});
|
||||
};
|
||||
|
||||
export const openShuffleAllModal = async () => {
|
||||
openContextModal({
|
||||
innerProps: {},
|
||||
modalKey: 'shuffleAll',
|
||||
size: 'sm',
|
||||
title: i18n.t('player.playRandom', { postProcess: 'sentenceCase' }) as string,
|
||||
});
|
||||
};
|
||||
|
||||
const GenreSelect = () => {
|
||||
const { t } = useTranslation();
|
||||
const server = useCurrentServer();
|
||||
const { genre } = useShuffleAllStore();
|
||||
const { data: genres } = useGenreList();
|
||||
const { setStore } = useShuffleAllStoreActions();
|
||||
|
||||
const genreData = useMemo(() => {
|
||||
if (!genres) return [];
|
||||
|
||||
@@ -125,125 +275,16 @@ export const ShuffleAllModal = ({ genres, queryClient, server }: ShuffleAllModal
|
||||
value,
|
||||
};
|
||||
});
|
||||
}, [genres, server?.type]);
|
||||
}, [genres, server.type]);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<NumberInput
|
||||
label="How many tracks?"
|
||||
max={500}
|
||||
min={1}
|
||||
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
||||
required
|
||||
value={limit}
|
||||
/>
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
label="From year"
|
||||
max={2050}
|
||||
min={1850}
|
||||
onChange={(e) => setStore({ minYear: e ? Number(e) : 0 })}
|
||||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMinYear}
|
||||
onChange={(e) => setStore({ enableMinYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
}
|
||||
value={minYear}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="To year"
|
||||
max={2050}
|
||||
min={1850}
|
||||
onChange={(e) => setStore({ maxYear: e ? Number(e) : 0 })}
|
||||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMaxYear}
|
||||
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
}
|
||||
value={maxYear}
|
||||
/>
|
||||
</Group>
|
||||
<Select
|
||||
clearable
|
||||
data={genreData}
|
||||
label="Genre"
|
||||
onChange={(e) => setStore({ genre: e || '' })}
|
||||
value={genre}
|
||||
/>
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<Select
|
||||
clearable
|
||||
data={PLAYED_DATA}
|
||||
label="Play filter"
|
||||
onChange={(e) => {
|
||||
setStore({ played: e as Played });
|
||||
}}
|
||||
value={played}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<Group grow>
|
||||
<Button
|
||||
disabled={!limit}
|
||||
leftSection={<Icon icon="mediaPlayLast" />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!limit}
|
||||
leftSection={<Icon icon="mediaPlayNext" />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
<Button
|
||||
disabled={!limit}
|
||||
leftSection={<Icon icon="mediaPlay" />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Select
|
||||
clearable
|
||||
data={genreData}
|
||||
label={t('form.shuffleAll.input_genre', { postProcess: 'sentenceCase' })}
|
||||
onChange={(e) => setStore({ genre: e || '' })}
|
||||
searchable
|
||||
value={genre}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const openShuffleAllModal = async (props: Pick<ShuffleAllModalProps, 'queryClient'>) => {
|
||||
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: <ShuffleAllModal genres={genres} server={server} {...props} />,
|
||||
size: 'sm',
|
||||
title: i18n.t('player.playRandom', { postProcess: 'sentenceCase' }) as string,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
<RouterErrorBoundary>
|
||||
|
||||
Reference in New Issue
Block a user