import React, { useMemo, useState, useEffect, useCallback, memo } from 'react'; import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp, Platform } from 'react-native'; 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'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; 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[]; loading?: boolean; } const { width } = Dimensions.get('window'); const CARD_WIDTH = Math.min(width * 0.8, 480); const CARD_HEIGHT = Math.round(CARD_WIDTH * 9 / 16) + 310; // increased for more vertical space 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); const [failedLogoIds, setFailedLogoIds] = useState>(new Set()); // Note: do not early-return before hooks. Loading UI is returned later. const hasData = data.length > 0; // 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 }), []); const keyExtractor = useCallback((item: StreamingContent) => item.id, []); const getItemLayout = useCallback( (_: unknown, index: number) => { const length = CARD_WIDTH + 16; const offset = length * index; return { length, offset, index }; }, [] ); const handleNavigateToMetadata = useCallback((id: string, type: any) => { navigation.navigate('Metadata', { id, type }); }, [navigation]); const handleNavigateToStreams = useCallback((id: string, type: any) => { navigation.navigate('Streams', { id, type }); }, [navigation]); if (loading) { return ( }> String(i)} horizontal showsHorizontalScrollIndicator={false} snapToInterval={CARD_WIDTH + 16} decelerationRate="fast" contentContainerStyle={{ paddingHorizontal: (width - CARD_WIDTH) / 2 }} renderItem={() => ( }> } /> } /> } /> } /> )} /> ); } // Memoized background component with improved timing const BackgroundImage = React.memo(({ item, insets }: { item: StreamingContent; insets: any; }) => { const animatedOpacity = useSharedValue(1); useEffect(() => { // Start with opacity 0 and animate to 1 animatedOpacity.value = 0; animatedOpacity.value = withTiming(1, { duration: 400 }); }, [item.id]); const animatedStyle = useAnimatedStyle(() => ({ opacity: animatedOpacity.value, })); return ( } pointerEvents="none" > ); }); if (!hasData) return null; return ( {settings.enableHomeHeroBackground && data.length > 0 && ( {data[activeIndex + 1] && ( )} {activeIndex > 0 && data[activeIndex - 1] && ( )} )} {settings.enableHomeHeroBackground && data[activeIndex] && ( )} {/* Bottom blend to HomeScreen background (not the card) */} {settings.enableHomeHeroBackground && ( )} ( setFailedLogoIds((prev) => new Set(prev).add(item.id))} onPressInfo={() => handleNavigateToMetadata(item.id, item.type)} onPressPlay={() => handleNavigateToStreams(item.id, item.type)} /> )} /> ); }; interface CarouselCardProps { item: StreamingContent; colors: any; logoFailed: boolean; onLogoError: () => void; onPressPlay: () => void; onPressInfo: () => void; } const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressPlay, onPressInfo }) => { return ( }> {item.logo && !logoFailed ? ( ) : ( {item.name} )} {item.genres && ( {item.genres.slice(0, 3).join(' • ')} )} Play Info ); }); const styles = StyleSheet.create({ container: { paddingVertical: 12, }, backgroundContainer: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, backgroundImage: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, backgroundOverlay: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, bottomBlend: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 160, }, card: { width: CARD_WIDTH, height: CARD_HEIGHT, borderRadius: 16, overflow: 'hidden', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, }, skeletonCard: { width: CARD_WIDTH, height: CARD_HEIGHT, borderRadius: 16, overflow: 'hidden', }, skeletonBannerFull: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(255,255,255,0.06)' }, bannerOverlay: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, skeletonInfo: { paddingHorizontal: 16, paddingTop: 10, }, skeletonLine: { height: 14, borderRadius: 7, backgroundColor: 'rgba(255,255,255,0.08)' }, skeletonActions: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 12, }, skeletonPill: { height: 36, borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.1)' }, bannerContainer: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, banner: { width: '100%', height: '100%', }, bannerGradient: { position: 'absolute', left: 0, right: 0, bottom: 0, top: 0, }, info: { position: 'absolute', left: 0, right: 0, bottom: 0, paddingHorizontal: 16, paddingTop: 10, paddingBottom: 12, alignItems: 'center', }, title: { fontSize: 18, fontWeight: '800', }, genres: { marginTop: 2, fontSize: 13, }, actions: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 12, justifyContent: 'center', }, logo: { width: Math.round(CARD_WIDTH * 0.72), height: 64, marginBottom: 6, }, playButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 24, }, playText: { fontWeight: '700', marginLeft: 6, fontSize: 14, }, secondaryButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 9, borderRadius: 22, borderWidth: 1, }, secondaryText: { fontWeight: '600', marginLeft: 6, fontSize: 14, }, }); export default React.memo(HeroCarousel);