diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx
index 14636af5..d434c333 100644
--- a/components/home/Favorites.tsx
+++ b/components/home/Favorites.tsx
@@ -9,7 +9,7 @@ import { Image, Text, View } from "react-native";
import heart from "@/assets/icons/heart.fill.png";
import { Colors } from "@/constants/Colors";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-import { ScrollingCollectionList } from "./ScrollingCollectionList";
+import { InfiniteScrollingCollectionList } from "./InfiniteScrollingCollectionList";
type FavoriteTypes =
| "Series"
@@ -33,7 +33,11 @@ export const Favorites = () => {
});
const fetchFavoritesByType = useCallback(
- async (itemType: BaseItemKind) => {
+ async (
+ itemType: BaseItemKind,
+ startIndex: number = 0,
+ limit: number = 20,
+ ) => {
const response = await getItemsApi(api as Api).getItems({
userId: user?.Id,
sortBy: ["SeriesSortName", "SortName"],
@@ -44,16 +48,19 @@ export const Favorites = () => {
collapseBoxSetItems: false,
excludeLocationTypes: ["Virtual"],
enableTotalRecordCount: false,
- limit: 20,
+ startIndex: startIndex,
+ limit: limit,
includeItemTypes: [itemType],
});
const items = response.data.Items || [];
- // Update empty state for this specific type
- setEmptyState((prev) => ({
- ...prev,
- [itemType as FavoriteTypes]: items.length === 0,
- }));
+ // Update empty state for this specific type only for the first page
+ if (startIndex === 0) {
+ setEmptyState((prev) => ({
+ ...prev,
+ [itemType as FavoriteTypes]: items.length === 0,
+ }));
+ }
return items;
},
@@ -82,27 +89,33 @@ export const Favorites = () => {
};
const fetchFavoriteSeries = useCallback(
- () => fetchFavoritesByType("Series"),
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Series", pageParam),
[fetchFavoritesByType],
);
const fetchFavoriteMovies = useCallback(
- () => fetchFavoritesByType("Movie"),
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Movie", pageParam),
[fetchFavoritesByType],
);
const fetchFavoriteEpisodes = useCallback(
- () => fetchFavoritesByType("Episode"),
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Episode", pageParam),
[fetchFavoritesByType],
);
const fetchFavoriteVideos = useCallback(
- () => fetchFavoritesByType("Video"),
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Video", pageParam),
[fetchFavoritesByType],
);
const fetchFavoriteBoxsets = useCallback(
- () => fetchFavoritesByType("BoxSet"),
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("BoxSet", pageParam),
[fetchFavoritesByType],
);
const fetchFavoritePlaylists = useCallback(
- () => fetchFavoritesByType("Playlist"),
+ ({ pageParam }: { pageParam: number }) =>
+ fetchFavoritesByType("Playlist", pageParam),
[fetchFavoritesByType],
);
@@ -123,38 +136,38 @@ export const Favorites = () => {
)}
-
-
-
-
-
- ;
+ hideIfEmpty?: boolean;
+ pageSize?: number;
+}
+
+export const InfiniteScrollingCollectionList: React.FC = ({
+ title,
+ orientation = "vertical",
+ disabled = false,
+ queryFn,
+ queryKey,
+ hideIfEmpty = false,
+ pageSize = 20,
+ ...props
+}) => {
+ const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
+ useInfiniteQuery({
+ queryKey: queryKey,
+ queryFn: ({ pageParam = 0, ...context }) =>
+ queryFn({ ...context, queryKey, pageParam }),
+ getNextPageParam: (lastPage, allPages) => {
+ // If the last page has fewer items than pageSize, we've reached the end
+ if (lastPage.length < pageSize) {
+ return undefined;
+ }
+ // Otherwise, return the next start index
+ return allPages.length * pageSize;
+ },
+ initialPageParam: 0,
+ staleTime: 0,
+ refetchOnMount: true,
+ refetchOnWindowFocus: true,
+ refetchOnReconnect: true,
+ });
+
+ const { t } = useTranslation();
+
+ // Flatten all pages into a single array
+ const allItems = data?.pages.flat() || [];
+
+ if (hideIfEmpty === true && allItems.length === 0 && !isLoading) return null;
+ if (disabled || !title) return null;
+
+ const handleScroll = (event: any) => {
+ const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
+ const paddingToBottom = 20;
+
+ // Check if we're near the end of the scroll
+ if (
+ layoutMeasurement.width + contentOffset.x >=
+ contentSize.width - paddingToBottom
+ ) {
+ if (hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }
+ };
+
+ return (
+
+
+ {title}
+
+ {isLoading === false && allItems.length === 0 && (
+
+ {t("home.no_items")}
+
+ )}
+ {isLoading ? (
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+ Nisi mollit voluptate amet.
+
+
+
+
+ Lorem ipsum
+
+
+
+ ))}
+
+ ) : (
+
+
+ {allItems.map((item) => (
+
+ {item.Type === "Episode" && orientation === "horizontal" && (
+
+ )}
+ {item.Type === "Episode" && orientation === "vertical" && (
+
+ )}
+ {item.Type === "Movie" && orientation === "horizontal" && (
+
+ )}
+ {item.Type === "Movie" && orientation === "vertical" && (
+
+ )}
+ {item.Type === "Series" && orientation === "vertical" && (
+
+ )}
+ {item.Type === "Series" && orientation === "horizontal" && (
+
+ )}
+ {item.Type === "Program" && (
+
+ )}
+ {item.Type === "BoxSet" && orientation === "vertical" && (
+
+ )}
+ {item.Type === "BoxSet" && orientation === "horizontal" && (
+
+ )}
+ {item.Type === "Playlist" && orientation === "vertical" && (
+
+ )}
+ {item.Type === "Playlist" && orientation === "horizontal" && (
+
+ )}
+ {item.Type === "Video" && orientation === "vertical" && (
+
+ )}
+ {item.Type === "Video" && orientation === "horizontal" && (
+
+ )}
+
+
+ ))}
+ {/* Loading indicator for next page */}
+ {isFetchingNextPage && (
+
+
+
+ )}
+
+
+ )}
+
+ );
+};