diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 3354fcf..103c8c9 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, @@ -13,6 +13,9 @@ import Animated, { useAnimatedStyle, interpolate, Extrapolate, + useSharedValue, + withTiming, + withSpring, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; @@ -259,6 +262,48 @@ const HeroSection: React.FC = ({ setLogoLoadError, }) => { const { currentTheme } = useTheme(); + + // State for backdrop image loading + const [imageLoaded, setImageLoaded] = useState(false); + const [imageError, setImageError] = useState(false); + + // Animation values for smooth backdrop transitions + const backdropOpacity = useSharedValue(1); // Start visible + const backdropScale = useSharedValue(1); // Start at normal scale + + // Handle image load success + const handleImageLoad = () => { + setImageLoaded(true); + setImageError(false); + // Enhance the image with subtle animation + backdropOpacity.value = withTiming(1, { duration: 300 }); + backdropScale.value = withSpring(1, { + damping: 25, + stiffness: 120, + mass: 1 + }); + }; + + // Handle image load error + const handleImageError = () => { + logger.warn(`[HeroSection] Banner failed to load: ${bannerImage}`); + setImageError(true); + backdropOpacity.value = withTiming(0.7, { duration: 200 }); // Dim on error + if (bannerImage !== metadata.banner) { + setBannerImage(metadata.banner || metadata.poster); + } + }; + + // Reset animations when banner image changes + useEffect(() => { + if (bannerImage && !loadingBanner) { + setImageLoaded(false); + setImageError(false); + backdropOpacity.value = 0.8; // Start slightly dimmed + backdropScale.value = 0.98; // Start slightly smaller + } + }, [bannerImage, loadingBanner]); + // Enhanced animated styles with sophisticated micro-animations const heroAnimatedStyle = useAnimatedStyle(() => ({ width: '100%', @@ -317,7 +362,6 @@ const HeroSection: React.FC = ({ })); const watchProgressBarStyle = useAnimatedStyle(() => ({ - width: `${watchProgressWidth.value * 100}%`, transform: [ { scaleX: interpolate(watchProgressWidth.value, [0, 1], [0.8, 1]) } ] @@ -373,7 +417,7 @@ const HeroSection: React.FC = ({ [0, 150, 300], [1.08, 1.05, 1.02], Extrapolate.CLAMP - ) + ) * backdropScale.value }, { rotateZ: interpolate( @@ -386,6 +430,42 @@ const HeroSection: React.FC = ({ ], })); + // Backdrop image animated style for smooth transitions + const backdropImageStyle = useAnimatedStyle(() => ({ + opacity: backdropOpacity.value, + transform: [ + { + translateY: interpolate( + dampedScrollY.value, + [0, 100, 300], + [0, -35, -90], + Extrapolate.CLAMP + ) + }, + { + scale: interpolate( + dampedScrollY.value, + [0, 150, 300], + [1.08, 1.05, 1.02], + Extrapolate.CLAMP + ) * backdropScale.value + }, + { + rotateZ: interpolate( + dampedScrollY.value, + [0, 300], + [0, -0.1], + Extrapolate.CLAMP + ) + 'deg' + } + ], + })); + + // Loading skeleton animated style + const skeletonStyle = useAnimatedStyle(() => ({ + opacity: loadingBanner ? 0.2 : 0, + })); + // Render genres const renderGenres = () => { if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) { @@ -411,19 +491,22 @@ const HeroSection: React.FC = ({ return ( - {loadingBanner ? ( - - ) : ( + {/* Fallback dark background */} + + + {/* Loading state with skeleton */} + {loadingBanner && ( + + )} + + {/* Background image with smooth loading */} + {!loadingBanner && (bannerImage || metadata.banner || metadata.poster) && ( { - logger.warn(`[HeroSection] Banner failed to load: ${bannerImage}`); - if (bannerImage !== metadata.banner) { - setBannerImage(metadata.banner || metadata.poster); - } - }} + onError={handleImageError} + onLoad={handleImageLoad} /> )}