From dbb7c6c9a5366167698b040f91cf6eb7e2b7a03b Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 18 Aug 2024 10:52:48 +0200 Subject: [PATCH] fix: use scrollview for better design --- .../library/collections/[collectionId].tsx | 348 ++++++++++-------- 1 file changed, 191 insertions(+), 157 deletions(-) diff --git a/app/(auth)/(tabs)/library/collections/[collectionId].tsx b/app/(auth)/(tabs)/library/collections/[collectionId].tsx index 626366b6..629ccf8e 100644 --- a/app/(auth)/(tabs)/library/collections/[collectionId].tsx +++ b/app/(auth)/(tabs)/library/collections/[collectionId].tsx @@ -1,4 +1,4 @@ -import { ColumnItem } from "@/components/common/ColumnItem"; +import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { FilterButton } from "@/components/filters/FilterButton"; import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; @@ -20,12 +20,23 @@ import { BaseItemKind, } from "@jellyfin/sdk/lib/generated-client/models"; import { getFilterApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -import { FlashList } from "@shopify/flash-list"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { useLocalSearchParams } from "expo-router"; +import { Stack, useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; -import React, { useCallback, useMemo } from "react"; -import { ScrollView, View } from "react-native"; +import React, { useCallback, useEffect, useMemo } from "react"; +import { NativeScrollEvent, ScrollView, View } from "react-native"; + +const isCloseToBottom = ({ + layoutMeasurement, + contentOffset, + contentSize, +}: NativeScrollEvent) => { + const paddingToBottom = 200; + return ( + layoutMeasurement.height + contentOffset.y >= + contentSize.height - paddingToBottom + ); +}; const page: React.FC = () => { const searchParams = useLocalSearchParams(); @@ -33,6 +44,7 @@ const page: React.FC = () => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); + const navigation = useNavigation(); const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom); const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom); @@ -85,7 +97,7 @@ const page: React.FC = () => { const response = await getItemsApi(api).getItems({ userId: user?.Id, parentId: collectionId, - limit: 50, + limit: 66, startIndex: pageParam, sortBy: [sortBy[0].key, "SortName", "ProductionYear"], sortOrder: [sortOrder[0].key], @@ -126,9 +138,21 @@ const page: React.FC = () => { ], queryFn: fetchItems, getNextPageParam: (lastPage, pages) => { - const totalItems = lastPage?.TotalRecordCount || 0; - if ((lastPage?.Items?.length || 0) < totalItems) { - return lastPage?.Items?.length; + 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; } else { return undefined; } @@ -141,160 +165,170 @@ const page: React.FC = () => { return data?.pages.flatMap((page) => page?.Items)[0]?.Type || null; }, [data]); + const flatData = useMemo(() => { + return data?.pages.flatMap((p) => p?.Items) || []; + }, [data]); + if (!collection || !collection.CollectionType) return null; return ( - <> - page?.Items) || []} - horizontal={false} - contentInsetAdjustmentBehavior="automatic" - contentContainerStyle={{ - paddingTop: 17, - paddingHorizontal: 10, - paddingBottom: 150, - }} - onEndReached={fetchNextPage} - onEndReachedThreshold={0.5} - renderItem={({ item, index }) => - item ? ( - - - - - - - ) : null + { + if (isCloseToBottom(nativeEvent)) { + fetchNextPage(); } - numColumns={3} - estimatedItemSize={200} - ListHeaderComponent={ - - - - - { - if (!api) return null; - const response = await getFilterApi( - api - ).getQueryFiltersLegacy({ - userId: user?.Id, - includeItemTypes: type ? [type] : [], - parentId: collectionId, - }); - return response.data.Genres || []; - }} - set={setSelectedGenres} - values={selectedGenres} - title="Genres" - renderItemLabel={(item) => item.toString()} - searchFilter={(item, search) => - item.toLowerCase().includes(search.toLowerCase()) - } - /> - { - if (!api) return null; - const response = await getFilterApi( - api - ).getQueryFiltersLegacy({ - userId: user?.Id, - includeItemTypes: type ? [type] : [], - parentId: collectionId, - }); - return response.data.Tags || []; - }} - set={setSelectedTags} - values={selectedTags} - title="Tags" - renderItemLabel={(item) => item.toString()} - searchFilter={(item, search) => - item.toLowerCase().includes(search.toLowerCase()) - } - /> - { - if (!api) return null; - const response = await getFilterApi( - api - ).getQueryFiltersLegacy({ - userId: user?.Id, - includeItemTypes: type ? [type] : [], - parentId: collectionId, - }); - return ( - response.data.Years?.sort((a, b) => b - a).map((y) => - y.toString() - ) || [] - ); - }} - set={setSelectedYears} - values={selectedYears} - title="Years" - renderItemLabel={(item) => item.toString()} - searchFilter={(item, search) => - item.toLowerCase().includes(search.toLowerCase()) - } - /> - { - return sortOptions; - }} - set={setSortBy} - values={sortBy} - title="Sort by" - renderItemLabel={(item) => item.value} - searchFilter={(item, search) => - item.value.toLowerCase().includes(search.toLowerCase()) || - item.value.toLowerCase().includes(search.toLowerCase()) - } - showSearch={false} - /> - { - return sortOrderOptions; - }} - set={setSortOrder} - values={sortOrder} - title="Order by" - renderItemLabel={(item) => item.value} - searchFilter={(item, search) => - item.value.toLowerCase().includes(search.toLowerCase()) || - item.value.toLowerCase().includes(search.toLowerCase()) - } - /> - - - {!type && isFetching && ( - + + + + + + { + if (!api) return null; + const response = await getFilterApi( + api + ).getQueryFiltersLegacy({ + userId: user?.Id, + includeItemTypes: type ? [type] : [], + parentId: collectionId, + }); + return response.data.Genres || []; }} + set={setSelectedGenres} + values={selectedGenres} + title="Genres" + renderItemLabel={(item) => item.toString()} + searchFilter={(item, search) => + item.toLowerCase().includes(search.toLowerCase()) + } /> - )} - - } - /> - + { + if (!api) return null; + const response = await getFilterApi( + api + ).getQueryFiltersLegacy({ + userId: user?.Id, + includeItemTypes: type ? [type] : [], + parentId: collectionId, + }); + return response.data.Tags || []; + }} + set={setSelectedTags} + values={selectedTags} + title="Tags" + renderItemLabel={(item) => item.toString()} + searchFilter={(item, search) => + item.toLowerCase().includes(search.toLowerCase()) + } + /> + { + if (!api) return null; + const response = await getFilterApi( + api + ).getQueryFiltersLegacy({ + userId: user?.Id, + includeItemTypes: type ? [type] : [], + parentId: collectionId, + }); + return ( + response.data.Years?.sort((a, b) => b - a).map((y) => + y.toString() + ) || [] + ); + }} + set={setSelectedYears} + values={selectedYears} + title="Years" + renderItemLabel={(item) => item.toString()} + searchFilter={(item, search) => + item.toLowerCase().includes(search.toLowerCase()) + } + /> + { + return sortOptions; + }} + set={setSortBy} + values={sortBy} + title="Sort by" + renderItemLabel={(item) => item.value} + searchFilter={(item, search) => + item.value.toLowerCase().includes(search.toLowerCase()) || + item.value.toLowerCase().includes(search.toLowerCase()) + } + showSearch={false} + /> + { + return sortOrderOptions; + }} + set={setSortOrder} + values={sortOrder} + title="Order by" + renderItemLabel={(item) => item.value} + searchFilter={(item, search) => + item.value.toLowerCase().includes(search.toLowerCase()) || + item.value.toLowerCase().includes(search.toLowerCase()) + } + /> + + + {!type && isFetching && ( + + )} + + + {flatData.map( + (item, index) => + item && ( + + + + + ) + )} + {flatData.length % 3 !== 0 && ( + + )} + + + ); };