mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-16 08:08:18 +00:00
Strengthens code quality guidelines by establishing strict TypeScript practices that prohibit `any` types and minimize type suppressions. Updates Copilot instructions to emphasize production-ready code with comprehensive type safety rules, error handling requirements, and reliability standards. Explicitly discourages type escape hatches in favor of proper type definitions. Refactors navigation implementation to use URLSearchParams instead of object-based params, eliminating the need for type suppression while maintaining functionality. Removes unnecessary type error suppressions and unused properties throughout codebase, aligning with new standards.
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} />
|
|
))}
|
|
</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;
|