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 && (