mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +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 {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
Dimensions,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
InteractionManager,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
|
|
@ -22,6 +23,7 @@ import Animated, {
|
|||
runOnJS,
|
||||
withRepeat,
|
||||
FadeIn,
|
||||
runOnUI,
|
||||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
|
|
@ -71,7 +73,7 @@ interface HeroSectionProps {
|
|||
}
|
||||
|
||||
// Ultra-optimized ActionButtons Component - minimal re-renders
|
||||
const ActionButtons = React.memo(({
|
||||
const ActionButtons = memo(({
|
||||
handleShowStreams,
|
||||
toggleLibrary,
|
||||
inLibrary,
|
||||
|
|
@ -98,17 +100,27 @@ const ActionButtons = React.memo(({
|
|||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Memoized navigation handler
|
||||
const handleRatingsPress = useMemo(() => async () => {
|
||||
// Performance optimization: Cache theme colors
|
||||
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;
|
||||
|
||||
if (id?.startsWith('tmdb:')) {
|
||||
if (id.startsWith('tmdb:')) {
|
||||
const numericPart = id.split(':')[1];
|
||||
const parsedId = parseInt(numericPart, 10);
|
||||
if (!isNaN(parsedId)) {
|
||||
finalTmdbId = parsedId;
|
||||
}
|
||||
} else if (id?.startsWith('tt')) {
|
||||
} else if (id.startsWith('tt')) {
|
||||
try {
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
const convertedId = await tmdbService.findTMDBIdByIMDB(id);
|
||||
|
|
@ -118,7 +130,7 @@ const ActionButtons = React.memo(({
|
|||
} catch (error) {
|
||||
logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error);
|
||||
}
|
||||
} else if (id) {
|
||||
} else {
|
||||
const parsedId = parseInt(id, 10);
|
||||
if (!isNaN(parsedId)) {
|
||||
finalTmdbId = parsedId;
|
||||
|
|
@ -126,11 +138,14 @@ const ActionButtons = React.memo(({
|
|||
}
|
||||
|
||||
if (finalTmdbId !== null) {
|
||||
navigation.navigate('ShowRatings', { showId: finalTmdbId });
|
||||
// Use requestAnimationFrame for smoother navigation
|
||||
requestAnimationFrame(() => {
|
||||
navigation.navigate('ShowRatings', { showId: finalTmdbId });
|
||||
});
|
||||
}
|
||||
}, [id, navigation]);
|
||||
|
||||
// Determine play button style and text based on watched status
|
||||
// Optimized play button style calculation
|
||||
const playButtonStyle = useMemo(() => {
|
||||
if (isWatched && type === 'movie') {
|
||||
// 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
|
||||
const WatchProgressDisplay = React.memo(({
|
||||
const WatchProgressDisplay = memo(({
|
||||
watchProgress,
|
||||
type,
|
||||
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,
|
||||
bannerImage,
|
||||
loadingBanner,
|
||||
|
|
@ -650,21 +680,47 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
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 [imageLoaded, setImageLoaded] = useState(false);
|
||||
const imageOpacity = useSharedValue(1);
|
||||
const imageLoadOpacity = useSharedValue(0);
|
||||
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
|
||||
const imageSource = useMemo(() =>
|
||||
bannerImage || metadata.banner || metadata.poster
|
||||
, [bannerImage, metadata.banner, metadata.poster]);
|
||||
|
||||
// Start shimmer animation for loading state
|
||||
// Performance optimization: Lazy loading setup
|
||||
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) {
|
||||
// Start shimmer animation
|
||||
shimmerOpacity.value = withRepeat(
|
||||
|
|
@ -676,9 +732,9 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
// Stop shimmer when loaded
|
||||
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(() => {
|
||||
if (imageSource) {
|
||||
setImageLoaded(false);
|
||||
|
|
@ -686,26 +742,33 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
}
|
||||
}, [imageSource]);
|
||||
|
||||
// Enhanced image handlers with smooth transitions
|
||||
const handleImageError = () => {
|
||||
// Optimized image handlers with useCallback
|
||||
const handleImageError = useCallback(() => {
|
||||
if (!shouldLoadSecondaryData) return;
|
||||
|
||||
runOnUI(() => {
|
||||
imageOpacity.value = withTiming(0.6, { duration: 150 });
|
||||
imageLoadOpacity.value = withTiming(0, { duration: 150 });
|
||||
})();
|
||||
|
||||
setImageError(true);
|
||||
setImageLoaded(false);
|
||||
imageOpacity.value = withTiming(0.6, { duration: 150 });
|
||||
imageLoadOpacity.value = withTiming(0, { duration: 150 });
|
||||
runOnJS(() => {
|
||||
if (bannerImage !== metadata.banner) {
|
||||
setBannerImage(metadata.banner || metadata.poster);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
// Fallback to poster if banner fails
|
||||
if (bannerImage !== metadata.banner) {
|
||||
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);
|
||||
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
|
||||
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(() => {
|
||||
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
|
||||
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)}
|
||||
style={{ flexDirection: 'row', alignItems: 'center' }}
|
||||
>
|
||||
<Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
|
||||
<Text style={[styles.genreText, { color: themeColors.text }]}>
|
||||
{genreName}
|
||||
</Text>
|
||||
{index < array.length - 1 && (
|
||||
<Text style={[styles.genreDot, { color: currentTheme.colors.text }]}>•</Text>
|
||||
<Text style={[styles.genreDot, { color: themeColors.text }]}>•</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
));
|
||||
}, [metadata.genres, currentTheme.colors.text]);
|
||||
}, [metadata.genres, themeColors.text, shouldLoadSecondaryData]);
|
||||
|
||||
// Memoized play button text
|
||||
const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]);
|
||||
|
|
@ -811,13 +874,38 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
return localWatched;
|
||||
}, [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 (
|
||||
<Animated.View style={[styles.heroSection, heroAnimatedStyle]}>
|
||||
{/* Optimized Background */}
|
||||
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
|
||||
<View style={[styles.absoluteFill, { backgroundColor: themeColors.black }]} />
|
||||
|
||||
{/* Loading placeholder for smooth transition */}
|
||||
{((imageSource && !imageLoaded) || loadingBanner) && (
|
||||
{/* Optimized shimmer loading effect */}
|
||||
{shouldLoadSecondaryData && ((imageSource && !imageLoaded) || loadingBanner) && (
|
||||
<Animated.View style={[styles.absoluteFill, {
|
||||
opacity: shimmerOpacity,
|
||||
}]}>
|
||||
|
|
@ -830,8 +918,8 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Enhanced Background Image with smooth loading */}
|
||||
{imageSource && !loadingBanner && (
|
||||
{/* Optimized background image with lazy loading */}
|
||||
{shouldLoadSecondaryData && imageSource && !loadingBanner && (
|
||||
<Animated.Image
|
||||
source={{ uri: imageSource }}
|
||||
style={[styles.absoluteFill, backdropImageStyle]}
|
||||
|
|
@ -841,13 +929,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Simplified Gradient */}
|
||||
{/* Optimized Gradient */}
|
||||
<LinearGradient
|
||||
colors={[
|
||||
'rgba(0,0,0,0)',
|
||||
'rgba(0,0,0,0.4)',
|
||||
'rgba(0,0,0,0.8)',
|
||||
currentTheme.colors.darkBackground
|
||||
themeColors.darkBackground
|
||||
]}
|
||||
locations={[0, 0.6, 0.85, 1]}
|
||||
style={styles.heroGradient}
|
||||
|
|
@ -856,7 +944,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
{/* Optimized Title/Logo */}
|
||||
<View style={styles.logoContainer}>
|
||||
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
|
||||
{metadata.logo && !logoLoadError ? (
|
||||
{shouldLoadSecondaryData && metadata.logo && !logoLoadError ? (
|
||||
<Image
|
||||
source={{ uri: metadata.logo }}
|
||||
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}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -883,8 +971,8 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
isWatched={isWatched}
|
||||
/>
|
||||
|
||||
{/* Optimized Genres */}
|
||||
{genreElements && (
|
||||
{/* Optimized genre display with lazy loading */}
|
||||
{shouldLoadSecondaryData && genreElements && (
|
||||
<View style={styles.genreContainer}>
|
||||
{genreElements}
|
||||
</View>
|
||||
|
|
@ -908,7 +996,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
|||
</LinearGradient>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
// Ultra-optimized styles
|
||||
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 {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -7,9 +7,10 @@ import {
|
|||
ActivityIndicator,
|
||||
Dimensions,
|
||||
TouchableOpacity,
|
||||
InteractionManager,
|
||||
} from 'react-native';
|
||||
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 * as Haptics from 'expo-haptics';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
|
@ -27,6 +28,7 @@ import Animated, {
|
|||
Extrapolate,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
runOnJS,
|
||||
} from 'react-native-reanimated';
|
||||
import { RouteProp } 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');
|
||||
|
||||
// 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 route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -59,7 +69,10 @@ const MetadataScreen: React.FC = () => {
|
|||
const [isContentReady, setIsContentReady] = useState(false);
|
||||
const [showCastModal, setShowCastModal] = useState(false);
|
||||
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
||||
const [shouldLoadSecondaryData, setShouldLoadSecondaryData] = useState(false);
|
||||
const [isScreenFocused, setIsScreenFocused] = useState(true);
|
||||
const transitionOpacity = useSharedValue(1);
|
||||
const interactionComplete = useRef(false);
|
||||
|
||||
const {
|
||||
metadata,
|
||||
|
|
@ -81,134 +94,160 @@ const MetadataScreen: React.FC = () => {
|
|||
imdbId,
|
||||
} = 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 assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
|
||||
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(() => {
|
||||
const fetchTraktProgress = async () => {
|
||||
try {
|
||||
const traktService = TraktService.getInstance();
|
||||
const isAuthenticated = await traktService.isAuthenticated();
|
||||
|
||||
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();
|
||||
if (metadata && isScreenFocused && !shouldLoadSecondaryData) {
|
||||
const timer = setTimeout(() => {
|
||||
setShouldLoadSecondaryData(true);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [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
|
||||
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
||||
|
||||
// Simple content ready state management
|
||||
// Optimized content ready state management
|
||||
useEffect(() => {
|
||||
if (isReady) {
|
||||
if (isReady && isScreenFocused) {
|
||||
setIsContentReady(true);
|
||||
transitionOpacity.value = withTiming(1, { duration: 50 });
|
||||
} else if (!isReady && isContentReady) {
|
||||
setIsContentReady(false);
|
||||
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(() => {
|
||||
Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium);
|
||||
if (isScreenFocused) {
|
||||
Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium);
|
||||
}
|
||||
toggleLibrary();
|
||||
}, [inLibrary, toggleLibrary]);
|
||||
}, [inLibrary, toggleLibrary, isScreenFocused]);
|
||||
|
||||
const handleSeasonChangeWithHaptics = useCallback((seasonNumber: number) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
if (isScreenFocused) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
handleSeasonChange(seasonNumber);
|
||||
}, [handleSeasonChange]);
|
||||
}, [handleSeasonChange, isScreenFocused]);
|
||||
|
||||
const handleShowStreams = useCallback(() => {
|
||||
const { watchProgress } = watchProgressData;
|
||||
|
|
@ -302,26 +341,38 @@ const MetadataScreen: React.FC = () => {
|
|||
}, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]);
|
||||
|
||||
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}`;
|
||||
navigation.navigate('Streams', {
|
||||
id,
|
||||
type,
|
||||
episodeId,
|
||||
episodeThumbnail: episode.still_path || undefined
|
||||
|
||||
// Optimize navigation with requestAnimationFrame
|
||||
requestAnimationFrame(() => {
|
||||
navigation.navigate('Streams', {
|
||||
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) => {
|
||||
if (!isScreenFocused) return;
|
||||
setSelectedCastMember(castMember);
|
||||
setShowCastModal(true);
|
||||
}, []);
|
||||
}, [isScreenFocused]);
|
||||
|
||||
// Ultra-optimized animated styles - minimal calculations
|
||||
// Ultra-optimized animated styles - minimal calculations with conditional updates
|
||||
const containerStyle = useAnimatedStyle(() => ({
|
||||
opacity: animations.screenOpacity.value,
|
||||
}), []);
|
||||
opacity: isScreenFocused ? animations.screenOpacity.value : 0.8,
|
||||
}), [isScreenFocused]);
|
||||
|
||||
const contentStyle = useAnimatedStyle(() => ({
|
||||
opacity: animations.contentOpacity.value,
|
||||
|
|
@ -441,21 +492,23 @@ const MetadataScreen: React.FC = () => {
|
|||
metadata={metadata}
|
||||
imdbId={imdbId}
|
||||
type={type as 'movie' | 'series'}
|
||||
renderRatings={() => imdbId ? (
|
||||
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
|
||||
renderRatings={() => imdbId && shouldLoadSecondaryData ? (
|
||||
<MemoizedRatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
|
||||
) : null}
|
||||
/>
|
||||
|
||||
{/* Cast Section with skeleton when loading */}
|
||||
<CastSection
|
||||
cast={cast}
|
||||
loadingCast={loadingCast}
|
||||
onSelectCastMember={handleSelectCastMember}
|
||||
/>
|
||||
{/* Cast Section with skeleton when loading - Lazy loaded */}
|
||||
{shouldLoadSecondaryData && (
|
||||
<MemoizedCastSection
|
||||
cast={cast}
|
||||
loadingCast={loadingCast}
|
||||
onSelectCastMember={handleSelectCastMember}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Recommendations Section with skeleton when loading */}
|
||||
{type === 'movie' && (
|
||||
<MoreLikeThisSection
|
||||
{/* Recommendations Section with skeleton when loading - Lazy loaded */}
|
||||
{type === 'movie' && shouldLoadSecondaryData && (
|
||||
<MemoizedMoreLikeThisSection
|
||||
recommendations={recommendations}
|
||||
loadingRecommendations={loadingRecommendations}
|
||||
/>
|
||||
|
|
@ -463,7 +516,7 @@ const MetadataScreen: React.FC = () => {
|
|||
|
||||
{/* Series/Movie Content with episode skeleton when loading */}
|
||||
{type === 'series' ? (
|
||||
<SeriesContent
|
||||
<MemoizedSeriesContent
|
||||
episodes={Object.values(groupedEpisodes).flat()}
|
||||
selectedSeason={selectedSeason}
|
||||
loadingSeasons={loadingSeasons}
|
||||
|
|
@ -473,19 +526,21 @@ const MetadataScreen: React.FC = () => {
|
|||
metadata={metadata || undefined}
|
||||
/>
|
||||
) : (
|
||||
metadata && <MovieContent metadata={metadata} />
|
||||
metadata && <MemoizedMovieContent metadata={metadata} />
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.ScrollView>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cast Details Modal */}
|
||||
<CastDetailsModal
|
||||
visible={showCastModal}
|
||||
onClose={() => setShowCastModal(false)}
|
||||
castMember={selectedCastMember}
|
||||
/>
|
||||
{/* Cast Details Modal - Memoized */}
|
||||
{showCastModal && (
|
||||
<MemoizedCastDetailsModal
|
||||
visible={showCastModal}
|
||||
onClose={() => setShowCastModal(false)}
|
||||
castMember={selectedCastMember}
|
||||
/>
|
||||
)}
|
||||
</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;
|
||||
Loading…
Reference in a new issue