mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
perfomance optimziations
This commit is contained in:
parent
62a2ed0046
commit
cc494bdf17
2 changed files with 342 additions and 187 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Platform,
|
Platform,
|
||||||
|
InteractionManager,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
@ -22,6 +23,7 @@ import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
withRepeat,
|
withRepeat,
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
runOnUI,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { useTraktContext } from '../../contexts/TraktContext';
|
import { useTraktContext } from '../../contexts/TraktContext';
|
||||||
|
|
@ -71,7 +73,7 @@ interface HeroSectionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ultra-optimized ActionButtons Component - minimal re-renders
|
// Ultra-optimized ActionButtons Component - minimal re-renders
|
||||||
const ActionButtons = React.memo(({
|
const ActionButtons = memo(({
|
||||||
handleShowStreams,
|
handleShowStreams,
|
||||||
toggleLibrary,
|
toggleLibrary,
|
||||||
inLibrary,
|
inLibrary,
|
||||||
|
|
@ -98,17 +100,27 @@ const ActionButtons = React.memo(({
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
// Memoized navigation handler
|
// Performance optimization: Cache theme colors
|
||||||
const handleRatingsPress = useMemo(() => async () => {
|
const themeColors = useMemo(() => ({
|
||||||
|
white: currentTheme.colors.white,
|
||||||
|
black: '#000',
|
||||||
|
primary: currentTheme.colors.primary
|
||||||
|
}), [currentTheme.colors.white, currentTheme.colors.primary]);
|
||||||
|
|
||||||
|
// Optimized navigation handler with useCallback
|
||||||
|
const handleRatingsPress = useCallback(async () => {
|
||||||
|
// Early return if no ID
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
let finalTmdbId: number | null = null;
|
let finalTmdbId: number | null = null;
|
||||||
|
|
||||||
if (id?.startsWith('tmdb:')) {
|
if (id.startsWith('tmdb:')) {
|
||||||
const numericPart = id.split(':')[1];
|
const numericPart = id.split(':')[1];
|
||||||
const parsedId = parseInt(numericPart, 10);
|
const parsedId = parseInt(numericPart, 10);
|
||||||
if (!isNaN(parsedId)) {
|
if (!isNaN(parsedId)) {
|
||||||
finalTmdbId = parsedId;
|
finalTmdbId = parsedId;
|
||||||
}
|
}
|
||||||
} else if (id?.startsWith('tt')) {
|
} else if (id.startsWith('tt')) {
|
||||||
try {
|
try {
|
||||||
const tmdbService = TMDBService.getInstance();
|
const tmdbService = TMDBService.getInstance();
|
||||||
const convertedId = await tmdbService.findTMDBIdByIMDB(id);
|
const convertedId = await tmdbService.findTMDBIdByIMDB(id);
|
||||||
|
|
@ -118,7 +130,7 @@ const ActionButtons = React.memo(({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error);
|
logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error);
|
||||||
}
|
}
|
||||||
} else if (id) {
|
} else {
|
||||||
const parsedId = parseInt(id, 10);
|
const parsedId = parseInt(id, 10);
|
||||||
if (!isNaN(parsedId)) {
|
if (!isNaN(parsedId)) {
|
||||||
finalTmdbId = parsedId;
|
finalTmdbId = parsedId;
|
||||||
|
|
@ -126,11 +138,14 @@ const ActionButtons = React.memo(({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalTmdbId !== null) {
|
if (finalTmdbId !== null) {
|
||||||
navigation.navigate('ShowRatings', { showId: finalTmdbId });
|
// Use requestAnimationFrame for smoother navigation
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
navigation.navigate('ShowRatings', { showId: finalTmdbId });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [id, navigation]);
|
}, [id, navigation]);
|
||||||
|
|
||||||
// Determine play button style and text based on watched status
|
// Optimized play button style calculation
|
||||||
const playButtonStyle = useMemo(() => {
|
const playButtonStyle = useMemo(() => {
|
||||||
if (isWatched && type === 'movie') {
|
if (isWatched && type === 'movie') {
|
||||||
// Only movies get the dark watched style for "Watch Again"
|
// Only movies get the dark watched style for "Watch Again"
|
||||||
|
|
@ -285,7 +300,7 @@ const ActionButtons = React.memo(({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enhanced WatchProgress Component with Trakt integration and watched status
|
// Enhanced WatchProgress Component with Trakt integration and watched status
|
||||||
const WatchProgressDisplay = React.memo(({
|
const WatchProgressDisplay = memo(({
|
||||||
watchProgress,
|
watchProgress,
|
||||||
type,
|
type,
|
||||||
getEpisodeDetails,
|
getEpisodeDetails,
|
||||||
|
|
@ -623,7 +638,22 @@ const WatchProgressDisplay = React.memo(({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const HeroSection: React.FC<HeroSectionProps> = ({
|
/**
|
||||||
|
* HeroSection Component - Performance Optimized
|
||||||
|
*
|
||||||
|
* Optimizations Applied:
|
||||||
|
* - Component memoization with React.memo
|
||||||
|
* - Lazy loading system using InteractionManager
|
||||||
|
* - Optimized image loading with useCallback handlers
|
||||||
|
* - Cached theme colors to reduce re-renders
|
||||||
|
* - Conditional rendering based on shouldLoadSecondaryData
|
||||||
|
* - Memory management with cleanup on unmount
|
||||||
|
* - Development-mode performance monitoring
|
||||||
|
* - Optimized animated styles and memoized calculations
|
||||||
|
* - Reduced re-renders through strategic memoization
|
||||||
|
* - runOnUI for animation performance
|
||||||
|
*/
|
||||||
|
const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
metadata,
|
metadata,
|
||||||
bannerImage,
|
bannerImage,
|
||||||
loadingBanner,
|
loadingBanner,
|
||||||
|
|
@ -650,21 +680,47 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
|
const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
|
||||||
|
|
||||||
// Enhanced state for smooth image loading
|
// Performance optimization: Refs for avoiding re-renders
|
||||||
|
const interactionComplete = useRef(false);
|
||||||
|
const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false);
|
||||||
|
|
||||||
|
// Image loading state with optimized management
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const imageOpacity = useSharedValue(1);
|
const imageOpacity = useSharedValue(1);
|
||||||
const imageLoadOpacity = useSharedValue(0);
|
const imageLoadOpacity = useSharedValue(0);
|
||||||
const shimmerOpacity = useSharedValue(0.3);
|
const shimmerOpacity = useSharedValue(0.3);
|
||||||
|
|
||||||
|
// Performance optimization: Cache theme colors
|
||||||
|
const themeColors = useMemo(() => ({
|
||||||
|
black: currentTheme.colors.black,
|
||||||
|
darkBackground: currentTheme.colors.darkBackground,
|
||||||
|
highEmphasis: currentTheme.colors.highEmphasis,
|
||||||
|
text: currentTheme.colors.text
|
||||||
|
}), [currentTheme.colors.black, currentTheme.colors.darkBackground, currentTheme.colors.highEmphasis, currentTheme.colors.text]);
|
||||||
|
|
||||||
// Memoized image source
|
// Memoized image source
|
||||||
const imageSource = useMemo(() =>
|
const imageSource = useMemo(() =>
|
||||||
bannerImage || metadata.banner || metadata.poster
|
bannerImage || metadata.banner || metadata.poster
|
||||||
, [bannerImage, metadata.banner, metadata.poster]);
|
, [bannerImage, metadata.banner, metadata.poster]);
|
||||||
|
|
||||||
// Start shimmer animation for loading state
|
// Performance optimization: Lazy loading setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timer = InteractionManager.runAfterInteractions(() => {
|
||||||
|
if (!interactionComplete.current) {
|
||||||
|
interactionComplete.current = true;
|
||||||
|
setShouldLoadSecondaryData(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => timer.cancel();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Optimized shimmer animation for loading state
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldLoadSecondaryData) return;
|
||||||
|
|
||||||
if (!imageLoaded && imageSource) {
|
if (!imageLoaded && imageSource) {
|
||||||
// Start shimmer animation
|
// Start shimmer animation
|
||||||
shimmerOpacity.value = withRepeat(
|
shimmerOpacity.value = withRepeat(
|
||||||
|
|
@ -676,9 +732,9 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
// Stop shimmer when loaded
|
// Stop shimmer when loaded
|
||||||
shimmerOpacity.value = withTiming(0.3, { duration: 300 });
|
shimmerOpacity.value = withTiming(0.3, { duration: 300 });
|
||||||
}
|
}
|
||||||
}, [imageLoaded, imageSource]);
|
}, [imageLoaded, imageSource, shouldLoadSecondaryData]);
|
||||||
|
|
||||||
// Reset loading state when image source changes
|
// Optimized loading state reset when image source changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imageSource) {
|
if (imageSource) {
|
||||||
setImageLoaded(false);
|
setImageLoaded(false);
|
||||||
|
|
@ -686,26 +742,33 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
}
|
}
|
||||||
}, [imageSource]);
|
}, [imageSource]);
|
||||||
|
|
||||||
// Enhanced image handlers with smooth transitions
|
// Optimized image handlers with useCallback
|
||||||
const handleImageError = () => {
|
const handleImageError = useCallback(() => {
|
||||||
|
if (!shouldLoadSecondaryData) return;
|
||||||
|
|
||||||
|
runOnUI(() => {
|
||||||
|
imageOpacity.value = withTiming(0.6, { duration: 150 });
|
||||||
|
imageLoadOpacity.value = withTiming(0, { duration: 150 });
|
||||||
|
})();
|
||||||
|
|
||||||
setImageError(true);
|
setImageError(true);
|
||||||
setImageLoaded(false);
|
setImageLoaded(false);
|
||||||
imageOpacity.value = withTiming(0.6, { duration: 150 });
|
|
||||||
imageLoadOpacity.value = withTiming(0, { duration: 150 });
|
// Fallback to poster if banner fails
|
||||||
runOnJS(() => {
|
if (bannerImage !== metadata.banner) {
|
||||||
if (bannerImage !== metadata.banner) {
|
setBannerImage(metadata.banner || metadata.poster);
|
||||||
setBannerImage(metadata.banner || metadata.poster);
|
}
|
||||||
}
|
}, [shouldLoadSecondaryData, bannerImage, metadata.banner, metadata.poster, setBannerImage]);
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
const handleImageLoad = useCallback(() => {
|
||||||
|
runOnUI(() => {
|
||||||
|
imageOpacity.value = withTiming(1, { duration: 150 });
|
||||||
|
imageLoadOpacity.value = withTiming(1, { duration: 400 });
|
||||||
|
})();
|
||||||
|
|
||||||
setImageError(false);
|
setImageError(false);
|
||||||
setImageLoaded(true);
|
setImageLoaded(true);
|
||||||
imageOpacity.value = withTiming(1, { duration: 150 });
|
}, []);
|
||||||
// Smooth fade-in for the loaded image
|
|
||||||
imageLoadOpacity.value = withTiming(1, { duration: 400 });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ultra-optimized animated styles - single calculations
|
// Ultra-optimized animated styles - single calculations
|
||||||
const heroAnimatedStyle = useAnimatedStyle(() => ({
|
const heroAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
|
@ -768,9 +831,9 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
}]
|
}]
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
// Ultra-optimized genre rendering with smooth animation
|
// Optimized genre rendering with lazy loading and memory management
|
||||||
const genreElements = useMemo(() => {
|
const genreElements = useMemo(() => {
|
||||||
if (!metadata?.genres?.length) return null;
|
if (!shouldLoadSecondaryData || !metadata?.genres?.length) return null;
|
||||||
|
|
||||||
const genresToDisplay = metadata.genres.slice(0, 3); // Reduced to 3 for performance
|
const genresToDisplay = metadata.genres.slice(0, 3); // Reduced to 3 for performance
|
||||||
return genresToDisplay.map((genreName: string, index: number, array: string[]) => (
|
return genresToDisplay.map((genreName: string, index: number, array: string[]) => (
|
||||||
|
|
@ -779,15 +842,15 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
entering={FadeIn.duration(400).delay(200 + index * 100)}
|
entering={FadeIn.duration(400).delay(200 + index * 100)}
|
||||||
style={{ flexDirection: 'row', alignItems: 'center' }}
|
style={{ flexDirection: 'row', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.genreText, { color: themeColors.text }]}>
|
||||||
{genreName}
|
{genreName}
|
||||||
</Text>
|
</Text>
|
||||||
{index < array.length - 1 && (
|
{index < array.length - 1 && (
|
||||||
<Text style={[styles.genreDot, { color: currentTheme.colors.text }]}>•</Text>
|
<Text style={[styles.genreDot, { color: themeColors.text }]}>•</Text>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
));
|
));
|
||||||
}, [metadata.genres, currentTheme.colors.text]);
|
}, [metadata.genres, themeColors.text, shouldLoadSecondaryData]);
|
||||||
|
|
||||||
// Memoized play button text
|
// Memoized play button text
|
||||||
const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]);
|
const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]);
|
||||||
|
|
@ -811,13 +874,38 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
return localWatched;
|
return localWatched;
|
||||||
}, [watchProgress, isTraktAuthenticated]);
|
}, [watchProgress, isTraktAuthenticated]);
|
||||||
|
|
||||||
|
// Memory management and cleanup
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Reset animation values on unmount
|
||||||
|
imageOpacity.value = 1;
|
||||||
|
imageLoadOpacity.value = 0;
|
||||||
|
shimmerOpacity.value = 0.3;
|
||||||
|
interactionComplete.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Development-only performance monitoring
|
||||||
|
useEffect(() => {
|
||||||
|
if (__DEV__) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const renderTime = Date.now() - startTime;
|
||||||
|
if (renderTime > 100) {
|
||||||
|
console.warn(`[HeroSection] Slow render detected: ${renderTime}ms`);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.heroSection, heroAnimatedStyle]}>
|
<Animated.View style={[styles.heroSection, heroAnimatedStyle]}>
|
||||||
{/* Optimized Background */}
|
{/* Optimized Background */}
|
||||||
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
|
<View style={[styles.absoluteFill, { backgroundColor: themeColors.black }]} />
|
||||||
|
|
||||||
{/* Loading placeholder for smooth transition */}
|
{/* Optimized shimmer loading effect */}
|
||||||
{((imageSource && !imageLoaded) || loadingBanner) && (
|
{shouldLoadSecondaryData && ((imageSource && !imageLoaded) || loadingBanner) && (
|
||||||
<Animated.View style={[styles.absoluteFill, {
|
<Animated.View style={[styles.absoluteFill, {
|
||||||
opacity: shimmerOpacity,
|
opacity: shimmerOpacity,
|
||||||
}]}>
|
}]}>
|
||||||
|
|
@ -830,8 +918,8 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Enhanced Background Image with smooth loading */}
|
{/* Optimized background image with lazy loading */}
|
||||||
{imageSource && !loadingBanner && (
|
{shouldLoadSecondaryData && imageSource && !loadingBanner && (
|
||||||
<Animated.Image
|
<Animated.Image
|
||||||
source={{ uri: imageSource }}
|
source={{ uri: imageSource }}
|
||||||
style={[styles.absoluteFill, backdropImageStyle]}
|
style={[styles.absoluteFill, backdropImageStyle]}
|
||||||
|
|
@ -841,13 +929,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Simplified Gradient */}
|
{/* Optimized Gradient */}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[
|
colors={[
|
||||||
'rgba(0,0,0,0)',
|
'rgba(0,0,0,0)',
|
||||||
'rgba(0,0,0,0.4)',
|
'rgba(0,0,0,0.4)',
|
||||||
'rgba(0,0,0,0.8)',
|
'rgba(0,0,0,0.8)',
|
||||||
currentTheme.colors.darkBackground
|
themeColors.darkBackground
|
||||||
]}
|
]}
|
||||||
locations={[0, 0.6, 0.85, 1]}
|
locations={[0, 0.6, 0.85, 1]}
|
||||||
style={styles.heroGradient}
|
style={styles.heroGradient}
|
||||||
|
|
@ -856,7 +944,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
{/* Optimized Title/Logo */}
|
{/* Optimized Title/Logo */}
|
||||||
<View style={styles.logoContainer}>
|
<View style={styles.logoContainer}>
|
||||||
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
|
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
|
||||||
{metadata.logo && !logoLoadError ? (
|
{shouldLoadSecondaryData && metadata.logo && !logoLoadError ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: metadata.logo }}
|
source={{ uri: metadata.logo }}
|
||||||
style={styles.titleLogo}
|
style={styles.titleLogo}
|
||||||
|
|
@ -867,7 +955,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.heroTitle, { color: currentTheme.colors.highEmphasis }]}>
|
<Text style={[styles.heroTitle, { color: themeColors.highEmphasis }]}>
|
||||||
{metadata.name}
|
{metadata.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
@ -883,8 +971,8 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
isWatched={isWatched}
|
isWatched={isWatched}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Optimized Genres */}
|
{/* Optimized genre display with lazy loading */}
|
||||||
{genreElements && (
|
{shouldLoadSecondaryData && genreElements && (
|
||||||
<View style={styles.genreContainer}>
|
<View style={styles.genreContainer}>
|
||||||
{genreElements}
|
{genreElements}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -908,7 +996,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
// Ultra-optimized styles
|
// Ultra-optimized styles
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|
@ -1305,4 +1393,4 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(HeroSection);
|
export default HeroSection;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useState, useEffect, useMemo, useRef, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -7,9 +7,10 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
InteractionManager,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
@ -27,6 +28,7 @@ import Animated, {
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
|
runOnJS,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { RouteProp } from '@react-navigation/native';
|
import { RouteProp } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
|
|
@ -45,6 +47,14 @@ import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
||||||
|
|
||||||
const { height } = Dimensions.get('window');
|
const { height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
// Memoized components for better performance
|
||||||
|
const MemoizedCastSection = memo(CastSection);
|
||||||
|
const MemoizedSeriesContent = memo(SeriesContent);
|
||||||
|
const MemoizedMovieContent = memo(MovieContent);
|
||||||
|
const MemoizedMoreLikeThisSection = memo(MoreLikeThisSection);
|
||||||
|
const MemoizedRatingsSection = memo(RatingsSection);
|
||||||
|
const MemoizedCastDetailsModal = memo(CastDetailsModal);
|
||||||
|
|
||||||
const MetadataScreen: React.FC = () => {
|
const MetadataScreen: React.FC = () => {
|
||||||
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
|
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
|
@ -59,7 +69,10 @@ const MetadataScreen: React.FC = () => {
|
||||||
const [isContentReady, setIsContentReady] = useState(false);
|
const [isContentReady, setIsContentReady] = useState(false);
|
||||||
const [showCastModal, setShowCastModal] = useState(false);
|
const [showCastModal, setShowCastModal] = useState(false);
|
||||||
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
||||||
|
const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false);
|
||||||
|
const [isScreenFocused, setIsScreenFocused] = useState(true);
|
||||||
const transitionOpacity = useSharedValue(1);
|
const transitionOpacity = useSharedValue(1);
|
||||||
|
const interactionComplete = useRef(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
|
|
@ -81,134 +94,160 @@ const MetadataScreen: React.FC = () => {
|
||||||
imdbId,
|
imdbId,
|
||||||
} = useMetadata({ id, type, addonId });
|
} = useMetadata({ id, type, addonId });
|
||||||
|
|
||||||
// Optimized hooks with memoization
|
// Optimized hooks with memoization and conditional loading
|
||||||
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
|
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
|
||||||
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
|
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
|
||||||
const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress);
|
const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress);
|
||||||
|
|
||||||
// Fetch and log Trakt progress data when entering the screen
|
// Focus effect for performance optimization
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
setIsScreenFocused(true);
|
||||||
|
|
||||||
|
// Delay secondary data loading until interactions are complete
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!interactionComplete.current) {
|
||||||
|
InteractionManager.runAfterInteractions(() => {
|
||||||
|
setShouldLoadSecondaryData(true);
|
||||||
|
interactionComplete.current = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setIsScreenFocused(false);
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optimize secondary data loading
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTraktProgress = async () => {
|
if (metadata && isScreenFocused && !shouldLoadSecondaryData) {
|
||||||
try {
|
const timer = setTimeout(() => {
|
||||||
const traktService = TraktService.getInstance();
|
setShouldLoadSecondaryData(true);
|
||||||
const isAuthenticated = await traktService.isAuthenticated();
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
console.log(`[MetadataScreen] === TRAKT PROGRESS DATA FOR ${type.toUpperCase()}: ${metadata?.name || id} ===`);
|
|
||||||
console.log(`[MetadataScreen] IMDB ID: ${id}`);
|
|
||||||
console.log(`[MetadataScreen] Trakt authenticated: ${isAuthenticated}`);
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
console.log(`[MetadataScreen] Not authenticated with Trakt, no progress data available`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all playback progress from Trakt
|
|
||||||
const allProgress = await traktService.getPlaybackProgress();
|
|
||||||
console.log(`[MetadataScreen] Total Trakt progress items: ${allProgress.length}`);
|
|
||||||
|
|
||||||
if (allProgress.length === 0) {
|
|
||||||
console.log(`[MetadataScreen] No Trakt progress data found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter progress for current content
|
|
||||||
let relevantProgress: TraktPlaybackItem[] = [];
|
|
||||||
|
|
||||||
if (type === 'movie') {
|
|
||||||
relevantProgress = allProgress.filter(item =>
|
|
||||||
item.type === 'movie' &&
|
|
||||||
item.movie?.ids.imdb === id.replace('tt', '')
|
|
||||||
);
|
|
||||||
} else if (type === 'series') {
|
|
||||||
relevantProgress = allProgress.filter(item =>
|
|
||||||
item.type === 'episode' &&
|
|
||||||
item.show?.ids.imdb === id.replace('tt', '')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[MetadataScreen] Relevant progress items for this ${type}: ${relevantProgress.length}`);
|
|
||||||
|
|
||||||
if (relevantProgress.length === 0) {
|
|
||||||
console.log(`[MetadataScreen] No Trakt progress found for this ${type}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log detailed progress information
|
|
||||||
relevantProgress.forEach((item, index) => {
|
|
||||||
console.log(`[MetadataScreen] --- Progress Item ${index + 1} ---`);
|
|
||||||
console.log(`[MetadataScreen] Type: ${item.type}`);
|
|
||||||
console.log(`[MetadataScreen] Progress: ${item.progress.toFixed(2)}%`);
|
|
||||||
console.log(`[MetadataScreen] Paused at: ${item.paused_at}`);
|
|
||||||
console.log(`[MetadataScreen] Trakt ID: ${item.id}`);
|
|
||||||
|
|
||||||
if (item.movie) {
|
|
||||||
console.log(`[MetadataScreen] Movie: ${item.movie.title} (${item.movie.year})`);
|
|
||||||
console.log(`[MetadataScreen] Movie IMDB: tt${item.movie.ids.imdb}`);
|
|
||||||
console.log(`[MetadataScreen] Movie TMDB: ${item.movie.ids.tmdb}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.episode && item.show) {
|
|
||||||
console.log(`[MetadataScreen] Show: ${item.show.title} (${item.show.year})`);
|
|
||||||
console.log(`[MetadataScreen] Show IMDB: tt${item.show.ids.imdb}`);
|
|
||||||
console.log(`[MetadataScreen] Episode: S${item.episode.season}E${item.episode.number} - ${item.episode.title}`);
|
|
||||||
console.log(`[MetadataScreen] Episode IMDB: ${item.episode.ids.imdb || 'N/A'}`);
|
|
||||||
console.log(`[MetadataScreen] Episode TMDB: ${item.episode.ids.tmdb || 'N/A'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[MetadataScreen] Raw item:`, JSON.stringify(item, null, 2));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find most recent progress if multiple episodes
|
|
||||||
if (type === 'series' && relevantProgress.length > 1) {
|
|
||||||
const mostRecent = relevantProgress.sort((a, b) =>
|
|
||||||
new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
console.log(`[MetadataScreen] === MOST RECENT EPISODE PROGRESS ===`);
|
|
||||||
if (mostRecent.episode && mostRecent.show) {
|
|
||||||
console.log(`[MetadataScreen] Most recent: S${mostRecent.episode.season}E${mostRecent.episode.number} - ${mostRecent.episode.title}`);
|
|
||||||
console.log(`[MetadataScreen] Progress: ${mostRecent.progress.toFixed(2)}%`);
|
|
||||||
console.log(`[MetadataScreen] Watched on: ${new Date(mostRecent.paused_at).toLocaleString()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[MetadataScreen] === END TRAKT PROGRESS DATA ===`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MetadataScreen] Failed to fetch Trakt progress:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only fetch when we have metadata loaded
|
|
||||||
if (metadata && id) {
|
|
||||||
fetchTraktProgress();
|
|
||||||
}
|
}
|
||||||
}, [metadata, id, type]);
|
}, [metadata, isScreenFocused, shouldLoadSecondaryData]);
|
||||||
|
|
||||||
|
// Optimized Trakt progress fetching - only when secondary data should load
|
||||||
|
const fetchTraktProgress = useCallback(async () => {
|
||||||
|
if (!shouldLoadSecondaryData || !metadata || !id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const traktService = TraktService.getInstance();
|
||||||
|
const isAuthenticated = await traktService.isAuthenticated();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
console.log(`[MetadataScreen] Not authenticated with Trakt`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all playback progress from Trakt (cached)
|
||||||
|
const allProgress = await traktService.getPlaybackProgress();
|
||||||
|
|
||||||
|
if (allProgress.length === 0) return;
|
||||||
|
|
||||||
|
// Filter progress for current content
|
||||||
|
let relevantProgress: TraktPlaybackItem[] = [];
|
||||||
|
|
||||||
|
if (type === 'movie') {
|
||||||
|
relevantProgress = allProgress.filter(item =>
|
||||||
|
item.type === 'movie' &&
|
||||||
|
item.movie?.ids.imdb === id.replace('tt', '')
|
||||||
|
);
|
||||||
|
} else if (type === 'series') {
|
||||||
|
relevantProgress = allProgress.filter(item =>
|
||||||
|
item.type === 'episode' &&
|
||||||
|
item.show?.ids.imdb === id.replace('tt', '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relevantProgress.length === 0) return;
|
||||||
|
|
||||||
|
// Log only essential progress information for performance
|
||||||
|
console.log(`[MetadataScreen] Found ${relevantProgress.length} Trakt progress items for ${type}`);
|
||||||
|
|
||||||
|
// Find most recent progress if multiple episodes
|
||||||
|
if (type === 'series' && relevantProgress.length > 1) {
|
||||||
|
const mostRecent = relevantProgress.sort((a, b) =>
|
||||||
|
new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (mostRecent.episode && mostRecent.show) {
|
||||||
|
console.log(`[MetadataScreen] Most recent: S${mostRecent.episode.season}E${mostRecent.episode.number} - ${mostRecent.progress.toFixed(1)}%`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MetadataScreen] Failed to fetch Trakt progress:`, error);
|
||||||
|
}
|
||||||
|
}, [shouldLoadSecondaryData, metadata, id, type]);
|
||||||
|
|
||||||
|
// Debounced Trakt progress fetching
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldLoadSecondaryData && metadata && id) {
|
||||||
|
const timer = setTimeout(fetchTraktProgress, 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [shouldLoadSecondaryData, metadata, id, fetchTraktProgress]);
|
||||||
|
|
||||||
|
// Memory management and cleanup
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Cleanup on unmount
|
||||||
|
if (transitionOpacity.value !== 0) {
|
||||||
|
transitionOpacity.value = 0;
|
||||||
|
}
|
||||||
|
// Reset secondary data loading state
|
||||||
|
setShouldLoadSecondaryData(false);
|
||||||
|
interactionComplete.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Performance monitoring (development only)
|
||||||
|
useEffect(() => {
|
||||||
|
if (__DEV__ && metadata) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const renderTime = Date.now() - startTime;
|
||||||
|
if (renderTime > 100) {
|
||||||
|
console.warn(`[MetadataScreen] Slow render detected: ${renderTime}ms for ${metadata.name}`);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [metadata]);
|
||||||
|
|
||||||
// Memoized derived values for performance
|
// Memoized derived values for performance
|
||||||
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
||||||
|
|
||||||
// Simple content ready state management
|
// Optimized content ready state management
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady) {
|
if (isReady && isScreenFocused) {
|
||||||
setIsContentReady(true);
|
setIsContentReady(true);
|
||||||
transitionOpacity.value = withTiming(1, { duration: 50 });
|
transitionOpacity.value = withTiming(1, { duration: 50 });
|
||||||
} else if (!isReady && isContentReady) {
|
} else if (!isReady && isContentReady) {
|
||||||
setIsContentReady(false);
|
setIsContentReady(false);
|
||||||
transitionOpacity.value = 0;
|
transitionOpacity.value = 0;
|
||||||
}
|
}
|
||||||
}, [isReady, isContentReady]);
|
}, [isReady, isContentReady, isScreenFocused]);
|
||||||
|
|
||||||
// Optimized callback functions with reduced dependencies
|
// Optimized callback functions with reduced dependencies and haptics throttling
|
||||||
const handleToggleLibrary = useCallback(() => {
|
const handleToggleLibrary = useCallback(() => {
|
||||||
Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium);
|
if (isScreenFocused) {
|
||||||
|
Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium);
|
||||||
|
}
|
||||||
toggleLibrary();
|
toggleLibrary();
|
||||||
}, [inLibrary, toggleLibrary]);
|
}, [inLibrary, toggleLibrary, isScreenFocused]);
|
||||||
|
|
||||||
const handleSeasonChangeWithHaptics = useCallback((seasonNumber: number) => {
|
const handleSeasonChangeWithHaptics = useCallback((seasonNumber: number) => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
if (isScreenFocused) {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
}
|
||||||
handleSeasonChange(seasonNumber);
|
handleSeasonChange(seasonNumber);
|
||||||
}, [handleSeasonChange]);
|
}, [handleSeasonChange, isScreenFocused]);
|
||||||
|
|
||||||
const handleShowStreams = useCallback(() => {
|
const handleShowStreams = useCallback(() => {
|
||||||
const { watchProgress } = watchProgressData;
|
const { watchProgress } = watchProgressData;
|
||||||
|
|
@ -302,26 +341,38 @@ const MetadataScreen: React.FC = () => {
|
||||||
}, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]);
|
}, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]);
|
||||||
|
|
||||||
const handleEpisodeSelect = useCallback((episode: Episode) => {
|
const handleEpisodeSelect = useCallback((episode: Episode) => {
|
||||||
console.log('[MetadataScreen] Selected Episode:', JSON.stringify(episode, null, 2));
|
if (!isScreenFocused) return;
|
||||||
|
|
||||||
|
console.log('[MetadataScreen] Selected Episode:', episode.episode_number, episode.season_number);
|
||||||
const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`;
|
const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`;
|
||||||
navigation.navigate('Streams', {
|
|
||||||
id,
|
// Optimize navigation with requestAnimationFrame
|
||||||
type,
|
requestAnimationFrame(() => {
|
||||||
episodeId,
|
navigation.navigate('Streams', {
|
||||||
episodeThumbnail: episode.still_path || undefined
|
id,
|
||||||
|
type,
|
||||||
|
episodeId,
|
||||||
|
episodeThumbnail: episode.still_path || undefined
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, [navigation, id, type]);
|
}, [navigation, id, type, isScreenFocused]);
|
||||||
|
|
||||||
const handleBack = useCallback(() => navigation.goBack(), [navigation]);
|
const handleBack = useCallback(() => {
|
||||||
|
if (isScreenFocused) {
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
|
}, [navigation, isScreenFocused]);
|
||||||
|
|
||||||
const handleSelectCastMember = useCallback((castMember: any) => {
|
const handleSelectCastMember = useCallback((castMember: any) => {
|
||||||
|
if (!isScreenFocused) return;
|
||||||
setSelectedCastMember(castMember);
|
setSelectedCastMember(castMember);
|
||||||
setShowCastModal(true);
|
setShowCastModal(true);
|
||||||
}, []);
|
}, [isScreenFocused]);
|
||||||
|
|
||||||
// Ultra-optimized animated styles - minimal calculations
|
// Ultra-optimized animated styles - minimal calculations with conditional updates
|
||||||
const containerStyle = useAnimatedStyle(() => ({
|
const containerStyle = useAnimatedStyle(() => ({
|
||||||
opacity: animations.screenOpacity.value,
|
opacity: isScreenFocused ? animations.screenOpacity.value : 0.8,
|
||||||
}), []);
|
}), [isScreenFocused]);
|
||||||
|
|
||||||
const contentStyle = useAnimatedStyle(() => ({
|
const contentStyle = useAnimatedStyle(() => ({
|
||||||
opacity: animations.contentOpacity.value,
|
opacity: animations.contentOpacity.value,
|
||||||
|
|
@ -441,21 +492,23 @@ const MetadataScreen: React.FC = () => {
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
imdbId={imdbId}
|
imdbId={imdbId}
|
||||||
type={type as 'movie' | 'series'}
|
type={type as 'movie' | 'series'}
|
||||||
renderRatings={() => imdbId ? (
|
renderRatings={() => imdbId && shouldLoadSecondaryData ? (
|
||||||
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
|
<MemoizedRatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
|
||||||
) : null}
|
) : null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Cast Section with skeleton when loading */}
|
{/* Cast Section with skeleton when loading - Lazy loaded */}
|
||||||
<CastSection
|
{shouldLoadSecondaryData && (
|
||||||
cast={cast}
|
<MemoizedCastSection
|
||||||
loadingCast={loadingCast}
|
cast={cast}
|
||||||
onSelectCastMember={handleSelectCastMember}
|
loadingCast={loadingCast}
|
||||||
/>
|
onSelectCastMember={handleSelectCastMember}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Recommendations Section with skeleton when loading */}
|
{/* Recommendations Section with skeleton when loading - Lazy loaded */}
|
||||||
{type === 'movie' && (
|
{type === 'movie' && shouldLoadSecondaryData && (
|
||||||
<MoreLikeThisSection
|
<MemoizedMoreLikeThisSection
|
||||||
recommendations={recommendations}
|
recommendations={recommendations}
|
||||||
loadingRecommendations={loadingRecommendations}
|
loadingRecommendations={loadingRecommendations}
|
||||||
/>
|
/>
|
||||||
|
|
@ -463,7 +516,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Series/Movie Content with episode skeleton when loading */}
|
{/* Series/Movie Content with episode skeleton when loading */}
|
||||||
{type === 'series' ? (
|
{type === 'series' ? (
|
||||||
<SeriesContent
|
<MemoizedSeriesContent
|
||||||
episodes={Object.values(groupedEpisodes).flat()}
|
episodes={Object.values(groupedEpisodes).flat()}
|
||||||
selectedSeason={selectedSeason}
|
selectedSeason={selectedSeason}
|
||||||
loadingSeasons={loadingSeasons}
|
loadingSeasons={loadingSeasons}
|
||||||
|
|
@ -473,19 +526,21 @@ const MetadataScreen: React.FC = () => {
|
||||||
metadata={metadata || undefined}
|
metadata={metadata || undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
metadata && <MovieContent metadata={metadata} />
|
metadata && <MemoizedMovieContent metadata={metadata} />
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cast Details Modal */}
|
{/* Cast Details Modal - Memoized */}
|
||||||
<CastDetailsModal
|
{showCastModal && (
|
||||||
visible={showCastModal}
|
<MemoizedCastDetailsModal
|
||||||
onClose={() => setShowCastModal(false)}
|
visible={showCastModal}
|
||||||
castMember={selectedCastMember}
|
onClose={() => setShowCastModal(false)}
|
||||||
/>
|
castMember={selectedCastMember}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -577,4 +632,16 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Performance Optimizations Applied:
|
||||||
|
// 1. Memoized components (Cast, Series, Movie, Ratings, Modal)
|
||||||
|
// 2. Lazy loading of secondary data (cast, recommendations, ratings)
|
||||||
|
// 3. Focus-based rendering and interaction management
|
||||||
|
// 4. Debounced Trakt progress fetching with reduced logging
|
||||||
|
// 5. Optimized callback functions with screen focus checks
|
||||||
|
// 6. Conditional haptics feedback based on screen focus
|
||||||
|
// 7. Memory management and cleanup on unmount
|
||||||
|
// 8. Performance monitoring in development mode
|
||||||
|
// 9. Reduced re-renders through better state management
|
||||||
|
// 10. RequestAnimationFrame for navigation optimization
|
||||||
|
|
||||||
export default MetadataScreen;
|
export default MetadataScreen;
|
||||||
Loading…
Reference in a new issue