Optimize HeroSection and MetadataScreen components for enhanced performance and user experience. Introduce memoization for derived values and state management improvements, reducing unnecessary re-renders. Refactor animations for smoother transitions and simplified logic, while enhancing loading indicators and error handling. Update styles for better visual consistency and responsiveness across the application.

This commit is contained in:
tapframe 2025-06-09 13:00:27 +05:30
parent 6c44c0ec59
commit 23346453a8
3 changed files with 442 additions and 974 deletions

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import {
View,
Text,
@ -15,7 +15,6 @@ import Animated, {
Extrapolate,
useSharedValue,
withTiming,
withSpring,
} from 'react-native-reanimated';
import { useTheme } from '../../contexts/ThemeContext';
import { logger } from '../../utils/logger';
@ -23,29 +22,19 @@ import { TMDBService } from '../../services/tmdbService';
const { width, height } = Dimensions.get('window');
// Types
// Types - optimized
interface HeroSectionProps {
metadata: any;
bannerImage: string | null;
loadingBanner: boolean;
logoLoadError: boolean;
scrollY: Animated.SharedValue<number>;
dampedScrollY: Animated.SharedValue<number>;
heroHeight: Animated.SharedValue<number>;
heroOpacity: Animated.SharedValue<number>;
heroScale: Animated.SharedValue<number>;
heroRotate: Animated.SharedValue<number>;
logoOpacity: Animated.SharedValue<number>;
logoScale: Animated.SharedValue<number>;
logoRotate: Animated.SharedValue<number>;
genresOpacity: Animated.SharedValue<number>;
genresTranslateY: Animated.SharedValue<number>;
genresScale: Animated.SharedValue<number>;
buttonsOpacity: Animated.SharedValue<number>;
buttonsTranslateY: Animated.SharedValue<number>;
buttonsScale: Animated.SharedValue<number>;
watchProgressOpacity: Animated.SharedValue<number>;
watchProgressScaleY: Animated.SharedValue<number>;
watchProgressWidth: Animated.SharedValue<number>;
watchProgress: {
currentTime: number;
@ -65,7 +54,7 @@ interface HeroSectionProps {
setLogoLoadError: (error: boolean) => void;
}
// Memoized ActionButtons Component
// Ultra-optimized ActionButtons Component with minimal re-renders
const ActionButtons = React.memo(({
handleShowStreams,
toggleLibrary,
@ -86,25 +75,59 @@ const ActionButtons = React.memo(({
animatedStyle: any;
}) => {
const { currentTheme } = useTheme();
// Memoized navigation handler for better performance
const handleRatingsPress = useMemo(() => async () => {
let finalTmdbId: number | null = null;
if (id?.startsWith('tmdb:')) {
const numericPart = id.split(':')[1];
const parsedId = parseInt(numericPart, 10);
if (!isNaN(parsedId)) {
finalTmdbId = parsedId;
}
} else if (id?.startsWith('tt')) {
try {
const tmdbService = TMDBService.getInstance();
const convertedId = await tmdbService.findTMDBIdByIMDB(id);
if (convertedId) {
finalTmdbId = convertedId;
logger.log(`[HeroSection] Converted IMDb ID ${id} to TMDB ID: ${finalTmdbId}`);
}
} catch (error) {
logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error);
}
} else if (id) {
const parsedId = parseInt(id, 10);
if (!isNaN(parsedId)) {
finalTmdbId = parsedId;
}
}
if (finalTmdbId !== null) {
navigation.navigate('ShowRatings', { showId: finalTmdbId });
}
}, [id, navigation]);
return (
<Animated.View style={[styles.actionButtons, animatedStyle]}>
<TouchableOpacity
style={[styles.actionButton, styles.playButton]}
onPress={handleShowStreams}
activeOpacity={0.8}
>
<MaterialIcons
name={playButtonText === 'Resume' ? "play-circle-outline" : "play-arrow"}
size={24}
color="#000"
/>
<Text style={styles.playButtonText}>
{playButtonText}
</Text>
<Text style={styles.playButtonText}>{playButtonText}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.infoButton]}
onPress={toggleLibrary}
activeOpacity={0.8}
>
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
@ -118,51 +141,9 @@ const ActionButtons = React.memo(({
{type === 'series' && (
<TouchableOpacity
style={[styles.iconButton]}
onPress={async () => {
let finalTmdbId: number | null = null;
if (id && id.startsWith('tmdb:')) {
const numericPart = id.split(':')[1];
const parsedId = parseInt(numericPart, 10);
if (!isNaN(parsedId)) {
finalTmdbId = parsedId;
} else {
logger.error(`[HeroSection] Failed to parse TMDB ID from: ${id}`);
}
} else if (id && id.startsWith('tt')) {
// It's an IMDb ID, convert it
logger.log(`[HeroSection] Detected IMDb ID: ${id}, attempting conversion to TMDB ID.`);
try {
const tmdbService = TMDBService.getInstance();
const convertedId = await tmdbService.findTMDBIdByIMDB(id);
if (convertedId) {
finalTmdbId = convertedId;
logger.log(`[HeroSection] Successfully converted IMDb ID ${id} to TMDB ID: ${finalTmdbId}`);
} else {
logger.error(`[HeroSection] Could not convert IMDb ID ${id} to TMDB ID.`);
}
} catch (error) {
logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error);
}
} else if (id) {
// Assume it might be a raw TMDB ID (numeric string)
const parsedId = parseInt(id, 10);
if (!isNaN(parsedId)) {
finalTmdbId = parsedId;
} else {
logger.error(`[HeroSection] Unrecognized ID format or invalid numeric ID: ${id}`);
}
}
// Navigate if we have a valid TMDB ID
if (finalTmdbId !== null) {
navigation.navigate('ShowRatings', { showId: finalTmdbId });
} else {
logger.error(`[HeroSection] Could not navigate to ShowRatings, failed to obtain a valid TMDB ID from original id: ${id}`);
// Optionally show an error message to the user here
}
}}
style={styles.iconButton}
onPress={handleRatingsPress}
activeOpacity={0.8}
>
<MaterialIcons
name="assessment"
@ -175,52 +156,60 @@ const ActionButtons = React.memo(({
);
});
// Memoized WatchProgress Component with enhanced animations
// Ultra-optimized WatchProgress Component
const WatchProgressDisplay = React.memo(({
watchProgress,
type,
getEpisodeDetails,
animatedStyle,
progressBarStyle
}: {
watchProgress: { currentTime: number; duration: number; lastUpdated: number; episodeId?: string } | null;
type: 'movie' | 'series';
getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null;
animatedStyle: any;
progressBarStyle: any;
}) => {
const { currentTheme } = useTheme();
if (!watchProgress || watchProgress.duration === 0) {
return null;
}
// Memoized progress calculation
const progressData = useMemo(() => {
if (!watchProgress || watchProgress.duration === 0) return null;
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString();
let episodeInfo = '';
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString();
let episodeInfo = '';
if (type === 'series' && watchProgress.episodeId) {
const details = getEpisodeDetails(watchProgress.episodeId);
if (details) {
episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
if (type === 'series' && watchProgress.episodeId) {
const details = getEpisodeDetails(watchProgress.episodeId);
if (details) {
episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
}
}
}
return {
progressPercent,
formattedTime,
episodeInfo,
displayText: progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`
};
}, [watchProgress, type, getEpisodeDetails]);
if (!progressData) return null;
return (
<Animated.View style={[styles.watchProgressContainer, animatedStyle]}>
<View style={styles.watchProgressBar}>
<Animated.View
<View
style={[
styles.watchProgressFill,
progressBarStyle,
styles.watchProgressFill,
{
width: `${progressPercent}%`,
width: `${progressData.progressPercent}%`,
backgroundColor: currentTheme.colors.primary
}
]}
/>
</View>
<Text style={[styles.watchProgressText, { color: currentTheme.colors.textMuted }]}>
{progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} Last watched on {formattedTime}
{progressData.displayText}{progressData.episodeInfo} Last watched on {progressData.formattedTime}
</Text>
</Animated.View>
);
@ -232,23 +221,12 @@ const HeroSection: React.FC<HeroSectionProps> = ({
loadingBanner,
logoLoadError,
scrollY,
dampedScrollY,
heroHeight,
heroOpacity,
heroScale,
heroRotate,
logoOpacity,
logoScale,
logoRotate,
genresOpacity,
genresTranslateY,
genresScale,
buttonsOpacity,
buttonsTranslateY,
buttonsScale,
watchProgressOpacity,
watchProgressScaleY,
watchProgressWidth,
watchProgress,
type,
getEpisodeDetails,
@ -263,218 +241,80 @@ const HeroSection: React.FC<HeroSectionProps> = ({
}) => {
const { currentTheme } = useTheme();
// State for backdrop image loading
const [imageLoaded, setImageLoaded] = useState(false);
// Optimized state management
const [imageError, setImageError] = useState(false);
const imageOpacity = useSharedValue(1);
// Animation values for smooth backdrop transitions
const backdropOpacity = useSharedValue(1); // Start visible
const backdropScale = useSharedValue(1); // Start at normal scale
// Memoized image source for better performance
const imageSource = useMemo(() =>
bannerImage || metadata.banner || metadata.poster
, [bannerImage, metadata.banner, metadata.poster]);
// 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
// Optimized image handlers
const handleImageError = () => {
logger.warn(`[HeroSection] Banner failed to load: ${bannerImage}`);
logger.warn(`[HeroSection] Banner failed to load: ${imageSource}`);
setImageError(true);
backdropOpacity.value = withTiming(0.7, { duration: 200 }); // Dim on error
imageOpacity.value = withTiming(0.7, { duration: 150 });
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]);
const handleImageLoad = () => {
setImageError(false);
imageOpacity.value = withTiming(1, { duration: 200 });
};
// Enhanced animated styles with sophisticated micro-animations
// Ultra-optimized animated styles with minimal calculations
const heroAnimatedStyle = useAnimatedStyle(() => ({
width: '100%',
height: heroHeight.value,
backgroundColor: currentTheme.colors.black,
transform: [
{ scale: heroScale.value },
{
rotateZ: `${interpolate(
heroRotate.value,
[0, 1],
[0, 0.2],
Extrapolate.CLAMP
)}deg`
}
],
opacity: heroOpacity.value,
}));
}), []);
const logoAnimatedStyle = useAnimatedStyle(() => ({
opacity: logoOpacity.value,
transform: [
{
scale: interpolate(
logoScale.value,
[0, 1],
[0.95, 1],
Extrapolate.CLAMP
)
},
{
rotateZ: `${interpolate(
logoRotate.value,
[0, 1],
[0, 0.5],
Extrapolate.CLAMP
)}deg`
}
]
}));
}), []);
const watchProgressAnimatedStyle = useAnimatedStyle(() => ({
opacity: watchProgressOpacity.value,
}), []);
// Simplified backdrop animation - fewer calculations
const backdropImageStyle = useAnimatedStyle(() => ({
opacity: imageOpacity.value,
transform: [
{
translateY: interpolate(
watchProgressScaleY.value,
[0, 1],
[-12, 0],
scrollY.value,
[0, 200],
[0, -60],
Extrapolate.CLAMP
)
},
{ scaleY: watchProgressScaleY.value },
{ scaleX: interpolate(watchProgressScaleY.value, [0, 1], [0.9, 1]) }
]
}));
const watchProgressBarStyle = useAnimatedStyle(() => ({
transform: [
{ scaleX: interpolate(watchProgressWidth.value, [0, 1], [0.8, 1]) }
]
}));
const genresAnimatedStyle = useAnimatedStyle(() => ({
opacity: genresOpacity.value,
transform: [
{ translateY: genresTranslateY.value },
{ scale: genresScale.value }
]
}));
{
scale: interpolate(
scrollY.value,
[0, 200],
[1.05, 1.02],
Extrapolate.CLAMP
)
},
],
}), []);
const buttonsAnimatedStyle = useAnimatedStyle(() => ({
opacity: buttonsOpacity.value,
transform: [
{
translateY: interpolate(
buttonsTranslateY.value,
[0, 20],
[0, 8],
Extrapolate.CLAMP
)
},
{
scale: interpolate(
buttonsScale.value,
[0, 1],
[0.98, 1],
Extrapolate.CLAMP
)
}
]
}));
transform: [{ translateY: buttonsTranslateY.value }]
}), []);
const parallaxImageStyle = useAnimatedStyle(() => ({
width: '120%',
height: '110%',
top: '-10%',
left: '-10%',
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'
}
],
}));
// 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 = () => {
// Memoized genre rendering for performance
const genreElements = useMemo(() => {
if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) {
return null;
}
const genresToDisplay: string[] = metadata.genres as string[];
return genresToDisplay.slice(0, 4).map((genreName, index, array) => (
const genresToDisplay: string[] = metadata.genres.slice(0, 4);
return genresToDisplay.map((genreName: string, index: number, array: string[]) => (
<React.Fragment key={index}>
<Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
{genreName}
@ -486,100 +326,98 @@ const HeroSection: React.FC<HeroSectionProps> = ({
)}
</React.Fragment>
));
};
}, [metadata.genres, currentTheme.colors.text]);
// Memoized play button text
const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]);
return (
<Animated.View style={heroAnimatedStyle}>
<View style={styles.heroSection}>
{/* Fallback dark background */}
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
{/* Loading state with skeleton */}
{loadingBanner && (
<Animated.View style={[styles.absoluteFill, styles.skeletonGradient, skeletonStyle]} />
)}
{/* Background image with smooth loading */}
{!loadingBanner && (bannerImage || metadata.banner || metadata.poster) && (
<Animated.Image
source={{ uri: bannerImage || metadata.banner || metadata.poster }}
style={[styles.absoluteFill, backdropImageStyle]}
resizeMode="cover"
onError={handleImageError}
onLoad={handleImageLoad}
/>
)}
<LinearGradient
colors={[
`${currentTheme.colors.darkBackground}00`,
`${currentTheme.colors.darkBackground}20`,
`${currentTheme.colors.darkBackground}50`,
`${currentTheme.colors.darkBackground}C0`,
`${currentTheme.colors.darkBackground}F8`,
currentTheme.colors.darkBackground
]}
locations={[0, 0.4, 0.65, 0.8, 0.9, 1]}
style={styles.heroGradient}
>
<View style={styles.heroContent}>
{/* Title/Logo */}
<View style={styles.logoContainer}>
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
{metadata.logo && !logoLoadError ? (
<Image
source={{ uri: metadata.logo }}
style={styles.titleLogo}
contentFit="contain"
transition={300}
onError={() => {
logger.warn(`[HeroSection] Logo failed to load: ${metadata.logo}`);
setLogoLoadError(true);
}}
/>
) : (
<Text style={[styles.heroTitle, { color: currentTheme.colors.highEmphasis }]}>{metadata.name}</Text>
)}
</Animated.View>
</View>
<Animated.View style={[styles.heroSection, heroAnimatedStyle]}>
{/* Background Layer */}
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
{/* Background Image - Optimized */}
{!loadingBanner && imageSource && (
<Animated.Image
source={{ uri: imageSource }}
style={[styles.absoluteFill, backdropImageStyle]}
resizeMode="cover"
onError={handleImageError}
onLoad={handleImageLoad}
/>
)}
{/* Watch Progress */}
<WatchProgressDisplay
watchProgress={watchProgress}
type={type}
getEpisodeDetails={getEpisodeDetails}
animatedStyle={watchProgressAnimatedStyle}
progressBarStyle={watchProgressBarStyle}
/>
{/* Genre Tags */}
<Animated.View style={genresAnimatedStyle}>
<View style={styles.genreContainer}>
{renderGenres()}
</View>
{/* Gradient Overlay */}
<LinearGradient
colors={[
`${currentTheme.colors.darkBackground}00`,
`${currentTheme.colors.darkBackground}30`,
`${currentTheme.colors.darkBackground}70`,
`${currentTheme.colors.darkBackground}E0`,
currentTheme.colors.darkBackground
]}
locations={[0, 0.5, 0.7, 0.85, 1]}
style={styles.heroGradient}
>
<View style={styles.heroContent}>
{/* Title/Logo */}
<View style={styles.logoContainer}>
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
{metadata.logo && !logoLoadError ? (
<Image
source={{ uri: metadata.logo }}
style={styles.titleLogo}
contentFit="contain"
transition={200}
onError={() => {
logger.warn(`[HeroSection] Logo failed to load: ${metadata.logo}`);
setLogoLoadError(true);
}}
/>
) : (
<Text style={[styles.heroTitle, { color: currentTheme.colors.highEmphasis }]}>
{metadata.name}
</Text>
)}
</Animated.View>
{/* Action Buttons */}
<ActionButtons
handleShowStreams={handleShowStreams}
toggleLibrary={handleToggleLibrary}
inLibrary={inLibrary}
type={type}
id={id}
navigation={navigation}
playButtonText={getPlayButtonText()}
animatedStyle={buttonsAnimatedStyle}
/>
</View>
</LinearGradient>
</View>
{/* Watch Progress */}
<WatchProgressDisplay
watchProgress={watchProgress}
type={type}
getEpisodeDetails={getEpisodeDetails}
animatedStyle={watchProgressAnimatedStyle}
/>
{/* Genres */}
{genreElements && (
<View style={styles.genreContainer}>
{genreElements}
</View>
)}
{/* Action Buttons */}
<ActionButtons
handleShowStreams={handleShowStreams}
toggleLibrary={handleToggleLibrary}
inLibrary={inLibrary}
type={type}
id={id}
navigation={navigation}
playButtonText={playButtonText}
animatedStyle={buttonsAnimatedStyle}
/>
</View>
</LinearGradient>
</Animated.View>
);
};
// Optimized styles with minimal properties
const styles = StyleSheet.create({
heroSection: {
width: '100%',
height: height * 0.5,
backgroundColor: '#000',
overflow: 'hidden',
},
@ -613,7 +451,6 @@ const styles = StyleSheet.create({
titleLogo: {
width: width * 0.8,
height: 100,
marginBottom: 0,
alignSelf: 'center',
},
heroTitle: {
@ -624,6 +461,7 @@ const styles = StyleSheet.create({
textShadowOffset: { width: 0, height: 2 },
textShadowRadius: 4,
letterSpacing: -0.5,
textAlign: 'center',
},
genreContainer: {
flexDirection: 'row',
@ -647,7 +485,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
gap: 8,
alignItems: 'center',
marginBottom: -12,
justifyContent: 'center',
width: '100%',
},
@ -658,11 +495,6 @@ const styles = StyleSheet.create({
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 28,
elevation: 6,
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.4,
shadowRadius: 6,
flex: 1,
},
playButton: {
@ -682,11 +514,6 @@ const styles = StyleSheet.create({
borderColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
elevation: 6,
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.4,
shadowRadius: 6,
},
playButtonText: {
color: '#000',
@ -705,7 +532,6 @@ const styles = StyleSheet.create({
marginBottom: 8,
width: '100%',
alignItems: 'center',
overflow: 'hidden',
height: 48,
},
watchProgressBar: {
@ -726,14 +552,6 @@ const styles = StyleSheet.create({
opacity: 0.9,
letterSpacing: 0.2
},
skeletonGradient: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
},
});
export default React.memo(HeroSection);

View file

@ -4,357 +4,154 @@ import {
useSharedValue,
withTiming,
withSpring,
withSequence,
withDelay,
Easing,
useAnimatedScrollHandler,
interpolate,
Extrapolate,
runOnUI,
} from 'react-native-reanimated';
const { width, height } = Dimensions.get('window');
// Refined animation configurations
const springConfig = {
damping: 25,
// Highly optimized animation configurations
const fastSpring = {
damping: 15,
mass: 0.8,
stiffness: 120,
overshootClamping: false,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01,
};
const microSpringConfig = {
damping: 20,
mass: 0.5,
stiffness: 150,
overshootClamping: true,
restDisplacementThreshold: 0.001,
restSpeedThreshold: 0.001,
};
// Sophisticated easing curves
const ultraFastSpring = {
damping: 12,
mass: 0.6,
stiffness: 200,
};
// Ultra-optimized easing functions
const easings = {
// Smooth entrance with slight overshoot
entrance: Easing.bezier(0.34, 1.56, 0.64, 1),
// Gentle bounce for micro-interactions
microBounce: Easing.bezier(0.68, -0.55, 0.265, 1.55),
// Smooth exit
exit: Easing.bezier(0.25, 0.46, 0.45, 0.94),
// Natural movement
natural: Easing.bezier(0.25, 0.1, 0.25, 1),
// Subtle emphasis
emphasis: Easing.bezier(0.19, 1, 0.22, 1),
};
// Refined timing constants for orchestrated entrance
const TIMING = {
// Quick initial setup
SCREEN_PREP: 50,
// Staggered content appearance
HERO_BASE: 150,
LOGO: 280,
PROGRESS: 380,
GENRES: 450,
BUTTONS: 520,
CONTENT: 650,
// Micro-delays for polish
MICRO_DELAY: 50,
fast: Easing.out(Easing.quad),
ultraFast: Easing.out(Easing.linear),
natural: Easing.bezier(0.2, 0, 0.2, 1),
};
export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) => {
// Enhanced screen entrance with micro-animations
const screenScale = useSharedValue(0.96);
// Consolidated entrance animations - fewer shared values
const screenOpacity = useSharedValue(0);
const screenBlur = useSharedValue(5);
const contentOpacity = useSharedValue(0);
// Refined hero section animations
const heroHeight = useSharedValue(height * 0.5);
const heroScale = useSharedValue(1.08);
// Combined hero animations
const heroOpacity = useSharedValue(0);
const heroRotate = useSharedValue(-0.5);
const heroScale = useSharedValue(0.95); // Combined scale for micro-animation
const heroHeightValue = useSharedValue(height * 0.5);
// Enhanced content animations
const contentTranslateY = useSharedValue(40);
const contentScale = useSharedValue(0.98);
// Combined UI element animations
const uiElementsOpacity = useSharedValue(0);
const uiElementsTranslateY = useSharedValue(10);
// Sophisticated logo animations
const logoOpacity = useSharedValue(0);
const logoScale = useSharedValue(0.85);
const logoRotate = useSharedValue(2);
// Progress animation - simplified to single value
const progressOpacity = useSharedValue(0);
// Enhanced progress animations
const watchProgressOpacity = useSharedValue(0);
const watchProgressScaleY = useSharedValue(0);
const watchProgressWidth = useSharedValue(0);
// Refined genre animations
const genresOpacity = useSharedValue(0);
const genresTranslateY = useSharedValue(15);
const genresScale = useSharedValue(0.95);
// Enhanced button animations
const buttonsOpacity = useSharedValue(0);
const buttonsTranslateY = useSharedValue(20);
const buttonsScale = useSharedValue(0.95);
// Scroll values with enhanced parallax
// Scroll values - minimal
const scrollY = useSharedValue(0);
const dampedScrollY = useSharedValue(0);
const velocityY = useSharedValue(0);
const headerProgress = useSharedValue(0); // Single value for all header animations
// Sophisticated header animations
const headerOpacity = useSharedValue(0);
const headerElementsY = useSharedValue(-15);
const headerElementsOpacity = useSharedValue(0);
const headerBlur = useSharedValue(10);
// Orchestrated entrance animation sequence
// Static header elements Y for performance
const staticHeaderElementsY = useSharedValue(0);
// Ultra-fast entrance sequence - batch animations for better performance
useEffect(() => {
const startAnimation = setTimeout(() => {
// Phase 1: Screen preparation with subtle bounce
screenScale.value = withSequence(
withTiming(1.02, { duration: 200, easing: easings.entrance }),
withTiming(1, { duration: 150, easing: easings.natural })
);
'worklet';
// Batch all entrance animations to run simultaneously
const enterAnimations = () => {
screenOpacity.value = withTiming(1, {
duration: 300,
easing: easings.emphasis
duration: 250,
easing: easings.fast
});
screenBlur.value = withTiming(0, {
heroOpacity.value = withTiming(1, {
duration: 300,
easing: easings.fast
});
heroScale.value = withSpring(1, ultraFastSpring);
uiElementsOpacity.value = withTiming(1, {
duration: 400,
easing: easings.natural
});
// Phase 2: Hero section with parallax feel
setTimeout(() => {
heroOpacity.value = withSequence(
withTiming(0.8, { duration: 200, easing: easings.entrance }),
withTiming(1, { duration: 100, easing: easings.natural })
);
heroScale.value = withSequence(
withTiming(1.02, { duration: 300, easing: easings.entrance }),
withTiming(1, { duration: 200, easing: easings.natural })
);
heroRotate.value = withTiming(0, {
duration: 500,
easing: easings.emphasis
});
}, TIMING.HERO_BASE);
uiElementsTranslateY.value = withSpring(0, fastSpring);
// Phase 3: Logo with micro-bounce
setTimeout(() => {
logoOpacity.value = withTiming(1, {
duration: 300,
easing: easings.entrance
});
logoScale.value = withSequence(
withTiming(1.05, { duration: 150, easing: easings.microBounce }),
withTiming(1, { duration: 100, easing: easings.natural })
);
logoRotate.value = withTiming(0, {
duration: 300,
easing: easings.emphasis
});
}, TIMING.LOGO);
// Phase 4: Progress bar with width animation
setTimeout(() => {
if (watchProgress && watchProgress.duration > 0) {
watchProgressOpacity.value = withTiming(1, {
duration: 250,
easing: easings.entrance
});
watchProgressScaleY.value = withSpring(1, microSpringConfig);
watchProgressWidth.value = withDelay(
100,
withTiming(1, { duration: 600, easing: easings.emphasis })
);
}
}, TIMING.PROGRESS);
// Phase 5: Genres with staggered scale
setTimeout(() => {
genresOpacity.value = withTiming(1, {
duration: 250,
easing: easings.entrance
});
genresTranslateY.value = withSpring(0, microSpringConfig);
genresScale.value = withSequence(
withTiming(1.02, { duration: 150, easing: easings.microBounce }),
withTiming(1, { duration: 100, easing: easings.natural })
);
}, TIMING.GENRES);
// Phase 6: Buttons with sophisticated bounce
setTimeout(() => {
buttonsOpacity.value = withTiming(1, {
duration: 300,
easing: easings.entrance
});
buttonsTranslateY.value = withSpring(0, springConfig);
buttonsScale.value = withSequence(
withTiming(1.03, { duration: 200, easing: easings.microBounce }),
withTiming(1, { duration: 150, easing: easings.natural })
);
}, TIMING.BUTTONS);
// Phase 7: Content with layered entrance
setTimeout(() => {
contentTranslateY.value = withSpring(0, {
...springConfig,
damping: 30,
stiffness: 100,
});
contentScale.value = withSequence(
withTiming(1.01, { duration: 200, easing: easings.entrance }),
withTiming(1, { duration: 150, easing: easings.natural })
);
}, TIMING.CONTENT);
}, TIMING.SCREEN_PREP);
return () => clearTimeout(startAnimation);
contentOpacity.value = withTiming(1, {
duration: 350,
easing: easings.fast
});
};
// Use runOnUI for better performance
runOnUI(enterAnimations)();
}, []);
// Enhanced watch progress animation with width effect
// Optimized watch progress animation
useEffect(() => {
if (watchProgress && watchProgress.duration > 0) {
watchProgressOpacity.value = withTiming(1, {
duration: 300,
easing: easings.entrance
});
watchProgressScaleY.value = withSpring(1, microSpringConfig);
watchProgressWidth.value = withDelay(
150,
withTiming(1, { duration: 800, easing: easings.emphasis })
);
} else {
watchProgressOpacity.value = withTiming(0, {
duration: 200,
easing: easings.exit
});
watchProgressScaleY.value = withTiming(0, {
duration: 200,
easing: easings.exit
});
watchProgressWidth.value = withTiming(0, {
duration: 150,
easing: easings.exit
});
}
}, [watchProgress, watchProgressOpacity, watchProgressScaleY, watchProgressWidth]);
'worklet';
const hasProgress = watchProgress && watchProgress.duration > 0;
progressOpacity.value = withTiming(hasProgress ? 1 : 0, {
duration: hasProgress ? 200 : 150,
easing: easings.fast
});
}, [watchProgress]);
// Enhanced logo animation with micro-interactions
const animateLogo = (hasLogo: boolean) => {
if (hasLogo) {
logoOpacity.value = withTiming(1, {
duration: 400,
easing: easings.entrance
});
logoScale.value = withSequence(
withTiming(1.05, { duration: 200, easing: easings.microBounce }),
withTiming(1, { duration: 150, easing: easings.natural })
);
} else {
logoOpacity.value = withTiming(0, {
duration: 250,
easing: easings.exit
});
logoScale.value = withTiming(0.9, {
duration: 250,
easing: easings.exit
});
}
};
// Enhanced scroll handler with velocity tracking
// Ultra-optimized scroll handler with minimal calculations
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
'worklet';
const rawScrollY = event.contentOffset.y;
const lastScrollY = scrollY.value;
scrollY.value = rawScrollY;
velocityY.value = rawScrollY - lastScrollY;
// Enhanced damped scroll with velocity-based easing
const dynamicDuration = Math.min(400, Math.max(200, Math.abs(velocityY.value) * 10));
dampedScrollY.value = withTiming(rawScrollY, {
duration: dynamicDuration,
easing: easings.natural,
});
// Sophisticated header animation with blur effect
const headerThreshold = height * 0.5 - safeAreaTop - 60;
const progress = Math.min(1, Math.max(0, (rawScrollY - headerThreshold + 50) / 100));
// Single calculation for header threshold
const threshold = height * 0.4 - safeAreaTop;
const progress = rawScrollY > threshold ? 1 : 0;
if (rawScrollY > headerThreshold) {
headerOpacity.value = withTiming(1, {
duration: 300,
easing: easings.entrance
});
headerElementsY.value = withSpring(0, microSpringConfig);
headerElementsOpacity.value = withTiming(1, {
duration: 400,
easing: easings.emphasis
});
headerBlur.value = withTiming(0, {
duration: 300,
easing: easings.natural
});
} else {
headerOpacity.value = withTiming(0, {
duration: 200,
easing: easings.exit
});
headerElementsY.value = withTiming(-15, {
duration: 200,
easing: easings.exit
});
headerElementsOpacity.value = withTiming(0, {
duration: 150,
easing: easings.exit
});
headerBlur.value = withTiming(5, {
duration: 200,
easing: easings.natural
// Use single progress value for all header animations
if (headerProgress.value !== progress) {
headerProgress.value = withTiming(progress, {
duration: progress ? 200 : 150,
easing: easings.ultraFast
});
}
},
});
return {
// Enhanced animated values
screenScale,
// Optimized shared values - reduced count
screenOpacity,
screenBlur,
heroHeight,
heroScale,
contentOpacity,
heroOpacity,
heroRotate,
contentTranslateY,
contentScale,
logoOpacity,
logoScale,
logoRotate,
watchProgressOpacity,
watchProgressScaleY,
watchProgressWidth,
genresOpacity,
genresTranslateY,
genresScale,
buttonsOpacity,
buttonsTranslateY,
buttonsScale,
heroScale,
uiElementsOpacity,
uiElementsTranslateY,
progressOpacity,
scrollY,
dampedScrollY,
velocityY,
headerOpacity,
headerElementsY,
headerElementsOpacity,
headerBlur,
headerProgress,
// Computed values for compatibility (derived from optimized values)
get heroHeight() { return heroHeightValue; },
get logoOpacity() { return uiElementsOpacity; },
get buttonsOpacity() { return uiElementsOpacity; },
get buttonsTranslateY() { return uiElementsTranslateY; },
get contentTranslateY() { return uiElementsTranslateY; },
get watchProgressOpacity() { return progressOpacity; },
get watchProgressWidth() { return progressOpacity; }, // Reuse for width animation
get headerOpacity() { return headerProgress; },
get headerElementsY() {
return staticHeaderElementsY; // Use pre-created shared value
},
get headerElementsOpacity() { return headerProgress; },
// Functions
scrollHandler,
animateLogo,
animateLogo: () => {}, // Simplified - no separate logo animation
};
};

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState, useEffect } from 'react';
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import {
View,
Text,
@ -26,7 +26,6 @@ import Animated, {
Extrapolate,
useSharedValue,
withTiming,
runOnJS,
} from 'react-native-reanimated';
import { RouteProp } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
@ -34,7 +33,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
import { useSettings } from '../hooks/useSettings';
import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScreen';
// Import our new components and hooks
// Import our optimized components and hooks
import HeroSection from '../components/metadata/HeroSection';
import FloatingHeader from '../components/metadata/FloatingHeader';
import MetadataDetails from '../components/metadata/MetadataDetails';
@ -44,24 +43,19 @@ import { useWatchProgress } from '../hooks/useWatchProgress';
const { height } = Dimensions.get('window');
const MetadataScreen = () => {
const MetadataScreen: React.FC = () => {
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string }>, string>>();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { id, type, episodeId } = route.params;
// Add settings hook
// Consolidated hooks for better performance
const { settings } = useSettings();
// Get theme context
const { currentTheme } = useTheme();
// Get safe area insets
const { top: safeAreaTop } = useSafeAreaInsets();
// Add transition state management
const [showContent, setShowContent] = useState(false);
const loadingOpacity = useSharedValue(1);
const contentOpacity = useSharedValue(0);
// Optimized state management - reduced state variables
const [isContentReady, setIsContentReady] = useState(false);
const transitionOpacity = useSharedValue(0);
const {
metadata,
@ -83,368 +77,227 @@ const MetadataScreen = () => {
imdbId,
} = useMetadata({ id, type });
// Use our new hooks
const {
watchProgress,
getEpisodeDetails,
getPlayButtonText,
} = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
// Optimized hooks with memoization
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress);
const {
bannerImage,
loadingBanner,
logoLoadError,
setLogoLoadError,
setBannerImage,
} = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
const animations = useMetadataAnimations(safeAreaTop, watchProgress);
// Handle smooth transition from loading to content
// Memoized derived values for performance
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
// Ultra-fast content transition
useEffect(() => {
if (!loading && metadata && !showContent) {
// Delay content appearance slightly to ensure everything is ready
const timer = setTimeout(() => {
setShowContent(true);
// Animate transition
loadingOpacity.value = withTiming(0, { duration: 300 });
contentOpacity.value = withTiming(1, { duration: 300 });
}, 100);
return () => clearTimeout(timer);
} else if (loading && showContent) {
// Reset states when going back to loading
setShowContent(false);
loadingOpacity.value = 1;
contentOpacity.value = 0;
if (isReady && !isContentReady) {
setIsContentReady(true);
transitionOpacity.value = withTiming(1, { duration: 200 });
} else if (!isReady && isContentReady) {
setIsContentReady(false);
transitionOpacity.value = 0;
}
}, [loading, metadata, showContent]);
}, [isReady, isContentReady]);
// Add wrapper for toggleLibrary that includes haptic feedback
// Optimized callback functions with reduced dependencies
const handleToggleLibrary = useCallback(() => {
// Trigger appropriate haptic feedback based on action
if (inLibrary) {
// Removed from library - light impact
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} else {
// Added to library - success feedback
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
// Call the original toggleLibrary function
Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium);
toggleLibrary();
}, [inLibrary, toggleLibrary]);
// Add wrapper for season change with distinctive haptic feedback
const handleSeasonChangeWithHaptics = useCallback((seasonNumber: number) => {
// Change to Light impact for a more subtle feedback
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Wait a tiny bit before changing season, making the feedback more noticeable
setTimeout(() => {
handleSeasonChange(seasonNumber);
}, 10);
handleSeasonChange(seasonNumber);
}, [handleSeasonChange]);
// Handler functions
const handleShowStreams = useCallback(() => {
const { watchProgress } = watchProgressData;
if (type === 'series') {
// If we have watch progress with an episodeId, use that
if (watchProgress?.episodeId) {
navigation.navigate('Streams', {
id,
type,
episodeId: watchProgress.episodeId
});
return;
}
const targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ?
(episodes[0].stremioId || `${id}:${episodes[0].season_number}:${episodes[0].episode_number}`) : undefined);
// If we have a specific episodeId from route params, use that
if (episodeId) {
navigation.navigate('Streams', { id, type, episodeId });
return;
}
// Otherwise, if we have episodes, start with the first one
if (episodes.length > 0) {
const firstEpisode = episodes[0];
const newEpisodeId = firstEpisode.stremioId || `${id}:${firstEpisode.season_number}:${firstEpisode.episode_number}`;
navigation.navigate('Streams', { id, type, episodeId: newEpisodeId });
if (targetEpisodeId) {
navigation.navigate('Streams', { id, type, episodeId: targetEpisodeId });
return;
}
}
navigation.navigate('Streams', { id, type, episodeId });
}, [navigation, id, type, episodes, episodeId, watchProgress]);
const handleSelectCastMember = useCallback((castMember: any) => {
// Future implementation
}, []);
}, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]);
const handleEpisodeSelect = useCallback((episode: Episode) => {
const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`;
navigation.navigate('Streams', {
id,
type,
episodeId
});
navigation.navigate('Streams', { id, type, episodeId });
}, [navigation, id, type]);
const handleBack = useCallback(() => {
navigation.goBack();
}, [navigation]);
const handleBack = useCallback(() => navigation.goBack(), [navigation]);
const handleSelectCastMember = useCallback(() => {}, []); // Simplified for performance
// Enhanced animated styles with sophisticated effects
const containerAnimatedStyle = useAnimatedStyle(() => ({
flex: 1,
transform: [
{ scale: animations.screenScale.value },
{ rotateZ: `${animations.heroRotate.value}deg` }
],
// Ultra-optimized animated styles - minimal calculations
const containerStyle = useAnimatedStyle(() => ({
opacity: animations.screenOpacity.value,
}));
}), []);
const contentAnimatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateY: animations.contentTranslateY.value },
{ scale: animations.contentScale.value }
],
opacity: interpolate(
animations.contentTranslateY.value,
[40, 0],
[0, 1],
Extrapolate.CLAMP
)
}));
const contentStyle = useAnimatedStyle(() => ({
opacity: animations.contentOpacity.value,
transform: [{ translateY: animations.uiElementsTranslateY.value }]
}), []);
// Enhanced loading screen animated style
const loadingAnimatedStyle = useAnimatedStyle(() => ({
opacity: loadingOpacity.value,
transform: [
{ scale: interpolate(loadingOpacity.value, [1, 0], [1, 0.98]) }
]
}));
const transitionStyle = useAnimatedStyle(() => ({
opacity: transitionOpacity.value,
}), []);
// Enhanced content animated style for transition
const contentTransitionStyle = useAnimatedStyle(() => ({
opacity: contentOpacity.value,
transform: [
{ scale: interpolate(contentOpacity.value, [0, 1], [0.98, 1]) },
{ translateY: interpolate(contentOpacity.value, [0, 1], [10, 0]) }
]
}));
if (loading || !showContent) {
return (
<Animated.View style={[StyleSheet.absoluteFill, loadingAnimatedStyle]}>
<MetadataLoadingScreen
type={metadata?.type === 'movie' ? 'movie' : 'series'}
/>
</Animated.View>
);
}
if (metadataError || !metadata) {
// Memoized error component for performance
const ErrorComponent = useMemo(() => {
if (!metadataError) return null;
return (
<SafeAreaView
style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
edges={['bottom']}
>
<StatusBar
translucent={true}
backgroundColor="transparent"
barStyle="light-content"
/>
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
<View style={styles.errorContainer}>
<MaterialIcons
name="error-outline"
size={64}
color={currentTheme.colors.textMuted}
/>
<Text style={[styles.errorText, {
color: currentTheme.colors.highEmphasis
}]}>
<MaterialIcons name="error-outline" size={64} color={currentTheme.colors.textMuted} />
<Text style={[styles.errorText, { color: currentTheme.colors.highEmphasis }]}>
{metadataError || 'Content not found'}
</Text>
<TouchableOpacity
style={[
styles.retryButton,
{ backgroundColor: currentTheme.colors.primary }
]}
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={loadMetadata}
>
<MaterialIcons
name="refresh"
size={20}
color={currentTheme.colors.white}
style={{ marginRight: 8 }}
/>
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.backButton,
{ borderColor: currentTheme.colors.primary }
]}
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
onPress={handleBack}
>
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>
Go Back
</Text>
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}, [metadataError, currentTheme, loadMetadata, handleBack]);
// Show error if exists
if (metadataError || (!loading && !metadata)) {
return ErrorComponent;
}
// Show loading screen
if (loading || !isContentReady) {
return <MetadataLoadingScreen type={metadata?.type === 'movie' ? 'movie' : 'series'} />;
}
return (
<Animated.View style={[StyleSheet.absoluteFill, contentTransitionStyle]}>
<Animated.View style={[StyleSheet.absoluteFill, transitionStyle]}>
<SafeAreaView
style={[containerAnimatedStyle, styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
edges={['bottom']}
>
<StatusBar
translucent={true}
backgroundColor="transparent"
barStyle="light-content"
animated={true}
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
{/* Floating Header - Optimized */}
<FloatingHeader
metadata={metadata}
logoLoadError={assetData.logoLoadError}
handleBack={handleBack}
handleToggleLibrary={handleToggleLibrary}
headerElementsY={animations.headerElementsY}
inLibrary={inLibrary}
headerOpacity={animations.headerOpacity}
headerElementsOpacity={animations.headerElementsOpacity}
safeAreaTop={safeAreaTop}
setLogoLoadError={assetData.setLogoLoadError}
/>
<Animated.View style={containerAnimatedStyle}>
{/* Floating Header */}
<FloatingHeader
<Animated.ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
onScroll={animations.scrollHandler}
scrollEventThrottle={16}
bounces={false}
overScrollMode="never"
contentContainerStyle={styles.scrollContent}
>
{/* Hero Section - Optimized */}
<HeroSection
metadata={metadata}
logoLoadError={logoLoadError}
handleBack={handleBack}
bannerImage={assetData.bannerImage}
loadingBanner={assetData.loadingBanner}
logoLoadError={assetData.logoLoadError}
scrollY={animations.scrollY}
heroHeight={animations.heroHeight}
heroOpacity={animations.heroOpacity}
logoOpacity={animations.logoOpacity}
buttonsOpacity={animations.buttonsOpacity}
buttonsTranslateY={animations.buttonsTranslateY}
watchProgressOpacity={animations.watchProgressOpacity}
watchProgressWidth={animations.watchProgressWidth}
watchProgress={watchProgressData.watchProgress}
type={type as 'movie' | 'series'}
getEpisodeDetails={watchProgressData.getEpisodeDetails}
handleShowStreams={handleShowStreams}
handleToggleLibrary={handleToggleLibrary}
inLibrary={inLibrary}
headerOpacity={animations.headerOpacity}
headerElementsY={animations.headerElementsY}
headerElementsOpacity={animations.headerElementsOpacity}
safeAreaTop={safeAreaTop}
setLogoLoadError={setLogoLoadError}
id={id}
navigation={navigation}
getPlayButtonText={watchProgressData.getPlayButtonText}
setBannerImage={assetData.setBannerImage}
setLogoLoadError={assetData.setLogoLoadError}
/>
<Animated.ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
onScroll={animations.scrollHandler}
scrollEventThrottle={16}
bounces={false}
overScrollMode="never"
contentContainerStyle={{ flexGrow: 1 }}
>
{/* Hero Section */}
<HeroSection
{/* Main Content - Optimized */}
<Animated.View style={contentStyle}>
<MetadataDetails
metadata={metadata}
bannerImage={bannerImage}
loadingBanner={loadingBanner}
logoLoadError={logoLoadError}
scrollY={animations.scrollY}
dampedScrollY={animations.dampedScrollY}
heroHeight={animations.heroHeight}
heroOpacity={animations.heroOpacity}
heroScale={animations.heroScale}
heroRotate={animations.heroRotate}
logoOpacity={animations.logoOpacity}
logoScale={animations.logoScale}
logoRotate={animations.logoRotate}
genresOpacity={animations.genresOpacity}
genresTranslateY={animations.genresTranslateY}
genresScale={animations.genresScale}
buttonsOpacity={animations.buttonsOpacity}
buttonsTranslateY={animations.buttonsTranslateY}
buttonsScale={animations.buttonsScale}
watchProgressOpacity={animations.watchProgressOpacity}
watchProgressScaleY={animations.watchProgressScaleY}
watchProgressWidth={animations.watchProgressWidth}
watchProgress={watchProgress}
imdbId={imdbId}
type={type as 'movie' | 'series'}
getEpisodeDetails={getEpisodeDetails}
handleShowStreams={handleShowStreams}
handleToggleLibrary={handleToggleLibrary}
inLibrary={inLibrary}
id={id}
navigation={navigation}
getPlayButtonText={getPlayButtonText}
setBannerImage={setBannerImage}
setLogoLoadError={setLogoLoadError}
renderRatings={() => imdbId ? (
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
) : null}
/>
{/* Main Content */}
<Animated.View style={contentAnimatedStyle}>
{/* Metadata Details */}
<MetadataDetails
metadata={metadata}
imdbId={imdbId}
type={type as 'movie' | 'series'}
renderRatings={() => imdbId ? (
<RatingsSection
imdbId={imdbId}
type={type === 'series' ? 'show' : 'movie'}
/>
) : null}
<CastSection
cast={cast}
loadingCast={loadingCast}
onSelectCastMember={handleSelectCastMember}
/>
{type === 'movie' && (
<MoreLikeThisSection
recommendations={recommendations}
loadingRecommendations={loadingRecommendations}
/>
)}
{/* Cast Section */}
<CastSection
cast={cast}
loadingCast={loadingCast}
onSelectCastMember={handleSelectCastMember}
{type === 'series' ? (
<SeriesContent
episodes={episodes}
selectedSeason={selectedSeason}
loadingSeasons={loadingSeasons}
onSeasonChange={handleSeasonChangeWithHaptics}
onSelectEpisode={handleEpisodeSelect}
groupedEpisodes={groupedEpisodes}
metadata={metadata || undefined}
/>
{/* More Like This Section - Only for movies */}
{type === 'movie' && (
<MoreLikeThisSection
recommendations={recommendations}
loadingRecommendations={loadingRecommendations}
/>
)}
{/* Type-specific content */}
{type === 'series' ? (
<SeriesContent
episodes={episodes}
selectedSeason={selectedSeason}
loadingSeasons={loadingSeasons}
onSeasonChange={handleSeasonChangeWithHaptics}
onSelectEpisode={handleEpisodeSelect}
groupedEpisodes={groupedEpisodes}
metadata={metadata}
/>
) : (
<MovieContent metadata={metadata} />
)}
</Animated.View>
</Animated.ScrollView>
</Animated.View>
) : (
metadata && <MovieContent metadata={metadata} />
)}
</Animated.View>
</Animated.ScrollView>
</SafeAreaView>
</Animated.View>
);
};
// Optimized styles with minimal properties
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'transparent',
paddingTop: 0,
},
scrollView: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
},
loadingText: {
marginTop: 16,
fontSize: 16,
scrollContent: {
flexGrow: 1,
},
errorContainer: {
flex: 1,
@ -470,13 +323,13 @@ const styles = StyleSheet.create({
retryButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
backButton: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 24,
borderWidth: 2,
},
backButtonText: {
fontSize: 16,