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 {
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<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>(({
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<MetadataLoadingScreenRef, Metada
}));
useEffect(() => {
// 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;
}) => (
<View style={[
{
width: elementWidth,
height: elementHeight,
borderRadius,
marginBottom,
backgroundColor: currentTheme.colors.card,
overflow: 'hidden',
},
style
]}>
{/* Pulsating overlay removed */}
{/* Shimmer overlay removed */}
</View>
);
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 (
<SafeAreaView
<SafeAreaView
style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground,
}]}
@ -144,107 +225,325 @@ export const MetadataLoadingScreen = forwardRef<MetadataLoadingScreenRef, Metada
backgroundColor="transparent"
barStyle="light-content"
/>
<Animated.View
style={[
styles.content,
{
opacity: sceneOpacity,
transform: [
{ scale: sceneScale },
{ translateY: sceneTranslateY }
],
}
]}
>
{/* Hero Skeleton */}
<View style={styles.heroSection}>
<SkeletonElement
width="100%"
height={height * 0.6}
<Animated.View style={[styles.content, containerStyle]}>
{/* Hero Section Skeleton */}
<Animated.View style={[styles.heroSection, { height: height * 0.65 }, heroStyle]}>
<ShimmerSkeleton
width="100%"
height={height * 0.65}
borderRadius={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}>
<LinearGradient
colors={[
'transparent',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
'rgba(0,0,0,0.05)',
'rgba(0,0,0,0.15)',
'rgba(0,0,0,0.35)',
'rgba(0,0,0,0.65)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.3, 0.55, 0.75, 0.9, 1]}
style={StyleSheet.absoluteFill}
/>
{/* Bottom hero content skeleton */}
<View style={styles.heroBottomContent}>
<SkeletonElement width="60%" height={32} borderRadius={16} />
<SkeletonElement width="40%" height={20} borderRadius={10} />
<View style={styles.genresRow}>
<SkeletonElement width={80} height={24} borderRadius={12} marginBottom={0} style={{ marginRight: 8 }} />
<SkeletonElement width={90} height={24} borderRadius={12} marginBottom={0} style={{ marginRight: 8 }} />
<SkeletonElement width={70} height={24} borderRadius={12} marginBottom={0} />
{/* Hero bottom content - Matches HeroSection.tsx structure */}
<View style={[styles.heroBottomContent, { paddingHorizontal: horizontalPadding }]}>
{/* Logo placeholder - Centered and larger */}
<View style={{ alignItems: 'center', width: '100%', marginBottom: 16 }}>
<ShimmerSkeleton
width={isTV ? 400 : isLargeTablet ? 300 : width * 0.65}
height={isTV ? 120 : isLargeTablet ? 100 : 90}
borderRadius={12}
marginBottom={0}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
</View>
<View style={styles.buttonsRow}>
<SkeletonElement width={120} height={44} borderRadius={22} marginBottom={0} style={{ marginRight: 12 }} />
<SkeletonElement width={100} height={44} borderRadius={22} marginBottom={0} />
{/* Watch Progress Placeholder - Centered Glass Bar */}
<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>
</Animated.View>
{/* Content Section Skeletons */}
<View style={styles.contentSection}>
{/* Synopsis skeleton */}
<View style={styles.synopsisSection}>
<SkeletonElement width="30%" height={24} borderRadius={12} />
<SkeletonElement width="100%" height={16} borderRadius={8} />
<SkeletonElement width="95%" height={16} borderRadius={8} />
<SkeletonElement width="80%" height={16} borderRadius={8} />
{/* Content Section */}
<Animated.View style={[styles.contentSection, { paddingHorizontal: horizontalPadding }, contentStyle]}>
{/* Description skeleton */}
<View style={styles.descriptionSection}>
<ShimmerSkeleton
width="100%"
height={isTV ? 18 : 15}
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>
</Animated.View>
{/* Cast section skeleton */}
<View style={styles.castSection}>
<SkeletonElement width="20%" height={24} borderRadius={12} />
<View style={styles.castRow}>
{[1, 2, 3, 4].map((item) => (
<View key={item} style={styles.castItem}>
<SkeletonElement width={80} height={80} borderRadius={40} marginBottom={8} />
<SkeletonElement width={60} height={12} borderRadius={6} marginBottom={4} />
<SkeletonElement width={70} height={10} borderRadius={5} marginBottom={0} />
</View>
))}
</View>
{/* Cast Section */}
<Animated.View style={[styles.castSection, { paddingHorizontal: horizontalPadding }, castStyle]}>
<ShimmerSkeleton
width={isTV ? 80 : 60}
height={isTV ? 24 : 20}
borderRadius={4}
marginBottom={16}
shimmerProgress={shimmerProgress}
baseColor={baseColor}
highlightColor={highlightColor}
/>
<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>
</Animated.View>
{/* Episodes/Details skeleton based on type */}
{type === 'series' ? (
<View style={styles.episodesSection}>
<SkeletonElement width="25%" height={24} borderRadius={12} />
<SkeletonElement width={150} height={36} borderRadius={18} />
{/* Episodes/Recommendations Section */}
{type === 'series' ? (
<Animated.View style={[styles.episodesSection, { paddingHorizontal: horizontalPadding }, castStyle]}>
<ShimmerSkeleton
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) => (
<View key={item} style={styles.episodeItem}>
<SkeletonElement width={120} height={68} borderRadius={8} marginBottom={0} style={{ marginRight: 12 }} />
<View key={item} style={styles.episodeCard}>
<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}>
<SkeletonElement width="80%" height={16} borderRadius={8} />
<SkeletonElement width="60%" height={14} borderRadius={7} />
<SkeletonElement width="90%" height={12} borderRadius={6} />
<ShimmerSkeleton
width="80%"
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 style={styles.detailsSection}>
<SkeletonElement width="25%" height={24} borderRadius={12} />
<View style={styles.detailsGrid}>
<SkeletonElement width="48%" height={60} borderRadius={8} />
<SkeletonElement width="48%" height={60} borderRadius={8} />
</View>
</Animated.View>
) : (
<Animated.View style={[styles.recommendationsSection, { paddingHorizontal: horizontalPadding }, castStyle]}>
<ShimmerSkeleton
width={isTV ? 140 : 110}
height={isTV ? 24 : 20}
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>
)}
</Animated.View>
</SafeAreaView>
);
@ -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,
},
});