imrpoved metadataloadingscreen UI

This commit is contained in:
tapframe 2025-12-15 02:05:52 +05:30
parent 51064a65b2
commit 4ac45a041a

View file

@ -1,19 +1,37 @@
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; import React, { useEffect, useRef, forwardRef, useImperativeHandle, useMemo } from 'react';
import { import {
View, View,
Text,
StyleSheet, StyleSheet,
Dimensions, Dimensions,
Animated,
StatusBar, StatusBar,
Easing, Platform,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { LinearGradient } from 'expo-linear-gradient'; 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'; import { useTheme } from '../../contexts/ThemeContext';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
// Responsive breakpoints
const BREAKPOINTS = {
phone: 0,
tablet: 768,
largeTablet: 1024,
tv: 1440,
};
interface MetadataLoadingScreenProps { interface MetadataLoadingScreenProps {
type?: 'movie' | 'series'; type?: 'movie' | 'series';
onExitComplete?: () => void; onExitComplete?: () => void;
@ -23,44 +41,120 @@ export interface MetadataLoadingScreenRef {
exit: () => void; 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<number>;
baseColor: string;
highlightColor: string;
}) => {
const animatedStyle = useAnimatedStyle(() => {
const translateX = interpolate(
shimmerProgress.value,
[0, 1],
[-width, width]
);
return {
transform: [{ translateX }],
};
});
return (
<View style={[
{
width: elementWidth,
height: elementHeight,
borderRadius,
marginBottom,
backgroundColor: baseColor,
overflow: 'hidden',
},
style
]}>
<Animated.View
style={[
StyleSheet.absoluteFill,
animatedStyle,
]}
>
<LinearGradient
colors={[
'transparent',
highlightColor,
highlightColor,
'transparent',
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[StyleSheet.absoluteFill, { width: width * 2 }]}
/>
</Animated.View>
</View>
);
};
export const MetadataLoadingScreen = forwardRef<MetadataLoadingScreenRef, MetadataLoadingScreenProps>(({ export const MetadataLoadingScreen = forwardRef<MetadataLoadingScreenRef, MetadataLoadingScreenProps>(({
type = 'movie', type = 'movie',
onExitComplete onExitComplete
}, ref) => { }, ref) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
// Animation values - shimmer removed // Responsive sizing
const deviceWidth = Dimensions.get('window').width;
// Scene transition animation values (matching tab navigator) const deviceType = useMemo(() => {
const sceneOpacity = useRef(new Animated.Value(0)).current; if (deviceWidth >= BREAKPOINTS.tv) return 'tv';
const sceneScale = useRef(new Animated.Value(0.95)).current; if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet';
const sceneTranslateY = useRef(new Animated.Value(8)).current; 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 // Exit animation function
const exit = () => { const exit = () => {
const exitAnimation = Animated.parallel([ exitProgress.value = withTiming(1, {
Animated.timing(sceneOpacity, { duration: 200,
toValue: 0, easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
duration: 100, }, (finished) => {
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), 'worklet';
useNativeDriver: true, if (finished && onExitComplete) {
}), runOnJS(onExitComplete)();
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?.();
}); });
}; };
@ -70,70 +164,57 @@ export const MetadataLoadingScreen = forwardRef<MetadataLoadingScreenRef, Metada
})); }));
useEffect(() => { useEffect(() => {
// Scene entrance animation (matching tab navigator) // Start shimmer animation
const sceneAnimation = Animated.parallel([ shimmerProgress.value = withRepeat(
Animated.timing(sceneOpacity, { withTiming(1, {
toValue: 1, duration: 1500,
duration: 100, easing: Easing.bezier(0.25, 0.1, 0.25, 1.0)
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
useNativeDriver: true,
}), }),
Animated.timing(sceneScale, { -1, // infinite
toValue: 1, false
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,
}),
]);
sceneAnimation.start(); // Staggered entrance animations
heroOpacity.value = withTiming(1, { duration: 300 });
// Shimmer effect removed contentOpacity.value = withDelay(100, withTiming(1, { duration: 300 }));
castOpacity.value = withDelay(200, withTiming(1, { duration: 300 }));
return () => { 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 = ({ const heroStyle = useAnimatedStyle(() => ({
width: elementWidth, opacity: heroOpacity.value,
height: elementHeight, }));
borderRadius = 8,
marginBottom = 8, const contentStyle = useAnimatedStyle(() => ({
style = {}, opacity: contentOpacity.value,
}: { transform: [
width: number | string; { translateY: interpolate(contentOpacity.value, [0, 1], [10, 0]) },
height: number; ],
borderRadius?: number; }));
marginBottom?: number;
style?: any; const castStyle = useAnimatedStyle(() => ({
}) => ( opacity: castOpacity.value,
<View style={[ transform: [
{ { translateY: interpolate(castOpacity.value, [0, 1], [10, 0]) },
width: elementWidth, ],
height: elementHeight, }));
borderRadius,
marginBottom,
backgroundColor: currentTheme.colors.card,
overflow: 'hidden',
},
style
]}>
{/* Pulsating overlay removed */}
{/* Shimmer overlay removed */}
</View>
);
return ( return (
<SafeAreaView <SafeAreaView
style={[styles.container, { style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: currentTheme.colors.darkBackground,
}]} }]}
@ -144,107 +225,325 @@ export const MetadataLoadingScreen = forwardRef<MetadataLoadingScreenRef, Metada
backgroundColor="transparent" backgroundColor="transparent"
barStyle="light-content" barStyle="light-content"
/> />
<Animated.View <Animated.View style={[styles.content, containerStyle]}>
style={[ {/* Hero Section Skeleton */}
styles.content, <Animated.View style={[styles.heroSection, { height: height * 0.65 }, heroStyle]}>
{ <ShimmerSkeleton
opacity: sceneOpacity, width="100%"
transform: [ height={height * 0.65}
{ scale: sceneScale },
{ translateY: sceneTranslateY }
],
}
]}
>
{/* Hero Skeleton */}
<View style={styles.heroSection}>
<SkeletonElement
width="100%"
height={height * 0.6}
borderRadius={0} borderRadius={0}
marginBottom={0} marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/> />
{/* Overlay content on hero */} {/* Back Button Skeleton */}
<View style={{
position: 'absolute',
top: Platform.OS === 'android' ? 40 : 50,
left: isTablet ? 32 : 16,
zIndex: 10
}}>
<ShimmerSkeleton
width={40}
height={40}
borderRadius={20}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View>
{/* Gradient overlay */}
<View style={styles.heroOverlay}> <View style={styles.heroOverlay}>
<LinearGradient <LinearGradient
colors={[ colors={[
'transparent', 'transparent',
'rgba(0,0,0,0.4)', 'rgba(0,0,0,0.05)',
'rgba(0,0,0,0.8)', 'rgba(0,0,0,0.15)',
'rgba(0,0,0,0.35)',
'rgba(0,0,0,0.65)',
currentTheme.colors.darkBackground, currentTheme.colors.darkBackground,
]} ]}
locations={[0, 0.3, 0.55, 0.75, 0.9, 1]}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
{/* Bottom hero content skeleton */} {/* Hero bottom content - Matches HeroSection.tsx structure */}
<View style={styles.heroBottomContent}> <View style={[styles.heroBottomContent, { paddingHorizontal: horizontalPadding }]}>
<SkeletonElement width="60%" height={32} borderRadius={16} /> {/* Logo placeholder - Centered and larger */}
<SkeletonElement width="40%" height={20} borderRadius={10} /> <View style={{ alignItems: 'center', width: '100%', marginBottom: 16 }}>
<View style={styles.genresRow}> <ShimmerSkeleton
<SkeletonElement width={80} height={24} borderRadius={12} marginBottom={0} style={{ marginRight: 8 }} /> width={isTV ? 400 : isLargeTablet ? 300 : width * 0.65}
<SkeletonElement width={90} height={24} borderRadius={12} marginBottom={0} style={{ marginRight: 8 }} /> height={isTV ? 120 : isLargeTablet ? 100 : 90}
<SkeletonElement width={70} height={24} borderRadius={12} marginBottom={0} /> borderRadius={12}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View> </View>
<View style={styles.buttonsRow}>
<SkeletonElement width={120} height={44} borderRadius={22} marginBottom={0} style={{ marginRight: 12 }} /> {/* Watch Progress Placeholder - Centered Glass Bar */}
<SkeletonElement width={100} height={44} borderRadius={22} marginBottom={0} /> <View style={{ alignItems: 'center', width: '100%', marginBottom: 16 }}>
<ShimmerSkeleton
width="75%"
height={45} // Matches glass background height + padding
borderRadius={12}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
style={{ opacity: 0.5 }} // Slight transparency for glass effect
/>
</View>
{/* Genre Info Row - Centered */}
<View style={[styles.metaRow, { justifyContent: 'center', marginBottom: 20 }]}>
<ShimmerSkeleton
width={isTV ? 60 : 50}
height={12}
borderRadius={6}
marginBottom={0}
style={{ marginRight: 8 }}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.3)', marginRight: 8 }} />
<ShimmerSkeleton
width={isTV ? 80 : 70}
height={12}
borderRadius={6}
marginBottom={0}
style={{ marginRight: 8 }}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: 'rgba(255,255,255,0.3)', marginRight: 8 }} />
<ShimmerSkeleton
width={isTV ? 50 : 40}
height={12}
borderRadius={6}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View>
{/* Action buttons row - Play, Save, Collection, Rates */}
<View style={[styles.buttonsRow, { justifyContent: 'center', gap: 6 }]}>
{/* Play Button */}
<ShimmerSkeleton
width={isTV ? 180 : isLargeTablet ? 160 : isTablet ? 150 : (width - 32 - 100 - 24) / 2} // Calc based on screen width
height={isTV ? 52 : isLargeTablet ? 48 : 46}
borderRadius={isTV ? 26 : 23}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
{/* Save Button */}
<ShimmerSkeleton
width={isTV ? 180 : isLargeTablet ? 160 : isTablet ? 150 : (width - 32 - 100 - 24) / 2}
height={isTV ? 52 : isLargeTablet ? 48 : 46}
borderRadius={isTV ? 26 : 23}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
{/* Collection Icon */}
<ShimmerSkeleton
width={isTV ? 52 : isLargeTablet ? 48 : 46}
height={isTV ? 52 : isLargeTablet ? 48 : 46}
borderRadius={isTV ? 26 : 23}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
{/* Ratings Icon (if series) - Always show for skeleton consistency */}
<ShimmerSkeleton
width={isTV ? 52 : isLargeTablet ? 48 : 46}
height={isTV ? 52 : isLargeTablet ? 48 : 46}
borderRadius={isTV ? 26 : 23}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View> </View>
</View> </View>
</View> </View>
</View> </Animated.View>
{/* Content Section Skeletons */} {/* Content Section */}
<View style={styles.contentSection}> <Animated.View style={[styles.contentSection, { paddingHorizontal: horizontalPadding }, contentStyle]}>
{/* Synopsis skeleton */} {/* Description skeleton */}
<View style={styles.synopsisSection}> <View style={styles.descriptionSection}>
<SkeletonElement width="30%" height={24} borderRadius={12} /> <ShimmerSkeleton
<SkeletonElement width="100%" height={16} borderRadius={8} /> width="100%"
<SkeletonElement width="95%" height={16} borderRadius={8} /> height={isTV ? 18 : 15}
<SkeletonElement width="80%" height={16} borderRadius={8} /> borderRadius={4}
marginBottom={10}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<ShimmerSkeleton
width="95%"
height={isTV ? 18 : 15}
borderRadius={4}
marginBottom={10}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<ShimmerSkeleton
width="75%"
height={isTV ? 18 : 15}
borderRadius={4}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View> </View>
</Animated.View>
{/* Cast section skeleton */} {/* Cast Section */}
<View style={styles.castSection}> <Animated.View style={[styles.castSection, { paddingHorizontal: horizontalPadding }, castStyle]}>
<SkeletonElement width="20%" height={24} borderRadius={12} /> <ShimmerSkeleton
<View style={styles.castRow}> width={isTV ? 80 : 60}
{[1, 2, 3, 4].map((item) => ( height={isTV ? 24 : 20}
<View key={item} style={styles.castItem}> borderRadius={4}
<SkeletonElement width={80} height={80} borderRadius={40} marginBottom={8} /> marginBottom={16}
<SkeletonElement width={60} height={12} borderRadius={6} marginBottom={4} /> shimmerProgress={shimmerProgress}
<SkeletonElement width={70} height={10} borderRadius={5} marginBottom={0} /> baseColor={baseColor}
</View> highlightColor={highlightColor}
))} />
</View> <View style={styles.castRow}>
{[1, 2, 3, 4, 5].map((item) => (
<View key={item} style={styles.castItem}>
<ShimmerSkeleton
width={isTV ? 100 : isLargeTablet ? 90 : isTablet ? 85 : 80}
height={isTV ? 100 : isLargeTablet ? 90 : isTablet ? 85 : 80}
borderRadius={isTV ? 50 : isLargeTablet ? 45 : isTablet ? 42 : 40}
marginBottom={8}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<ShimmerSkeleton
width={isTV ? 70 : 60}
height={isTV ? 14 : 12}
borderRadius={4}
marginBottom={4}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View>
))}
</View> </View>
</Animated.View>
{/* Episodes/Details skeleton based on type */} {/* Episodes/Recommendations Section */}
{type === 'series' ? ( {type === 'series' ? (
<View style={styles.episodesSection}> <Animated.View style={[styles.episodesSection, { paddingHorizontal: horizontalPadding }, castStyle]}>
<SkeletonElement width="25%" height={24} borderRadius={12} /> <ShimmerSkeleton
<SkeletonElement width={150} height={36} borderRadius={18} /> width={isTV ? 120 : 100}
height={isTV ? 24 : 20}
borderRadius={4}
marginBottom={16}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
{/* Season selector */}
<ShimmerSkeleton
width={isTV ? 180 : 140}
height={isTV ? 40 : 36}
borderRadius={20}
marginBottom={20}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
{/* Episode cards */}
<View style={styles.episodeList}>
{[1, 2, 3].map((item) => ( {[1, 2, 3].map((item) => (
<View key={item} style={styles.episodeItem}> <View key={item} style={styles.episodeCard}>
<SkeletonElement width={120} height={68} borderRadius={8} marginBottom={0} style={{ marginRight: 12 }} /> <ShimmerSkeleton
width={isTV ? 200 : isLargeTablet ? 180 : isTablet ? 160 : 140}
height={isTV ? 112 : isLargeTablet ? 100 : isTablet ? 90 : 80}
borderRadius={8}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<View style={styles.episodeInfo}> <View style={styles.episodeInfo}>
<SkeletonElement width="80%" height={16} borderRadius={8} /> <ShimmerSkeleton
<SkeletonElement width="60%" height={14} borderRadius={7} /> width="80%"
<SkeletonElement width="90%" height={12} borderRadius={6} /> height={isTV ? 16 : 14}
borderRadius={4}
marginBottom={6}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<ShimmerSkeleton
width="60%"
height={isTV ? 14 : 12}
borderRadius={4}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View> </View>
</View> </View>
))} ))}
</View> </View>
) : ( </Animated.View>
<View style={styles.detailsSection}> ) : (
<SkeletonElement width="25%" height={24} borderRadius={12} /> <Animated.View style={[styles.recommendationsSection, { paddingHorizontal: horizontalPadding }, castStyle]}>
<View style={styles.detailsGrid}> <ShimmerSkeleton
<SkeletonElement width="48%" height={60} borderRadius={8} /> width={isTV ? 140 : 110}
<SkeletonElement width="48%" height={60} borderRadius={8} /> height={isTV ? 24 : 20}
</View> borderRadius={4}
marginBottom={16}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<View style={styles.posterRow}>
{[1, 2, 3, 4].map((item) => (
<ShimmerSkeleton
key={item}
width={isTV ? 140 : isLargeTablet ? 120 : isTablet ? 110 : 100}
height={isTV ? 210 : isLargeTablet ? 180 : isTablet ? 165 : 150}
borderRadius={8}
marginBottom={0}
style={{ marginRight: 12 }}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
))}
</View> </View>
)} </Animated.View>
</View> )}
</Animated.View> </Animated.View>
</SafeAreaView> </SafeAreaView>
); );
@ -258,7 +557,6 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
heroSection: { heroSection: {
height: height * 0.6,
position: 'relative', position: 'relative',
}, },
heroOverlay: { heroOverlay: {
@ -266,54 +564,52 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end', justifyContent: 'flex-end',
}, },
heroBottomContent: { heroBottomContent: {
position: 'absolute', paddingBottom: 20,
bottom: 20,
left: 20,
right: 20,
}, },
genresRow: { metaRow: {
flexDirection: 'row', flexDirection: 'row',
marginBottom: 16, alignItems: 'center',
marginBottom: 8,
}, },
buttonsRow: { buttonsRow: {
flexDirection: 'row', flexDirection: 'row',
marginBottom: 8, alignItems: 'center',
}, },
contentSection: { contentSection: {
padding: 20, paddingTop: 16,
}, },
synopsisSection: { descriptionSection: {
marginBottom: 32, marginBottom: 24,
}, },
castSection: { castSection: {
marginBottom: 32, marginBottom: 24,
}, },
castRow: { castRow: {
flexDirection: 'row', flexDirection: 'row',
marginTop: 16,
}, },
castItem: { castItem: {
alignItems: 'center', alignItems: 'center',
marginRight: 16, marginRight: 16,
}, },
episodesSection: { episodesSection: {
marginBottom: 32, marginBottom: 24,
}, },
episodeItem: { episodeList: {
gap: 16,
},
episodeCard: {
flexDirection: 'row', flexDirection: 'row',
marginBottom: 16, gap: 12,
alignItems: 'center',
}, },
episodeInfo: { episodeInfo: {
flex: 1, flex: 1,
justifyContent: 'center',
}, },
detailsSection: { recommendationsSection: {
marginBottom: 32, marginBottom: 24,
}, },
detailsGrid: { posterRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 16,
}, },
}); });