import type { BaseItemDto, BaseItemDtoQueryResult, } from "@jellyfin/sdk/lib/generated-client/models"; import { FlashList, type FlashListProps } from "@shopify/flash-list"; import { useInfiniteQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { useAtom } from "jotai"; import React, { useEffect, useMemo } from "react"; import { View, type ViewStyle } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { Loader } from "../Loader"; import { Text } from "./Text"; interface HorizontalScrollProps extends Omit, "renderItem" | "data" | "style"> { queryFn: ({ pageParam, }: { pageParam: number; }) => Promise; queryKey: string[]; initialData?: BaseItemDto[]; renderItem: (item: BaseItemDto, index: number) => React.ReactNode; containerStyle?: ViewStyle; contentContainerStyle?: ViewStyle; loadingContainerStyle?: ViewStyle; height?: number; loading?: boolean; } export function InfiniteHorizontalScroll({ queryFn, queryKey, initialData = [], renderItem, containerStyle, contentContainerStyle, loadingContainerStyle, loading = false, height = 164, ...props }: HorizontalScrollProps): React.ReactElement { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const animatedOpacity = useSharedValue(0); const animatedStyle1 = useAnimatedStyle(() => { return { opacity: withTiming(animatedOpacity.value, { duration: 250 }), }; }); const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey, queryFn, staleTime: 60 * 1000, // 1 minute getNextPageParam: (lastPage, pages) => { if ( !lastPage?.Items || !lastPage?.TotalRecordCount || lastPage?.TotalRecordCount === 0 ) return undefined; const totalItems = lastPage.TotalRecordCount; const accumulatedItems = pages.reduce( (acc, curr) => acc + (curr?.Items?.length || 0), 0, ); if (accumulatedItems < totalItems) { return lastPage?.Items?.length * pages.length; } return undefined; }, initialPageParam: 0, enabled: !!api && !!user?.Id, }); const flatData = useMemo(() => { return ( (data?.pages.flatMap((p) => p?.Items).filter(Boolean) as BaseItemDto[]) || [] ); }, [data]); useEffect(() => { if (data) { animatedOpacity.value = 1; } }, [data]); if (data === undefined || data === null || loading) { return ( ); } return ( ( {renderItem(item, index)} )} horizontal onEndReached={() => { if (hasNextPage) { fetchNextPage(); } }} onEndReachedThreshold={0.5} contentContainerStyle={{ paddingHorizontal: 16, ...contentContainerStyle, }} showsHorizontalScrollIndicator={false} ListEmptyComponent={ {t("item_card.no_data_available")} } {...props} /> ); }