diff --git a/src/components/loading/MetadataLoadingScreen.tsx b/src/components/loading/MetadataLoadingScreen.tsx index 344f838f..426d7456 100644 --- a/src/components/loading/MetadataLoadingScreen.tsx +++ b/src/components/loading/MetadataLoadingScreen.tsx @@ -1,19 +1,37 @@ -import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; +import React, { useEffect, useRef, forwardRef, useImperativeHandle, useMemo } from 'react'; import { View, - Text, StyleSheet, Dimensions, - Animated, StatusBar, - Easing, + Platform, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { LinearGradient } from 'expo-linear-gradient'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withRepeat, + withTiming, + withSequence, + withDelay, + Easing, + interpolate, + cancelAnimation, + runOnJS, +} from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; const { width, height } = Dimensions.get('window'); +// Responsive breakpoints +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + interface MetadataLoadingScreenProps { type?: 'movie' | 'series'; onExitComplete?: () => void; @@ -23,44 +41,120 @@ export interface MetadataLoadingScreenRef { exit: () => void; } +// Animated shimmer skeleton component +const ShimmerSkeleton = ({ + width: elementWidth, + height: elementHeight, + borderRadius = 8, + marginBottom = 8, + style = {}, + delay = 0, + shimmerProgress, + baseColor, + highlightColor, +}: { + width: number | string; + height: number; + borderRadius?: number; + marginBottom?: number; + style?: any; + delay?: number; + shimmerProgress: Animated.SharedValue; + baseColor: string; + highlightColor: string; +}) => { + const animatedStyle = useAnimatedStyle(() => { + const translateX = interpolate( + shimmerProgress.value, + [0, 1], + [-width, width] + ); + return { + transform: [{ translateX }], + }; + }); + + return ( + + + + + + ); +}; + export const MetadataLoadingScreen = forwardRef(({ type = 'movie', onExitComplete }, ref) => { const { currentTheme } = useTheme(); - - // Animation values - shimmer removed - - // Scene transition animation values (matching tab navigator) - const sceneOpacity = useRef(new Animated.Value(0)).current; - const sceneScale = useRef(new Animated.Value(0.95)).current; - const sceneTranslateY = useRef(new Animated.Value(8)).current; + + // Responsive sizing + const deviceWidth = Dimensions.get('window').width; + const deviceType = useMemo(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const isTV = deviceType === 'tv'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTablet = deviceType === 'tablet'; + + const horizontalPadding = isTV ? 48 : isLargeTablet ? 32 : isTablet ? 24 : 16; + + + // Shimmer animation + const shimmerProgress = useSharedValue(0); + + // Staggered fade-in for sections + const heroOpacity = useSharedValue(0); + const contentOpacity = useSharedValue(0); + const castOpacity = useSharedValue(0); + + // Exit animation value + const exitProgress = useSharedValue(0); + + // Colors for skeleton + const baseColor = currentTheme.colors.elevation1 || 'rgba(255,255,255,0.08)'; + const highlightColor = 'rgba(255,255,255,0.12)'; // Exit animation function const exit = () => { - const exitAnimation = Animated.parallel([ - Animated.timing(sceneOpacity, { - toValue: 0, - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), - useNativeDriver: true, - }), - Animated.timing(sceneScale, { - toValue: 0.95, - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), - useNativeDriver: true, - }), - Animated.timing(sceneTranslateY, { - toValue: 8, - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), - useNativeDriver: true, - }), - ]); - - exitAnimation.start(() => { - onExitComplete?.(); + exitProgress.value = withTiming(1, { + duration: 200, + easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), + }, (finished) => { + 'worklet'; + if (finished && onExitComplete) { + runOnJS(onExitComplete)(); + } }); }; @@ -70,70 +164,57 @@ export const MetadataLoadingScreen = forwardRef { - // Scene entrance animation (matching tab navigator) - const sceneAnimation = Animated.parallel([ - Animated.timing(sceneOpacity, { - toValue: 1, - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), - useNativeDriver: true, + // Start shimmer animation + shimmerProgress.value = withRepeat( + withTiming(1, { + duration: 1500, + easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) }), - Animated.timing(sceneScale, { - toValue: 1, - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), - useNativeDriver: true, - }), - Animated.timing(sceneTranslateY, { - toValue: 0, - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), - useNativeDriver: true, - }), - ]); + -1, // infinite + false + ); - sceneAnimation.start(); - - // Shimmer effect removed + // Staggered entrance animations + heroOpacity.value = withTiming(1, { duration: 300 }); + contentOpacity.value = withDelay(100, withTiming(1, { duration: 300 })); + castOpacity.value = withDelay(200, withTiming(1, { duration: 300 })); return () => { - sceneAnimation.stop(); + cancelAnimation(shimmerProgress); + cancelAnimation(heroOpacity); + cancelAnimation(contentOpacity); + cancelAnimation(castOpacity); }; }, []); - // Shimmer translate removed + // Animated styles + const containerStyle = useAnimatedStyle(() => ({ + opacity: interpolate(exitProgress.value, [0, 1], [1, 0]), + transform: [ + { scale: interpolate(exitProgress.value, [0, 1], [1, 0.98]) }, + ], + })); - const SkeletonElement = ({ - width: elementWidth, - height: elementHeight, - borderRadius = 8, - marginBottom = 8, - style = {}, - }: { - width: number | string; - height: number; - borderRadius?: number; - marginBottom?: number; - style?: any; - }) => ( - - {/* Pulsating overlay removed */} - {/* Shimmer overlay removed */} - - ); + const heroStyle = useAnimatedStyle(() => ({ + opacity: heroOpacity.value, + })); + + const contentStyle = useAnimatedStyle(() => ({ + opacity: contentOpacity.value, + transform: [ + { translateY: interpolate(contentOpacity.value, [0, 1], [10, 0]) }, + ], + })); + + const castStyle = useAnimatedStyle(() => ({ + opacity: castOpacity.value, + transform: [ + { translateY: interpolate(castOpacity.value, [0, 1], [10, 0]) }, + ], + })); return ( - - - - {/* Hero Skeleton */} - - + {/* Hero Section Skeleton */} + + - - {/* Overlay content on hero */} + + {/* Back Button Skeleton */} + + + + + {/* Gradient overlay */} - {/* Bottom hero content skeleton */} - - - - - - - + {/* Hero bottom content - Matches HeroSection.tsx structure */} + + {/* Logo placeholder - Centered and larger */} + + - - - + + {/* Watch Progress Placeholder - Centered Glass Bar */} + + + + + {/* Genre Info Row - Centered */} + + + + + + + + + {/* Action buttons row - Play, Save, Collection, Rates */} + + {/* Play Button */} + + + {/* Save Button */} + + + {/* Collection Icon */} + + + {/* Ratings Icon (if series) - Always show for skeleton consistency */} + - + - {/* Content Section Skeletons */} - - {/* Synopsis skeleton */} - - - - - + {/* Content Section */} + + {/* Description skeleton */} + + + + + - {/* Cast section skeleton */} - - - - {[1, 2, 3, 4].map((item) => ( - - - - - - ))} - + {/* Cast Section */} + + + + {[1, 2, 3, 4, 5].map((item) => ( + + + + + ))} + - {/* Episodes/Details skeleton based on type */} - {type === 'series' ? ( - - - + {/* Episodes/Recommendations Section */} + {type === 'series' ? ( + + + {/* Season selector */} + + {/* Episode cards */} + {[1, 2, 3].map((item) => ( - - + + - - - + + ))} - ) : ( - - - - - - + + ) : ( + + + + {[1, 2, 3, 4].map((item) => ( + + ))} - )} - + + )} ); @@ -258,7 +557,6 @@ const styles = StyleSheet.create({ flex: 1, }, heroSection: { - height: height * 0.6, position: 'relative', }, heroOverlay: { @@ -266,54 +564,52 @@ const styles = StyleSheet.create({ justifyContent: 'flex-end', }, heroBottomContent: { - position: 'absolute', - bottom: 20, - left: 20, - right: 20, + paddingBottom: 20, }, - genresRow: { + metaRow: { flexDirection: 'row', - marginBottom: 16, + alignItems: 'center', + marginBottom: 8, }, buttonsRow: { flexDirection: 'row', - marginBottom: 8, + alignItems: 'center', }, contentSection: { - padding: 20, + paddingTop: 16, }, - synopsisSection: { - marginBottom: 32, + descriptionSection: { + marginBottom: 24, }, castSection: { - marginBottom: 32, + marginBottom: 24, }, castRow: { flexDirection: 'row', - marginTop: 16, }, castItem: { alignItems: 'center', marginRight: 16, }, episodesSection: { - marginBottom: 32, + marginBottom: 24, }, - episodeItem: { + episodeList: { + gap: 16, + }, + episodeCard: { flexDirection: 'row', - marginBottom: 16, - alignItems: 'center', + gap: 12, }, episodeInfo: { flex: 1, + justifyContent: 'center', }, - detailsSection: { - marginBottom: 32, + recommendationsSection: { + marginBottom: 24, }, - detailsGrid: { + posterRow: { flexDirection: 'row', - justifyContent: 'space-between', - marginTop: 16, }, });