import React, { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'; import { View, Text, StyleSheet, Dimensions, TouchableOpacity, Platform, InteractionManager, AppState, } from 'react-native'; import { useFocusEffect, useIsFocused } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { Image } from 'expo-image'; import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as CommunityBlurView } from '@react-native-community/blur'; import Constants, { ExecutionEnvironment } from 'expo-constants'; import Animated, { useAnimatedStyle, interpolate, Extrapolate, useSharedValue, withTiming, runOnJS, withRepeat, FadeIn, runOnUI, useDerivedValue, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { useTraktContext } from '../../contexts/TraktContext'; import { useSettings } from '../../hooks/useSettings'; import { useTrailer } from '../../contexts/TrailerContext'; import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; import TrailerService from '../../services/trailerService'; import TrailerPlayer from '../video/TrailerPlayer'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; // Ultra-optimized animation constants const SCALE_FACTOR = 1.02; const FADE_THRESHOLD = 200; // Types - streamlined interface HeroSectionProps { metadata: any; bannerImage: string | null; loadingBanner: boolean; logoLoadError: boolean; scrollY: Animated.SharedValue; heroHeight: Animated.SharedValue; heroOpacity: Animated.SharedValue; logoOpacity: Animated.SharedValue; buttonsOpacity: Animated.SharedValue; buttonsTranslateY: Animated.SharedValue; watchProgressOpacity: Animated.SharedValue; watchProgressWidth: Animated.SharedValue; watchProgress: { currentTime: number; duration: number; lastUpdated: number; episodeId?: string; traktSynced?: boolean; traktProgress?: number; } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; handleShowStreams: () => void; handleToggleLibrary: () => void; inLibrary: boolean; id: string; navigation: any; getPlayButtonText: () => string; setBannerImage: (bannerImage: string | null) => void; setLogoLoadError: (error: boolean) => void; groupedEpisodes?: { [seasonNumber: number]: any[] }; dynamicBackgroundColor?: string; handleBack: () => void; tmdbId?: number | null; } // Ultra-optimized ActionButtons Component - minimal re-renders const ActionButtons = memo(({ handleShowStreams, toggleLibrary, inLibrary, type, id, navigation, playButtonText, animatedStyle, isWatched, watchProgress, groupedEpisodes, metadata, aiChatEnabled, settings }: { handleShowStreams: () => void; toggleLibrary: () => void; inLibrary: boolean; type: 'movie' | 'series'; id: string; navigation: any; playButtonText: string; animatedStyle: any; isWatched: boolean; watchProgress: any; groupedEpisodes?: { [seasonNumber: number]: any[] }; metadata: any; aiChatEnabled?: boolean; settings: any; }) => { const { currentTheme } = useTheme(); // Performance optimization: Cache theme colors const themeColors = useMemo(() => ({ white: currentTheme.colors.white, black: '#000', primary: currentTheme.colors.primary }), [currentTheme.colors.white, currentTheme.colors.primary]); // Optimized navigation handler with useCallback const handleRatingsPress = useCallback(async () => { // Early return if no ID if (!id) return; let finalTmdbId: number | null = null; if (id.startsWith('tmdb:')) { const numericPart = id.split(':')[1]; const parsedId = parseInt(numericPart, 10); if (!isNaN(parsedId)) { finalTmdbId = parsedId; } } else if (id.startsWith('tt') && settings.enrichMetadataWithTMDB) { try { const tmdbService = TMDBService.getInstance(); const convertedId = await tmdbService.findTMDBIdByIMDB(id); if (convertedId) { finalTmdbId = convertedId; } } catch (error) { logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error); } } else { const parsedId = parseInt(id, 10); if (!isNaN(parsedId)) { finalTmdbId = parsedId; } } if (finalTmdbId !== null) { // Use requestAnimationFrame for smoother navigation requestAnimationFrame(() => { navigation.navigate('ShowRatings', { showId: finalTmdbId }); }); } }, [id, navigation, settings.enrichMetadataWithTMDB]); // Optimized play button style calculation const playButtonStyle = useMemo(() => { if (isWatched && type === 'movie') { // Only movies get the dark watched style for "Watch Again" return [styles.actionButton, styles.playButton, styles.watchedPlayButton]; } // All other buttons (Resume, Play SxxEyy, regular Play) get white background return [styles.actionButton, styles.playButton]; }, [isWatched, type]); const playButtonTextStyle = useMemo(() => { if (isWatched && type === 'movie') { // Only movies get white text for "Watch Again" return [styles.playButtonText, styles.watchedPlayButtonText]; } // All other buttons get black text return styles.playButtonText; }, [isWatched, type]); const finalPlayButtonText = useMemo(() => { // For movies, handle watched state if (type === 'movie') { return isWatched ? 'Watch Again' : playButtonText; } // For series, validate next episode existence for both watched and resume cases if (type === 'series' && watchProgress?.episodeId && groupedEpisodes) { let seasonNum: number | null = null; let episodeNum: number | null = null; const parts = watchProgress.episodeId.split(':'); if (parts.length === 3) { // Format: showId:season:episode seasonNum = parseInt(parts[1], 10); episodeNum = parseInt(parts[2], 10); } else if (parts.length === 2) { // Format: season:episode (no show id) seasonNum = parseInt(parts[0], 10); episodeNum = parseInt(parts[1], 10); } else { // Try pattern s1e2 const match = watchProgress.episodeId.match(/s(\d+)e(\d+)/i); if (match) { seasonNum = parseInt(match[1], 10); episodeNum = parseInt(match[2], 10); } } if (seasonNum !== null && episodeNum !== null && !isNaN(seasonNum) && !isNaN(episodeNum)) { if (isWatched) { // For watched episodes, check if next episode exists const nextEpisode = episodeNum + 1; const currentSeasonEpisodes = groupedEpisodes[seasonNum] || []; const nextEpisodeExists = currentSeasonEpisodes.some(ep => ep.episode_number === nextEpisode ); if (nextEpisodeExists) { // Show the NEXT episode number only if it exists const seasonStr = seasonNum.toString().padStart(2, '0'); const episodeStr = nextEpisode.toString().padStart(2, '0'); return `Play S${seasonStr}E${episodeStr}`; } else { // If next episode doesn't exist, show generic text return 'Completed'; } } else { // For non-watched episodes, check if current episode exists const currentSeasonEpisodes = groupedEpisodes[seasonNum] || []; const currentEpisodeExists = currentSeasonEpisodes.some(ep => ep.episode_number === episodeNum ); if (currentEpisodeExists) { // Current episode exists, use original button text return playButtonText; } else { // Current episode doesn't exist, fallback to generic play return 'Play'; } } } // Fallback label if parsing fails return isWatched ? 'Play Next Episode' : playButtonText; } // Default fallback for non-series or missing data return isWatched ? 'Play' : playButtonText; }, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]); return ( { if (isWatched) { return type === 'movie' ? 'replay' : 'play-arrow'; } return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow'; })()} size={isTablet ? 28 : 24} color={isWatched && type === 'movie' ? "#fff" : "#000"} /> {finalPlayButtonText} {Platform.OS === 'ios' ? ( ) : ( )} {inLibrary ? 'Saved' : 'Save'} {/* AI Chat Button */} {aiChatEnabled && ( { // Extract episode info if it's a series let episodeData = null; if (type === 'series' && watchProgress?.episodeId) { const parts = watchProgress.episodeId.split(':'); if (parts.length >= 3) { episodeData = { seasonNumber: parseInt(parts[1], 10), episodeNumber: parseInt(parts[2], 10) }; } } navigation.navigate('AIChat', { contentId: id, contentType: type, episodeId: episodeData ? watchProgress.episodeId : undefined, seasonNumber: episodeData?.seasonNumber, episodeNumber: episodeData?.episodeNumber, title: metadata?.name || metadata?.title || 'Unknown' }); }} activeOpacity={0.85} > {Platform.OS === 'ios' ? ( ) : ( )} )} {type === 'series' && ( {Platform.OS === 'ios' ? ( ) : ( )} )} ); }); // Enhanced WatchProgress Component with Trakt integration and watched status const WatchProgressDisplay = memo(({ watchProgress, type, getEpisodeDetails, animatedStyle, isWatched, isTrailerPlaying, trailerMuted, trailerReady }: { watchProgress: { currentTime: number; duration: number; lastUpdated: number; episodeId?: string; traktSynced?: boolean; traktProgress?: number; } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; isWatched: boolean; isTrailerPlaying: boolean; trailerMuted: boolean; trailerReady: boolean; }) => { const { currentTheme } = useTheme(); const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext(); // State to trigger refresh after manual sync const [refreshTrigger, setRefreshTrigger] = useState(0); const [isSyncing, setIsSyncing] = useState(false); // Animated values for enhanced effects const completionGlow = useSharedValue(0); const celebrationScale = useSharedValue(1); const progressPulse = useSharedValue(1); const progressBoxOpacity = useSharedValue(0); const progressBoxScale = useSharedValue(0.8); const progressBoxTranslateY = useSharedValue(20); const syncRotation = useSharedValue(0); // Animate the sync icon when syncing useEffect(() => { if (isSyncing) { syncRotation.value = withRepeat( withTiming(360, { duration: 1000 }), -1, // Infinite repeats false // No reverse ); } else { syncRotation.value = 0; } }, [isSyncing, syncRotation]); // Handle manual Trakt sync const handleTraktSync = useMemo(() => async () => { if (isTraktAuthenticated && forceSyncTraktProgress) { logger.log('[HeroSection] Manual Trakt sync requested'); setIsSyncing(true); try { const success = await forceSyncTraktProgress(); logger.log(`[HeroSection] Manual Trakt sync ${success ? 'successful' : 'failed'}`); // Force component to re-render after a short delay to update sync status if (success) { setTimeout(() => { setRefreshTrigger(prev => prev + 1); setIsSyncing(false); }, 500); } else { setIsSyncing(false); } } catch (error) { logger.error('[HeroSection] Manual Trakt sync error:', error); setIsSyncing(false); } } }, [isTraktAuthenticated, forceSyncTraktProgress, setRefreshTrigger]); // Sync rotation animation style const syncIconStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${syncRotation.value}deg` }], })); // Memoized progress calculation with Trakt integration const progressData = useMemo(() => { // If content is fully watched, show watched status instead of progress if (isWatched) { let episodeInfo = ''; if (type === 'series' && watchProgress?.episodeId) { const details = getEpisodeDetails(watchProgress.episodeId); if (details) { episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`; } } const watchedDate = watchProgress?.lastUpdated ? new Date(watchProgress.lastUpdated).toLocaleDateString('en-US') : new Date().toLocaleDateString('en-US'); // Determine if watched via Trakt or local const watchedViaTrakt = isTraktAuthenticated && watchProgress?.traktProgress !== undefined && watchProgress.traktProgress >= 95; return { progressPercent: 100, formattedTime: watchedDate, episodeInfo, displayText: watchedViaTrakt ? 'Watched on Trakt' : 'Watched', syncStatus: isTraktAuthenticated && watchProgress?.traktSynced ? '' : '', // Clean look for watched isTraktSynced: watchProgress?.traktSynced && isTraktAuthenticated, isWatched: true }; } if (!watchProgress || watchProgress.duration === 0) return null; // Determine which progress to show - prioritize Trakt if available and authenticated let progressPercent; let isUsingTraktProgress = false; if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) { progressPercent = watchProgress.traktProgress; isUsingTraktProgress = true; } else { progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; } const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString('en-US'); let episodeInfo = ''; if (type === 'series' && watchProgress.episodeId) { const details = getEpisodeDetails(watchProgress.episodeId); if (details) { episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`; } } // Enhanced display text with Trakt integration let displayText = progressPercent >= 85 ? 'Watched' : `${Math.round(progressPercent)}% watched`; let syncStatus = ''; // Show Trakt sync status if user is authenticated if (isTraktAuthenticated) { if (isUsingTraktProgress) { syncStatus = ' • Using Trakt progress'; if (watchProgress.traktSynced) { syncStatus = ' • Synced with Trakt'; } } else if (watchProgress.traktSynced) { syncStatus = ' • Synced with Trakt'; // If we have specific Trakt progress that differs from local, mention it if (watchProgress.traktProgress !== undefined && Math.abs(progressPercent - watchProgress.traktProgress) > 5) { displayText = `${Math.round(progressPercent)}% watched (${Math.round(watchProgress.traktProgress)}% on Trakt)`; } } else { // Do not show "Sync pending" label anymore; leave status empty. syncStatus = ''; } } return { progressPercent, formattedTime, episodeInfo, displayText, syncStatus, isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated, isWatched: false }; }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated, isWatched, refreshTrigger]); // Trigger appearance and completion animations useEffect(() => { if (progressData) { // Smooth entrance animation for the glassmorphic box progressBoxOpacity.value = withTiming(1, { duration: 400 }); progressBoxScale.value = withTiming(1, { duration: 400 }); progressBoxTranslateY.value = withTiming(0, { duration: 400 }); if (progressData.isWatched || (progressData.progressPercent && progressData.progressPercent >= 85)) { // Celebration animation sequence celebrationScale.value = withRepeat( withTiming(1.05, { duration: 200 }), 2, true ); // Glow effect completionGlow.value = withRepeat( withTiming(1, { duration: 1500 }), -1, true ); } else { // Subtle progress pulse for ongoing content progressPulse.value = withRepeat( withTiming(1.02, { duration: 2000 }), -1, true ); } } else { // Hide animation when no progress data progressBoxOpacity.value = withTiming(0, { duration: 300 }); progressBoxScale.value = withTiming(0.8, { duration: 300 }); progressBoxTranslateY.value = withTiming(20, { duration: 300 }); } }, [progressData]); // Animated styles for enhanced effects const celebrationAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: celebrationScale.value }], })); const glowAnimatedStyle = useAnimatedStyle(() => ({ opacity: interpolate(completionGlow.value, [0, 1], [0.3, 0.8], Extrapolate.CLAMP), })); const progressPulseStyle = useAnimatedStyle(() => ({ transform: [{ scale: progressPulse.value }], })); const progressBoxAnimatedStyle = useAnimatedStyle(() => ({ opacity: progressBoxOpacity.value, transform: [ { scale: progressBoxScale.value }, { translateY: progressBoxTranslateY.value } ], })); if (!progressData) return null; // Hide watch progress when trailer is playing AND unmuted AND trailer is ready if (isTrailerPlaying && !trailerMuted && trailerReady) return null; const isCompleted = progressData.isWatched || progressData.progressPercent >= 85; return ( {/* Glass morphism background with entrance animation */} {Platform.OS === 'ios' ? ( ) : ( )} {/* Enhanced progress bar with glow effects */} {/* Background glow for completed content */} {isCompleted && ( )} {/* Shimmer effect for active progress */} {!isCompleted && progressData.progressPercent > 0 && ( )} {/* Enhanced text container with better typography */} {progressData.displayText} {/* Only show episode info for series */} {progressData.episodeInfo && ( {progressData.episodeInfo} )} {/* Trakt sync status with enhanced styling */} {progressData.syncStatus && ( {progressData.syncStatus} {/* Enhanced manual Trakt sync button - moved inline */} {isTraktAuthenticated && forceSyncTraktProgress && ( )} )} ); }); /** * HeroSection Component - Performance Optimized * * Optimizations Applied: * - Component memoization with React.memo * - Lazy loading system using InteractionManager * - Optimized image loading with useCallback handlers * - Cached theme colors to reduce re-renders * - Conditional rendering based on shouldLoadSecondaryData * - Memory management with cleanup on unmount * - Development-mode performance monitoring * - Optimized animated styles and memoized calculations * - Reduced re-renders through strategic memoization * - runOnUI for animation performance */ const HeroSection: React.FC = memo(({ metadata, bannerImage, loadingBanner, logoLoadError, scrollY, heroHeight, heroOpacity, logoOpacity, buttonsOpacity, buttonsTranslateY, watchProgressOpacity, watchProgress, type, getEpisodeDetails, handleShowStreams, handleToggleLibrary, inLibrary, id, navigation, getPlayButtonText, setBannerImage, setLogoLoadError, groupedEpisodes, dynamicBackgroundColor, handleBack, tmdbId, }) => { const { currentTheme } = useTheme(); const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); const { settings, updateSetting } = useSettings(); const { isTrailerPlaying: globalTrailerPlaying, setTrailerPlaying } = useTrailer(); const isFocused = useIsFocused(); // Performance optimization: Refs for avoiding re-renders const interactionComplete = useRef(false); const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false); const appState = useRef(AppState.currentState); // Image loading state with optimized management const [imageError, setImageError] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); const [trailerUrl, setTrailerUrl] = useState(null); const [trailerLoading, setTrailerLoading] = useState(false); const [trailerError, setTrailerError] = useState(false); // Use persistent setting instead of local state const trailerMuted = settings.trailerMuted; const [trailerReady, setTrailerReady] = useState(false); const [trailerPreloaded, setTrailerPreloaded] = useState(false); const trailerVideoRef = useRef(null); const imageOpacity = useSharedValue(1); const imageLoadOpacity = useSharedValue(0); const shimmerOpacity = useSharedValue(0.3); const trailerOpacity = useSharedValue(0); const thumbnailOpacity = useSharedValue(1); // Scroll-based pause/resume control const pausedByScrollSV = useSharedValue(0); const scrollGuardEnabledSV = useSharedValue(0); const isPlayingSV = useSharedValue(0); const isFocusedSV = useSharedValue(0); // Guards to avoid repeated auto-starts const startedOnFocusRef = useRef(false); const startedOnReadyRef = useRef(false); // Animation values for trailer unmute effects const actionButtonsOpacity = useSharedValue(1); const titleCardTranslateY = useSharedValue(0); const genreOpacity = useSharedValue(1); // Performance optimization: Cache theme colors const themeColors = useMemo(() => ({ black: currentTheme.colors.black, darkBackground: currentTheme.colors.darkBackground, highEmphasis: currentTheme.colors.highEmphasis, text: currentTheme.colors.text }), [currentTheme.colors.black, currentTheme.colors.darkBackground, currentTheme.colors.highEmphasis, currentTheme.colors.text]); // Handle trailer preload completion const handleTrailerPreloaded = useCallback(() => { setTrailerPreloaded(true); logger.info('HeroSection', 'Trailer preloaded successfully'); }, []); // Handle smooth transition when trailer is ready to play const handleTrailerReady = useCallback(() => { if (!isFocused) return; if (!trailerPreloaded) { setTrailerPreloaded(true); } setTrailerReady(true); // Smooth transition: fade out thumbnail, fade in trailer thumbnailOpacity.value = withTiming(0, { duration: 500 }); trailerOpacity.value = withTiming(1, { duration: 500 }); // Enable scroll guard after a brief delay to avoid immediate pause on entry scrollGuardEnabledSV.value = 0; setTimeout(() => { scrollGuardEnabledSV.value = 1; }, 1000); }, [thumbnailOpacity, trailerOpacity, trailerPreloaded, isFocused]); // Auto-start trailer when ready on initial entry if enabled useEffect(() => { if (trailerReady && settings?.showTrailers && isFocused && !globalTrailerPlaying && !startedOnReadyRef.current) { startedOnReadyRef.current = true; logger.info('HeroSection', 'Trailer ready - auto-starting playback'); setTrailerPlaying(true); isPlayingSV.value = 1; } }, [trailerReady, settings?.showTrailers, isFocused, globalTrailerPlaying, setTrailerPlaying]); // Handle fullscreen toggle const handleFullscreenToggle = useCallback(async () => { try { logger.info('HeroSection', 'Fullscreen button pressed'); if (trailerVideoRef.current) { // Use the native fullscreen player await trailerVideoRef.current.presentFullscreenPlayer(); } else { logger.warn('HeroSection', 'Trailer video ref not available'); } } catch (error) { logger.error('HeroSection', 'Error toggling fullscreen:', error); } }, []); // Handle trailer error - fade back to thumbnail const handleTrailerError = useCallback(() => { setTrailerError(true); setTrailerReady(false); setTrailerPlaying(false); // Fade back to thumbnail trailerOpacity.value = withTiming(0, { duration: 300 }); thumbnailOpacity.value = withTiming(1, { duration: 300 }); }, [trailerOpacity, thumbnailOpacity]); // Handle trailer end - seamless transition back to thumbnail const handleTrailerEnd = useCallback(() => { logger.info('HeroSection', 'Trailer ended - transitioning back to thumbnail'); setTrailerPlaying(false); // Reset trailer state to prevent auto-restart setTrailerReady(false); setTrailerPreloaded(false); // Smooth fade transition: trailer out, thumbnail in trailerOpacity.value = withTiming(0, { duration: 500 }); thumbnailOpacity.value = withTiming(1, { duration: 500 }); // Show UI elements again actionButtonsOpacity.value = withTiming(1, { duration: 500 }); genreOpacity.value = withTiming(1, { duration: 500 }); titleCardTranslateY.value = withTiming(0, { duration: 500 }); watchProgressOpacity.value = withTiming(1, { duration: 500 }); }, [trailerOpacity, thumbnailOpacity, actionButtonsOpacity, genreOpacity, titleCardTranslateY, watchProgressOpacity, setTrailerPlaying]); // Memoized image source const imageSource = useMemo(() => bannerImage || metadata.banner || metadata.poster , [bannerImage, metadata.banner, metadata.poster]); // Performance optimization: Lazy loading setup useEffect(() => { const timer = InteractionManager.runAfterInteractions(() => { if (!interactionComplete.current) { interactionComplete.current = true; setShouldLoadSecondaryData(true); } }); return () => timer.cancel(); }, []); // Fetch trailer URL when component mounts (only if trailers are enabled) useEffect(() => { let alive = true as boolean; let timerId: any = null; const fetchTrailer = async () => { if (!metadata?.name || !metadata?.year || !settings?.showTrailers || !isFocused) return; // If we expect TMDB ID but don't have it yet, wait a bit more if (!metadata?.tmdbId && metadata?.id?.startsWith('tmdb:')) { logger.info('HeroSection', `Waiting for TMDB ID for ${metadata.name}`); return; } setTrailerLoading(true); setTrailerError(false); setTrailerReady(false); setTrailerPreloaded(false); try { // Use requestIdleCallback or setTimeout to prevent blocking main thread const fetchWithDelay = () => { // Extract TMDB ID if available const tmdbIdString = tmdbId ? String(tmdbId) : undefined; const contentType = type === 'series' ? 'tv' : 'movie'; // Debug logging to see what we have logger.info('HeroSection', `Trailer request for ${metadata.name}:`, { hasTmdbId: !!tmdbId, tmdbId: tmdbId, contentType, metadataKeys: Object.keys(metadata || {}), metadataId: metadata?.id }); TrailerService.getTrailerUrl(metadata.name, metadata.year, tmdbIdString, contentType) .then(url => { if (url) { const bestUrl = TrailerService.getBestFormatUrl(url); setTrailerUrl(bestUrl); logger.info('HeroSection', `Trailer URL loaded for ${metadata.name}${tmdbId ? ` (TMDB: ${tmdbId})` : ''}`); } else { logger.info('HeroSection', `No trailer found for ${metadata.name}`); } }) .catch(error => { logger.error('HeroSection', 'Error fetching trailer:', error); setTrailerError(true); }) .finally(() => { setTrailerLoading(false); }); }; // Delay trailer fetch to prevent blocking UI timerId = setTimeout(() => { if (!alive) return; fetchWithDelay(); }, 100); } catch (error) { logger.error('HeroSection', 'Error in trailer fetch setup:', error); setTrailerError(true); setTrailerLoading(false); } }; fetchTrailer(); return () => { alive = false; try { if (timerId) clearTimeout(timerId); } catch (_e) {} }; }, [metadata?.name, metadata?.year, tmdbId, settings?.showTrailers, isFocused]); // Optimized shimmer animation for loading state useEffect(() => { if (!shouldLoadSecondaryData) return; if (!imageLoaded && imageSource) { // Start shimmer animation shimmerOpacity.value = withRepeat( withTiming(0.8, { duration: 1200 }), -1, true ); } else { // Stop shimmer when loaded shimmerOpacity.value = withTiming(0.3, { duration: 300 }); } }, [imageLoaded, imageSource, shouldLoadSecondaryData]); // Optimized loading state reset when image source changes useEffect(() => { if (imageSource) { setImageLoaded(false); imageLoadOpacity.value = 0; } }, [imageSource]); // Optimized image handlers with useCallback const handleImageError = useCallback(() => { if (!shouldLoadSecondaryData) return; runOnUI(() => { imageOpacity.value = withTiming(0.6, { duration: 150 }); imageLoadOpacity.value = withTiming(0, { duration: 150 }); })(); setImageError(true); setImageLoaded(false); // Fallback to poster if banner fails if (bannerImage !== metadata.banner) { setBannerImage(metadata.banner || metadata.poster); } }, [shouldLoadSecondaryData, bannerImage, metadata.banner, metadata.poster, setBannerImage]); const handleImageLoad = useCallback(() => { runOnUI(() => { imageOpacity.value = withTiming(1, { duration: 150 }); imageLoadOpacity.value = withTiming(1, { duration: 400 }); })(); setImageError(false); setImageLoaded(true); }, []); // Ultra-optimized animated styles - single calculations const heroAnimatedStyle = useAnimatedStyle(() => ({ height: heroHeight.value, opacity: heroOpacity.value, }), []); const logoAnimatedStyle = useAnimatedStyle(() => { // Determine if progress bar should be shown const hasProgress = watchProgress && watchProgress.duration > 0; // Scale down logo when progress bar is present const logoScale = hasProgress ? 0.85 : 1; return { opacity: logoOpacity.value, transform: [ // Keep logo stable by not applying translateY based on scroll { scale: withTiming(logoScale, { duration: 300 }) } ] }; }, [watchProgress]); const watchProgressAnimatedStyle = useAnimatedStyle(() => ({ opacity: watchProgressOpacity.value, }), []); // Enhanced backdrop with smooth loading animation const backdropImageStyle = useAnimatedStyle(() => { 'worklet'; const scale = 1 + (scrollY.value * 0.0001); // Micro scale effect return { opacity: imageOpacity.value * imageLoadOpacity.value, transform: [ { scale: Math.min(scale, SCALE_FACTOR) } // Cap scale ], }; }, []); // Simplified buttons animation const buttonsAnimatedStyle = useAnimatedStyle(() => ({ opacity: buttonsOpacity.value * actionButtonsOpacity.value, transform: [{ translateY: interpolate( buttonsTranslateY.value, [0, 20], [0, 20], Extrapolate.CLAMP ) }] }), []); // Title card animation for lowering position when trailer is unmuted const titleCardAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: titleCardTranslateY.value }] }), []); // Genre animation for hiding when trailer is unmuted const genreAnimatedStyle = useAnimatedStyle(() => ({ opacity: genreOpacity.value }), []); // Optimized genre rendering with lazy loading and memory management const genreElements = useMemo(() => { if (!shouldLoadSecondaryData || !metadata?.genres?.length) return null; const genresToDisplay = metadata.genres.slice(0, 3); // Reduced to 3 for performance const elements: React.ReactNode[] = []; genresToDisplay.forEach((genreName: string, index: number) => { // Add genre text elements.push( {genreName} ); // Add dot separator if not the last element if (index < genresToDisplay.length - 1) { elements.push( ); } }); return ( {elements} ); }, [metadata.genres, themeColors.text, shouldLoadSecondaryData, isTablet]); // Memoized play button text const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]); // Calculate if content is watched (>=85% progress) - check both local and Trakt progress const isWatched = useMemo(() => { if (!watchProgress) return false; // Check Trakt progress first if available and user is authenticated if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) { const traktWatched = watchProgress.traktProgress >= 95; // Removed excessive logging for Trakt progress return traktWatched; } // Fall back to local progress if (watchProgress.duration === 0) return false; const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; const localWatched = progressPercent >= 85; // Removed excessive logging for local progress return localWatched; }, [watchProgress, isTraktAuthenticated]); // App state management to prevent background ANR useEffect(() => { const handleAppStateChange = (nextAppState: any) => { if (appState.current.match(/inactive|background/) && nextAppState === 'active') { // App came to foreground logger.info('HeroSection', 'App came to foreground'); // Don't automatically resume trailer - let TrailerPlayer handle it } else if (appState.current === 'active' && nextAppState.match(/inactive|background/)) { // App going to background - only pause if trailer is actually playing logger.info('HeroSection', 'App going to background - pausing operations'); // Only pause if trailer is currently playing to avoid unnecessary state changes if (globalTrailerPlaying) { setTrailerPlaying(false); } } appState.current = nextAppState; }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => subscription?.remove(); }, [setTrailerPlaying, globalTrailerPlaying]); // Navigation focus effect - conservative approach to prevent unwanted trailer resumption useFocusEffect( useCallback(() => { // Screen is focused - only resume trailer if it was previously playing and got interrupted logger.info('HeroSection', 'Screen focused'); // If trailers are enabled and not playing, start playback (unless scrolled past resume threshold) if (settings?.showTrailers) { setTimeout(() => { try { const y = (scrollY as any).value || 0; const resumeThreshold = heroHeight.value * 0.4; if (y < resumeThreshold && !startedOnFocusRef.current && isPlayingSV.value === 0) { setTrailerPlaying(true); isPlayingSV.value = 1; startedOnFocusRef.current = true; } } catch (_e) { if (!startedOnFocusRef.current && isPlayingSV.value === 0) { setTrailerPlaying(true); isPlayingSV.value = 1; startedOnFocusRef.current = true; } } }, 50); } return () => { // Stop trailer when leaving this screen to prevent background playback/heat logger.info('HeroSection', 'Screen unfocused - stopping trailer playback'); setTrailerPlaying(false); isPlayingSV.value = 0; startedOnFocusRef.current = false; startedOnReadyRef.current = false; }; }, [setTrailerPlaying, settings?.showTrailers]) ); // Mirror playing state to shared value to use inside worklets useEffect(() => { isPlayingSV.value = globalTrailerPlaying ? 1 : 0; }, [globalTrailerPlaying]); // Mirror focus state to shared value for worklets and enforce pause when unfocused useEffect(() => { isFocusedSV.value = isFocused ? 1 : 0; if (!isFocused) { // Ensure trailer is not playing when screen loses focus setTrailerPlaying(false); isPlayingSV.value = 0; startedOnFocusRef.current = false; startedOnReadyRef.current = false; // Also reset trailer state to prevent background start try { setTrailerReady(false); setTrailerPreloaded(false); setTrailerUrl(null); trailerOpacity.value = 0; thumbnailOpacity.value = 1; } catch (_e) {} } }, [isFocused, setTrailerPlaying]); // Pause/resume trailer based on scroll with hysteresis and guard useDerivedValue(() => { 'worklet'; try { if (!scrollGuardEnabledSV.value || isFocusedSV.value === 0) return; const pauseThreshold = heroHeight.value * 0.7; // pause when beyond 70% const resumeThreshold = heroHeight.value * 0.4; // resume when back within 40% const y = scrollY.value; if (y > pauseThreshold && isPlayingSV.value === 1 && pausedByScrollSV.value === 0) { pausedByScrollSV.value = 1; runOnJS(setTrailerPlaying)(false); isPlayingSV.value = 0; } else if (y < resumeThreshold && pausedByScrollSV.value === 1) { pausedByScrollSV.value = 0; runOnJS(setTrailerPlaying)(true); isPlayingSV.value = 1; } } catch (e) { // no-op } }); // Memory management and cleanup useEffect(() => { return () => { // Don't stop trailer playback when component unmounts // Let the new hero section (if any) take control of trailer state // This prevents the trailer from stopping when navigating between screens // Reset animation values on unmount to prevent memory leaks try { imageOpacity.value = 1; imageLoadOpacity.value = 0; shimmerOpacity.value = 0.3; trailerOpacity.value = 0; thumbnailOpacity.value = 1; actionButtonsOpacity.value = 1; titleCardTranslateY.value = 0; genreOpacity.value = 1; watchProgressOpacity.value = 1; buttonsOpacity.value = 1; buttonsTranslateY.value = 0; logoOpacity.value = 1; heroOpacity.value = 1; heroHeight.value = height * 0.6; } catch (error) { logger.error('HeroSection', 'Error cleaning up animation values:', error); } interactionComplete.current = false; }; }, [imageOpacity, imageLoadOpacity, shimmerOpacity, trailerOpacity, thumbnailOpacity, actionButtonsOpacity, titleCardTranslateY, genreOpacity, watchProgressOpacity, buttonsOpacity, buttonsTranslateY, logoOpacity, heroOpacity, heroHeight]); // Disabled performance monitoring to reduce CPU overhead in production // useEffect(() => { // if (__DEV__) { // const startTime = Date.now(); // const timer = setTimeout(() => { // const renderTime = Date.now() - startTime; // if (renderTime > 100) { // console.warn(`[HeroSection] Slow render detected: ${renderTime}ms`); // } // }, 0); // return () => clearTimeout(timer); // } // }); return ( {/* Optimized Background */} {/* Optimized shimmer loading effect */} {shouldLoadSecondaryData && (trailerLoading || ((imageSource && !imageLoaded) || loadingBanner)) && ( )} {/* Background thumbnail image - always rendered when available */} {shouldLoadSecondaryData && imageSource && !loadingBanner && ( )} {/* Hidden preload trailer player - loads in background */} {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && !trailerPreloaded && ( )} {/* Visible trailer player - rendered on top with fade transition */} {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && ( { if (status.isLoaded && !trailerReady) { handleTrailerReady(); } }} /> )} {/* Trailer control buttons (unmute and fullscreen) */} {settings?.showTrailers && trailerReady && trailerUrl && ( = 768 ? 32 : 16, zIndex: 1000, opacity: trailerOpacity, flexDirection: 'row', gap: 8, }}> {/* Fullscreen button */} e.stopPropagation()} onPressOut={(e) => e.stopPropagation()} style={{ padding: 8, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 20, }} > {/* Unmute button */} { logger.info('HeroSection', 'Mute toggle button pressed, current muted state:', trailerMuted); updateSetting('trailerMuted', !trailerMuted); if (trailerMuted) { // When unmuting, hide action buttons, genre, title card, and watch progress actionButtonsOpacity.value = withTiming(0, { duration: 300 }); genreOpacity.value = withTiming(0, { duration: 300 }); titleCardTranslateY.value = withTiming(60, { duration: 300 }); watchProgressOpacity.value = withTiming(0, { duration: 300 }); } else { // When muting, show action buttons, genre, title card, and watch progress actionButtonsOpacity.value = withTiming(1, { duration: 300 }); genreOpacity.value = withTiming(1, { duration: 300 }); titleCardTranslateY.value = withTiming(0, { duration: 300 }); watchProgressOpacity.value = withTiming(1, { duration: 300 }); } }} activeOpacity={0.7} onPressIn={(e) => e.stopPropagation()} onPressOut={(e) => e.stopPropagation()} style={{ padding: 8, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 20, }} > )} {/* Ultra-light Gradient with subtle dynamic background blend */} {/* Enhanced bottom fade with stronger gradient */} {/* Optimized Title/Logo */} {shouldLoadSecondaryData && metadata.logo && !logoLoadError ? ( { runOnJS(setLogoLoadError)(true); }} /> ) : ( {metadata.name} )} {/* Enhanced Watch Progress with Trakt integration */} {/* Optimized genre display with lazy loading */} {shouldLoadSecondaryData && genreElements && ( {genreElements} )} {/* Optimized Action Buttons */} ); }); // Ultra-optimized styles const styles = StyleSheet.create({ heroSection: { width: '100%', backgroundColor: '#000', overflow: 'hidden', }, absoluteFill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, }, backButtonContainer: { position: 'absolute', top: Platform.OS === 'android' ? 40 : 50, left: isTablet ? 32 : 16, zIndex: 10, }, backButton: { padding: 8, }, backButtonIcon: { textShadowColor: 'rgba(0, 0, 0, 0.75)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 3, }, heroGradient: { flex: 1, justifyContent: 'flex-end', paddingBottom: 20, }, bottomFadeGradient: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 400, zIndex: 1, }, heroContent: { padding: isTablet ? 32 : 16, paddingTop: isTablet ? 16 : 8, paddingBottom: isTablet ? 16 : 8, position: 'relative', zIndex: 2, }, logoContainer: { alignItems: 'center', justifyContent: 'center', width: '100%', marginBottom: 4, flex: 0, display: 'flex', maxWidth: isTablet ? 600 : '100%', alignSelf: 'center', }, titleLogoContainer: { alignItems: 'center', justifyContent: 'center', width: '100%', flex: 0, display: 'flex', maxWidth: isTablet ? 600 : '100%', alignSelf: 'center', }, titleLogo: { width: width * 0.75, height: 90, alignSelf: 'center', textAlign: 'center', }, heroTitle: { fontSize: 26, fontWeight: '900', marginBottom: 8, textShadowColor: 'rgba(0,0,0,0.8)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 3, letterSpacing: -0.3, textAlign: 'center', }, genreContainer: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', marginTop: 6, marginBottom: 14, gap: 0, maxWidth: isTablet ? 600 : '100%', alignSelf: 'center', }, genreText: { fontSize: 12, fontWeight: '500', opacity: 0.9, marginLeft: 0, paddingLeft: 0, marginRight: 0, paddingRight: 0, marginVertical: 0, paddingVertical: 0, }, genreDot: { fontSize: 12, fontWeight: '500', opacity: 0.6, marginHorizontal: 4, paddingHorizontal: 0, marginVertical: 0, paddingVertical: 0, }, actionButtons: { flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'center', width: '100%', position: 'relative', maxWidth: isTablet ? 600 : '100%', alignSelf: 'center', }, actionButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 11, paddingHorizontal: 16, borderRadius: 26, flex: 1, }, playButton: { backgroundColor: '#fff', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 4, }, infoButton: { borderWidth: 1.5, borderColor: 'rgba(255,255,255,0.7)', overflow: 'hidden', }, iconButton: { width: 50, height: 50, borderRadius: 25, borderWidth: 1.5, borderColor: 'rgba(255,255,255,0.7)', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, playButtonText: { color: '#000', fontWeight: '700', marginLeft: 6, fontSize: 15, }, infoButtonText: { color: '#fff', marginLeft: 6, fontWeight: '600', fontSize: 15, }, watchProgressContainer: { marginTop: 4, marginBottom: 4, width: '100%', alignItems: 'center', minHeight: 36, position: 'relative', maxWidth: isTablet ? 600 : '100%', alignSelf: 'center', }, progressGlassBackground: { width: '75%', backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 12, padding: 8, borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', overflow: 'hidden', }, androidProgressBlur: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 16, backgroundColor: 'rgba(0,0,0,0.3)', }, watchProgressBarContainer: { position: 'relative', marginBottom: 6, }, watchProgressBar: { width: '100%', height: 3, backgroundColor: 'rgba(255, 255, 255, 0.15)', borderRadius: 1.5, overflow: 'hidden', position: 'relative', }, watchProgressFill: { height: '100%', borderRadius: 1.25, }, traktSyncIndicator: { position: 'absolute', right: 2, top: -2, bottom: -2, width: 12, alignItems: 'center', justifyContent: 'center', }, traktSyncIndicatorEnhanced: { position: 'absolute', right: 4, top: -2, bottom: -2, width: 16, height: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, watchedProgressIndicator: { position: 'absolute', right: 2, top: -1, bottom: -1, width: 10, alignItems: 'center', justifyContent: 'center', }, watchProgressTextContainer: { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', width: '100%', }, watchProgressText: { fontSize: 11, textAlign: 'center', opacity: 0.85, letterSpacing: 0.1, flex: 1, }, traktSyncButton: { padding: 4, borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.1)', }, blurBackground: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 20, }, androidFallbackBlur: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.15)', }, blurBackgroundRound: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 25, }, androidFallbackBlurRound: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 25, backgroundColor: 'rgba(255,255,255,0.15)', }, watchedIndicator: { position: 'absolute', top: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: 8, width: 16, height: 16, alignItems: 'center', justifyContent: 'center', }, watchedPlayButton: { backgroundColor: '#1e1e1e', borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 4, }, watchedPlayButtonText: { color: '#fff', fontWeight: '700', marginLeft: 6, fontSize: 15, }, // Enhanced progress indicator styles progressShimmer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.1)', }, completionGlow: { position: 'absolute', top: -2, left: -2, right: -2, bottom: -2, borderRadius: 4, backgroundColor: 'rgba(0,255,136,0.2)', }, completionIndicator: { position: 'absolute', right: 4, top: -6, bottom: -6, width: 16, height: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, completionGradient: { width: 16, height: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, sparkleContainer: { position: 'absolute', top: -10, left: 0, right: 0, bottom: -10, borderRadius: 2, }, sparkle: { position: 'absolute', width: 8, height: 8, borderRadius: 4, alignItems: 'center', justifyContent: 'center', }, progressInfoMain: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: 2, }, watchProgressMainText: { fontSize: 11, fontWeight: '600', textAlign: 'center', }, watchProgressSubText: { fontSize: 9, textAlign: 'center', opacity: 0.8, marginBottom: 1, }, syncStatusContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginTop: 2, width: '100%', flexWrap: 'wrap', }, syncStatusText: { fontSize: 9, marginLeft: 4, fontWeight: '500', }, traktSyncButtonEnhanced: { position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 12, overflow: 'hidden', }, traktSyncButtonInline: { marginLeft: 8, width: 20, height: 20, borderRadius: 10, overflow: 'hidden', }, syncButtonGradient: { width: 24, height: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, syncButtonGradientInline: { width: 20, height: 20, borderRadius: 10, alignItems: 'center', justifyContent: 'center', }, traktIndicatorGradient: { width: 16, height: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, // Tablet-specific styles tabletActionButtons: { flexDirection: 'row', gap: 16, alignItems: 'center', justifyContent: 'center', width: '100%', position: 'relative', maxWidth: 600, alignSelf: 'center', }, tabletPlayButton: { paddingVertical: 16, paddingHorizontal: 24, borderRadius: 32, minWidth: 180, }, tabletPlayButtonText: { fontSize: 18, fontWeight: '700', marginLeft: 8, }, tabletInfoButton: { paddingVertical: 14, paddingHorizontal: 20, borderRadius: 28, minWidth: 140, }, tabletInfoButtonText: { fontSize: 16, fontWeight: '600', marginLeft: 8, }, tabletIconButton: { width: 60, height: 60, borderRadius: 30, }, tabletHeroTitle: { fontSize: 36, fontWeight: '900', marginBottom: 12, textShadowColor: 'rgba(0,0,0,0.8)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, letterSpacing: -0.5, textAlign: 'center', lineHeight: 42, }, tabletTitleLogo: { width: width * 0.5, height: 120, alignSelf: 'center', maxWidth: 400, textAlign: 'center', }, tabletGenreContainer: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', marginTop: 8, marginBottom: 20, gap: 0, }, tabletGenreText: { fontSize: 16, fontWeight: '500', opacity: 0.9, marginLeft: 0, paddingLeft: 0, marginRight: 0, paddingRight: 0, marginVertical: 0, paddingVertical: 0, }, tabletGenreDot: { fontSize: 16, fontWeight: '500', opacity: 0.6, marginHorizontal: 6, paddingHorizontal: 0, marginVertical: 0, paddingVertical: 0, }, tabletWatchProgressContainer: { marginTop: 8, marginBottom: 8, width: '100%', alignItems: 'center', minHeight: 44, position: 'relative', maxWidth: 800, alignSelf: 'center', }, tabletProgressGlassBackground: { width: width * 0.7, maxWidth: 700, backgroundColor: 'rgba(255,255,255,0.08)', borderRadius: 16, padding: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', overflow: 'hidden', alignSelf: 'center', }, tabletWatchProgressMainText: { fontSize: 14, fontWeight: '600', textAlign: 'center', }, tabletWatchProgressSubText: { fontSize: 12, textAlign: 'center', opacity: 0.8, marginBottom: 1, }, }); export default HeroSection;