diff --git a/src/renderer/components/item-list/helpers/item-list-loader.ts b/src/renderer/components/item-list/helpers/item-list-loader.ts new file mode 100644 index 000000000..abc767bf7 --- /dev/null +++ b/src/renderer/components/item-list/helpers/item-list-loader.ts @@ -0,0 +1,151 @@ +import { + useQueryClient, + UseQueryOptions, + useSuspenseQuery, + UseSuspenseQueryOptions, +} from '@tanstack/react-query'; +import throttle from 'lodash/throttle'; +import { useMemo, useRef, useState } from 'react'; + +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { getServerById } from '/@/renderer/store'; +import { AlbumListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types'; + +export interface InfiniteListProps { + query: Omit; + serverId: string; +} + +interface UseItemListInfiniteLoaderProps { + itemsPerPage: number; + itemType: LibraryItem; + listCountQuery: UseSuspenseQueryOptions; + listQuery: UseQueryOptions; + params: Record; + serverId: string; +} + +function getInitialData(itemCount: number) { + return Array.from({ length: itemCount }, () => undefined); +} + +export const useItemListInfiniteLoader = ({ + itemsPerPage = 50, // Default items per page + itemType, + listCountQuery, + listQuery, + params = {}, + serverId, +}: UseItemListInfiniteLoaderProps) => { + const queryClient = useQueryClient(); + + // Scroll state management + const scrollStateRef = useRef({ + direction: 'unknown', + lastRange: null, + lastScrollTime: 0, + }); + + const { data: totalItemCount } = useSuspenseQuery(listCountQuery); + + const [data, setData] = useState(getInitialData(totalItemCount)); + + const onRangeChanged = useMemo(() => { + return throttle(async (range: { endIndex: number; startIndex: number }) => { + const fetchRange = getFetchRange(range, scrollStateRef, itemsPerPage); + const startIndex = fetchRange.startIndex; + const endIndex = fetchRange.startIndex + fetchRange.limit; + + const query = { + limit: fetchRange.limit, + sortBy: AlbumListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: fetchRange.startIndex, + }; + + // Check if data exists in cache and is fresh + const queryState = queryClient.getQueryState(queryKeys.albums.list(serverId, query)); + + if (!queryState || queryState.isInvalidated) { + const result = await queryClient.fetchQuery({ + gcTime: 1000 * 30, + queryFn: async ({ signal }) => { + const result = await api.controller.getAlbumList({ + apiClientProps: { + server: getServerById(serverId), + signal, + }, + query, + }); + + return result.items; + }, + queryKey: queryKeys.albums.list(serverId, query), + staleTime: 1000 * 30, + }); + + // Only run setData if we actually fetched fresh data + setData((oldData: any) => { + return [...oldData.slice(0, startIndex), ...result, ...oldData.slice(endIndex)]; + }); + } + }, 50); + }, [itemsPerPage, queryClient, serverId]); + + return { data, onRangeChanged }; +}; + +export const parseListCountQuery = (query: any) => { + return { + ...query, + limit: 1, + startIndex: 0, + }; +}; + +interface ScrollState { + direction: 'down' | 'unknown' | 'up'; + lastRange: null | { endIndex: number; startIndex: number }; + lastScrollTime: number; +} + +const getFetchRange = ( + range: { endIndex: number; startIndex: number }, + scrollState: React.MutableRefObject, + itemsPerPage: number, +) => { + const currentTime = Date.now(); + const { lastRange } = scrollState.current; + + // Determine scroll direction + let newDirection: 'down' | 'unknown' | 'up' = 'unknown'; + if (lastRange) { + if (range.startIndex < lastRange.startIndex) { + newDirection = 'up'; + } else if (range.startIndex > lastRange.startIndex) { + newDirection = 'down'; + } + } + + scrollState.current = { + direction: newDirection, + lastRange: { ...range }, + lastScrollTime: currentTime, + }; + + let pageIndex = 0; + if (newDirection === 'down') { + pageIndex = Math.floor(range.endIndex / itemsPerPage); + } else if (newDirection === 'up') { + pageIndex = Math.floor(range.startIndex / itemsPerPage); + } else { + pageIndex = Math.floor(range.endIndex / itemsPerPage); + } + + return { + direction: newDirection, + limit: itemsPerPage, + startIndex: pageIndex * itemsPerPage, + }; +};