diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index bf29062..d3cd9d3 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState, useEffect, useCallback, memo } from 'react'; import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp } from 'react-native'; -import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle } from 'react-native-reanimated'; +import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { Image as ExpoImage } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; @@ -10,6 +10,7 @@ import { RootStackParamList } from '../../navigation/AppNavigator'; import { StreamingContent } from '../../services/catalogService'; import { useTheme } from '../../contexts/ThemeContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useSettings } from '../../hooks/useSettings'; interface HeroCarouselProps { items: StreamingContent[]; @@ -25,6 +26,7 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = const navigation = useNavigation>(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); + const { settings } = useSettings(); const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]); const [activeIndex, setActiveIndex] = useState(0); @@ -34,18 +36,29 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = const hasData = data.length > 0; - const handleMomentumEnd = useCallback((event: any) => { - const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0; - const interval = CARD_WIDTH + 16; - const idx = Math.round(offsetX / interval); - const clamped = Math.max(0, Math.min(idx, data.length - 1)); - if (clamped !== activeIndex) { - // Small delay to ensure smooth transition - setTimeout(() => { - setActiveIndex(clamped); - }, 50); - } - }, [activeIndex, data.length]); + // Optimized: update background as soon as scroll starts, without waiting for momentum end + const scrollX = useSharedValue(0); + const interval = CARD_WIDTH + 16; + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollX.value = event.contentOffset.x; + }, + }); + + // Derive the index reactively and only set state when it changes + useAnimatedReaction( + () => { + const idx = Math.round(scrollX.value / interval); + return idx; + }, + (idx, prevIdx) => { + if (idx == null || idx === prevIdx) return; + // Clamp to bounds to avoid out-of-range access + const clamped = Math.max(0, Math.min(idx, data.length - 1)); + runOnJS(setActiveIndex)(clamped); + }, + [data.length] + ); const contentPadding = useMemo(() => ({ paddingHorizontal: (width - CARD_WIDTH) / 2 }), []); @@ -164,7 +177,7 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = return ( - {data.length > 0 && ( + {settings.enableHomeHeroBackground && data.length > 0 && ( {data[activeIndex + 1] && ( = ({ items, loading = false }) = )} )} - {data[activeIndex] && ( + {settings.enableHomeHeroBackground && data[activeIndex] && ( )} {/* Bottom blend to HomeScreen background (not the card) */} - - + )} + = ({ items, loading = false }) = snapToInterval={CARD_WIDTH + 16} decelerationRate="fast" contentContainerStyle={contentPadding} - onMomentumScrollEnd={handleMomentumEnd} + onScroll={scrollHandler} + scrollEventThrottle={16} initialNumToRender={3} windowSize={3} maxToRenderPerBatch={3} diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 323ddd8..81e42c9 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -67,6 +67,8 @@ export interface AppSettings { postersPerRow: number; // 3-6 range for number of posters per row // Home screen content item showPosterTitles: boolean; // Show text titles under posters + // Home screen background behavior + enableHomeHeroBackground: boolean; // Enable dynamic hero background on Home // Trailer settings showTrailers: boolean; // Enable/disable trailer playback in hero section trailerMuted: boolean; // Default to muted for better user experience @@ -110,6 +112,7 @@ export const DEFAULT_SETTINGS: AppSettings = { posterBorderRadius: 12, postersPerRow: 4, showPosterTitles: true, + enableHomeHeroBackground: true, // Trailer settings showTrailers: true, // Enable trailers by default trailerMuted: true, // Default to muted for better user experience diff --git a/src/screens/HomeScreenSettings.tsx b/src/screens/HomeScreenSettings.tsx index 49eadae..d2270b7 100644 --- a/src/screens/HomeScreenSettings.tsx +++ b/src/screens/HomeScreenSettings.tsx @@ -358,6 +358,25 @@ const HomeScreenSettings: React.FC = () => { )} + + {settings.heroStyle === 'carousel' && ( + + ( + handleUpdateSetting('enableHomeHeroBackground', value)} + /> + )} + /> + May impact performance on low-end devices. + + )} )} @@ -489,6 +508,16 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingLeft: 12, }, + settingInlineNote: { + fontSize: 12, + opacity: 0.7, + marginTop: 8, + marginBottom: 8, + textAlign: 'center', + alignSelf: 'center', + width: '100%', + paddingHorizontal: 16, + }, radioCardContainer: { marginHorizontal: 16, marginVertical: 8,