diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 103c8c9b..6734ac3f 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, @@ -15,7 +15,6 @@ import Animated, { Extrapolate, useSharedValue, withTiming, - withSpring, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; @@ -23,29 +22,19 @@ import { TMDBService } from '../../services/tmdbService'; const { width, height } = Dimensions.get('window'); -// Types +// Types - optimized interface HeroSectionProps { metadata: any; bannerImage: string | null; loadingBanner: boolean; logoLoadError: boolean; scrollY: Animated.SharedValue; - dampedScrollY: Animated.SharedValue; heroHeight: Animated.SharedValue; heroOpacity: Animated.SharedValue; - heroScale: Animated.SharedValue; - heroRotate: Animated.SharedValue; logoOpacity: Animated.SharedValue; - logoScale: Animated.SharedValue; - logoRotate: Animated.SharedValue; - genresOpacity: Animated.SharedValue; - genresTranslateY: Animated.SharedValue; - genresScale: Animated.SharedValue; buttonsOpacity: Animated.SharedValue; buttonsTranslateY: Animated.SharedValue; - buttonsScale: Animated.SharedValue; watchProgressOpacity: Animated.SharedValue; - watchProgressScaleY: Animated.SharedValue; watchProgressWidth: Animated.SharedValue; watchProgress: { currentTime: number; @@ -65,7 +54,7 @@ interface HeroSectionProps { setLogoLoadError: (error: boolean) => void; } -// Memoized ActionButtons Component +// Ultra-optimized ActionButtons Component with minimal re-renders const ActionButtons = React.memo(({ handleShowStreams, toggleLibrary, @@ -86,25 +75,59 @@ const ActionButtons = React.memo(({ animatedStyle: any; }) => { const { currentTheme } = useTheme(); + + // Memoized navigation handler for better performance + const handleRatingsPress = useMemo(() => async () => { + let finalTmdbId: number | null = null; + + if (id?.startsWith('tmdb:')) { + const numericPart = id.split(':')[1]; + const parsedId = parseInt(numericPart, 10); + if (!isNaN(parsedId)) { + finalTmdbId = parsedId; + } + } else if (id?.startsWith('tt')) { + try { + const tmdbService = TMDBService.getInstance(); + const convertedId = await tmdbService.findTMDBIdByIMDB(id); + if (convertedId) { + finalTmdbId = convertedId; + logger.log(`[HeroSection] Converted IMDb ID ${id} to TMDB ID: ${finalTmdbId}`); + } + } catch (error) { + logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error); + } + } else if (id) { + const parsedId = parseInt(id, 10); + if (!isNaN(parsedId)) { + finalTmdbId = parsedId; + } + } + + if (finalTmdbId !== null) { + navigation.navigate('ShowRatings', { showId: finalTmdbId }); + } + }, [id, navigation]); + return ( - - {playButtonText} - + {playButtonText} { - let finalTmdbId: number | null = null; - - if (id && id.startsWith('tmdb:')) { - const numericPart = id.split(':')[1]; - const parsedId = parseInt(numericPart, 10); - if (!isNaN(parsedId)) { - finalTmdbId = parsedId; - } else { - logger.error(`[HeroSection] Failed to parse TMDB ID from: ${id}`); - } - } else if (id && id.startsWith('tt')) { - // It's an IMDb ID, convert it - logger.log(`[HeroSection] Detected IMDb ID: ${id}, attempting conversion to TMDB ID.`); - try { - const tmdbService = TMDBService.getInstance(); - const convertedId = await tmdbService.findTMDBIdByIMDB(id); - if (convertedId) { - finalTmdbId = convertedId; - logger.log(`[HeroSection] Successfully converted IMDb ID ${id} to TMDB ID: ${finalTmdbId}`); - } else { - logger.error(`[HeroSection] Could not convert IMDb ID ${id} to TMDB ID.`); - } - } catch (error) { - logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error); - } - } else if (id) { - // Assume it might be a raw TMDB ID (numeric string) - const parsedId = parseInt(id, 10); - if (!isNaN(parsedId)) { - finalTmdbId = parsedId; - } else { - logger.error(`[HeroSection] Unrecognized ID format or invalid numeric ID: ${id}`); - } - } - - // Navigate if we have a valid TMDB ID - if (finalTmdbId !== null) { - navigation.navigate('ShowRatings', { showId: finalTmdbId }); - } else { - logger.error(`[HeroSection] Could not navigate to ShowRatings, failed to obtain a valid TMDB ID from original id: ${id}`); - // Optionally show an error message to the user here - } - }} + style={styles.iconButton} + onPress={handleRatingsPress} + activeOpacity={0.8} > { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; - progressBarStyle: any; }) => { const { currentTheme } = useTheme(); - if (!watchProgress || watchProgress.duration === 0) { - return null; - } + + // Memoized progress calculation + const progressData = useMemo(() => { + if (!watchProgress || watchProgress.duration === 0) return null; - const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; - const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString(); - let episodeInfo = ''; + const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; + const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString(); + let episodeInfo = ''; - if (type === 'series' && watchProgress.episodeId) { - const details = getEpisodeDetails(watchProgress.episodeId); - if (details) { - episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`; + if (type === 'series' && watchProgress.episodeId) { + const details = getEpisodeDetails(watchProgress.episodeId); + if (details) { + episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`; + } } - } + + return { + progressPercent, + formattedTime, + episodeInfo, + displayText: progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched` + }; + }, [watchProgress, type, getEpisodeDetails]); + + if (!progressData) return null; return ( - - {progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} • Last watched on {formattedTime} + {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime} ); @@ -232,23 +221,12 @@ const HeroSection: React.FC = ({ loadingBanner, logoLoadError, scrollY, - dampedScrollY, heroHeight, heroOpacity, - heroScale, - heroRotate, logoOpacity, - logoScale, - logoRotate, - genresOpacity, - genresTranslateY, - genresScale, buttonsOpacity, buttonsTranslateY, - buttonsScale, watchProgressOpacity, - watchProgressScaleY, - watchProgressWidth, watchProgress, type, getEpisodeDetails, @@ -263,218 +241,80 @@ const HeroSection: React.FC = ({ }) => { const { currentTheme } = useTheme(); - // State for backdrop image loading - const [imageLoaded, setImageLoaded] = useState(false); + // Optimized state management const [imageError, setImageError] = useState(false); + const imageOpacity = useSharedValue(1); - // Animation values for smooth backdrop transitions - const backdropOpacity = useSharedValue(1); // Start visible - const backdropScale = useSharedValue(1); // Start at normal scale + // Memoized image source for better performance + const imageSource = useMemo(() => + bannerImage || metadata.banner || metadata.poster + , [bannerImage, metadata.banner, metadata.poster]); - // Handle image load success - const handleImageLoad = () => { - setImageLoaded(true); - setImageError(false); - // Enhance the image with subtle animation - backdropOpacity.value = withTiming(1, { duration: 300 }); - backdropScale.value = withSpring(1, { - damping: 25, - stiffness: 120, - mass: 1 - }); - }; - - // Handle image load error + // Optimized image handlers const handleImageError = () => { - logger.warn(`[HeroSection] Banner failed to load: ${bannerImage}`); + logger.warn(`[HeroSection] Banner failed to load: ${imageSource}`); setImageError(true); - backdropOpacity.value = withTiming(0.7, { duration: 200 }); // Dim on error + imageOpacity.value = withTiming(0.7, { duration: 150 }); if (bannerImage !== metadata.banner) { setBannerImage(metadata.banner || metadata.poster); } }; - // Reset animations when banner image changes - useEffect(() => { - if (bannerImage && !loadingBanner) { - setImageLoaded(false); - setImageError(false); - backdropOpacity.value = 0.8; // Start slightly dimmed - backdropScale.value = 0.98; // Start slightly smaller - } - }, [bannerImage, loadingBanner]); + const handleImageLoad = () => { + setImageError(false); + imageOpacity.value = withTiming(1, { duration: 200 }); + }; - // Enhanced animated styles with sophisticated micro-animations + // Ultra-optimized animated styles with minimal calculations const heroAnimatedStyle = useAnimatedStyle(() => ({ - width: '100%', height: heroHeight.value, - backgroundColor: currentTheme.colors.black, - transform: [ - { scale: heroScale.value }, - { - rotateZ: `${interpolate( - heroRotate.value, - [0, 1], - [0, 0.2], - Extrapolate.CLAMP - )}deg` - } - ], opacity: heroOpacity.value, - })); + }), []); const logoAnimatedStyle = useAnimatedStyle(() => ({ opacity: logoOpacity.value, - transform: [ - { - scale: interpolate( - logoScale.value, - [0, 1], - [0.95, 1], - Extrapolate.CLAMP - ) - }, - { - rotateZ: `${interpolate( - logoRotate.value, - [0, 1], - [0, 0.5], - Extrapolate.CLAMP - )}deg` - } - ] - })); + }), []); const watchProgressAnimatedStyle = useAnimatedStyle(() => ({ opacity: watchProgressOpacity.value, + }), []); + + // Simplified backdrop animation - fewer calculations + const backdropImageStyle = useAnimatedStyle(() => ({ + opacity: imageOpacity.value, transform: [ { translateY: interpolate( - watchProgressScaleY.value, - [0, 1], - [-12, 0], + scrollY.value, + [0, 200], + [0, -60], Extrapolate.CLAMP ) }, - { scaleY: watchProgressScaleY.value }, - { scaleX: interpolate(watchProgressScaleY.value, [0, 1], [0.9, 1]) } - ] - })); - - const watchProgressBarStyle = useAnimatedStyle(() => ({ - transform: [ - { scaleX: interpolate(watchProgressWidth.value, [0, 1], [0.8, 1]) } - ] - })); - - const genresAnimatedStyle = useAnimatedStyle(() => ({ - opacity: genresOpacity.value, - transform: [ - { translateY: genresTranslateY.value }, - { scale: genresScale.value } - ] - })); + { + scale: interpolate( + scrollY.value, + [0, 200], + [1.05, 1.02], + Extrapolate.CLAMP + ) + }, + ], + }), []); const buttonsAnimatedStyle = useAnimatedStyle(() => ({ opacity: buttonsOpacity.value, - transform: [ - { - translateY: interpolate( - buttonsTranslateY.value, - [0, 20], - [0, 8], - Extrapolate.CLAMP - ) - }, - { - scale: interpolate( - buttonsScale.value, - [0, 1], - [0.98, 1], - Extrapolate.CLAMP - ) - } - ] - })); + transform: [{ translateY: buttonsTranslateY.value }] + }), []); - const parallaxImageStyle = useAnimatedStyle(() => ({ - width: '120%', - height: '110%', - top: '-10%', - left: '-10%', - transform: [ - { - translateY: interpolate( - dampedScrollY.value, - [0, 100, 300], - [0, -35, -90], - Extrapolate.CLAMP - ) - }, - { - scale: interpolate( - dampedScrollY.value, - [0, 150, 300], - [1.08, 1.05, 1.02], - Extrapolate.CLAMP - ) * backdropScale.value - }, - { - rotateZ: interpolate( - dampedScrollY.value, - [0, 300], - [0, -0.1], - Extrapolate.CLAMP - ) + 'deg' - } - ], - })); - - // Backdrop image animated style for smooth transitions - const backdropImageStyle = useAnimatedStyle(() => ({ - opacity: backdropOpacity.value, - transform: [ - { - translateY: interpolate( - dampedScrollY.value, - [0, 100, 300], - [0, -35, -90], - Extrapolate.CLAMP - ) - }, - { - scale: interpolate( - dampedScrollY.value, - [0, 150, 300], - [1.08, 1.05, 1.02], - Extrapolate.CLAMP - ) * backdropScale.value - }, - { - rotateZ: interpolate( - dampedScrollY.value, - [0, 300], - [0, -0.1], - Extrapolate.CLAMP - ) + 'deg' - } - ], - })); - - // Loading skeleton animated style - const skeletonStyle = useAnimatedStyle(() => ({ - opacity: loadingBanner ? 0.2 : 0, - })); - - // Render genres - const renderGenres = () => { + // Memoized genre rendering for performance + const genreElements = useMemo(() => { if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) { return null; } - const genresToDisplay: string[] = metadata.genres as string[]; - - return genresToDisplay.slice(0, 4).map((genreName, index, array) => ( + const genresToDisplay: string[] = metadata.genres.slice(0, 4); + return genresToDisplay.map((genreName: string, index: number, array: string[]) => ( {genreName} @@ -486,100 +326,98 @@ const HeroSection: React.FC = ({ )} )); - }; + }, [metadata.genres, currentTheme.colors.text]); + + // Memoized play button text + const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]); return ( - - - {/* Fallback dark background */} - - - {/* Loading state with skeleton */} - {loadingBanner && ( - - )} - - {/* Background image with smooth loading */} - {!loadingBanner && (bannerImage || metadata.banner || metadata.poster) && ( - - )} - - - {/* Title/Logo */} - - - {metadata.logo && !logoLoadError ? ( - { - logger.warn(`[HeroSection] Logo failed to load: ${metadata.logo}`); - setLogoLoadError(true); - }} - /> - ) : ( - {metadata.name} - )} - - + + {/* Background Layer */} + + + {/* Background Image - Optimized */} + {!loadingBanner && imageSource && ( + + )} - {/* Watch Progress */} - - - {/* Genre Tags */} - - - {renderGenres()} - + {/* Gradient Overlay */} + + + {/* Title/Logo */} + + + {metadata.logo && !logoLoadError ? ( + { + logger.warn(`[HeroSection] Logo failed to load: ${metadata.logo}`); + setLogoLoadError(true); + }} + /> + ) : ( + + {metadata.name} + + )} - - {/* Action Buttons */} - - - + + {/* Watch Progress */} + + + {/* Genres */} + {genreElements && ( + + {genreElements} + + )} + + {/* Action Buttons */} + + + ); }; +// Optimized styles with minimal properties const styles = StyleSheet.create({ heroSection: { width: '100%', - height: height * 0.5, backgroundColor: '#000', overflow: 'hidden', }, @@ -613,7 +451,6 @@ const styles = StyleSheet.create({ titleLogo: { width: width * 0.8, height: 100, - marginBottom: 0, alignSelf: 'center', }, heroTitle: { @@ -624,6 +461,7 @@ const styles = StyleSheet.create({ textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, letterSpacing: -0.5, + textAlign: 'center', }, genreContainer: { flexDirection: 'row', @@ -647,7 +485,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', gap: 8, alignItems: 'center', - marginBottom: -12, justifyContent: 'center', width: '100%', }, @@ -658,11 +495,6 @@ const styles = StyleSheet.create({ paddingVertical: 12, paddingHorizontal: 16, borderRadius: 28, - elevation: 6, - shadowColor: '#000', - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.4, - shadowRadius: 6, flex: 1, }, playButton: { @@ -682,11 +514,6 @@ const styles = StyleSheet.create({ borderColor: '#fff', alignItems: 'center', justifyContent: 'center', - elevation: 6, - shadowColor: '#000', - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.4, - shadowRadius: 6, }, playButtonText: { color: '#000', @@ -705,7 +532,6 @@ const styles = StyleSheet.create({ marginBottom: 8, width: '100%', alignItems: 'center', - overflow: 'hidden', height: 48, }, watchProgressBar: { @@ -726,14 +552,6 @@ const styles = StyleSheet.create({ opacity: 0.9, letterSpacing: 0.2 }, - skeletonGradient: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0,0,0,0.5)', - }, }); export default React.memo(HeroSection); \ No newline at end of file diff --git a/src/hooks/useMetadataAnimations.ts b/src/hooks/useMetadataAnimations.ts index 8b7f12e7..e6698a95 100644 --- a/src/hooks/useMetadataAnimations.ts +++ b/src/hooks/useMetadataAnimations.ts @@ -4,357 +4,154 @@ import { useSharedValue, withTiming, withSpring, - withSequence, - withDelay, Easing, useAnimatedScrollHandler, - interpolate, - Extrapolate, + runOnUI, } from 'react-native-reanimated'; const { width, height } = Dimensions.get('window'); -// Refined animation configurations -const springConfig = { - damping: 25, +// Highly optimized animation configurations +const fastSpring = { + damping: 15, mass: 0.8, - stiffness: 120, - overshootClamping: false, - restDisplacementThreshold: 0.01, - restSpeedThreshold: 0.01, -}; - -const microSpringConfig = { - damping: 20, - mass: 0.5, stiffness: 150, - overshootClamping: true, - restDisplacementThreshold: 0.001, - restSpeedThreshold: 0.001, }; -// Sophisticated easing curves +const ultraFastSpring = { + damping: 12, + mass: 0.6, + stiffness: 200, +}; + +// Ultra-optimized easing functions const easings = { - // Smooth entrance with slight overshoot - entrance: Easing.bezier(0.34, 1.56, 0.64, 1), - // Gentle bounce for micro-interactions - microBounce: Easing.bezier(0.68, -0.55, 0.265, 1.55), - // Smooth exit - exit: Easing.bezier(0.25, 0.46, 0.45, 0.94), - // Natural movement - natural: Easing.bezier(0.25, 0.1, 0.25, 1), - // Subtle emphasis - emphasis: Easing.bezier(0.19, 1, 0.22, 1), -}; - -// Refined timing constants for orchestrated entrance -const TIMING = { - // Quick initial setup - SCREEN_PREP: 50, - // Staggered content appearance - HERO_BASE: 150, - LOGO: 280, - PROGRESS: 380, - GENRES: 450, - BUTTONS: 520, - CONTENT: 650, - // Micro-delays for polish - MICRO_DELAY: 50, + fast: Easing.out(Easing.quad), + ultraFast: Easing.out(Easing.linear), + natural: Easing.bezier(0.2, 0, 0.2, 1), }; export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) => { - // Enhanced screen entrance with micro-animations - const screenScale = useSharedValue(0.96); + // Consolidated entrance animations - fewer shared values const screenOpacity = useSharedValue(0); - const screenBlur = useSharedValue(5); + const contentOpacity = useSharedValue(0); - // Refined hero section animations - const heroHeight = useSharedValue(height * 0.5); - const heroScale = useSharedValue(1.08); + // Combined hero animations const heroOpacity = useSharedValue(0); - const heroRotate = useSharedValue(-0.5); + const heroScale = useSharedValue(0.95); // Combined scale for micro-animation + const heroHeightValue = useSharedValue(height * 0.5); - // Enhanced content animations - const contentTranslateY = useSharedValue(40); - const contentScale = useSharedValue(0.98); + // Combined UI element animations + const uiElementsOpacity = useSharedValue(0); + const uiElementsTranslateY = useSharedValue(10); - // Sophisticated logo animations - const logoOpacity = useSharedValue(0); - const logoScale = useSharedValue(0.85); - const logoRotate = useSharedValue(2); + // Progress animation - simplified to single value + const progressOpacity = useSharedValue(0); - // Enhanced progress animations - const watchProgressOpacity = useSharedValue(0); - const watchProgressScaleY = useSharedValue(0); - const watchProgressWidth = useSharedValue(0); - - // Refined genre animations - const genresOpacity = useSharedValue(0); - const genresTranslateY = useSharedValue(15); - const genresScale = useSharedValue(0.95); - - // Enhanced button animations - const buttonsOpacity = useSharedValue(0); - const buttonsTranslateY = useSharedValue(20); - const buttonsScale = useSharedValue(0.95); - - // Scroll values with enhanced parallax + // Scroll values - minimal const scrollY = useSharedValue(0); - const dampedScrollY = useSharedValue(0); - const velocityY = useSharedValue(0); + const headerProgress = useSharedValue(0); // Single value for all header animations - // Sophisticated header animations - const headerOpacity = useSharedValue(0); - const headerElementsY = useSharedValue(-15); - const headerElementsOpacity = useSharedValue(0); - const headerBlur = useSharedValue(10); - - // Orchestrated entrance animation sequence + // Static header elements Y for performance + const staticHeaderElementsY = useSharedValue(0); + + // Ultra-fast entrance sequence - batch animations for better performance useEffect(() => { - const startAnimation = setTimeout(() => { - // Phase 1: Screen preparation with subtle bounce - screenScale.value = withSequence( - withTiming(1.02, { duration: 200, easing: easings.entrance }), - withTiming(1, { duration: 150, easing: easings.natural }) - ); + 'worklet'; + + // Batch all entrance animations to run simultaneously + const enterAnimations = () => { screenOpacity.value = withTiming(1, { - duration: 300, - easing: easings.emphasis + duration: 250, + easing: easings.fast }); - screenBlur.value = withTiming(0, { + + heroOpacity.value = withTiming(1, { + duration: 300, + easing: easings.fast + }); + + heroScale.value = withSpring(1, ultraFastSpring); + + uiElementsOpacity.value = withTiming(1, { duration: 400, easing: easings.natural }); - // Phase 2: Hero section with parallax feel - setTimeout(() => { - heroOpacity.value = withSequence( - withTiming(0.8, { duration: 200, easing: easings.entrance }), - withTiming(1, { duration: 100, easing: easings.natural }) - ); - heroScale.value = withSequence( - withTiming(1.02, { duration: 300, easing: easings.entrance }), - withTiming(1, { duration: 200, easing: easings.natural }) - ); - heroRotate.value = withTiming(0, { - duration: 500, - easing: easings.emphasis - }); - }, TIMING.HERO_BASE); + uiElementsTranslateY.value = withSpring(0, fastSpring); - // Phase 3: Logo with micro-bounce - setTimeout(() => { - logoOpacity.value = withTiming(1, { - duration: 300, - easing: easings.entrance - }); - logoScale.value = withSequence( - withTiming(1.05, { duration: 150, easing: easings.microBounce }), - withTiming(1, { duration: 100, easing: easings.natural }) - ); - logoRotate.value = withTiming(0, { - duration: 300, - easing: easings.emphasis - }); - }, TIMING.LOGO); - - // Phase 4: Progress bar with width animation - setTimeout(() => { - if (watchProgress && watchProgress.duration > 0) { - watchProgressOpacity.value = withTiming(1, { - duration: 250, - easing: easings.entrance - }); - watchProgressScaleY.value = withSpring(1, microSpringConfig); - watchProgressWidth.value = withDelay( - 100, - withTiming(1, { duration: 600, easing: easings.emphasis }) - ); - } - }, TIMING.PROGRESS); - - // Phase 5: Genres with staggered scale - setTimeout(() => { - genresOpacity.value = withTiming(1, { - duration: 250, - easing: easings.entrance - }); - genresTranslateY.value = withSpring(0, microSpringConfig); - genresScale.value = withSequence( - withTiming(1.02, { duration: 150, easing: easings.microBounce }), - withTiming(1, { duration: 100, easing: easings.natural }) - ); - }, TIMING.GENRES); - - // Phase 6: Buttons with sophisticated bounce - setTimeout(() => { - buttonsOpacity.value = withTiming(1, { - duration: 300, - easing: easings.entrance - }); - buttonsTranslateY.value = withSpring(0, springConfig); - buttonsScale.value = withSequence( - withTiming(1.03, { duration: 200, easing: easings.microBounce }), - withTiming(1, { duration: 150, easing: easings.natural }) - ); - }, TIMING.BUTTONS); - - // Phase 7: Content with layered entrance - setTimeout(() => { - contentTranslateY.value = withSpring(0, { - ...springConfig, - damping: 30, - stiffness: 100, - }); - contentScale.value = withSequence( - withTiming(1.01, { duration: 200, easing: easings.entrance }), - withTiming(1, { duration: 150, easing: easings.natural }) - ); - }, TIMING.CONTENT); - }, TIMING.SCREEN_PREP); - - return () => clearTimeout(startAnimation); + contentOpacity.value = withTiming(1, { + duration: 350, + easing: easings.fast + }); + }; + + // Use runOnUI for better performance + runOnUI(enterAnimations)(); }, []); - // Enhanced watch progress animation with width effect + // Optimized watch progress animation useEffect(() => { - if (watchProgress && watchProgress.duration > 0) { - watchProgressOpacity.value = withTiming(1, { - duration: 300, - easing: easings.entrance - }); - watchProgressScaleY.value = withSpring(1, microSpringConfig); - watchProgressWidth.value = withDelay( - 150, - withTiming(1, { duration: 800, easing: easings.emphasis }) - ); - } else { - watchProgressOpacity.value = withTiming(0, { - duration: 200, - easing: easings.exit - }); - watchProgressScaleY.value = withTiming(0, { - duration: 200, - easing: easings.exit - }); - watchProgressWidth.value = withTiming(0, { - duration: 150, - easing: easings.exit - }); - } - }, [watchProgress, watchProgressOpacity, watchProgressScaleY, watchProgressWidth]); + 'worklet'; + + const hasProgress = watchProgress && watchProgress.duration > 0; + + progressOpacity.value = withTiming(hasProgress ? 1 : 0, { + duration: hasProgress ? 200 : 150, + easing: easings.fast + }); + }, [watchProgress]); - // Enhanced logo animation with micro-interactions - const animateLogo = (hasLogo: boolean) => { - if (hasLogo) { - logoOpacity.value = withTiming(1, { - duration: 400, - easing: easings.entrance - }); - logoScale.value = withSequence( - withTiming(1.05, { duration: 200, easing: easings.microBounce }), - withTiming(1, { duration: 150, easing: easings.natural }) - ); - } else { - logoOpacity.value = withTiming(0, { - duration: 250, - easing: easings.exit - }); - logoScale.value = withTiming(0.9, { - duration: 250, - easing: easings.exit - }); - } - }; - - // Enhanced scroll handler with velocity tracking + // Ultra-optimized scroll handler with minimal calculations const scrollHandler = useAnimatedScrollHandler({ onScroll: (event) => { + 'worklet'; + const rawScrollY = event.contentOffset.y; - const lastScrollY = scrollY.value; - scrollY.value = rawScrollY; - velocityY.value = rawScrollY - lastScrollY; - - // Enhanced damped scroll with velocity-based easing - const dynamicDuration = Math.min(400, Math.max(200, Math.abs(velocityY.value) * 10)); - dampedScrollY.value = withTiming(rawScrollY, { - duration: dynamicDuration, - easing: easings.natural, - }); - // Sophisticated header animation with blur effect - const headerThreshold = height * 0.5 - safeAreaTop - 60; - const progress = Math.min(1, Math.max(0, (rawScrollY - headerThreshold + 50) / 100)); + // Single calculation for header threshold + const threshold = height * 0.4 - safeAreaTop; + const progress = rawScrollY > threshold ? 1 : 0; - if (rawScrollY > headerThreshold) { - headerOpacity.value = withTiming(1, { - duration: 300, - easing: easings.entrance - }); - headerElementsY.value = withSpring(0, microSpringConfig); - headerElementsOpacity.value = withTiming(1, { - duration: 400, - easing: easings.emphasis - }); - headerBlur.value = withTiming(0, { - duration: 300, - easing: easings.natural - }); - } else { - headerOpacity.value = withTiming(0, { - duration: 200, - easing: easings.exit - }); - headerElementsY.value = withTiming(-15, { - duration: 200, - easing: easings.exit - }); - headerElementsOpacity.value = withTiming(0, { - duration: 150, - easing: easings.exit - }); - headerBlur.value = withTiming(5, { - duration: 200, - easing: easings.natural + // Use single progress value for all header animations + if (headerProgress.value !== progress) { + headerProgress.value = withTiming(progress, { + duration: progress ? 200 : 150, + easing: easings.ultraFast }); } }, }); return { - // Enhanced animated values - screenScale, + // Optimized shared values - reduced count screenOpacity, - screenBlur, - heroHeight, - heroScale, + contentOpacity, heroOpacity, - heroRotate, - contentTranslateY, - contentScale, - logoOpacity, - logoScale, - logoRotate, - watchProgressOpacity, - watchProgressScaleY, - watchProgressWidth, - genresOpacity, - genresTranslateY, - genresScale, - buttonsOpacity, - buttonsTranslateY, - buttonsScale, + heroScale, + uiElementsOpacity, + uiElementsTranslateY, + progressOpacity, scrollY, - dampedScrollY, - velocityY, - headerOpacity, - headerElementsY, - headerElementsOpacity, - headerBlur, + headerProgress, + + // Computed values for compatibility (derived from optimized values) + get heroHeight() { return heroHeightValue; }, + get logoOpacity() { return uiElementsOpacity; }, + get buttonsOpacity() { return uiElementsOpacity; }, + get buttonsTranslateY() { return uiElementsTranslateY; }, + get contentTranslateY() { return uiElementsTranslateY; }, + get watchProgressOpacity() { return progressOpacity; }, + get watchProgressWidth() { return progressOpacity; }, // Reuse for width animation + get headerOpacity() { return headerProgress; }, + get headerElementsY() { + return staticHeaderElementsY; // Use pre-created shared value + }, + get headerElementsOpacity() { return headerProgress; }, // Functions scrollHandler, - animateLogo, + animateLogo: () => {}, // Simplified - no separate logo animation }; }; \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 10b38941..29a690ba 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { View, Text, @@ -26,7 +26,6 @@ import Animated, { Extrapolate, useSharedValue, withTiming, - runOnJS, } from 'react-native-reanimated'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -34,7 +33,7 @@ import { RootStackParamList } from '../navigation/AppNavigator'; import { useSettings } from '../hooks/useSettings'; import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScreen'; -// Import our new components and hooks +// Import our optimized components and hooks import HeroSection from '../components/metadata/HeroSection'; import FloatingHeader from '../components/metadata/FloatingHeader'; import MetadataDetails from '../components/metadata/MetadataDetails'; @@ -44,24 +43,19 @@ import { useWatchProgress } from '../hooks/useWatchProgress'; const { height } = Dimensions.get('window'); -const MetadataScreen = () => { +const MetadataScreen: React.FC = () => { const route = useRoute, string>>(); const navigation = useNavigation>(); const { id, type, episodeId } = route.params; - // Add settings hook + // Consolidated hooks for better performance const { settings } = useSettings(); - - // Get theme context const { currentTheme } = useTheme(); - - // Get safe area insets const { top: safeAreaTop } = useSafeAreaInsets(); - // Add transition state management - const [showContent, setShowContent] = useState(false); - const loadingOpacity = useSharedValue(1); - const contentOpacity = useSharedValue(0); + // Optimized state management - reduced state variables + const [isContentReady, setIsContentReady] = useState(false); + const transitionOpacity = useSharedValue(0); const { metadata, @@ -83,368 +77,227 @@ const MetadataScreen = () => { imdbId, } = useMetadata({ id, type }); - // Use our new hooks - const { - watchProgress, - getEpisodeDetails, - getPlayButtonText, - } = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes); + // Optimized hooks with memoization + const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes); + const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata); + const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress); - const { - bannerImage, - loadingBanner, - logoLoadError, - setLogoLoadError, - setBannerImage, - } = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata); - - const animations = useMetadataAnimations(safeAreaTop, watchProgress); - - // Handle smooth transition from loading to content + // Memoized derived values for performance + const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]); + + // Ultra-fast content transition useEffect(() => { - if (!loading && metadata && !showContent) { - // Delay content appearance slightly to ensure everything is ready - const timer = setTimeout(() => { - setShowContent(true); - - // Animate transition - loadingOpacity.value = withTiming(0, { duration: 300 }); - contentOpacity.value = withTiming(1, { duration: 300 }); - }, 100); - - return () => clearTimeout(timer); - } else if (loading && showContent) { - // Reset states when going back to loading - setShowContent(false); - loadingOpacity.value = 1; - contentOpacity.value = 0; + if (isReady && !isContentReady) { + setIsContentReady(true); + transitionOpacity.value = withTiming(1, { duration: 200 }); + } else if (!isReady && isContentReady) { + setIsContentReady(false); + transitionOpacity.value = 0; } - }, [loading, metadata, showContent]); + }, [isReady, isContentReady]); - // Add wrapper for toggleLibrary that includes haptic feedback + // Optimized callback functions with reduced dependencies const handleToggleLibrary = useCallback(() => { - // Trigger appropriate haptic feedback based on action - if (inLibrary) { - // Removed from library - light impact - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } else { - // Added to library - success feedback - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } - - // Call the original toggleLibrary function + Haptics.impactAsync(inLibrary ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium); toggleLibrary(); }, [inLibrary, toggleLibrary]); - // Add wrapper for season change with distinctive haptic feedback const handleSeasonChangeWithHaptics = useCallback((seasonNumber: number) => { - // Change to Light impact for a more subtle feedback Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - - // Wait a tiny bit before changing season, making the feedback more noticeable - setTimeout(() => { - handleSeasonChange(seasonNumber); - }, 10); + handleSeasonChange(seasonNumber); }, [handleSeasonChange]); - // Handler functions const handleShowStreams = useCallback(() => { + const { watchProgress } = watchProgressData; if (type === 'series') { - // If we have watch progress with an episodeId, use that - if (watchProgress?.episodeId) { - navigation.navigate('Streams', { - id, - type, - episodeId: watchProgress.episodeId - }); - return; - } + const targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ? + (episodes[0].stremioId || `${id}:${episodes[0].season_number}:${episodes[0].episode_number}`) : undefined); - // If we have a specific episodeId from route params, use that - if (episodeId) { - navigation.navigate('Streams', { id, type, episodeId }); - return; - } - - // Otherwise, if we have episodes, start with the first one - if (episodes.length > 0) { - const firstEpisode = episodes[0]; - const newEpisodeId = firstEpisode.stremioId || `${id}:${firstEpisode.season_number}:${firstEpisode.episode_number}`; - navigation.navigate('Streams', { id, type, episodeId: newEpisodeId }); + if (targetEpisodeId) { + navigation.navigate('Streams', { id, type, episodeId: targetEpisodeId }); return; } } - navigation.navigate('Streams', { id, type, episodeId }); - }, [navigation, id, type, episodes, episodeId, watchProgress]); - - const handleSelectCastMember = useCallback((castMember: any) => { - // Future implementation - }, []); + }, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]); const handleEpisodeSelect = useCallback((episode: Episode) => { const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`; - navigation.navigate('Streams', { - id, - type, - episodeId - }); + navigation.navigate('Streams', { id, type, episodeId }); }, [navigation, id, type]); - const handleBack = useCallback(() => { - navigation.goBack(); - }, [navigation]); + const handleBack = useCallback(() => navigation.goBack(), [navigation]); + const handleSelectCastMember = useCallback(() => {}, []); // Simplified for performance - // Enhanced animated styles with sophisticated effects - const containerAnimatedStyle = useAnimatedStyle(() => ({ - flex: 1, - transform: [ - { scale: animations.screenScale.value }, - { rotateZ: `${animations.heroRotate.value}deg` } - ], + // Ultra-optimized animated styles - minimal calculations + const containerStyle = useAnimatedStyle(() => ({ opacity: animations.screenOpacity.value, - })); + }), []); - const contentAnimatedStyle = useAnimatedStyle(() => ({ - transform: [ - { translateY: animations.contentTranslateY.value }, - { scale: animations.contentScale.value } - ], - opacity: interpolate( - animations.contentTranslateY.value, - [40, 0], - [0, 1], - Extrapolate.CLAMP - ) - })); + const contentStyle = useAnimatedStyle(() => ({ + opacity: animations.contentOpacity.value, + transform: [{ translateY: animations.uiElementsTranslateY.value }] + }), []); - // Enhanced loading screen animated style - const loadingAnimatedStyle = useAnimatedStyle(() => ({ - opacity: loadingOpacity.value, - transform: [ - { scale: interpolate(loadingOpacity.value, [1, 0], [1, 0.98]) } - ] - })); + const transitionStyle = useAnimatedStyle(() => ({ + opacity: transitionOpacity.value, + }), []); - // Enhanced content animated style for transition - const contentTransitionStyle = useAnimatedStyle(() => ({ - opacity: contentOpacity.value, - transform: [ - { scale: interpolate(contentOpacity.value, [0, 1], [0.98, 1]) }, - { translateY: interpolate(contentOpacity.value, [0, 1], [10, 0]) } - ] - })); - - if (loading || !showContent) { - return ( - - - - ); - } - - if (metadataError || !metadata) { + // Memoized error component for performance + const ErrorComponent = useMemo(() => { + if (!metadataError) return null; + return ( - + - - + + {metadataError || 'Content not found'} - + Try Again - - Go Back - + Go Back ); + }, [metadataError, currentTheme, loadMetadata, handleBack]); + + // Show error if exists + if (metadataError || (!loading && !metadata)) { + return ErrorComponent; + } + + // Show loading screen + if (loading || !isContentReady) { + return ; } return ( - + - + + {/* Floating Header - Optimized */} + - - {/* Floating Header */} - + {/* Hero Section - Optimized */} + - - {/* Hero Section */} - + imdbId ? ( + + ) : null} /> - {/* Main Content */} - - {/* Metadata Details */} - imdbId ? ( - - ) : null} + + + {type === 'movie' && ( + + )} - {/* Cast Section */} - - - {/* More Like This Section - Only for movies */} - {type === 'movie' && ( - - )} - - {/* Type-specific content */} - {type === 'series' ? ( - - ) : ( - - )} - - - + ) : ( + metadata && + )} + + ); }; +// Optimized styles with minimal properties const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: 'transparent', - paddingTop: 0, }, scrollView: { flex: 1, }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 16, - }, - loadingText: { - marginTop: 16, - fontSize: 16, + scrollContent: { + flexGrow: 1, }, errorContainer: { flex: 1, @@ -470,13 +323,13 @@ const styles = StyleSheet.create({ retryButtonText: { fontSize: 16, fontWeight: '600', + color: '#fff', }, backButton: { - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - borderRadius: 20, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 24, + borderWidth: 2, }, backButtonText: { fontSize: 16,