perfomance optimziations

This commit is contained in:
tapframe 2025-08-06 21:41:23 +05:30
parent 62a2ed0046
commit cc494bdf17
2 changed files with 342 additions and 187 deletions

View file

@ -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;

View file

@ -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;