diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index df1a045e..6d62d164 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -700,9 +700,13 @@ const HeroSection: React.FC = memo(({ const [trailerError, setTrailerError] = useState(false); const [trailerMuted, setTrailerMuted] = useState(true); const [isTrailerPlaying, setIsTrailerPlaying] = useState(false); + const [trailerReady, setTrailerReady] = useState(false); + const [trailerPreloaded, setTrailerPreloaded] = useState(false); const imageOpacity = useSharedValue(1); const imageLoadOpacity = useSharedValue(0); const shimmerOpacity = useSharedValue(0.3); + const trailerOpacity = useSharedValue(0); + const thumbnailOpacity = useSharedValue(1); // Performance optimization: Cache theme colors const themeColors = useMemo(() => ({ @@ -711,6 +715,36 @@ const HeroSection: React.FC = memo(({ 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 (!trailerPreloaded) { + setTrailerPreloaded(true); + } + setTrailerReady(true); + setIsTrailerPlaying(true); + + // Smooth transition: fade out thumbnail, fade in trailer + thumbnailOpacity.value = withTiming(0, { duration: 500 }); + trailerOpacity.value = withTiming(1, { duration: 500 }); + }, [thumbnailOpacity, trailerOpacity, trailerPreloaded]); + + // Handle trailer error - fade back to thumbnail + const handleTrailerError = useCallback(() => { + setTrailerError(true); + setTrailerReady(false); + setIsTrailerPlaying(false); + + // Fade back to thumbnail + trailerOpacity.value = withTiming(0, { duration: 300 }); + thumbnailOpacity.value = withTiming(1, { duration: 300 }); + }, [trailerOpacity, thumbnailOpacity]); // Memoized image source const imageSource = useMemo(() => @@ -736,12 +770,15 @@ const HeroSection: React.FC = memo(({ setTrailerLoading(true); setTrailerError(false); + setTrailerReady(false); + setTrailerPreloaded(false); try { const url = await TrailerService.getTrailerUrl(metadata.name, metadata.year); if (url) { - setTrailerUrl(TrailerService.getBestFormatUrl(url)); - logger.info('HeroSection', `Trailer loaded for ${metadata.name}`); + const bestUrl = TrailerService.getBestFormatUrl(url); + setTrailerUrl(bestUrl); + logger.info('HeroSection', `Trailer URL loaded for ${metadata.name}`); } else { logger.info('HeroSection', `No trailer found for ${metadata.name}`); } @@ -950,44 +987,83 @@ const HeroSection: React.FC = memo(({ )} - {/* Trailer player or background image */} - {shouldLoadSecondaryData && trailerUrl && !trailerLoading && !trailerError ? ( - { - logger.warn('HeroSection', 'Trailer playback failed, falling back to image'); - setTrailerError(true); - }} - onPlaybackStatusUpdate={(status) => { - setIsTrailerPlaying(status.isLoaded && !status.didJustFinish); - }} - /> - ) : shouldLoadSecondaryData && imageSource && !loadingBanner ? ( - - ) : null} + {/* Background thumbnail image - always rendered when available */} + {shouldLoadSecondaryData && imageSource && !loadingBanner && ( + + + + )} + + {/* Hidden preload trailer player - loads in background */} + {shouldLoadSecondaryData && trailerUrl && !trailerLoading && !trailerError && !trailerPreloaded && ( + + + + )} + + {/* Visible trailer player - rendered on top with fade transition */} + {shouldLoadSecondaryData && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && ( + + { + if (status.isLoaded && !trailerReady) { + handleTrailerReady(); + } + }} + /> + + )} {/* Unmute button for trailer */} - {isTrailerPlaying && trailerUrl && ( - setTrailerMuted(!trailerMuted)} - activeOpacity={0.7} - > - - + {trailerReady && trailerUrl && ( + = 768 ? 32 : 16, + zIndex: 10, + opacity: trailerOpacity + }}> + setTrailerMuted(!trailerMuted)} + activeOpacity={0.7} + style={{ + padding: 8, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + borderRadius: 20, + }} + > + + + )} @@ -1031,7 +1107,7 @@ const HeroSection: React.FC = memo(({ pointerEvents="none" /> {/* Optimized Title/Logo */} @@ -1118,15 +1194,7 @@ const styles = StyleSheet.create({ textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 3, }, - unmuteButton: { - position: 'absolute', - top: Platform.OS === 'android' ? 40 : 50, - right: width >= 768 ? 32 : 16, - zIndex: 10, - padding: 8, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - borderRadius: 20, - }, + heroGradient: { flex: 1, justifyContent: 'flex-end', diff --git a/src/components/video/TrailerPlayer.tsx b/src/components/video/TrailerPlayer.tsx index 5d84ac0d..8eb63721 100644 --- a/src/components/video/TrailerPlayer.tsx +++ b/src/components/video/TrailerPlayer.tsx @@ -33,6 +33,7 @@ interface TrailerPlayerProps { onProgress?: (data: OnProgressData) => void; onPlaybackStatusUpdate?: (status: { isLoaded: boolean; didJustFinish: boolean }) => void; style?: any; + hideLoadingSpinner?: boolean; } const TrailerPlayer: React.FC = memo(({ @@ -45,6 +46,7 @@ const TrailerPlayer: React.FC = memo(({ onProgress, onPlaybackStatusUpdate, style, + hideLoadingSpinner = false, }) => { const { currentTheme } = useTheme(); const videoRef = useRef(null); @@ -121,10 +123,11 @@ const TrailerPlayer: React.FC = memo(({ const handleLoadStart = useCallback(() => { setIsLoading(true); setHasError(false); - loadingOpacity.value = 1; + // Only show loading spinner if not hidden + loadingOpacity.value = hideLoadingSpinner ? 0 : 1; onLoadStart?.(); logger.info('TrailerPlayer', 'Video load started'); - }, [loadingOpacity, onLoadStart]); + }, [loadingOpacity, onLoadStart, hideLoadingSpinner]); const handleLoad = useCallback((data: OnLoadData) => { setIsLoading(false); @@ -218,10 +221,12 @@ const TrailerPlayer: React.FC = memo(({ }} /> - {/* Loading indicator */} - - - + {/* Loading indicator - hidden during smooth transitions */} + {!hideLoadingSpinner && ( + + + + )} {/* Video controls overlay */}