From 4f04ae874fcedbdf76a9be39cc0719ac90a20df8 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 3 May 2025 15:05:46 +0530 Subject: [PATCH] Enhance FeaturedContent and HeroCatalogsScreen components with improved loading animations and settings updates. Refactor image loading logic for better performance and add saved indicator in HeroCatalogsScreen. Update HomeScreen to handle settings changes dynamically and adjust layout for better user experience. --- src/components/home/FeaturedContent.tsx | 185 +++++++++--------------- src/hooks/useFeaturedContent.ts | 13 ++ src/screens/HeroCatalogsScreen.tsx | 118 +++++++++++++-- src/screens/HomeScreen.tsx | 21 ++- src/screens/HomeScreenSettings.tsx | 6 +- 5 files changed, 214 insertions(+), 129 deletions(-) diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index e2dc413..8d5110f 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -8,7 +8,9 @@ import { Dimensions, ViewStyle, TextStyle, - ImageStyle + ImageStyle, + ActivityIndicator, + Platform } from 'react-native'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; @@ -40,17 +42,15 @@ const { width, height } = Dimensions.get('window'); const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => { const navigation = useNavigation>(); - const [posterLoaded, setPosterLoaded] = useState(false); - const [logoLoaded, setLogoLoaded] = useState(false); - const [imageError, setImageError] = useState(false); - const [bannerUrl, setBannerUrl] = useState(null); const [logoUrl, setLogoUrl] = useState(null); + const [bannerUrl, setBannerUrl] = useState(null); const prevContentIdRef = useRef(null); // Animation values const posterOpacity = useSharedValue(0); const logoOpacity = useSharedValue(0); - const contentOpacity = useSharedValue(0); + const contentOpacity = useSharedValue(1); // Start visible + const buttonsOpacity = useSharedValue(1); const posterAnimatedStyle = useAnimatedStyle(() => ({ opacity: posterOpacity.value, @@ -64,11 +64,13 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat opacity: contentOpacity.value, })); + const buttonsAnimatedStyle = useAnimatedStyle(() => ({ + opacity: buttonsOpacity.value, + })); + // Preload the image const preloadImage = async (url: string): Promise => { if (!url) return false; - - // If already cached, return true immediately if (imageCache[url]) return true; try { @@ -81,7 +83,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat } }; - // Load poster first, then logo + // Load poster and logo useEffect(() => { if (!featuredContent) return; @@ -91,49 +93,33 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat // Reset states for new content if (contentId !== prevContentIdRef.current) { - setPosterLoaded(false); - setLogoLoaded(false); - setImageError(false); posterOpacity.value = 0; logoOpacity.value = 0; - contentOpacity.value = 0; } prevContentIdRef.current = contentId; - // Sequential loading: poster first, then logo + // Set URLs immediately for instant display + if (posterUrl) setBannerUrl(posterUrl); + if (titleLogo) setLogoUrl(titleLogo); + + // Load images in background const loadImages = async () => { - // Step 1: Load poster + // Load poster if (posterUrl) { - setBannerUrl(posterUrl); const posterSuccess = await preloadImage(posterUrl); - if (posterSuccess) { - setPosterLoaded(true); - // Fade in poster posterOpacity.value = withTiming(1, { duration: 600, easing: Easing.bezier(0.25, 0.1, 0.25, 1) }); - - // After poster loads, start showing content with slight delay - contentOpacity.value = withDelay(150, withTiming(1, { - duration: 400, - easing: Easing.bezier(0.25, 0.1, 0.25, 1) - })); - } else { - setImageError(true); } } - // Step 2: Load logo if available + // Load logo if available if (titleLogo) { - setLogoUrl(titleLogo); const logoSuccess = await preloadImage(titleLogo); - if (logoSuccess) { - setLogoLoaded(true); - // Fade in logo with delay after poster logoOpacity.value = withDelay(300, withTiming(1, { duration: 500, easing: Easing.bezier(0.25, 0.1, 0.25, 1) @@ -145,37 +131,6 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat loadImages(); }, [featuredContent?.id]); - // Preload next content - useEffect(() => { - if (!featuredContent || !posterLoaded) return; - - // After current poster loads, prefetch for potential next items - const preloadNextContent = async () => { - // Simulate preloading next item (in a real app, you'd get this from allFeaturedContent) - if (featuredContent.type === 'movie' && featuredContent.id) { - // Try to preload related content by ID pattern - const relatedIds = [ - `tmdb:${parseInt(featuredContent.id.split(':')[1] || '0') + 1}`, - `tmdb:${parseInt(featuredContent.id.split(':')[1] || '0') + 2}` - ]; - - for (const id of relatedIds) { - // This is just a simulation - in real app you'd have actual next content URLs - const potentialNextPoster = featuredContent.poster?.replace( - featuredContent.id, - id - ); - - if (potentialNextPoster) { - await preloadImage(potentialNextPoster); - } - } - } - }; - - preloadNextContent(); - }, [posterLoaded, featuredContent]); - if (!featuredContent) { return ; } @@ -193,10 +148,9 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat > ))} - - - - - {isSaved ? "Saved" : "Save"} - - - - { - if (featuredContent) { - navigation.navigate('Streams', { - id: featuredContent.id, - type: featuredContent.type - }); - } - }} - > - - Play - + - { - if (featuredContent) { - navigation.navigate('Metadata', { - id: featuredContent.id, - type: featuredContent.type - }); - } - }} - > - - Info - - + + + + + {isSaved ? "Saved" : "Save"} + + + + { + if (featuredContent) { + navigation.navigate('Streams', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }} + > + + Play + + + { + if (featuredContent) { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }} + > + + Info + @@ -290,7 +245,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat const styles = StyleSheet.create({ featuredContainer: { width: '100%', - height: height * 0.6, + height: height * 0.5, marginTop: 0, marginBottom: 8, position: 'relative', @@ -330,7 +285,7 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'flex-end', paddingHorizontal: 16, - paddingBottom: 20, + paddingBottom: 12, }, featuredLogo: { width: width * 0.7, @@ -353,7 +308,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - marginBottom: 16, + marginBottom: 8, flexWrap: 'wrap', gap: 4, }, @@ -376,8 +331,8 @@ const styles = StyleSheet.create({ justifyContent: 'space-evenly', width: '100%', flex: 1, - maxHeight: 65, - paddingTop: 16, + maxHeight: 60, + paddingTop: 0, }, playButton: { flexDirection: 'row', diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index 827cfa2..9997cff 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -176,6 +176,19 @@ export function useFeaturedContent() { } }, [cleanup, genreMap, loadingGenres, contentSource, selectedCatalogs]); + // Subscribe directly to settings emitter for immediate updates + useEffect(() => { + const handleSettingsChange = () => { + // Force refresh when settings change + loadFeaturedContent(true); + }; + + // Subscribe to settings changes + const unsubscribe = settingsEmitter.addListener(handleSettingsChange); + + return unsubscribe; + }, [loadFeaturedContent]); + // Load featured content initially and when content source changes useEffect(() => { const shouldForceRefresh = contentSource === 'tmdb' && diff --git a/src/screens/HeroCatalogsScreen.tsx b/src/screens/HeroCatalogsScreen.tsx index 2fa27d7..6c84b81 100644 --- a/src/screens/HeroCatalogsScreen.tsx +++ b/src/screens/HeroCatalogsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; import { View, Text, @@ -12,9 +12,10 @@ import { useColorScheme, ActivityIndicator, Alert, + Animated } from 'react-native'; -import { useSettings } from '../hooks/useSettings'; -import { useNavigation } from '@react-navigation/native'; +import { useSettings, settingsEmitter } from '../hooks/useSettings'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { colors } from '../styles/colors'; import { catalogService, StreamingAddon } from '../services/catalogService'; @@ -38,6 +39,58 @@ const HeroCatalogsScreen: React.FC = () => { const [catalogs, setCatalogs] = useState([]); const [selectedCatalogs, setSelectedCatalogs] = useState(settings.selectedHeroCatalogs || []); const { getCustomName, isLoadingCustomNames } = useCustomCatalogNames(); + const [showSavedIndicator, setShowSavedIndicator] = useState(false); + const fadeAnim = useRef(new Animated.Value(0)).current; + + // Ensure selected catalogs state is refreshed whenever the screen gains focus + useFocusEffect( + useCallback(() => { + setSelectedCatalogs(settings.selectedHeroCatalogs || []); + }, [settings.selectedHeroCatalogs]) + ); + + // Subscribe to settings changes + useEffect(() => { + const unsubscribe = settingsEmitter.addListener(() => { + // Refresh selected catalogs when settings change + setSelectedCatalogs(settings.selectedHeroCatalogs || []); + }); + + return unsubscribe; + }, [settings.selectedHeroCatalogs]); + + // Fade in/out animation for the "Changes saved" indicator + useEffect(() => { + if (showSavedIndicator) { + Animated.sequence([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true + }), + Animated.delay(1500), + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true + }) + ]).start(() => setShowSavedIndicator(false)); + } + }, [showSavedIndicator, fadeAnim]); + + const handleSave = useCallback(() => { + // First update the settings + updateSetting('selectedHeroCatalogs', selectedCatalogs); + + // Show the confirmation indicator + setShowSavedIndicator(true); + + // Short delay before navigating back to allow settings to save + // and the user to see the confirmation message + setTimeout(() => { + navigation.goBack(); + }, 800); + }, [navigation, selectedCatalogs, updateSetting]); const handleBack = useCallback(() => { navigation.goBack(); @@ -84,11 +137,6 @@ const HeroCatalogsScreen: React.FC = () => { setSelectedCatalogs([]); }, []); - const handleSave = useCallback(() => { - updateSetting('selectedHeroCatalogs', selectedCatalogs); - navigation.goBack(); - }, [navigation, selectedCatalogs, updateSetting]); - const toggleCatalog = useCallback((catalogId: string) => { setSelectedCatalogs(prev => { if (prev.includes(catalogId)) { @@ -127,6 +175,21 @@ const HeroCatalogsScreen: React.FC = () => { + {/* Saved indicator */} + + + Settings Saved + + {loading || isLoadingCustomNames ? ( @@ -153,13 +216,14 @@ const HeroCatalogsScreen: React.FC = () => { style={[styles.saveButton, { backgroundColor: colors.primary }]} onPress={handleSave} > + Save - Select which catalogs to display in the hero section. If none are selected, all catalogs will be used. + Select which catalogs to display in the hero section. If none are selected, all catalogs will be used. Don't forget to press Save when you're done. @@ -256,6 +320,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingVertical: 12, justifyContent: 'space-between', + alignItems: 'center', }, actionButton: { paddingHorizontal: 12, @@ -271,12 +336,25 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8, + backgroundColor: colors.primary, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + minWidth: 100, + elevation: 2, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 1.5, }, saveButtonText: { color: colors.white, fontSize: 14, fontWeight: '600', }, + saveIcon: { + marginRight: 6, + }, infoCard: { marginHorizontal: 16, marginBottom: 16, @@ -320,6 +398,28 @@ const styles = StyleSheet.create({ fontSize: 14, marginTop: 2, }, + savedIndicator: { + position: 'absolute', + top: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 60 : 90, + alignSelf: 'center', + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 24, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + zIndex: 1000, + elevation: 5, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + }, + savedIndicatorText: { + color: '#FFFFFF', + marginLeft: 6, + fontWeight: '600', + }, }); export default HeroCatalogsScreen; \ No newline at end of file diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 777732c..4a78e29 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -387,6 +387,24 @@ const HomeScreen = () => { setFeaturedContentSource(settings.featuredContentSource); }, [settings]); + // Subscribe directly to settings emitter for immediate updates + useEffect(() => { + const handleSettingsChange = () => { + setShowHeroSection(settings.showHeroSection); + setFeaturedContentSource(settings.featuredContentSource); + + // If hero section is enabled, force a refresh of featured content + if (settings.showHeroSection) { + refreshFeatured(); + } + }; + + // Subscribe to settings changes + const unsubscribe = settingsEmitter.addListener(handleSettingsChange); + + return unsubscribe; + }, [refreshFeatured, settings]); + // Update the featured content refresh logic to handle persistence useEffect(() => { if (showHeroSection && featuredContentSource !== settings.featuredContentSource) { @@ -558,12 +576,13 @@ const HomeScreen = () => { } contentContainerStyle={[ homeStyles.scrollContent, - { paddingTop: Platform.OS === 'ios' ? 0 : 0 } + { paddingTop: Platform.OS === 'ios' ? 39 : 90 } ]} showsVerticalScrollIndicator={false} > {showHeroSection && ( { icon="settings-input-component" isDarkMode={isDarkMode} renderControl={() => } + isLast={!settings.showHeroSection || settings.featuredContentSource !== 'catalogs'} /> - {settings.featuredContentSource === 'catalogs' && ( + {settings.showHeroSection && settings.featuredContentSource === 'catalogs' && ( { isLast={true} /> )} - {settings.featuredContentSource !== 'catalogs' && ( - // Placeholder to maintain layout - )} {settings.showHeroSection && (