diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 8d5110f..405208d 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -245,7 +245,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat const styles = StyleSheet.create({ featuredContainer: { width: '100%', - height: height * 0.5, + height: height * 0.48, marginTop: 0, marginBottom: 8, position: 'relative', @@ -285,7 +285,7 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'flex-end', paddingHorizontal: 16, - paddingBottom: 12, + paddingBottom: 4, }, featuredLogo: { width: width * 0.7, @@ -308,7 +308,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - marginBottom: 8, + marginBottom: 4, flexWrap: 'wrap', gap: 4, }, @@ -331,7 +331,7 @@ const styles = StyleSheet.create({ justifyContent: 'space-evenly', width: '100%', flex: 1, - maxHeight: 60, + maxHeight: 55, paddingTop: 0, }, playButton: { diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index 9997cff..249fefe 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -11,7 +11,13 @@ const persistentStore = { featuredContent: null as StreamingContent | null, allFeaturedContent: [] as StreamingContent[], lastFetchTime: 0, - isFirstLoad: true + isFirstLoad: true, + // Track last used settings to detect changes on app restart + lastSettings: { + showHeroSection: true, + featuredContentSource: 'tmdb' as 'tmdb' | 'catalogs', + selectedHeroCatalogs: [] as string[] + } }; // Cache timeout in milliseconds (e.g., 5 minutes) @@ -30,7 +36,7 @@ export function useFeaturedContent() { const { genreMap, loadingGenres } = useGenres(); - // Update local state when settings change + // Simple update for state variables useEffect(() => { setContentSource(settings.featuredContentSource); setSelectedCatalogs(settings.selectedHeroCatalogs || []); @@ -44,6 +50,14 @@ export function useFeaturedContent() { }, []); const loadFeaturedContent = useCallback(async (forceRefresh = false) => { + // First, ensure contentSource matches current settings (could be outdated due to async updates) + if (contentSource !== settings.featuredContentSource) { + console.log(`Updating content source from ${contentSource} to ${settings.featuredContentSource}`); + setContentSource(settings.featuredContentSource); + // We return here and let the effect triggered by contentSource change handle the loading + return; + } + // Check if we should use cached data const now = Date.now(); const cacheAge = now - persistentStore.lastFetchTime; @@ -53,6 +67,7 @@ export function useFeaturedContent() { persistentStore.allFeaturedContent.length > 0 && cacheAge < CACHE_TIMEOUT) { // Use cached data + console.log('Using cached featured content data'); setFeaturedContent(persistentStore.featuredContent); setAllFeaturedContent(persistentStore.allFeaturedContent); setLoading(false); @@ -60,6 +75,7 @@ export function useFeaturedContent() { return; } + console.log(`Loading featured content from ${contentSource}`); setLoading(true); cleanup(); abortControllerRef.current = new AbortController(); @@ -176,32 +192,75 @@ export function useFeaturedContent() { } }, [cleanup, genreMap, loadingGenres, contentSource, selectedCatalogs]); + // Check for settings changes, including during app restart + useEffect(() => { + // Check if settings changed while app was closed + const settingsChanged = + persistentStore.lastSettings.showHeroSection !== settings.showHeroSection || + persistentStore.lastSettings.featuredContentSource !== settings.featuredContentSource || + JSON.stringify(persistentStore.lastSettings.selectedHeroCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs); + + // Update our tracking of last used settings + persistentStore.lastSettings = { + showHeroSection: settings.showHeroSection, + featuredContentSource: settings.featuredContentSource, + selectedHeroCatalogs: [...settings.selectedHeroCatalogs] + }; + + // Force refresh if settings changed during app restart + if (settingsChanged) { + loadFeaturedContent(true); + } + }, [settings, loadFeaturedContent]); + // Subscribe directly to settings emitter for immediate updates useEffect(() => { const handleSettingsChange = () => { - // Force refresh when settings change - loadFeaturedContent(true); + // Only refresh if current content source is different from settings + // This prevents duplicate refreshes when HomeScreen also handles this event + if (contentSource !== settings.featuredContentSource) { + console.log('Content source changed, refreshing featured content'); + console.log('Current content source:', contentSource); + console.log('New settings source:', 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 + console.log('Selected catalogs changed, refreshing featured content'); + loadFeaturedContent(true); + } }; // Subscribe to settings changes const unsubscribe = settingsEmitter.addListener(handleSettingsChange); return unsubscribe; - }, [loadFeaturedContent]); + }, [loadFeaturedContent, settings, contentSource, selectedCatalogs]); // Load featured content initially and when content source changes useEffect(() => { - const shouldForceRefresh = contentSource === 'tmdb' && - contentSource !== persistentStore.featuredContent?.type; - - if (shouldForceRefresh) { + // Force refresh when switching to catalogs or when catalog selection changes + if (contentSource === 'catalogs') { + // Clear cache when switching to catalogs mode setAllFeaturedContent([]); setFeaturedContent(null); persistentStore.allFeaturedContent = []; persistentStore.featuredContent = null; + loadFeaturedContent(true); + } else if (contentSource === 'tmdb' && contentSource !== persistentStore.featuredContent?.type) { + // Clear cache when switching to TMDB mode from catalogs + setAllFeaturedContent([]); + setFeaturedContent(null); + persistentStore.allFeaturedContent = []; + persistentStore.featuredContent = null; + loadFeaturedContent(true); + } else { + // Normal load (might use cache if available) + loadFeaturedContent(false); } - - loadFeaturedContent(shouldForceRefresh); }, [loadFeaturedContent, contentSource, selectedCatalogs]); useEffect(() => { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 426f44e..1899269 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -84,8 +84,11 @@ export const useSettings = () => { try { await AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)); setSettings(newSettings); + console.log(`Setting updated: ${key}`, value); + // Notify all subscribers that settings have changed (if requested) if (emitEvent) { + console.log('Emitting settings change event'); settingsEmitter.emit(); } } catch (error) { diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 4a78e29..977df42 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -393,40 +393,27 @@ const HomeScreen = () => { setShowHeroSection(settings.showHeroSection); setFeaturedContentSource(settings.featuredContentSource); - // If hero section is enabled, force a refresh of featured content - if (settings.showHeroSection) { - refreshFeatured(); - } + // The featured content refresh is now handled by the useFeaturedContent hook + // No need to call refreshFeatured() here to avoid duplicate refreshes }; // Subscribe to settings changes const unsubscribe = settingsEmitter.addListener(handleSettingsChange); return unsubscribe; - }, [refreshFeatured, settings]); + }, [settings]); // Update the featured content refresh logic to handle persistence useEffect(() => { + // This effect was causing duplicate refreshes - it's now handled in useFeaturedContent + // We'll keep it just to sync the local state with settings if (showHeroSection && featuredContentSource !== settings.featuredContentSource) { - // Clear any existing timeout - if (refreshTimeoutRef.current) { - clearTimeout(refreshTimeoutRef.current); - } - - // Set a new timeout to debounce the refresh - only when settings actually change - refreshTimeoutRef.current = setTimeout(() => { - refreshFeatured(); - refreshTimeoutRef.current = null; - }, 300); + // Just update the local state + setFeaturedContentSource(settings.featuredContentSource); } - // Cleanup the timeout on unmount - return () => { - if (refreshTimeoutRef.current) { - clearTimeout(refreshTimeoutRef.current); - } - }; - }, [featuredContentSource, settings.featuredContentSource, showHeroSection, refreshFeatured]); + // No timeout needed since we're not refreshing here + }, [settings.featuredContentSource, showHeroSection]); useFocusEffect( useCallback(() => { diff --git a/src/screens/HomeScreenSettings.tsx b/src/screens/HomeScreenSettings.tsx index b7be587..9b0d9b0 100644 --- a/src/screens/HomeScreenSettings.tsx +++ b/src/screens/HomeScreenSettings.tsx @@ -269,7 +269,10 @@ const HomeScreenSettings: React.FC = () => { handleUpdateSetting('featuredContentSource', 'tmdb')} + onPress={() => { + console.log('Selected TMDB source'); + handleUpdateSetting('featuredContentSource', 'tmdb'); + }} label="TMDB Trending Movies" /> @@ -282,7 +285,10 @@ const HomeScreenSettings: React.FC = () => { handleUpdateSetting('featuredContentSource', 'catalogs')} + onPress={() => { + console.log('Selected Catalogs source'); + handleUpdateSetting('featuredContentSource', 'catalogs'); + }} label="Installed Catalogs" /> diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index e76bef4..6c07846 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -147,12 +147,14 @@ class CatalogService { async getHomeCatalogs(): Promise { const addons = await this.getAllAddons(); - const catalogs: CatalogContent[] = []; - + // Load enabled/disabled settings const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY); const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {}; + // Create an array of promises for all catalog fetches + const catalogPromises: Promise[] = []; + // Process addons in order (they're already returned in order from getAllAddons) for (const addon of addons) { if (addon.catalogs) { @@ -161,54 +163,65 @@ class CatalogService { const isEnabled = catalogSettings[settingKey] ?? true; if (isEnabled) { - try { - const addonManifest = await stremioService.getInstalledAddonsAsync(); - const manifest = addonManifest.find(a => a.id === addon.id); - if (!manifest) continue; + // Create a promise for each catalog fetch + const catalogPromise = (async () => { + try { + const addonManifest = await stremioService.getInstalledAddonsAsync(); + const manifest = addonManifest.find(a => a.id === addon.id); + if (!manifest) return null; - const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1); - if (metas && metas.length > 0) { - const items = metas.map(meta => this.convertMetaToStreamingContent(meta)); - - // Get potentially custom display name - let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name); - - // Remove duplicate words and clean up the name (case-insensitive) - const words = displayName.split(' '); - const uniqueWords = []; - const seenWords = new Set(); - for (const word of words) { - const lowerWord = word.toLowerCase(); - if (!seenWords.has(lowerWord)) { - uniqueWords.push(word); - seenWords.add(lowerWord); + const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1); + if (metas && metas.length > 0) { + const items = metas.map(meta => this.convertMetaToStreamingContent(meta)); + + // Get potentially custom display name + let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name); + + // Remove duplicate words and clean up the name (case-insensitive) + const words = displayName.split(' '); + const uniqueWords = []; + const seenWords = new Set(); + for (const word of words) { + const lowerWord = word.toLowerCase(); + if (!seenWords.has(lowerWord)) { + uniqueWords.push(word); + seenWords.add(lowerWord); + } } + displayName = uniqueWords.join(' '); + + // Add content type if not present + const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; + if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { + displayName = `${displayName} ${contentType}`; + } + + return { + addon: addon.id, + type: catalog.type, + id: catalog.id, + name: displayName, + items + }; } - displayName = uniqueWords.join(' '); - - // Add content type if not present - const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; - if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { - displayName = `${displayName} ${contentType}`; - } - - catalogs.push({ - addon: addon.id, - type: catalog.type, - id: catalog.id, - name: displayName, - items - }); + return null; + } catch (error) { + logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error); + return null; } - } catch (error) { - logger.error(`Failed to load ${catalog.name} from ${addon.name}:`, error); - } + })(); + + catalogPromises.push(catalogPromise); } } } } - return catalogs; + // Wait for all catalog fetch promises to resolve in parallel + const catalogResults = await Promise.all(catalogPromises); + + // Filter out null results + return catalogResults.filter(catalog => catalog !== null) as CatalogContent[]; } async getCatalogByType(type: string, genreFilter?: string): Promise { @@ -222,46 +235,58 @@ class CatalogService { // Otherwise use the original Stremio addons method const addons = await this.getAllAddons(); - const catalogs: CatalogContent[] = []; - + const typeAddons = addons.filter(addon => addon.catalogs && addon.catalogs.some(catalog => catalog.type === type) ); + // Create an array of promises for all catalog fetches + const catalogPromises: Promise[] = []; + for (const addon of typeAddons) { const typeCatalogs = addon.catalogs.filter(catalog => catalog.type === type); for (const catalog of typeCatalogs) { - try { - const addonManifest = await stremioService.getInstalledAddonsAsync(); - const manifest = addonManifest.find(a => a.id === addon.id); - if (!manifest) continue; + const catalogPromise = (async () => { + try { + const addonManifest = await stremioService.getInstalledAddonsAsync(); + const manifest = addonManifest.find(a => a.id === addon.id); + if (!manifest) return null; - const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : []; - const metas = await stremioService.getCatalog(manifest, type, catalog.id, 1, filters); - - if (metas && metas.length > 0) { - const items = metas.map(meta => this.convertMetaToStreamingContent(meta)); + const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : []; + const metas = await stremioService.getCatalog(manifest, type, catalog.id, 1, filters); - // Get potentially custom display name - const displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name); - - catalogs.push({ - addon: addon.id, - type, - id: catalog.id, - name: displayName, - genre: genreFilter, - items - }); + if (metas && metas.length > 0) { + const items = metas.map(meta => this.convertMetaToStreamingContent(meta)); + + // Get potentially custom display name + const displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, catalog.name); + + return { + addon: addon.id, + type, + id: catalog.id, + name: displayName, + genre: genreFilter, + items + }; + } + return null; + } catch (error) { + logger.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error); + return null; } - } catch (error) { - logger.error(`Failed to get catalog ${catalog.id} for addon ${addon.id}:`, error); - } + })(); + + catalogPromises.push(catalogPromise); } } - return catalogs; + // Wait for all catalog fetch promises to resolve in parallel + const catalogResults = await Promise.all(catalogPromises); + + // Filter out null results + return catalogResults.filter(catalog => catalog !== null) as CatalogContent[]; } /** @@ -277,64 +302,75 @@ class CatalogService { // If no genre filter or All is selected, get multiple catalogs if (!genreFilter || genreFilter === 'All') { - // Get trending - const trendingItems = await tmdbService.getTrending(tmdbType, 'week'); - const trendingItemsPromises = trendingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); - const trendingStreamingItems = await Promise.all(trendingItemsPromises); + // Create an array of promises for all catalog fetches + const catalogFetchPromises = [ + // Trending catalog + (async () => { + const trendingItems = await tmdbService.getTrending(tmdbType, 'week'); + const trendingItemsPromises = trendingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); + const trendingStreamingItems = await Promise.all(trendingItemsPromises); + + return { + addon: 'tmdb', + type, + id: 'trending', + name: `Trending ${type === 'movie' ? 'Movies' : 'TV Shows'}`, + items: trendingStreamingItems + }; + })(), + + // Popular catalog + (async () => { + const popularItems = await tmdbService.getPopular(tmdbType, 1); + const popularItemsPromises = popularItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); + const popularStreamingItems = await Promise.all(popularItemsPromises); + + return { + addon: 'tmdb', + type, + id: 'popular', + name: `Popular ${type === 'movie' ? 'Movies' : 'TV Shows'}`, + items: popularStreamingItems + }; + })(), + + // Upcoming/on air catalog + (async () => { + const upcomingItems = await tmdbService.getUpcoming(tmdbType, 1); + const upcomingItemsPromises = upcomingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); + const upcomingStreamingItems = await Promise.all(upcomingItemsPromises); + + return { + addon: 'tmdb', + type, + id: 'upcoming', + name: type === 'movie' ? 'Upcoming Movies' : 'On Air TV Shows', + items: upcomingStreamingItems + }; + })() + ]; - catalogs.push({ - addon: 'tmdb', - type, - id: 'trending', - name: `Trending ${type === 'movie' ? 'Movies' : 'TV Shows'}`, - items: trendingStreamingItems - }); - - // Get popular - const popularItems = await tmdbService.getPopular(tmdbType, 1); - const popularItemsPromises = popularItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); - const popularStreamingItems = await Promise.all(popularItemsPromises); - - catalogs.push({ - addon: 'tmdb', - type, - id: 'popular', - name: `Popular ${type === 'movie' ? 'Movies' : 'TV Shows'}`, - items: popularStreamingItems - }); - - // Get upcoming/on air - const upcomingItems = await tmdbService.getUpcoming(tmdbType, 1); - const upcomingItemsPromises = upcomingItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); - const upcomingStreamingItems = await Promise.all(upcomingItemsPromises); - - catalogs.push({ - addon: 'tmdb', - type, - id: 'upcoming', - name: type === 'movie' ? 'Upcoming Movies' : 'On Air TV Shows', - items: upcomingStreamingItems - }); + // Wait for all catalog fetches to complete in parallel + return await Promise.all(catalogFetchPromises); } else { // Get content by genre const genreItems = await tmdbService.discoverByGenre(tmdbType, genreFilter); const streamingItemsPromises = genreItems.map(item => this.convertTMDBToStreamingContent(item, tmdbType)); const streamingItems = await Promise.all(streamingItemsPromises); - catalogs.push({ + return [{ addon: 'tmdb', type, id: 'discover', name: `${genreFilter} ${type === 'movie' ? 'Movies' : 'TV Shows'}`, genre: genreFilter, items: streamingItems - }); + }]; } } catch (error) { logger.error(`Failed to get catalog from TMDB for type ${type}, genre ${genreFilter}:`, error); + return []; } - - return catalogs; } /**