mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-15 16:04:19 +02:00
196 lines
6.0 KiB
TypeScript
196 lines
6.0 KiB
TypeScript
import { QueryFunctionContext, useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
|
import { Suspense, useCallback, useMemo } from 'react';
|
|
|
|
import { api } from '/@/renderer/api';
|
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
import {
|
|
GridCarousel,
|
|
GridCarouselSkeletonFallback,
|
|
useGridCarouselContainerQuery,
|
|
} from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
|
import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
|
import { DefaultItemControlProps } from '/@/renderer/components/item-list/types';
|
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
|
import { useCurrentServerId } from '/@/renderer/store';
|
|
import {
|
|
LibraryItem,
|
|
Song,
|
|
SongListQuery,
|
|
SongListResponse,
|
|
SongListSort,
|
|
SortOrder,
|
|
} from '/@/shared/types/domain-types';
|
|
import { ItemListKey, Play } from '/@/shared/types/types';
|
|
|
|
interface SongCarouselProps {
|
|
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
|
enableRefresh?: boolean;
|
|
excludeIds?: string[];
|
|
query?: Partial<Omit<SongListQuery, 'startIndex'>>;
|
|
queryKey?: QueryFunctionContext['queryKey'];
|
|
rowCount?: number;
|
|
sortBy: SongListSort;
|
|
sortOrder: SortOrder;
|
|
title: React.ReactNode | string;
|
|
}
|
|
|
|
const BaseSongInfiniteCarousel = (props: SongCarouselProps & { rows: DataRow[] }) => {
|
|
const {
|
|
containerQuery,
|
|
enableRefresh,
|
|
excludeIds,
|
|
query: additionalQuery,
|
|
queryKey,
|
|
rowCount = 1,
|
|
rows,
|
|
sortBy,
|
|
sortOrder,
|
|
title,
|
|
} = props;
|
|
const {
|
|
data: songs,
|
|
fetchNextPage,
|
|
hasNextPage,
|
|
isFetchingNextPage,
|
|
refetch,
|
|
} = useSongListInfinite(sortBy, sortOrder, 20, additionalQuery, queryKey);
|
|
|
|
const player = usePlayer();
|
|
const baseControls = useDefaultItemListControls();
|
|
|
|
const controls = useMemo(() => {
|
|
return {
|
|
...baseControls,
|
|
onPlay: ({ item, playType }: DefaultItemControlProps & { playType: Play }) => {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
player.addToQueueByData([item as Song], playType);
|
|
},
|
|
};
|
|
}, [baseControls, player]);
|
|
|
|
const cards = useMemo(() => {
|
|
// Flatten all pages and filter excluded IDs
|
|
const allItems = songs?.pages.flatMap((page: SongListResponse) => page.items) || [];
|
|
const filteredItems = excludeIds
|
|
? allItems.filter((song) => !excludeIds.includes(song.id))
|
|
: allItems;
|
|
|
|
return filteredItems.map((song: Song) => ({
|
|
content: (
|
|
<MemoizedItemCard
|
|
controls={controls}
|
|
data={song}
|
|
enableDrag
|
|
itemType={LibraryItem.SONG}
|
|
rows={rows}
|
|
type="poster"
|
|
withControls
|
|
/>
|
|
),
|
|
id: song.id,
|
|
}));
|
|
}, [songs, controls, excludeIds, rows]);
|
|
|
|
const handleNextPage = useCallback(() => {}, []);
|
|
|
|
const handlePrevPage = useCallback(() => {}, []);
|
|
|
|
const handleRefresh = useCallback(() => {
|
|
refetch();
|
|
}, [refetch]);
|
|
|
|
const firstPageItems = excludeIds
|
|
? songs?.pages[0]?.items.filter((song) => !excludeIds.includes(song.id)) || []
|
|
: songs?.pages[0]?.items || [];
|
|
|
|
if (firstPageItems.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<GridCarousel
|
|
cards={cards}
|
|
containerQuery={containerQuery}
|
|
enableRefresh={enableRefresh}
|
|
hasNextPage={hasNextPage}
|
|
isFetchingNextPage={isFetchingNextPage}
|
|
loadNextPage={fetchNextPage}
|
|
onNextPage={handleNextPage}
|
|
onPrevPage={handlePrevPage}
|
|
onRefresh={handleRefresh}
|
|
placeholderItemType={LibraryItem.SONG}
|
|
placeholderRows={rows}
|
|
rowCount={rowCount}
|
|
title={title}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const SongInfiniteCarousel = (props: SongCarouselProps) => {
|
|
const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG);
|
|
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<GridCarouselSkeletonFallback
|
|
containerQuery={props.containerQuery}
|
|
placeholderItemType={LibraryItem.SONG}
|
|
placeholderRows={rows}
|
|
title={props.title}
|
|
/>
|
|
}
|
|
>
|
|
<BaseSongInfiniteCarousel {...props} rows={rows} />
|
|
</Suspense>
|
|
);
|
|
};
|
|
|
|
function useSongListInfinite(
|
|
sortBy: SongListSort,
|
|
sortOrder: SortOrder,
|
|
itemLimit: number,
|
|
additionalQuery?: Partial<Omit<SongListQuery, 'startIndex'>>,
|
|
overrideQueryKey?: QueryFunctionContext['queryKey'],
|
|
) {
|
|
const serverId = useCurrentServerId();
|
|
|
|
const defaultQueryKey = queryKeys.songs.infiniteList(serverId, {
|
|
sortBy,
|
|
sortOrder,
|
|
...additionalQuery,
|
|
});
|
|
|
|
const query = useSuspenseInfiniteQuery<SongListResponse>({
|
|
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
|
if (lastPage.items.length < itemLimit) {
|
|
return undefined;
|
|
}
|
|
|
|
const nextPageParam = Number(lastPageParam) + itemLimit;
|
|
|
|
return String(nextPageParam);
|
|
},
|
|
initialPageParam: '0',
|
|
queryFn: ({ pageParam, signal }) => {
|
|
return api.controller.getSongList({
|
|
apiClientProps: { serverId, signal },
|
|
query: {
|
|
limit: itemLimit,
|
|
sortBy,
|
|
sortOrder,
|
|
startIndex: Number(pageParam),
|
|
...additionalQuery,
|
|
},
|
|
});
|
|
},
|
|
queryKey: overrideQueryKey || defaultQueryKey,
|
|
});
|
|
|
|
return query;
|
|
}
|