diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index ae5a08b..5c2205f 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp } from 'react-native'; -import Animated, { FadeIn, Easing } from 'react-native-reanimated'; +import Animated, { FadeIn, FadeOut, Easing } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { Image as ExpoImage } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; @@ -9,29 +9,115 @@ 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'; interface HeroCarouselProps { items: StreamingContent[]; + loading?: boolean; } const { width } = Dimensions.get('window'); -const CARD_WIDTH = Math.min(width * 0.88, 520); -const CARD_HEIGHT = Math.round(CARD_WIDTH * 9 / 16) + 160; // increased extra space for text/actions +const CARD_WIDTH = Math.min(width * 0.8, 480); +const CARD_HEIGHT = Math.round(CARD_WIDTH * 9 / 16) + 270; // further increased space for text/actions -const HeroCarousel: React.FC = ({ items }) => { +const HeroCarousel: React.FC = ({ items, loading = false }) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]); + const [activeIndex, setActiveIndex] = useState(0); - if (data.length === 0) { - return null; + if (loading) { + return ( + }> + + String(i)} + horizontal + showsHorizontalScrollIndicator={false} + snapToInterval={CARD_WIDTH + 16} + decelerationRate="fast" + contentContainerStyle={{ paddingHorizontal: (width - CARD_WIDTH) / 2 }} + renderItem={() => ( + + }> + + + + + + } /> + } /> + + } /> + } /> + + + + + )} + /> + + + ); } + if (data.length === 0) return null; + + const handleMomentumEnd = (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) setActiveIndex(clamped); + }; + return ( + {data[activeIndex] && ( + } + pointerEvents="none" + > + + + + + + )} + {/* Bottom blend to HomeScreen background (not the card) */} + item.id} @@ -40,6 +126,7 @@ const HeroCarousel: React.FC = ({ items }) => { snapToInterval={CARD_WIDTH + 16} decelerationRate="fast" contentContainerStyle={{ paddingHorizontal: (width - CARD_WIDTH) / 2 }} + onMomentumScrollEnd={handleMomentumEnd} renderItem={({ item }) => ( = ({ items }) => { cachePolicy="memory-disk" /> @@ -103,6 +190,34 @@ 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, @@ -114,6 +229,47 @@ const styles = StyleSheet.create({ shadowOpacity: 0.3, shadowRadius: 12, }, + 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, @@ -132,6 +288,7 @@ const styles = StyleSheet.create({ bottom: 0, top: 0, }, + info: { position: 'absolute', left: 0, diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index a3c03ba..092c84b 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -312,18 +312,31 @@ export function useFeaturedContent() { // Subscribe directly to settings emitter for immediate updates useEffect(() => { const handleSettingsChange = () => { - // Only refresh if current content source is different from settings - // This prevents duplicate refreshes when HomeScreen also handles this event - if (contentSource !== settings.featuredContentSource) { - logger.info('[useFeaturedContent] event:content-source-changed', { from: contentSource, to: settings.featuredContentSource }); - // Content source will be updated in the next render cycle due to state updates - // No need to call loadFeaturedContent here as it will be triggered by contentSource change - } else if ( - contentSource === 'catalogs' && - JSON.stringify(selectedCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs) - ) { - // Only refresh if using catalogs and selected catalogs changed - logger.info('[useFeaturedContent] event:selected-catalogs-changed'); + // Always reflect settings immediately in this hook + const nextSource = settings.featuredContentSource; + const nextSelected = settings.selectedHeroCatalogs || []; + + const sourceChanged = contentSource !== nextSource; + const catalogsChanged = JSON.stringify(selectedCatalogs) !== JSON.stringify(nextSelected); + + if (sourceChanged || (nextSource === 'catalogs' && catalogsChanged)) { + logger.info('[useFeaturedContent] event:settings-changed:immediate-refresh', { + fromSource: contentSource, + toSource: nextSource, + catalogsChanged + }); + + // Update internal state immediately so dependent effects are in sync + setContentSource(nextSource); + setSelectedCatalogs(nextSelected); + + // Clear current data to reflect change instantly in UI + setAllFeaturedContent([]); + setFeaturedContent(null); + persistentStore.allFeaturedContent = []; + persistentStore.featuredContent = null; + + // Force a fresh load loadFeaturedContent(true); } }; diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 14b6635..8354a32 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -612,6 +612,7 @@ const HomeScreen = () => { ) : (