mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-12 17:00:23 +01:00
fix: use scrollview for better design
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user