mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🌐 Translation Sync / sync-translations (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
import { FlashList } from "@shopify/flash-list";
|
|
import type React from "react";
|
|
import {
|
|
type PropsWithChildren,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { Animated, View, type ViewProps } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { Text } from "@/components/common/Text";
|
|
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
|
import { GridSkeleton } from "./GridSkeleton";
|
|
|
|
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;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const ParallaxSlideShow = <T,>({
|
|
data,
|
|
images,
|
|
logo,
|
|
HeaderContent,
|
|
MainContent,
|
|
listHeader,
|
|
renderItem,
|
|
keyExtractor,
|
|
onEndReached,
|
|
isLoading = false,
|
|
}: PropsWithChildren<Props<T> & ViewProps>) => {
|
|
const insets = useSafeAreaInsets();
|
|
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
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(() => {
|
|
Animated.sequence([enterAnimation(), exitAnimation()]).start(() => {
|
|
fadeAnim.setValue(0);
|
|
setCurrentIndex((prevIndex) => (prevIndex + 1) % images?.length);
|
|
});
|
|
}, BACKDROP_DURATION);
|
|
|
|
return () => {
|
|
clearInterval(intervalId);
|
|
};
|
|
}
|
|
}, [
|
|
fadeAnim,
|
|
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?.()}</View>
|
|
</View>
|
|
{MainContent?.()}
|
|
<View>
|
|
{isLoading ? (
|
|
<View>
|
|
<Text className='text-lg font-bold my-2'>{listHeader}</Text>
|
|
<View className='px-4'>
|
|
<View className='flex flex-row flex-wrap'>
|
|
{Array.from({ length: 9 }, (_, i) => (
|
|
<GridSkeleton key={i} index={i} />
|
|
))}
|
|
</View>
|
|
</View>
|
|
</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-expect-error
|
|
renderItem={({ item, index }) => renderItem(item, index)}
|
|
keyExtractor={keyExtractor}
|
|
numColumns={3}
|
|
ItemSeparatorComponent={() => <View className='h-2 w-2' />}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</ParallaxScrollView>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default ParallaxSlideShow;
|