diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 24ab07c2..bcd7084f 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -1106,6 +1106,9 @@ const HeroSection: React.FC = memo(({ // Guards to avoid repeated auto-starts const startedOnFocusRef = useRef(false); const startedOnReadyRef = useRef(false); + // Debounced pause/resume flag to avoid blocking scroll + const pendingPauseResumeSV = useSharedValue(0); // 0 = no action, 1 = pause, 2 = resume + const pauseResumeTimerRef = useRef(null); // Animation values for trailer unmute effects const actionButtonsOpacity = useSharedValue(1); @@ -1672,6 +1675,7 @@ const HeroSection: React.FC = memo(({ }, [isFocused, setTrailerPlaying]); // Ultra-optimized scroll-based pause/resume with cached calculations + // This worklet only sets flags - actual pause/resume happens asynchronously to avoid blocking scroll useDerivedValue(() => { 'worklet'; try { @@ -1684,21 +1688,49 @@ const HeroSection: React.FC = memo(({ const isPlaying = isPlayingSV.value === 1; const isPausedByScroll = pausedByScrollSV.value === 1; - // Optimized pause/resume logic with minimal branching + // Set flags for pause/resume - don't execute immediately to keep scroll smooth if (y > pauseThreshold && isPlaying && !isPausedByScroll) { - pausedByScrollSV.value = 1; - runOnJS(setTrailerPlaying)(false); - isPlayingSV.value = 0; + pendingPauseResumeSV.value = 1; // Request pause } else if (y < resumeThreshold && isPausedByScroll) { - pausedByScrollSV.value = 0; - runOnJS(setTrailerPlaying)(true); - isPlayingSV.value = 1; + pendingPauseResumeSV.value = 2; // Request resume } } catch (e) { // Silent error handling for performance } }); + // Debounced pause/resume effect - executes asynchronously to keep scroll smooth + useEffect(() => { + const checkPendingAction = () => { + const pendingAction = pendingPauseResumeSV.value; + + if (pendingAction === 1) { + // Execute pause + pausedByScrollSV.value = 1; + setTrailerPlaying(false); + isPlayingSV.value = 0; + pendingPauseResumeSV.value = 0; // Clear flag + } else if (pendingAction === 2) { + // Execute resume + pausedByScrollSV.value = 0; + setTrailerPlaying(true); + isPlayingSV.value = 1; + pendingPauseResumeSV.value = 0; // Clear flag + } + }; + + // Set up a recurring check with small delay to avoid blocking scroll + const intervalId = setInterval(checkPendingAction, 100); + + return () => { + clearInterval(intervalId); + if (pauseResumeTimerRef.current) { + clearTimeout(pauseResumeTimerRef.current); + pauseResumeTimerRef.current = null; + } + }; + }, [setTrailerPlaying, pendingPauseResumeSV, pausedByScrollSV, isPlayingSV]); + // Memory management and cleanup useEffect(() => { return () => { diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index 0f04e60a..2f485b36 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -132,16 +132,14 @@ export function useFeaturedContent() { genres: (item as any).genres, inLibrary: Boolean((item as any).inLibrary), }; + try { - // If enrichment is disabled, use addon logo if available if (!settings.enrichMetadataWithTMDB) { - if (base.logo && !isTmdbUrl(base.logo)) { - return base; - } - return { ...base, logo: undefined }; + // When enrichment is OFF, keep addon logo or undefined + return { ...base, logo: base.logo || undefined }; } - // Only proceed with TMDB enrichment if enrichment is enabled + // When enrichment is ON, fetch from TMDB with language preference const rawId = String(item.id); const isTmdb = rawId.startsWith('tmdb:'); const isImdb = rawId.startsWith('tt'); @@ -154,17 +152,15 @@ export function useFeaturedContent() { const found = await tmdbService.findTMDBIdByIMDB(imdbId); tmdbId = found ? String(found) : null; } - if (!tmdbId && !imdbId) return base; - // Try TMDB if we have a TMDB id + if (tmdbId) { const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage); - if (logoUrl) { - return { ...base, logo: logoUrl }; - } + return { ...base, logo: logoUrl || undefined }; // TMDB logo or undefined (no addon fallback) } - return base; + + return { ...base, logo: undefined }; // No TMDB ID means no logo } catch (error) { - return base; + return { ...base, logo: undefined }; // Error means no logo } }; diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index d7369da8..5b058c25 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -599,6 +599,17 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat logger.error('Failed to fetch credits for movie:', error); } + // Fetch movie logo from TMDB + try { + const preferredLanguage = settings.tmdbLanguagePreference || 'en'; + const logoUrl = await tmdbService.getContentLogo('movie', tmdbId, preferredLanguage); + formattedMovie.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback) + if (__DEV__) logger.log(`Successfully fetched logo for movie ${tmdbId} from TMDB`); + } catch (error) { + logger.error('Failed to fetch logo from TMDB:', error); + formattedMovie.logo = undefined; // Error means no logo + } + setMetadata(formattedMovie); cacheService.setMetadata(id, type, formattedMovie); const isInLib = catalogService.getLibraryItems().some(item => item.id === id); @@ -664,14 +675,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Fetch TV show logo from TMDB try { - const logoUrl = await tmdbService.getTvShowImages(tmdbId); - if (logoUrl) { - formattedShow.logo = logoUrl; - if (__DEV__) logger.log(`Successfully fetched logo for TV show ${tmdbId} from TMDB`); - } + const preferredLanguage = settings.tmdbLanguagePreference || 'en'; + const logoUrl = await tmdbService.getContentLogo('tv', tmdbId, preferredLanguage); + formattedShow.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback) + if (__DEV__) logger.log(`Successfully fetched logo for TV show ${tmdbId} from TMDB`); } catch (error) { logger.error('Failed to fetch logo from TMDB:', error); - // Continue with execution, logo is optional + formattedShow.logo = undefined; // Error means no logo } setMetadata(formattedShow); @@ -878,6 +888,55 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) console.log('[useMetadata] failed to merge localized TMDB text', e); } + // Centralized logo fetching logic + try { + if (settings.enrichMetadataWithTMDB) { + // Only use TMDB logos when enrichment is ON + const tmdbService = TMDBService.getInstance(); + const preferredLanguage = settings.tmdbLanguagePreference || 'en'; + const contentType = type === 'series' ? 'tv' : 'movie'; + + // Get TMDB ID + let tmdbIdForLogo = null; + if (tmdbId) { + tmdbIdForLogo = String(tmdbId); + } else if (finalMetadata.imdb_id) { + const foundId = await tmdbService.findTMDBIdByIMDB(finalMetadata.imdb_id); + tmdbIdForLogo = foundId ? String(foundId) : null; + } + + if (tmdbIdForLogo) { + const logoUrl = await tmdbService.getContentLogo(contentType, tmdbIdForLogo, preferredLanguage); + finalMetadata.logo = logoUrl || undefined; // TMDB logo or undefined (no addon fallback) + if (__DEV__) { + console.log('[useMetadata] Logo fetch result:', { + contentType, + tmdbIdForLogo, + preferredLanguage, + logoUrl: !!logoUrl, + enrichmentEnabled: true + }); + } + } else { + finalMetadata.logo = undefined; // No TMDB ID means no logo + if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, will show text title'); + } + } else { + // When enrichment is OFF, keep addon logo or undefined + finalMetadata.logo = finalMetadata.logo || undefined; + if (__DEV__) { + console.log('[useMetadata] TMDB enrichment disabled, using addon logo:', { + hasAddonLogo: !!finalMetadata.logo, + enrichmentEnabled: false + }); + } + } + } catch (error) { + // Handle error silently, keep existing logo behavior + if (__DEV__) console.error('[useMetadata] Unexpected error in logo fetch:', error); + finalMetadata.logo = undefined; + } + // Commit final metadata once and cache it // Clear banner field if TMDB enrichment is enabled to prevent flash if (settings.enrichMetadataWithTMDB) { diff --git a/src/hooks/useMetadataAssets.ts b/src/hooks/useMetadataAssets.ts index e9411a4e..a658f8c8 100644 --- a/src/hooks/useMetadataAssets.ts +++ b/src/hooks/useMetadataAssets.ts @@ -62,13 +62,6 @@ export const useMetadataAssets = ( // Add source tracking to prevent mixing sources const [bannerSource, setBannerSource] = useState<'tmdb' | 'metahub' | 'default' | null>(null); - // State for logo loading - const [logoLoadError, setLogoLoadError] = useState(false); - const logoFetchInProgress = useRef(false); - const logoRefreshCounter = useRef(0); - const MAX_LOGO_REFRESHES = 2; - const forcedLogoRefreshDone = useRef(false); - // For TMDB ID tracking const [foundTmdbId, setFoundTmdbId] = useState(null); @@ -78,173 +71,8 @@ export const useMetadataAssets = ( setBannerImage(null); setBannerSource(null); forcedBannerRefreshDone.current = false; - forcedLogoRefreshDone.current = false; - logoRefreshCounter.current = 0; - - // Mark that we need to refetch logo but DON'T clear it yet - // This prevents text from showing during the transition - logoFetchInProgress.current = false; }, [settings.logoSourcePreference]); - // Original reset logo load error effect - useEffect(() => { - setLogoLoadError(false); - }, [metadata?.logo]); - - // Optimized logo fetching - useEffect(() => { - const logoPreference = settings.logoSourcePreference || 'tmdb'; - - if (__DEV__) { - console.log('[useMetadataAssets] Logo fetch triggered:', { - id, - type, - logoPreference, - hasImdbId: !!imdbId, - tmdbEnrichmentEnabled: settings.enrichMetadataWithTMDB, - logoFetchInProgress: logoFetchInProgress.current - }); - } - const currentLogoUrl = metadata?.logo; - let shouldFetchLogo = false; - - // If enrichment is disabled, use addon logo and don't fetch from external sources - if (!settings.enrichMetadataWithTMDB) { - // If we have an addon logo, preload it immediately for instant display - if (metadata?.logo && !isTmdbUrl(metadata.logo)) { - // Preload addon logo for instant display - FastImage.preload([{ uri: metadata.logo }]); - // This is an addon logo, keep it - return; - } - // If no addon logo, don't fetch external logos when enrichment is disabled - return; - } - - // When TMDB enrichment is ON, remove addon logos immediately - // We don't want to show addon logos briefly before TMDB logos load - if (settings.enrichMetadataWithTMDB && currentLogoUrl && !isTmdbUrl(currentLogoUrl)) { - // Clear addon logo when enrichment is enabled - setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: undefined })); - shouldFetchLogo = true; - } - // Determine if we need to fetch a new logo (only when enrichment is enabled) - else if (!currentLogoUrl) { - shouldFetchLogo = true; - } else { - const isCurrentLogoTmdb = isTmdbUrl(currentLogoUrl); - - if (logoPreference === 'tmdb' && !isCurrentLogoTmdb) { - shouldFetchLogo = true; - } - } - - // Guard against infinite loops by checking if we're already fetching - if (shouldFetchLogo && !logoFetchInProgress.current) { - logoFetchInProgress.current = true; - - const fetchLogo = async () => { - // Store the original logo to restore if needed - const originalLogoUrl = currentLogoUrl; - - try { - const preferredLanguage = settings.tmdbLanguagePreference || 'en'; - - if (logoPreference === 'tmdb') { - // TMDB path - optimized flow - let tmdbId: string | null = null; - let contentType = type === 'series' ? 'tv' : 'movie'; - - // Extract or find TMDB ID in one step - if (id.startsWith('tmdb:')) { - tmdbId = id.split(':')[1]; - } else if (imdbId && settings.enrichMetadataWithTMDB) { - try { - const tmdbService = TMDBService.getInstance(); - const foundId = await tmdbService.findTMDBIdByIMDB(imdbId); - if (foundId) { - tmdbId = String(foundId); - setFoundTmdbId(tmdbId); // Save for banner fetching - } else if (__DEV__) { - console.log('[useMetadataAssets] Could not find TMDB ID for IMDB:', imdbId); - } - } catch (error) { - if (__DEV__) console.error('[useMetadataAssets] Error finding TMDB ID:', error); - } - } else { - const parsedId = parseInt(id, 10); - if (!isNaN(parsedId)) { - tmdbId = String(parsedId); - } - } - - if (tmdbId) { - try { - // Direct fetch - avoid multiple service calls - const tmdbService = TMDBService.getInstance(); - const logoUrl = await tmdbService.getContentLogo(contentType as 'tv' | 'movie', tmdbId, preferredLanguage); - - if (__DEV__) { - console.log('[useMetadataAssets] Logo fetch result:', { - contentType, - tmdbId, - preferredLanguage, - logoUrl, - logoPreference - }); - } - - if (logoUrl) { - // Preload the image before setting it - await FastImage.preload([{ uri: logoUrl }]); - - // Only update if we got a valid logo - setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: logoUrl })); - } else { - // TMDB logo not found - // When enrichment is ON, don't fallback to addon logos - show text instead - if (__DEV__) { - console.log('[useMetadataAssets] No TMDB logo found for ID:', tmdbId); - } - // Keep logo as undefined to show text title - } - } catch (error) { - // TMDB logo fetch failed - // When enrichment is ON, don't fallback to addon logos - show text instead - if (__DEV__) { - console.error('[useMetadataAssets] Logo fetch error:', error); - } - // Keep logo as undefined to show text title - } - } else { - // No TMDB ID found - // When enrichment is ON, don't use addon logos - show text instead - if (__DEV__) console.log('[useMetadataAssets] No TMDB ID found, will show text title'); - // Keep logo as undefined to show text title - } - } - } catch (error) { - // Handle error silently, keep existing logo - if (__DEV__) console.error('[useMetadataAssets] Unexpected error in logo fetch:', error); - } finally { - logoFetchInProgress.current = false; - } - }; - - // Execute fetch without awaiting - fetchLogo(); - } - }, [ - id, - type, - imdbId, - metadata?.logo, - settings.logoSourcePreference, - settings.tmdbLanguagePreference, - settings.enrichMetadataWithTMDB, - setMetadata - ]); - // Optimized banner fetching const fetchBanner = useCallback(async () => { if (!metadata) return; @@ -354,9 +182,7 @@ export const useMetadataAssets = ( return { bannerImage, loadingBanner, - logoLoadError, foundTmdbId, - setLogoLoadError, setBannerImage, bannerSource, };