mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-01 11:38:26 +01:00
[Jellyseerr] Show genre/studio/network discover sliders
implements #326
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { type PropsWithChildren, type ReactElement } from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import {NativeScrollEvent, NativeSyntheticEvent, View, ViewProps} from "react-native";
|
||||
import Animated, {
|
||||
interpolate,
|
||||
useAnimatedRef,
|
||||
@@ -13,6 +13,7 @@ interface Props extends ViewProps {
|
||||
logo?: ReactElement;
|
||||
episodePoster?: ReactElement;
|
||||
headerHeight?: number;
|
||||
onEndReached?: (() => void) | null | undefined;
|
||||
}
|
||||
|
||||
export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
@@ -21,6 +22,7 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
episodePoster,
|
||||
headerHeight = 400,
|
||||
logo,
|
||||
onEndReached,
|
||||
...props
|
||||
}: Props) => {
|
||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
@@ -47,6 +49,11 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function isCloseToBottom({layoutMeasurement, contentOffset, contentSize}: NativeScrollEvent) {
|
||||
return layoutMeasurement.height + contentOffset.y >= contentSize.height - 20;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1" {...props}>
|
||||
<Animated.ScrollView
|
||||
@@ -55,6 +62,10 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
}}
|
||||
ref={scrollRef}
|
||||
scrollEventThrottle={16}
|
||||
onScroll={e => {
|
||||
if (isCloseToBottom(e.nativeEvent))
|
||||
onEndReached?.()
|
||||
}}
|
||||
>
|
||||
{logo && (
|
||||
<View
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
TvResult,
|
||||
} from "@/utils/jellyseerr/server/models/Search";
|
||||
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
|
||||
import { sortBy } from "lodash";
|
||||
import { useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import {
|
||||
useAnimatedReaction,
|
||||
@@ -19,8 +18,8 @@ import { Text } from "../common/Text";
|
||||
import JellyseerrPoster from "../posters/JellyseerrPoster";
|
||||
import { LoadingSkeleton } from "../search/LoadingSkeleton";
|
||||
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
||||
import DiscoverSlide from "./DiscoverSlide";
|
||||
import PersonPoster from "./PersonPoster";
|
||||
import Discover from "@/components/jellyseerr/discover/Discover";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
searchQuery: string;
|
||||
@@ -101,12 +100,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
if (!searchQuery.length)
|
||||
return (
|
||||
<View className="flex flex-col">
|
||||
{sortBy?.(
|
||||
jellyseerrDiscoverSettings?.filter((s) => s.enabled),
|
||||
"order"
|
||||
).map((slide) => (
|
||||
<DiscoverSlide key={slide.id} slide={slide} />
|
||||
))}
|
||||
<Discover sliders={jellyseerrDiscoverSettings} />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
155
components/jellyseerr/ParallaxSlideShow.tsx
Normal file
155
components/jellyseerr/ParallaxSlideShow.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {Dimensions, View, ViewProps} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Animated } from "react-native";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import {useFocusEffect} from "expo-router";
|
||||
|
||||
const ANIMATION_ENTER = 250;
|
||||
const ANIMATION_EXIT = 250;
|
||||
const BACKDROP_DURATION = 5000;
|
||||
|
||||
type Render = React.ComponentType<any>
|
||||
| React.ReactElement
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
interface Props<T> {
|
||||
data: T[]
|
||||
images: string[];
|
||||
logo?: React.ReactElement;
|
||||
HeaderContent?: () => React.ReactElement;
|
||||
MainContent?: () => React.ReactElement;
|
||||
listHeader: string;
|
||||
renderItem: (item: T, index: number) => Render;
|
||||
keyExtractor: (item: T) => string;
|
||||
onEndReached?: (() => void) | null | undefined;
|
||||
}
|
||||
|
||||
const ParallaxSlideShow = <T extends unknown>({
|
||||
data,
|
||||
images,
|
||||
logo,
|
||||
HeaderContent,
|
||||
MainContent,
|
||||
listHeader,
|
||||
renderItem,
|
||||
keyExtractor,
|
||||
onEndReached,
|
||||
...props
|
||||
}: PropsWithChildren<Props<T> & ViewProps>
|
||||
) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [onEnd, setOnEnd] = useState<boolean>(true);
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const enterAnimation = useCallback(
|
||||
() =>
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: ANIMATION_ENTER,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
[fadeAnim]
|
||||
);
|
||||
|
||||
const exitAnimation = useCallback(
|
||||
() =>
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: ANIMATION_EXIT,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
[fadeAnim]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (images?.length) {
|
||||
enterAnimation().start();
|
||||
const intervalId = setInterval(() => {
|
||||
exitAnimation().start((end) => {
|
||||
if (end.finished)
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % images?.length);
|
||||
});
|
||||
}, BACKDROP_DURATION);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [images, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]);
|
||||
|
||||
return (
|
||||
<View
|
||||
className="flex-1 relative"
|
||||
style={{
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
>
|
||||
<ParallaxScrollView
|
||||
className="flex-1 opacity-100"
|
||||
headerHeight={300}
|
||||
onEndReached={onEndReached}
|
||||
headerImage={
|
||||
<Animated.Image
|
||||
key={images?.[currentIndex]}
|
||||
id={images?.[currentIndex]}
|
||||
source={{
|
||||
uri: images?.[currentIndex],
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
opacity: fadeAnim,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
logo={logo}
|
||||
>
|
||||
<View className="flex flex-col space-y-4 px-4">
|
||||
<View className="flex flex-row justify-between w-full">
|
||||
<View className="flex flex-col w-full">
|
||||
{HeaderContent && HeaderContent()}
|
||||
</View>
|
||||
</View>
|
||||
{MainContent && MainContent()}
|
||||
<View>
|
||||
<FlashList
|
||||
data={data}
|
||||
ListEmptyComponent={
|
||||
<View className="flex flex-col items-center justify-center h-full">
|
||||
<Text className="font-bold text-xl text-neutral-500">
|
||||
No results
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
ListHeaderComponent={
|
||||
<Text className="text-lg font-bold my-2">{listHeader}</Text>
|
||||
}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={false}
|
||||
//@ts-ignore
|
||||
renderItem={({ item, index}) => renderItem(item, index)}
|
||||
keyExtractor={keyExtractor}
|
||||
numColumns={3}
|
||||
estimatedItemSize={214}
|
||||
ItemSeparatorComponent={() => <View className="h-2 w-2" />}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ParallaxScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default ParallaxSlideShow;
|
||||
@@ -26,7 +26,7 @@ const PersonPoster: React.FC<Props & ViewProps> = ({
|
||||
|
||||
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
||||
return (
|
||||
<TouchableOpacity onPress={() => router.push(`/(auth)/(tabs)/${from}/jellyseerr/${id}`)}>
|
||||
<TouchableOpacity onPress={() => router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`)}>
|
||||
<View className="flex flex-col w-28" {...props}>
|
||||
<Poster
|
||||
id={id}
|
||||
|
||||
41
components/jellyseerr/discover/CompanySlide.tsx
Normal file
41
components/jellyseerr/discover/CompanySlide.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, {useCallback} from "react";
|
||||
import {
|
||||
useJellyseerr,
|
||||
} from "@/hooks/useJellyseerr";
|
||||
import {TouchableOpacity, ViewProps} from "react-native";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import {COMPANY_LOGO_IMAGE_FILTER, Network} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
||||
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||
import {Studio} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
||||
import {router, useSegments} from "expo-router";
|
||||
|
||||
const CompanySlide: React.FC<{data: Network[] | Studio[]} & SlideProps & ViewProps> = ({ slide, data, ...props }) => {
|
||||
const segments = useSegments();
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const from = segments[2];
|
||||
|
||||
const navigate = useCallback(({id, image, name}: Network | Studio) => router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
|
||||
params: {id, image, name, type: slide.type }
|
||||
}), [slide]);
|
||||
|
||||
return (
|
||||
<Slide
|
||||
{...props}
|
||||
slide={slide}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={(item, index) => (
|
||||
<TouchableOpacity className="mr-2" onPress={() => navigate(item)}>
|
||||
<GenericSlideCard
|
||||
className="w-28 rounded-lg overflow-hidden border border-neutral-900 p-4"
|
||||
id={item.id.toString()}
|
||||
url={jellyseerrApi?.imageProxy(item.image, COMPANY_LOGO_IMAGE_FILTER)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanySlide;
|
||||
47
components/jellyseerr/discover/Discover.tsx
Normal file
47
components/jellyseerr/discover/Discover.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, {useMemo} from "react";
|
||||
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||
import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
|
||||
import {sortBy} from "lodash";
|
||||
import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide";
|
||||
import CompanySlide from "@/components/jellyseerr/discover/CompanySlide";
|
||||
import {View} from "react-native";
|
||||
import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
||||
import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
||||
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
|
||||
|
||||
interface Props {
|
||||
sliders?: DiscoverSlider[];
|
||||
}
|
||||
const Discover: React.FC<Props> = ({ sliders }) => {
|
||||
if (!sliders)
|
||||
return;
|
||||
|
||||
const sortedSliders = useMemo(
|
||||
() => sortBy(sliders.filter((s) => s.enabled), 'order', 'asc'),
|
||||
[sliders]
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="flex flex-col space-y-4 mb-8">
|
||||
{sortedSliders.map(slide => {
|
||||
switch (slide.type) {
|
||||
case DiscoverSliderType.NETWORKS:
|
||||
return <CompanySlide key={slide.id} slide={slide} data={networks}/>
|
||||
case DiscoverSliderType.STUDIOS:
|
||||
return <CompanySlide key={slide.id} slide={slide} data={studios}/>
|
||||
case DiscoverSliderType.MOVIE_GENRES:
|
||||
case DiscoverSliderType.TV_GENRES:
|
||||
return <GenreSlide key={slide.id} slide={slide} />
|
||||
case DiscoverSliderType.TRENDING:
|
||||
case DiscoverSliderType.POPULAR_MOVIES:
|
||||
case DiscoverSliderType.UPCOMING_MOVIES:
|
||||
case DiscoverSliderType.POPULAR_TV:
|
||||
case DiscoverSliderType.UPCOMING_TV:
|
||||
return <MovieTvSlide key={slide.id} slide={slide}/>
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
};
|
||||
|
||||
export default Discover;
|
||||
59
components/jellyseerr/discover/GenericSlideCard.tsx
Normal file
59
components/jellyseerr/discover/GenericSlideCard.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import {StyleSheet, View, ViewProps} from "react-native";
|
||||
import {Image, ImageContentFit} from "expo-image";
|
||||
import {Text} from "@/components/common/Text";
|
||||
import {LinearGradient} from "expo-linear-gradient";
|
||||
|
||||
export const textShadowStyle = StyleSheet.create({
|
||||
shadow: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: .5,
|
||||
|
||||
elevation: 6,
|
||||
}
|
||||
})
|
||||
|
||||
const GenericSlideCard: React.FC<{id: string; url?: string, title?: string, colors?: string[], contentFit?: ImageContentFit} & ViewProps> = ({
|
||||
id,
|
||||
url,
|
||||
title,
|
||||
colors = ['#9333ea', 'transparent'],
|
||||
contentFit = "contain",
|
||||
...props
|
||||
}) => (
|
||||
<>
|
||||
<LinearGradient
|
||||
colors={colors}
|
||||
start={{x: 0.5, y: 1.75}}
|
||||
end={{x: 0.5, y: 0}}
|
||||
className="rounded-xl"
|
||||
>
|
||||
<View className="rounded-xl" {...props}>
|
||||
<Image
|
||||
key={id}
|
||||
id={id}
|
||||
source={url ? {uri: url} : null}
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit={contentFit}
|
||||
style={{
|
||||
aspectRatio: "4/3",
|
||||
}}
|
||||
/>
|
||||
{title &&
|
||||
<View
|
||||
className="absolute justify-center top-0 left-0 right-0 bottom-0 items-center"
|
||||
>
|
||||
<Text className="text-center font-bold" style={textShadowStyle.shadow}>{title}</Text>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</>
|
||||
);
|
||||
|
||||
export default GenericSlideCard;
|
||||
56
components/jellyseerr/discover/GenreSlide.tsx
Normal file
56
components/jellyseerr/discover/GenreSlide.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, {useCallback} from "react";
|
||||
import {Endpoints, useJellyseerr,} from "@/hooks/useJellyseerr";
|
||||
import {TouchableOpacity, ViewProps} from "react-native";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||
import {router, useSegments} from "expo-router";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
|
||||
import {genreColorMap} from "@/utils/jellyseerr/src/components/Discover/constants";
|
||||
import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
|
||||
|
||||
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const segments = useSegments();
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const from = segments[2];
|
||||
|
||||
const navigate = useCallback((genre: GenreSliderItem) => router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
|
||||
params: {type: slide.type, name: genre.name}
|
||||
}), [slide]);
|
||||
|
||||
const {data, isFetching, isLoading } = useQuery({
|
||||
queryKey: ['jellyseerr', 'discover', slide.type, slide.id],
|
||||
queryFn: async () => {
|
||||
return jellyseerrApi?.getGenreSliders(
|
||||
slide.type == DiscoverSliderType.MOVIE_GENRES
|
||||
? Endpoints.MOVIE
|
||||
: Endpoints.TV
|
||||
)
|
||||
},
|
||||
enabled: !!jellyseerrApi
|
||||
})
|
||||
|
||||
return (
|
||||
data && <Slide
|
||||
{...props}
|
||||
slide={slide}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={(item, index) => (
|
||||
<TouchableOpacity className="mr-2" onPress={() => navigate(item)}>
|
||||
<GenericSlideCard
|
||||
className="w-28 rounded-lg overflow-hidden border border-neutral-900"
|
||||
id={item.id.toString()}
|
||||
title={item.name}
|
||||
colors={[]}
|
||||
contentFit={"cover"}
|
||||
url={jellyseerrApi?.imageProxy(item.backdrops?.[0], `w780_filter(duotone,${genreColorMap[item.id] ?? genreColorMap[0]})`)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenreSlide;
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useMemo } from "react";
|
||||
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||
import React, {useMemo} from "react";
|
||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||
import {
|
||||
DiscoverEndpoint,
|
||||
@@ -9,17 +8,13 @@ import {
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { View } from "react-native";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import {ViewProps} from "react-native";
|
||||
|
||||
interface Props {
|
||||
slide: DiscoverSlider;
|
||||
}
|
||||
const DiscoverSlide: React.FC<Props> = ({ slide }) => {
|
||||
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
|
||||
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||
queryKey: ["jellyseerr", "discover", slide.id],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
let endpoint: DiscoverEndpoint | undefined = undefined;
|
||||
@@ -62,42 +57,28 @@ const DiscoverSlide: React.FC<Props> = ({ slide }) => {
|
||||
});
|
||||
|
||||
const flatData = useMemo(
|
||||
() =>
|
||||
data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
|
||||
() => data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
flatData &&
|
||||
flatData?.length > 0 && (
|
||||
<View className="mb-4">
|
||||
<Text className="font-bold text-lg mb-2 px-4">
|
||||
{DiscoverSliderType[slide.type].toString().toTitle()}
|
||||
</Text>
|
||||
<FlashList
|
||||
horizontal
|
||||
contentContainerStyle={{
|
||||
paddingLeft: 16,
|
||||
}}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(item) => item!!.id.toString()}
|
||||
estimatedItemSize={250}
|
||||
data={flatData}
|
||||
onEndReachedThreshold={1}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage) fetchNextPage();
|
||||
}}
|
||||
renderItem={({ item }) =>
|
||||
item ? (
|
||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Slide
|
||||
{...props}
|
||||
slide={slide}
|
||||
data={flatData}
|
||||
keyExtractor={(item) => item!!.id.toString()}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage)
|
||||
fetchNextPage()
|
||||
}}
|
||||
renderItem={(item) =>
|
||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscoverSlide;
|
||||
export default MovieTvSlide;
|
||||
55
components/jellyseerr/discover/Slide.tsx
Normal file
55
components/jellyseerr/discover/Slide.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, {PropsWithChildren} from "react";
|
||||
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import {View, ViewProps} from "react-native";
|
||||
|
||||
export interface SlideProps {
|
||||
slide: DiscoverSlider;
|
||||
}
|
||||
|
||||
interface Props<T> extends SlideProps {
|
||||
data: T[]
|
||||
renderItem: (item: T, index: number) =>
|
||||
| React.ComponentType<any>
|
||||
| React.ReactElement
|
||||
| null
|
||||
| undefined;
|
||||
keyExtractor: (item: T) => string;
|
||||
onEndReached?: (() => void) | null | undefined;
|
||||
}
|
||||
|
||||
const Slide = <T extends unknown>({
|
||||
data,
|
||||
slide,
|
||||
renderItem,
|
||||
keyExtractor,
|
||||
onEndReached,
|
||||
...props
|
||||
}: PropsWithChildren<Props<T> & ViewProps>
|
||||
) => {
|
||||
return (
|
||||
<View {...props}>
|
||||
<Text className="font-bold text-lg mb-2 px-4">
|
||||
{DiscoverSliderType[slide.type].toString().toTitle()}
|
||||
</Text>
|
||||
<FlashList
|
||||
horizontal
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={keyExtractor}
|
||||
estimatedItemSize={250}
|
||||
data={data}
|
||||
onEndReachedThreshold={1}
|
||||
onEndReached={onEndReached}
|
||||
//@ts-ignore
|
||||
renderItem={({item, index}) => item ? renderItem(item, index) : <></>}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slide;
|
||||
Reference in New Issue
Block a user