fix: use scrollview for better design

This commit is contained in:
Fredrik Burmester
2024-08-18 10:52:48 +02:00
parent 30280e8b3a
commit dbb7c6c9a5

View File

@@ -1,4 +1,4 @@
import { ColumnItem } from "@/components/common/ColumnItem"; import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton"; import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
@@ -20,12 +20,23 @@ import {
BaseItemKind, BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import { getFilterApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getFilterApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router"; import { Stack, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useEffect, useMemo } from "react";
import { ScrollView, View } from "react-native"; 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 page: React.FC = () => {
const searchParams = useLocalSearchParams(); const searchParams = useLocalSearchParams();
@@ -33,6 +44,7 @@ const page: React.FC = () => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const navigation = useNavigation();
const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom); const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom);
const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom); const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom);
@@ -85,7 +97,7 @@ const page: React.FC = () => {
const response = await getItemsApi(api).getItems({ const response = await getItemsApi(api).getItems({
userId: user?.Id, userId: user?.Id,
parentId: collectionId, parentId: collectionId,
limit: 50, limit: 66,
startIndex: pageParam, startIndex: pageParam,
sortBy: [sortBy[0].key, "SortName", "ProductionYear"], sortBy: [sortBy[0].key, "SortName", "ProductionYear"],
sortOrder: [sortOrder[0].key], sortOrder: [sortOrder[0].key],
@@ -126,9 +138,21 @@ const page: React.FC = () => {
], ],
queryFn: fetchItems, queryFn: fetchItems,
getNextPageParam: (lastPage, pages) => { getNextPageParam: (lastPage, pages) => {
const totalItems = lastPage?.TotalRecordCount || 0; if (
if ((lastPage?.Items?.length || 0) < totalItems) { !lastPage?.Items ||
return lastPage?.Items?.length; !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 { } else {
return undefined; return undefined;
} }
@@ -141,160 +165,170 @@ const page: React.FC = () => {
return data?.pages.flatMap((page) => page?.Items)[0]?.Type || null; return data?.pages.flatMap((page) => page?.Items)[0]?.Type || null;
}, [data]); }, [data]);
const flatData = useMemo(() => {
return data?.pages.flatMap((p) => p?.Items) || [];
}, [data]);
if (!collection || !collection.CollectionType) return null; if (!collection || !collection.CollectionType) return null;
return ( return (
<> <ScrollView
<FlashList contentInsetAdjustmentBehavior="automatic"
refreshing={isFetching} onScroll={({ nativeEvent }) => {
data={data?.pages.flatMap((page) => page?.Items) || []} if (isCloseToBottom(nativeEvent)) {
horizontal={false} fetchNextPage();
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{
paddingTop: 17,
paddingHorizontal: 10,
paddingBottom: 150,
}}
onEndReached={fetchNextPage}
onEndReachedThreshold={0.5}
renderItem={({ item, index }) =>
item ? (
<ColumnItem index={index} numColumns={3} style={{}}>
<TouchableItemRouter
style={{
width: "100%",
padding: 4,
}}
item={item}
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
</ColumnItem>
) : null
} }
numColumns={3} }}
estimatedItemSize={200} scrollEventThrottle={400}
ListHeaderComponent={ >
<View className="mb-4"> <View className="mt-4 mb-24">
<ScrollView horizontal> <View className="mb-4">
<View className="flex flex-row space-x-1"> <ScrollView horizontal showsHorizontalScrollIndicator={false}>
<ResetFiltersButton /> <View className="flex flex-row space-x-1 px-3">
<FilterButton <ResetFiltersButton />
collectionId={collectionId} <FilterButton
queryKey="genreFilter" collectionId={collectionId}
queryFn={async () => { queryKey="genreFilter"
if (!api) return null; queryFn={async () => {
const response = await getFilterApi( if (!api) return null;
api const response = await getFilterApi(
).getQueryFiltersLegacy({ api
userId: user?.Id, ).getQueryFiltersLegacy({
includeItemTypes: type ? [type] : [], userId: user?.Id,
parentId: collectionId, includeItemTypes: type ? [type] : [],
}); parentId: collectionId,
return response.data.Genres || []; });
}} return response.data.Genres || [];
set={setSelectedGenres}
values={selectedGenres}
title="Genres"
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/>
<FilterButton
collectionId={collectionId}
queryKey="tagsFilter"
queryFn={async () => {
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())
}
/>
<FilterButton
collectionId={collectionId}
queryKey="yearFilter"
queryFn={async () => {
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())
}
/>
<FilterButton
collectionId={collectionId}
queryKey="sortByFilter"
queryFn={async () => {
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}
/>
<FilterButton
showSearch={false}
collectionId={collectionId}
queryKey="orderByFilter"
queryFn={async () => {
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())
}
/>
</View>
</ScrollView>
{!type && isFetching && (
<Loader
style={{
marginTop: 300,
}} }}
set={setSelectedGenres}
values={selectedGenres}
title="Genres"
renderItemLabel={(item) => item.toString()}
searchFilter={(item, search) =>
item.toLowerCase().includes(search.toLowerCase())
}
/> />
)} <FilterButton
</View> collectionId={collectionId}
} queryKey="tagsFilter"
/> queryFn={async () => {
</> 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())
}
/>
<FilterButton
collectionId={collectionId}
queryKey="yearFilter"
queryFn={async () => {
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())
}
/>
<FilterButton
icon="sort"
collectionId={collectionId}
queryKey="sortByFilter"
queryFn={async () => {
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}
/>
<FilterButton
icon="sort"
showSearch={false}
collectionId={collectionId}
queryKey="orderByFilter"
queryFn={async () => {
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())
}
/>
</View>
</ScrollView>
{!type && isFetching && (
<Loader
style={{
marginTop: 300,
}}
/>
)}
</View>
<View className="flex flex-row flex-wrap px-4 justify-between after:content-['']">
{flatData.map(
(item, index) =>
item && (
<TouchableItemRouter
key={`${item.Id}`}
style={{
width: "32%",
marginBottom: 4,
}}
item={item}
className={`
`}
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)
)}
{flatData.length % 3 !== 0 && (
<View
style={{
width: "33%",
}}
></View>
)}
</View>
</View>
</ScrollView>
); );
}; };