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; enableRefresh?: boolean; excludeIds?: string[]; query?: Partial>; 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: ( ), 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 ( ); }; export const SongInfiniteCarousel = (props: SongCarouselProps) => { const rows = useGridRows(LibraryItem.SONG, ItemListKey.SONG); return ( } > ); }; function useSongListInfinite( sortBy: SongListSort, sortOrder: SortOrder, itemLimit: number, additionalQuery?: Partial>, overrideQueryKey?: QueryFunctionContext['queryKey'], ) { const serverId = useCurrentServerId(); const defaultQueryKey = queryKeys.songs.infiniteList(serverId, { sortBy, sortOrder, ...additionalQuery, }); const query = useSuspenseInfiniteQuery({ 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; }