mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add list query fetcher
This commit is contained in:
@@ -1,15 +1,20 @@
|
|||||||
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { QueryClient, useIsFetching, useQueryClient } from '@tanstack/react-query';
|
import { QueryClient, useIsFetching, useQueryClient } from '@tanstack/react-query';
|
||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import { createContext, useCallback, useContext, useMemo, useRef } from 'react';
|
import { createContext, useCallback, useContext, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
import { AddToQueueType, usePlayerActions } from '/@/renderer/store';
|
import { AddToQueueType, usePlayerActions } from '/@/renderer/store';
|
||||||
|
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import {
|
import {
|
||||||
instanceOfCancellationError,
|
instanceOfCancellationError,
|
||||||
@@ -31,7 +36,14 @@ export interface PlayerContext {
|
|||||||
id: string[],
|
id: string[],
|
||||||
itemType: LibraryItem,
|
itemType: LibraryItem,
|
||||||
type: AddToQueueType,
|
type: AddToQueueType,
|
||||||
|
skipConfirmation?: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
|
addToQueueByListQuery: (
|
||||||
|
serverId: string,
|
||||||
|
query: any,
|
||||||
|
itemType: LibraryItem,
|
||||||
|
type: AddToQueueType,
|
||||||
|
) => Promise<void>;
|
||||||
clearQueue: () => void;
|
clearQueue: () => void;
|
||||||
clearSelected: (items: QueueSong[]) => void;
|
clearSelected: (items: QueueSong[]) => void;
|
||||||
decreaseVolume: (amount: number) => void;
|
decreaseVolume: (amount: number) => void;
|
||||||
@@ -71,6 +83,7 @@ export interface PlayerContext {
|
|||||||
export const PlayerContext = createContext<PlayerContext>({
|
export const PlayerContext = createContext<PlayerContext>({
|
||||||
addToQueueByData: () => {},
|
addToQueueByData: () => {},
|
||||||
addToQueueByFetch: () => {},
|
addToQueueByFetch: () => {},
|
||||||
|
addToQueueByListQuery: async () => {},
|
||||||
clearQueue: () => {},
|
clearQueue: () => {},
|
||||||
clearSelected: () => {},
|
clearSelected: () => {},
|
||||||
decreaseVolume: () => {},
|
decreaseVolume: () => {},
|
||||||
@@ -126,6 +139,45 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const storeActions = usePlayerActions();
|
const storeActions = usePlayerActions();
|
||||||
const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
|
const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
|
||||||
|
const queueFetchConfirmThreshold = 100;
|
||||||
|
|
||||||
|
const confirmLargeFetch = useCallback(
|
||||||
|
(itemCount: number): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
openModal({
|
||||||
|
children: (
|
||||||
|
<ConfirmModal
|
||||||
|
labels={{
|
||||||
|
cancel: t('common.cancel', { postProcess: 'titleCase' }),
|
||||||
|
confirm: t('common.confirm', { postProcess: 'titleCase' }),
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
resolve(false);
|
||||||
|
closeAllModals();
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
resolve(true);
|
||||||
|
closeAllModals();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{t('player.confirmLargeFetch', {
|
||||||
|
count: itemCount,
|
||||||
|
defaultValue: `You are about to add ${itemCount} items to the queue. Continue?`,
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</ConfirmModal>
|
||||||
|
),
|
||||||
|
title: t('player.confirmLargeFetchTitle', {
|
||||||
|
defaultValue: 'Confirm Large Queue Addition',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const addToQueueByData = useCallback(
|
const addToQueueByData = useCallback(
|
||||||
(data: Song[], type: AddToQueueType) => {
|
(data: Song[], type: AddToQueueType) => {
|
||||||
@@ -140,7 +192,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addToQueueByFetch = useCallback(
|
const addToQueueByFetch = useCallback(
|
||||||
async (serverId: string, id: string[], itemType: LibraryItem, type: AddToQueueType) => {
|
async (
|
||||||
|
serverId: string,
|
||||||
|
id: string[],
|
||||||
|
itemType: LibraryItem,
|
||||||
|
type: AddToQueueType,
|
||||||
|
skipConfirmation?: boolean,
|
||||||
|
) => {
|
||||||
|
if (!skipConfirmation && id.length > queueFetchConfirmThreshold) {
|
||||||
|
const confirmed = await confirmLargeFetch(id.length);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let toastId: null | string = null;
|
let toastId: null | string = null;
|
||||||
const fetchId = nanoid();
|
const fetchId = nanoid();
|
||||||
|
|
||||||
@@ -209,7 +274,154 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[queryClient, storeActions, t],
|
[confirmLargeFetch, queueFetchConfirmThreshold, queryClient, storeActions, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addToQueueByListQuery = useCallback(
|
||||||
|
async (serverId: string, query: any, itemType: LibraryItem, type: AddToQueueType) => {
|
||||||
|
let toastId: null | string = null;
|
||||||
|
let fetchId: null | string = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get total count first
|
||||||
|
let totalCount = 0;
|
||||||
|
let listQueryFn: any;
|
||||||
|
let listCountQueryFn: any;
|
||||||
|
|
||||||
|
switch (itemType) {
|
||||||
|
case LibraryItem.ALBUM: {
|
||||||
|
listQueryFn = albumQueries.list;
|
||||||
|
listCountQueryFn = albumQueries.listCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LibraryItem.ALBUM_ARTIST: {
|
||||||
|
listQueryFn = artistsQueries.albumArtistList;
|
||||||
|
listCountQueryFn = artistsQueries.albumArtistListCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LibraryItem.ARTIST: {
|
||||||
|
listQueryFn = artistsQueries.artistList;
|
||||||
|
listCountQueryFn = artistsQueries.artistListCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LibraryItem.PLAYLIST: {
|
||||||
|
listQueryFn = playlistsQueries.list;
|
||||||
|
listCountQueryFn = playlistsQueries.listCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LibraryItem.SONG: {
|
||||||
|
listQueryFn = songsQueries.list;
|
||||||
|
listCountQueryFn = songsQueries.listCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported item type: ${itemType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
const countResult = (await queryClient.fetchQuery(
|
||||||
|
listCountQueryFn({
|
||||||
|
query: { ...query },
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
)) as number;
|
||||||
|
totalCount = countResult || 0;
|
||||||
|
|
||||||
|
// Check if we need confirmation
|
||||||
|
if (totalCount > queueFetchConfirmThreshold) {
|
||||||
|
const confirmed = await confirmLargeFetch(totalCount);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start timeout only after confirmation (if needed)
|
||||||
|
fetchId = nanoid();
|
||||||
|
|
||||||
|
timeoutIds.current = {
|
||||||
|
...timeoutIds.current,
|
||||||
|
[fetchId]: setTimeout(() => {
|
||||||
|
toastId = toast.info({
|
||||||
|
autoClose: false,
|
||||||
|
message: t('player.playbackFetchCancel', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
onClose: () => {
|
||||||
|
queryClient.cancelQueries({
|
||||||
|
exact: false,
|
||||||
|
queryKey: getRootQueryKey(itemType, serverId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: t('player.playbackFetchInProgress', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}, 2000),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Paginate through all items to collect IDs
|
||||||
|
const allIds: string[] = [];
|
||||||
|
const pageSize = 500; // Fetch in chunks
|
||||||
|
let startIndex = 0;
|
||||||
|
|
||||||
|
while (startIndex < totalCount) {
|
||||||
|
const pageQuery = {
|
||||||
|
...query,
|
||||||
|
limit: pageSize,
|
||||||
|
startIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageResult = (await queryClient.fetchQuery(
|
||||||
|
listQueryFn({
|
||||||
|
query: pageQuery,
|
||||||
|
serverId,
|
||||||
|
}),
|
||||||
|
)) as { items: any[] };
|
||||||
|
|
||||||
|
if (pageResult?.items) {
|
||||||
|
const pageIds = pageResult.items.map((item: any) => item.id);
|
||||||
|
allIds.push(...pageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got fewer items than requested, we've reached the end
|
||||||
|
if (!pageResult?.items || pageResult.items.length < pageSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex += pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchId && timeoutIds.current) {
|
||||||
|
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||||
|
delete timeoutIds.current[fetchId];
|
||||||
|
}
|
||||||
|
if (toastId) {
|
||||||
|
toast.hide(toastId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now call addToQueueByFetch with all collected IDs (skip confirmation since we already confirmed)
|
||||||
|
await addToQueueByFetch(serverId, allIds, itemType, type, true);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (instanceOfCancellationError(err)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchId && timeoutIds.current) {
|
||||||
|
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||||
|
delete timeoutIds.current[fetchId];
|
||||||
|
}
|
||||||
|
if (toastId) {
|
||||||
|
toast.hide(toastId);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error({
|
||||||
|
message: err.message,
|
||||||
|
title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[confirmLargeFetch, queueFetchConfirmThreshold, queryClient, addToQueueByFetch, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearQueue = useCallback(() => {
|
const clearQueue = useCallback(() => {
|
||||||
@@ -398,6 +610,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
() => ({
|
() => ({
|
||||||
addToQueueByData,
|
addToQueueByData,
|
||||||
addToQueueByFetch,
|
addToQueueByFetch,
|
||||||
|
addToQueueByListQuery,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
clearSelected,
|
clearSelected,
|
||||||
decreaseVolume,
|
decreaseVolume,
|
||||||
@@ -431,6 +644,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
[
|
[
|
||||||
addToQueueByData,
|
addToQueueByData,
|
||||||
addToQueueByFetch,
|
addToQueueByFetch,
|
||||||
|
addToQueueByListQuery,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
clearSelected,
|
clearSelected,
|
||||||
decreaseVolume,
|
decreaseVolume,
|
||||||
|
|||||||
Reference in New Issue
Block a user