mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
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:
parent
6c44c0ec59
commit
23346453a8
3 changed files with 442 additions and 974 deletions
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue