From 81897b7242474562704e69e3b3c4552707473191 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 18 Jun 2025 10:47:27 +0530 Subject: [PATCH] Refactor loading and image handling in Metadata components for smoother transitions This update removes the fade animation from the MetadataLoadingScreen, allowing the parent component to manage transitions. In the HeroSection, enhancements have been made to the image loading state, introducing shimmer animations for a better user experience during loading. The MetadataScreen now features a skeleton loading screen with a fade-out transition, improving the overall content loading experience. Additionally, state management for image loading has been optimized to prevent unnecessary re-renders. --- .../loading/MetadataLoadingScreen.tsx | 17 +- src/components/metadata/HeroSection.tsx | 56 ++++- src/hooks/useMetadataAssets.ts | 36 ++- src/screens/MetadataScreen.tsx | 229 ++++++++++-------- 4 files changed, 212 insertions(+), 126 deletions(-) diff --git a/src/components/loading/MetadataLoadingScreen.tsx b/src/components/loading/MetadataLoadingScreen.tsx index 39388bb8..c0a8ebf5 100644 --- a/src/components/loading/MetadataLoadingScreen.tsx +++ b/src/components/loading/MetadataLoadingScreen.tsx @@ -22,19 +22,11 @@ export const MetadataLoadingScreen: React.FC = ({ }) => { const { currentTheme } = useTheme(); - // Animation values - const fadeAnim = useRef(new Animated.Value(0)).current; + // Animation values - removed fadeAnim since parent handles transitions const pulseAnim = useRef(new Animated.Value(0.3)).current; const shimmerAnim = useRef(new Animated.Value(0)).current; useEffect(() => { - // Start entrance animation - Animated.timing(fadeAnim, { - toValue: 1, - duration: 800, - useNativeDriver: true, - }).start(); - // Continuous pulse animation for skeleton elements const pulseAnimation = Animated.loop( Animated.sequence([ @@ -138,10 +130,7 @@ export const MetadataLoadingScreen: React.FC = ({ barStyle="light-content" /> - + {/* Hero Skeleton */} = ({ )} - + ); }; diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 9c17a755..25756188 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -16,6 +16,7 @@ import Animated, { useSharedValue, withTiming, runOnJS, + withRepeat, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; @@ -246,19 +247,47 @@ const HeroSection: React.FC = ({ }) => { const { currentTheme } = useTheme(); - // Minimal state for image handling + // Enhanced state for smooth image loading const [imageError, setImageError] = useState(false); + const [imageLoaded, setImageLoaded] = useState(false); const imageOpacity = useSharedValue(1); + const imageLoadOpacity = useSharedValue(0); + const shimmerOpacity = useSharedValue(0.3); // Memoized image source const imageSource = useMemo(() => bannerImage || metadata.banner || metadata.poster , [bannerImage, metadata.banner, metadata.poster]); - // Ultra-fast image handlers + // Start shimmer animation for loading state + useEffect(() => { + 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]); + + // Reset loading state when image source changes + useEffect(() => { + if (imageSource) { + setImageLoaded(false); + imageLoadOpacity.value = 0; + } + }, [imageSource]); + + // Enhanced image handlers with smooth transitions const handleImageError = () => { setImageError(true); + setImageLoaded(false); imageOpacity.value = withTiming(0.6, { duration: 150 }); + imageLoadOpacity.value = withTiming(0, { duration: 150 }); runOnJS(() => { if (bannerImage !== metadata.banner) { setBannerImage(metadata.banner || metadata.poster); @@ -268,7 +297,10 @@ const HeroSection: React.FC = ({ const handleImageLoad = () => { setImageError(false); + setImageLoaded(true); imageOpacity.value = withTiming(1, { duration: 150 }); + // Smooth fade-in for the loaded image + imageLoadOpacity.value = withTiming(1, { duration: 400 }); }; // Ultra-optimized animated styles - single calculations @@ -293,14 +325,14 @@ const HeroSection: React.FC = ({ opacity: watchProgressOpacity.value, }), []); - // Ultra-optimized backdrop with minimal calculations + // Enhanced backdrop with smooth loading animation const backdropImageStyle = useAnimatedStyle(() => { 'worklet'; const translateY = scrollY.value * PARALLAX_FACTOR; const scale = 1 + (scrollY.value * 0.0001); // Micro scale effect return { - opacity: imageOpacity.value, + opacity: imageOpacity.value * imageLoadOpacity.value, transform: [ { translateY: -Math.min(translateY, 100) }, // Cap translation { scale: Math.min(scale, SCALE_FACTOR) } // Cap scale @@ -346,7 +378,21 @@ const HeroSection: React.FC = ({ {/* Optimized Background */} - {/* Ultra-optimized Background Image */} + {/* Loading placeholder for smooth transition */} + {((imageSource && !imageLoaded) || loadingBanner) && ( + + + + )} + + {/* Enhanced Background Image with smooth loading */} {imageSource && !loadingBanner && ( { logger.log(`[useMetadataAssets:Banner] Starting banner fetch.`); - setLoadingBanner(true); - setBannerImage(null); // Clear existing banner to prevent mixed sources - setBannerSource(null); // Clear source tracking + setLoadingBanner(true); + + // Show fallback banner immediately to prevent blank state + const fallbackBanner = metadata?.banner || metadata?.poster || null; + if (fallbackBanner && !bannerImage) { + setBannerImage(fallbackBanner); + setBannerSource('default'); + logger.log(`[useMetadataAssets:Banner] Setting immediate fallback banner: ${fallbackBanner}`); + } let finalBanner: string | null = null; let bannerSourceType: 'tmdb' | 'metahub' | 'default' = 'default'; @@ -419,17 +425,31 @@ export const useMetadataAssets = ( // Set the final state logger.log(`[useMetadataAssets:Banner] Final decision: Setting banner to ${finalBanner} (Source: ${bannerSourceType})`); - setBannerImage(finalBanner); - setBannerSource(bannerSourceType); // Track the source of the final image + + // Only update if the banner actually changed to avoid unnecessary re-renders + if (finalBanner !== bannerImage || bannerSourceType !== bannerSource) { + setBannerImage(finalBanner); + setBannerSource(bannerSourceType); // Track the source of the final image + logger.log(`[useMetadataAssets:Banner] Banner updated from ${bannerImage} to ${finalBanner}`); + } else { + logger.log(`[useMetadataAssets:Banner] Banner unchanged, skipping update`); + } + forcedBannerRefreshDone.current = true; // Mark this cycle as complete } catch (error) { logger.error(`[useMetadataAssets:Banner] Error in outer fetchBanner try block:`, error); // Ensure fallback to default even on outer error const defaultBanner = metadata?.banner || metadata?.poster || null; - setBannerImage(defaultBanner); - setBannerSource('default'); - logger.log(`[useMetadataAssets:Banner] Setting default banner due to outer error: ${defaultBanner}`); + + // Only set if it's different from current banner + if (defaultBanner !== bannerImage) { + setBannerImage(defaultBanner); + setBannerSource('default'); + logger.log(`[useMetadataAssets:Banner] Setting default banner due to outer error: ${defaultBanner}`); + } else { + logger.log(`[useMetadataAssets:Banner] Default banner already set, skipping update`); + } } finally { logger.log(`[useMetadataAssets:Banner] Finished banner fetch attempt.`); setLoadingBanner(false); diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 29a690ba..d6caeb69 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -55,7 +55,9 @@ const MetadataScreen: React.FC = () => { // Optimized state management - reduced state variables const [isContentReady, setIsContentReady] = useState(false); + const [showSkeleton, setShowSkeleton] = useState(true); const transitionOpacity = useSharedValue(0); + const skeletonOpacity = useSharedValue(1); const { metadata, @@ -85,14 +87,26 @@ const MetadataScreen: React.FC = () => { // Memoized derived values for performance const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]); - // Ultra-fast content transition + // Smooth skeleton to content transition useEffect(() => { if (isReady && !isContentReady) { - setIsContentReady(true); - transitionOpacity.value = withTiming(1, { duration: 200 }); + // Small delay to ensure skeleton is rendered before starting transition + setTimeout(() => { + // Start fade out skeleton and fade in content simultaneously + skeletonOpacity.value = withTiming(0, { duration: 300 }); + transitionOpacity.value = withTiming(1, { duration: 400 }); + + // Hide skeleton after fade out completes + setTimeout(() => { + setShowSkeleton(false); + setIsContentReady(true); + }, 300); + }, 100); } else if (!isReady && isContentReady) { setIsContentReady(false); + setShowSkeleton(true); transitionOpacity.value = 0; + skeletonOpacity.value = 1; } }, [isReady, isContentReady]); @@ -143,6 +157,10 @@ const MetadataScreen: React.FC = () => { opacity: transitionOpacity.value, }), []); + const skeletonStyle = useAnimatedStyle(() => ({ + opacity: skeletonOpacity.value, + }), []); + // Memoized error component for performance const ErrorComponent = useMemo(() => { if (!metadataError) return null; @@ -181,110 +199,123 @@ const MetadataScreen: React.FC = () => { return ErrorComponent; } - // Show loading screen - if (loading || !isContentReady) { - return ; - } - return ( - - - - - {/* Floating Header - Optimized */} - - - + {/* Skeleton Loading Screen - with fade out transition */} + {showSkeleton && ( + - {/* Hero Section - Optimized */} - + + + )} - {/* Main Content - Optimized */} - - + + + + {/* Floating Header - Optimized */} + imdbId ? ( - - ) : null} + logoLoadError={assetData.logoLoadError} + handleBack={handleBack} + handleToggleLibrary={handleToggleLibrary} + headerElementsY={animations.headerElementsY} + inLibrary={inLibrary} + headerOpacity={animations.headerOpacity} + headerElementsOpacity={animations.headerElementsOpacity} + safeAreaTop={safeAreaTop} + setLogoLoadError={assetData.setLogoLoadError} /> - - - {type === 'movie' && ( - + {/* Hero Section - Optimized */} + - )} - {type === 'series' ? ( - - ) : ( - metadata && - )} - - - - + {/* Main Content - Optimized */} + + imdbId ? ( + + ) : null} + /> + + + + {type === 'movie' && ( + + )} + + {type === 'series' ? ( + + ) : ( + metadata && + )} + + + + + )} + ); };