From 332cf99f67582bca7bd2fc3ddbcca5b640cea0af Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 2 Aug 2025 14:26:47 +0530 Subject: [PATCH] removed reanimated --- App.tsx | 2 +- package.json | 2 +- .../home/ContinueWatchingSection.tsx | 14 +- src/components/home/DropUpMenu.tsx | 215 ++- src/components/home/FeaturedContent.tsx | 656 +++------ src/components/home/ThisWeekSection.tsx | 15 +- src/components/metadata/CastDetailsModal.tsx | 713 +++++----- src/components/metadata/FloatingHeader.tsx | 39 +- src/components/metadata/HeroSection.tsx | 1184 +++++------------ .../player/modals/SubtitleModals.tsx | 21 +- src/hooks/useMetadataAnimations.ts | 202 --- src/screens/LibraryScreen.tsx | 205 ++- src/screens/MetadataScreen.tsx | 56 +- src/screens/OnboardingScreen.tsx | 38 +- 14 files changed, 1100 insertions(+), 2262 deletions(-) delete mode 100644 src/hooks/useMetadataAnimations.ts diff --git a/App.tsx b/App.tsx index 82d898f..ec4762d 100644 --- a/App.tsx +++ b/App.tsx @@ -19,7 +19,7 @@ import AppNavigator, { CustomNavigationDarkTheme, CustomDarkTheme } from './src/navigation/AppNavigator'; -import 'react-native-reanimated'; +// Removed react-native-reanimated import import { CatalogProvider } from './src/contexts/CatalogContext'; import { GenreProvider } from './src/contexts/GenreContext'; import { TraktProvider } from './src/contexts/TraktContext'; diff --git a/package.json b/package.json index 25b535a..a83f5fc 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react-native-gesture-handler": "~2.20.2", "react-native-immersive-mode": "^2.0.2", "react-native-paper": "^5.13.1", - "react-native-reanimated": "~3.6.0", + "react-native-reanimated": "@latest", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-svg": "^15.11.2", diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 9ed120f..8244ba5 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -11,7 +11,7 @@ import { Alert, ActivityIndicator } from 'react-native'; -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +// Removed react-native-reanimated import import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; @@ -572,7 +572,7 @@ const ContinueWatchingSection = React.forwardRef((props, re } return ( - + Continue Watching @@ -609,13 +609,9 @@ const ContinueWatchingSection = React.forwardRef((props, re {/* Delete Indicator Overlay */} {deletingItemId === item.id && ( - + - + )} @@ -700,7 +696,7 @@ const ContinueWatchingSection = React.forwardRef((props, re snapToAlignment="start" ItemSeparatorComponent={() => } /> - + ); }); diff --git a/src/components/home/DropUpMenu.tsx b/src/components/home/DropUpMenu.tsx index 7431655..27300d2 100644 --- a/src/components/home/DropUpMenu.tsx +++ b/src/components/home/DropUpMenu.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { View, Text, @@ -13,19 +13,6 @@ import { import { MaterialIcons } from '@expo/vector-icons'; import { Image as ExpoImage } from 'expo-image'; import { colors } from '../../styles/colors'; -import Animated, { - useAnimatedStyle, - withTiming, - useSharedValue, - interpolate, - Extrapolate, - runOnJS, -} from 'react-native-reanimated'; -import { - Gesture, - GestureDetector, - GestureHandlerRootView, -} from 'react-native-gesture-handler'; import { StreamingContent } from '../../services/catalogService'; interface DropUpMenuProps { @@ -36,144 +23,92 @@ interface DropUpMenuProps { } export const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { - const translateY = useSharedValue(300); - const opacity = useSharedValue(0); const isDarkMode = useColorScheme() === 'dark'; - const SNAP_THRESHOLD = 100; - - useEffect(() => { - if (visible) { - opacity.value = withTiming(1, { duration: 200 }); - translateY.value = withTiming(0, { duration: 300 }); - } else { - opacity.value = withTiming(0, { duration: 200 }); - translateY.value = withTiming(300, { duration: 300 }); - } - }, [visible]); - - const gesture = Gesture.Pan() - .onStart(() => { - // Store initial position if needed - }) - .onUpdate((event) => { - if (event.translationY > 0) { // Only allow dragging downwards - translateY.value = event.translationY; - opacity.value = interpolate( - event.translationY, - [0, 300], - [1, 0], - Extrapolate.CLAMP - ); - } - }) - .onEnd((event) => { - if (event.translationY > SNAP_THRESHOLD || event.velocityY > 500) { - translateY.value = withTiming(300, { duration: 300 }); - opacity.value = withTiming(0, { duration: 200 }); - runOnJS(onClose)(); - } else { - translateY.value = withTiming(0, { duration: 300 }); - opacity.value = withTiming(1, { duration: 200 }); - } - }); - - const overlayStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - const menuStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: translateY.value }], - borderTopLeftRadius: 24, - borderTopRightRadius: 24, - })); const menuOptions = [ - { - icon: item.inLibrary ? 'bookmark' : 'bookmark-border', - label: item.inLibrary ? 'Remove from Library' : 'Add to Library', - action: 'library' - }, - { - icon: 'check-circle', - label: 'Mark as Watched', - action: 'watched' - }, - { - icon: 'playlist-add', - label: 'Add to Playlist', - action: 'playlist' - }, - { - icon: 'share', - label: 'Share', - action: 'share' - } + { id: 'play', label: 'Play', icon: 'play-arrow' }, + { id: 'info', label: 'More Info', icon: 'info-outline' }, + { id: 'save', label: 'Add to My List', icon: 'bookmark-border' }, + { id: 'share', label: 'Share', icon: 'share' }, ]; - const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF'; + const handleOptionPress = (optionId: string) => { + onOptionSelect(optionId); + onClose(); + }; return ( - - - - - - - - + + + + {/* Drag Handle */} + + + {/* Header with item info */} + + + + + {item.name} + + {item.year && ( + + {item.year} + + )} + + + + {/* Menu Options */} + + {menuOptions.map((option, index) => ( + handleOptionPress(option.id)} + activeOpacity={0.7} + > + - - - {item.name} - - {item.year && ( - - {item.year} - - )} - - - - {menuOptions.map((option, index) => ( - { - onOptionSelect(option.action); - onClose(); - }} - > - - - {option.label} - - - ))} - - - - - + + {option.label} + + + ))} + + + ); }; @@ -254,4 +189,4 @@ const styles = StyleSheet.create({ }, }); -export default DropUpMenu; \ No newline at end of file +export default DropUpMenu; \ No newline at end of file diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 8a6dc27..ca478cd 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -17,14 +17,6 @@ import { RootStackParamList } from '../../navigation/AppNavigator'; import { LinearGradient } from 'expo-linear-gradient'; import { Image as ExpoImage } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, - useAnimatedStyle, - useSharedValue, - withTiming, - Easing, - withDelay -} from 'react-native-reanimated'; import { StreamingContent } from '../../services/catalogService'; import { SkeletonFeatured } from './SkeletonLoaders'; import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from '../../utils/logoUtils'; @@ -49,70 +41,18 @@ const NoFeaturedContent = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); - const styles = StyleSheet.create({ - noContentContainer: { - height: height * 0.55, - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 40, - backgroundColor: currentTheme.colors.elevation1, - borderRadius: 12, - marginBottom: 12, - }, - noContentTitle: { - fontSize: 22, - fontWeight: 'bold', - color: currentTheme.colors.highEmphasis, - marginTop: 16, - marginBottom: 8, - textAlign: 'center', - }, - noContentText: { - fontSize: 16, - color: currentTheme.colors.mediumEmphasis, - textAlign: 'center', - marginBottom: 24, - }, - noContentButtons: { - flexDirection: 'row', - justifyContent: 'center', - gap: 16, - width: '100%', - }, - noContentButton: { - paddingVertical: 12, - paddingHorizontal: 20, - borderRadius: 30, - backgroundColor: currentTheme.colors.elevation3, - alignItems: 'center', - justifyContent: 'center' - }, - noContentButtonText: { - color: currentTheme.colors.highEmphasis, - fontWeight: '600', - fontSize: 14, - } - }); - return ( - - - No Featured Content - - Install addons with catalogs or change the content source in your settings. - - + + + + + No featured content available + navigation.navigate('Addons')} + style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]} + onPress={() => navigation.navigate('Search')} > - Install Addons - - navigation.navigate('HomeScreenSettings')} - > - Settings + Explore Content @@ -122,456 +62,193 @@ const NoFeaturedContent = () => { const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); - const [bannerUrl, setBannerUrl] = useState(null); - const [logoUrl, setLogoUrl] = useState(null); - const [logoLoaded, setLogoLoaded] = useState(false); - const [bannerLoaded, setBannerLoaded] = useState(false); - const [showSkeleton, setShowSkeleton] = useState(true); - const [logoError, setLogoError] = useState(false); - const [bannerError, setBannerError] = useState(false); const { settings } = useSettings(); - const logoOpacity = useSharedValue(0); - const bannerOpacity = useSharedValue(0); - const posterOpacity = useSharedValue(0); - const prevContentIdRef = useRef(null); - // Add state for tracking logo load errors - const [logoLoadError, setLogoLoadError] = useState(false); - // Add a ref to track logo fetch in progress - const logoFetchInProgress = useRef(false); + const [logoUrl, setLogoUrl] = useState(null); + const [isLogoLoading, setIsLogoLoading] = useState(false); + const [imageLoaded, setImageLoaded] = useState(false); + const [imageError, setImageError] = useState(false); + // Removed TMDB service integration - // Enhanced poster transition animations - const posterScale = useSharedValue(1); - const posterTranslateY = useSharedValue(0); - const overlayOpacity = useSharedValue(0.15); - - // Animation values - const posterAnimatedStyle = useAnimatedStyle(() => ({ - opacity: posterOpacity.value, - transform: [ - { scale: posterScale.value }, - { translateY: posterTranslateY.value } - ], - })); - - const logoAnimatedStyle = useAnimatedStyle(() => ({ - opacity: logoOpacity.value, - })); - - const contentOpacity = useSharedValue(1); // Start visible - const buttonsOpacity = useSharedValue(1); - - const contentAnimatedStyle = useAnimatedStyle(() => ({ - opacity: contentOpacity.value, - })); - - const buttonsAnimatedStyle = useAnimatedStyle(() => ({ - opacity: buttonsOpacity.value, - })); - - const overlayAnimatedStyle = useAnimatedStyle(() => ({ - opacity: overlayOpacity.value, - })); - - // Preload the image - const preloadImage = async (url: string): Promise => { - // Skip if already cached to prevent redundant prefetch - if (imageCache[url]) return true; - - try { - // Simplified validation to reduce CPU overhead - if (!url || typeof url !== 'string') return false; - - // Use our optimized cache service instead of direct prefetch - await imageCacheService.getCachedImageUrl(url); - imageCache[url] = true; - return true; - } catch (error) { - // Clear any partial cache entry on error - delete imageCache[url]; - return false; + // Preload image when component mounts + useEffect(() => { + if (featuredContent?.poster && !imageCache[featuredContent.poster]) { + const preloadImage = async () => { + try { + await imageCacheService.getCachedImageUrl(featuredContent.poster!); + imageCache[featuredContent.poster!] = true; + } catch (error) { + logger.error('Failed to preload featured image:', error); + } + }; + preloadImage(); } - }; + }, [featuredContent?.poster]); - // Reset logo error state when content changes + // TMDB data fetching removed due to API limitations + + // Fetch logo when featured content changes useEffect(() => { - setLogoLoadError(false); - }, [featuredContent?.id]); - - // Fetch logo based on preference - useEffect(() => { - if (!featuredContent || logoFetchInProgress.current) return; - const fetchLogo = async () => { - logoFetchInProgress.current = true; - + if (!featuredContent || isLogoLoading) return; + + setIsLogoLoading(true); + setLogoUrl(null); + try { - const contentId = featuredContent.id; - const contentData = featuredContent; // Use a clearer variable name - const currentLogo = contentData.logo; - - // Get preferences - const logoPreference = settings.logoSourcePreference || 'metahub'; - const preferredLanguage = settings.tmdbLanguagePreference || 'en'; - - // Reset state for new fetch - setLogoUrl(null); - setLogoLoadError(false); - - // Extract IDs - let imdbId: string | null = null; - if (contentData.id.startsWith('tt')) { - imdbId = contentData.id; - } else if ((contentData as any).imdbId) { - imdbId = (contentData as any).imdbId; - } else if ((contentData as any).externalIds?.imdb_id) { - imdbId = (contentData as any).externalIds.imdb_id; + // Use existing logo logic + if (featuredContent.logo) { + setLogoUrl(featuredContent.logo); } - - let tmdbId: string | null = null; - if (contentData.id.startsWith('tmdb:')) { - tmdbId = contentData.id.split(':')[1]; - } else if ((contentData as any).tmdb_id) { - tmdbId = String((contentData as any).tmdb_id); - } - - // If we only have IMDB ID, try to find TMDB ID proactively - if (imdbId && !tmdbId) { - try { - const tmdbService = TMDBService.getInstance(); - const foundData = await tmdbService.findTMDBIdByIMDB(imdbId); - if (foundData) { - tmdbId = String(foundData); - } - } catch (findError) { - // logger.warn(`[FeaturedContent] Failed to find TMDB ID for ${imdbId}:`, findError); - } - } - - const tmdbType = contentData.type === 'series' ? 'tv' : 'movie'; - let finalLogoUrl: string | null = null; - let primaryAttempted = false; - let fallbackAttempted = false; - - // --- Logo Fetching Logic --- - - if (logoPreference === 'metahub') { - // Primary: Metahub (needs imdbId) - if (imdbId) { - primaryAttempted = true; - const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; - try { - const response = await fetch(metahubUrl, { method: 'HEAD' }); - if (response.ok) { - finalLogoUrl = metahubUrl; - } - } catch (error) { /* Log if needed */ } - } - - // Fallback: TMDB (needs tmdbId) - if (!finalLogoUrl && tmdbId) { - fallbackAttempted = true; - try { - const tmdbService = TMDBService.getInstance(); - const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage); - if (logoUrl) { - finalLogoUrl = logoUrl; - } - } catch (error) { /* Log if needed */ } - } - - } else { // logoPreference === 'tmdb' - // Primary: TMDB (needs tmdbId) - if (tmdbId) { - primaryAttempted = true; - try { - const tmdbService = TMDBService.getInstance(); - const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage); - if (logoUrl) { - finalLogoUrl = logoUrl; - } - } catch (error) { /* Log if needed */ } - } - - // Fallback: Metahub (needs imdbId) - if (!finalLogoUrl && imdbId) { - fallbackAttempted = true; - const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; - try { - const response = await fetch(metahubUrl, { method: 'HEAD' }); - if (response.ok) { - finalLogoUrl = metahubUrl; - } - } catch (error) { /* Log if needed */ } - } - } - - // --- Set Final Logo --- - if (finalLogoUrl) { - setLogoUrl(finalLogoUrl); - } else if (currentLogo) { - // Use existing logo only if primary and fallback failed or weren't applicable - setLogoUrl(currentLogo); - } else { - // No logo found from any source - setLogoLoadError(true); - // logger.warn(`[FeaturedContent] No logo found for ${contentData.name} (${contentId}) with preference ${logoPreference}. Primary attempted: ${primaryAttempted}, Fallback attempted: ${fallbackAttempted}`); - } - } catch (error) { - // logger.error('[FeaturedContent] Error in fetchLogo:', error); - setLogoLoadError(true); + logger.error('Error fetching logo:', error); } finally { - logoFetchInProgress.current = false; + setIsLogoLoading(false); } }; - // Trigger fetch when content changes fetchLogo(); - }, [featuredContent, settings.logoSourcePreference, settings.tmdbLanguagePreference]); + }, [featuredContent]); - // Load poster and logo - useEffect(() => { - if (!featuredContent) return; - - const posterUrl = featuredContent.banner || featuredContent.poster; - const contentId = featuredContent.id; - const isContentChange = contentId !== prevContentIdRef.current; - - // Enhanced content change detection and animations - if (isContentChange) { - // Animate out current content - if (prevContentIdRef.current) { - posterOpacity.value = withTiming(0, { - duration: 300, - easing: Easing.out(Easing.cubic) - }); - posterScale.value = withTiming(0.95, { - duration: 300, - easing: Easing.out(Easing.cubic) - }); - overlayOpacity.value = withTiming(0.6, { - duration: 300, - easing: Easing.out(Easing.cubic) - }); - contentOpacity.value = withTiming(0.3, { - duration: 200, - easing: Easing.out(Easing.cubic) - }); - buttonsOpacity.value = withTiming(0.3, { - duration: 200, - easing: Easing.out(Easing.cubic) - }); - } else { - // Initial load - start from 0 - posterOpacity.value = 0; - posterScale.value = 1.1; - overlayOpacity.value = 0; - contentOpacity.value = 0; - buttonsOpacity.value = 0; - } - logoOpacity.value = 0; + const handlePlayPress = () => { + if (featuredContent) { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); } - - prevContentIdRef.current = contentId; - - // Set poster URL for immediate display - if (posterUrl) setBannerUrl(posterUrl); - - // Load images with enhanced animations - const loadImages = async () => { - // Small delay to allow fade out animation to complete - await new Promise(resolve => setTimeout(resolve, isContentChange && prevContentIdRef.current ? 300 : 0)); - - // Load poster with enhanced transition - if (posterUrl) { - const posterSuccess = await preloadImage(posterUrl); - if (posterSuccess) { - // Animate in new poster with scale and fade - posterScale.value = withTiming(1, { - duration: 800, - easing: Easing.out(Easing.cubic) - }); - posterOpacity.value = withTiming(1, { - duration: 700, - easing: Easing.out(Easing.cubic) - }); - overlayOpacity.value = withTiming(0.15, { - duration: 600, - easing: Easing.out(Easing.cubic) - }); - - // Animate content back in with delay - contentOpacity.value = withDelay(200, withTiming(1, { - duration: 600, - easing: Easing.out(Easing.cubic) - })); - buttonsOpacity.value = withDelay(400, withTiming(1, { - duration: 500, - easing: Easing.out(Easing.cubic) - })); - } - } - - // Load logo if available with enhanced timing - if (logoUrl) { - const logoSuccess = await preloadImage(logoUrl); - if (logoSuccess) { - logoOpacity.value = withDelay(500, withTiming(1, { - duration: 600, - easing: Easing.out(Easing.cubic) - })); - } else { - setLogoLoadError(true); - } - } - }; - - loadImages(); - }, [featuredContent?.id, logoUrl]); - - const onLogoLoadError = () => { - setLogoLoaded(true); // Treat error as "loaded" to stop spinner - setLogoError(true); }; const handleInfoPress = () => { if (featuredContent) { navigation.navigate('Metadata', { - id: featuredContent.id, - type: featuredContent.type - }); + id: featuredContent.id, + type: featuredContent.type + }); } }; + const formatGenres = (genres: string[] | undefined) => { + if (!genres || genres.length === 0) return ''; + return genres.slice(0, 3).join(' • '); + }; + if (!featuredContent) { return ; } + const posterUrl = featuredContent.poster; + const formattedGenres = formatGenres(featuredContent.genres); + return ( - - { - navigation.navigate('Metadata', { - id: featuredContent.id, - type: featuredContent.type - }); - }} - style={styles.featuredContainer as ViewStyle} + + {/* Background Image */} + + {posterUrl && !imageError ? ( + setImageLoaded(true)} + onError={() => setImageError(true)} + placeholder={{ uri: 'https://via.placeholder.com/400x600' }} + placeholderContentFit="cover" + /> + ) : ( + + + + )} + + + {/* Content Overlay */} + + + {/* Gradient Overlay */} + - - - {/* Subtle content overlay for better readability */} - + + {/* Logo or Title */} + {logoUrl && !isLogoLoading ? ( + + ) : ( + + {featuredContent.name} + + )} - + + {formattedGenres} + + + )} + + {/* Action Buttons */} + + {/* Play Button */} + - - {logoUrl && !logoLoadError ? ( - - - - ) : ( - - {featuredContent.name} - - )} - - {featuredContent.genres?.slice(0, 3).map((genre, index, array) => ( - - - {genre} - - {index < array.length - 1 && ( - - )} - - ))} - - + + Play + - - - - - {isSaved ? "Saved" : "Save"} - - + {/* My List Button */} + + + + {isSaved ? 'Saved' : 'My List'} + + - { - if (featuredContent) { - navigation.navigate('Streams', { - id: featuredContent.id, - type: featuredContent.type - }); - } - }} - activeOpacity={0.8} - > - - - Play - - - - - - - Info - - - - - - - - + {/* Info Button */} + + + Info + + + + + ); }; const styles = StyleSheet.create({ featuredContainer: { width: '100%', - height: height * 0.55, // Slightly taller for better proportions + height: height * 0.55, marginTop: 0, marginBottom: 12, position: 'relative', @@ -596,7 +273,7 @@ const styles = StyleSheet.create({ featuredImage: { width: '100%', height: '100%', - transform: [{ scale: 1.05 }], // Subtle zoom for depth + transform: [{ scale: 1.05 }], }, backgroundFallback: { position: 'absolute', @@ -724,6 +401,23 @@ const styles = StyleSheet.create({ zIndex: 1, pointerEvents: 'none', }, + noContentText: { + fontSize: 16, + fontWeight: '500', + marginTop: 16, + marginBottom: 20, + textAlign: 'center', + }, + exploreButton: { + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + exploreButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, }); -export default React.memo(FeaturedContent); \ No newline at end of file +export default React.memo(FeaturedContent); \ No newline at end of file diff --git a/src/components/home/ThisWeekSection.tsx b/src/components/home/ThisWeekSection.tsx index 892ffff..741c359 100644 --- a/src/components/home/ThisWeekSection.tsx +++ b/src/components/home/ThisWeekSection.tsx @@ -20,7 +20,7 @@ import { tmdbService } from '../../services/tmdbService'; import { useLibrary } from '../../hooks/useLibrary'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns'; -import Animated, { FadeIn, FadeInRight } from 'react-native-reanimated'; +// Removed react-native-reanimated import import { useCalendarData } from '../../hooks/useCalendarData'; const { width } = Dimensions.get('window'); @@ -109,10 +109,7 @@ export const ThisWeekSection = React.memo(() => { item.poster); return ( - + { - + ); }; return ( - + This Week @@ -206,7 +203,7 @@ export const ThisWeekSection = React.memo(() => { snapToAlignment="start" ItemSeparatorComponent={() => } /> - + ); }); @@ -337,4 +334,4 @@ const styles = StyleSheet.create({ marginLeft: 6, letterSpacing: 0.3, }, -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/components/metadata/CastDetailsModal.tsx b/src/components/metadata/CastDetailsModal.tsx index a1c0104..2d95828 100644 --- a/src/components/metadata/CastDetailsModal.tsx +++ b/src/components/metadata/CastDetailsModal.tsx @@ -7,19 +7,11 @@ import { ActivityIndicator, Dimensions, Platform, + Modal, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import { Image } from 'expo-image'; -import Animated, { - FadeIn, - FadeOut, - useAnimatedStyle, - useSharedValue, - withTiming, - withSpring, - runOnJS, -} from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { useTheme } from '../../contexts/ThemeContext'; import { Cast } from '../../types/cast'; @@ -54,419 +46,360 @@ export const CastDetailsModal: React.FC = ({ const { currentTheme } = useTheme(); const [personDetails, setPersonDetails] = useState(null); const [loading, setLoading] = useState(false); - const [hasFetched, setHasFetched] = useState(false); - const modalOpacity = useSharedValue(0); - const modalScale = useSharedValue(0.9); + const [error, setError] = useState(null); useEffect(() => { - if (visible && castMember) { - modalOpacity.value = withTiming(1, { duration: 250 }); - modalScale.value = withSpring(1, { damping: 20, stiffness: 200 }); - - if (!hasFetched || personDetails?.id !== castMember.id) { - fetchPersonDetails(); - } - } else { - modalOpacity.value = withTiming(0, { duration: 200 }); - modalScale.value = withTiming(0.9, { duration: 200 }); - - if (!visible) { - setHasFetched(false); - setPersonDetails(null); - } + if (visible && castMember?.id) { + fetchPersonDetails(); } - }, [visible, castMember]); + }, [visible, castMember?.id]); const fetchPersonDetails = async () => { - if (!castMember || loading) return; + if (!castMember?.id) return; setLoading(true); + setError(null); + try { const details = await tmdbService.getPersonDetails(castMember.id); setPersonDetails(details); - setHasFetched(true); - } catch (error) { - console.error('Error fetching person details:', error); + } catch (err) { + console.error('Error fetching person details:', err); + setError('Failed to load cast member details'); } finally { setLoading(false); } }; - const modalStyle = useAnimatedStyle(() => ({ - opacity: modalOpacity.value, - transform: [{ scale: modalScale.value }], - })); - - const handleClose = () => { - modalOpacity.value = withTiming(0, { duration: 200 }); - modalScale.value = withTiming(0.9, { duration: 200 }, () => { - runOnJS(onClose)(); - }); - }; - const formatDate = (dateString: string | null) => { if (!dateString) return null; - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }); + try { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } catch { + return dateString; + } }; const calculateAge = (birthday: string | null) => { if (!birthday) return null; - const today = new Date(); - const birthDate = new Date(birthday); - let age = today.getFullYear() - birthDate.getFullYear(); - const monthDiff = today.getMonth() - birthDate.getMonth(); - - if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { - age--; + try { + const birthDate = new Date(birthday); + const today = new Date(); + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + return age; + } catch { + return null; } - - return age; }; if (!visible || !castMember) return null; return ( - - - - - {Platform.OS === 'ios' ? ( - - {renderContent()} - - ) : ( - renderContent() - )} - - - ); - - function renderContent() { - return ( - <> - {/* Header */} - - - - {castMember.profile_path ? ( - - ) : ( - - - {castMember.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)} - - - )} - - - - - {castMember.name} - - {castMember.character && ( - - as {castMember.character} - - )} - - - - + + + + + {Platform.OS === 'ios' ? ( + + ) : ( + + )} + + {/* Header */} + + + Cast Details + + + - - - {/* Content */} - - {loading ? ( - - - - Loading details... - - - ) : ( - - {/* Quick Info */} - {(personDetails?.known_for_department || personDetails?.birthday || personDetails?.place_of_birth) && ( - - {personDetails?.known_for_department && ( - - - - Department - - - {personDetails.known_for_department} - - - )} - - {personDetails?.birthday && ( - - - - Age - - - {calculateAge(personDetails.birthday)} years old - - - )} - - {personDetails?.place_of_birth && ( - - - - Born in - - - {personDetails.place_of_birth} - - - )} - - {personDetails?.birthday && ( - - - Born on {formatDate(personDetails.birthday)} - - - )} + + {/* Content */} + + {loading ? ( + + + + Loading details... + + + ) : error ? ( + + + + {error} + + + Retry + + + ) : personDetails ? ( + + {/* Profile Image and Basic Info */} + + + {personDetails.profile_path ? ( + + ) : ( + + + + )} + + + + + {personDetails.name} + + + + {personDetails.known_for_department} + + + {personDetails.birthday && ( + + + + {formatDate(personDetails.birthday)} + {calculateAge(personDetails.birthday) && ` (${calculateAge(personDetails.birthday)} years old)`} + + + )} + + {personDetails.place_of_birth && ( + + + + {personDetails.place_of_birth} + + + )} + - )} + + {/* Biography */} + {personDetails.biography && ( + + + Biography + + + {personDetails.biography} + + + )} + + {/* Also Known As */} + {personDetails.also_known_as && personDetails.also_known_as.length > 0 && ( + + + Also Known As + + + {personDetails.also_known_as.slice(0, 5).map((alias, index) => ( + + + {alias} + + + ))} + + + )} + + ) : null} + + + + + ); +}; - {/* Biography */} - {personDetails?.biography && ( - - - Biography - - - {personDetails.biography} - - - )} - - {/* Also Known As - Compact */} - {personDetails?.also_known_as && personDetails.also_known_as.length > 0 && ( - - - Also Known As - - - {personDetails.also_known_as.slice(0, 4).join(' • ')} - - - )} - - {/* No details available */} - {!loading && !personDetails?.biography && !personDetails?.birthday && !personDetails?.place_of_birth && ( - - - - No additional details available - - - )} - - )} - - - ); - } +const styles = { + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + justifyContent: 'center' as const, + alignItems: 'center' as const, + }, + backdrop: { + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + modalContainer: { + width: MODAL_WIDTH, + height: MODAL_HEIGHT, + borderRadius: 16, + overflow: 'hidden' as const, + elevation: 8, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + }, + blurBackground: { + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + androidBackground: { + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + opacity: 0.95, + }, + header: { + flexDirection: 'row' as const, + justifyContent: 'space-between' as const, + alignItems: 'center' as const, + padding: 20, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.1)', + }, + headerTitle: { + fontSize: 20, + fontWeight: '700' as const, + }, + closeButton: { + padding: 4, + }, + content: { + flex: 1, + padding: 20, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center' as const, + alignItems: 'center' as const, + paddingVertical: 40, + }, + loadingText: { + marginTop: 16, + fontSize: 16, + }, + errorContainer: { + flex: 1, + justifyContent: 'center' as const, + alignItems: 'center' as const, + paddingVertical: 40, + }, + errorText: { + marginTop: 16, + fontSize: 16, + textAlign: 'center' as const, + marginBottom: 20, + }, + retryButton: { + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + }, + retryButtonText: { + color: '#fff', + fontWeight: '600' as const, + }, + detailsContainer: { + flex: 1, + }, + profileSection: { + flexDirection: 'row' as const, + marginBottom: 24, + }, + imageContainer: { + marginRight: 16, + }, + profileImage: { + width: 100, + height: 150, + borderRadius: 8, + }, + placeholderImage: { + width: 100, + height: 150, + borderRadius: 8, + justifyContent: 'center' as const, + alignItems: 'center' as const, + }, + basicInfo: { + flex: 1, + justifyContent: 'flex-start' as const, + }, + name: { + fontSize: 24, + fontWeight: '700' as const, + marginBottom: 4, + }, + department: { + fontSize: 16, + fontWeight: '600' as const, + marginBottom: 12, + }, + infoRow: { + flexDirection: 'row' as const, + alignItems: 'center' as const, + marginBottom: 8, + }, + infoText: { + fontSize: 14, + marginLeft: 8, + flex: 1, + }, + section: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '700' as const, + marginBottom: 12, + }, + biography: { + fontSize: 14, + lineHeight: 20, + }, + aliasContainer: { + flexDirection: 'row' as const, + flexWrap: 'wrap' as const, + gap: 8, + }, + aliasChip: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + }, + aliasText: { + fontSize: 12, + fontWeight: '500' as const, + }, }; export default CastDetailsModal; \ No newline at end of file diff --git a/src/components/metadata/FloatingHeader.tsx b/src/components/metadata/FloatingHeader.tsx index 30bdfbb..57e1fda 100644 --- a/src/components/metadata/FloatingHeader.tsx +++ b/src/components/metadata/FloatingHeader.tsx @@ -11,11 +11,6 @@ import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as CommunityBlurView } from '@react-native-community/blur'; import { MaterialIcons } from '@expo/vector-icons'; import { Image } from 'expo-image'; -import Animated, { - useAnimatedStyle, - interpolate, - Extrapolate, -} from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; @@ -27,9 +22,6 @@ interface FloatingHeaderProps { handleBack: () => void; handleToggleLibrary: () => void; inLibrary: boolean; - headerOpacity: Animated.SharedValue; - headerElementsY: Animated.SharedValue; - headerElementsOpacity: Animated.SharedValue; safeAreaTop: number; setLogoLoadError: (error: boolean) => void; } @@ -40,37 +32,20 @@ const FloatingHeader: React.FC = ({ handleBack, handleToggleLibrary, inLibrary, - headerOpacity, - headerElementsY, - headerElementsOpacity, safeAreaTop, setLogoLoadError, }) => { const { currentTheme } = useTheme(); - // Animated styles for the header - const headerAnimatedStyle = useAnimatedStyle(() => ({ - opacity: headerOpacity.value, - transform: [ - { translateY: interpolate(headerOpacity.value, [0, 1], [-20, 0], Extrapolate.CLAMP) } - ] - })); - - // Animated style for header elements - const headerElementsStyle = useAnimatedStyle(() => ({ - opacity: headerElementsOpacity.value, - transform: [{ translateY: headerElementsY.value }] - })); - return ( - + {Platform.OS === 'ios' ? ( - + = ({ color={currentTheme.colors.highEmphasis} /> - + ) : ( @@ -121,7 +96,7 @@ const FloatingHeader: React.FC = ({ blurAmount={15} reducedTransparencyFallbackColor="rgba(20, 20, 20, 0.9)" /> - + = ({ color={currentTheme.colors.highEmphasis} /> - + )} {Platform.OS === 'ios' && } - + ); }; @@ -240,4 +215,4 @@ const styles = StyleSheet.create({ }, }); -export default React.memo(FloatingHeader); \ No newline at end of file +export default React.memo(FloatingHeader); \ No newline at end of file diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 85cc74e..d15a765 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -13,16 +13,6 @@ import { Image } from 'expo-image'; import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as CommunityBlurView } from '@react-native-community/blur'; import Constants, { ExecutionEnvironment } from 'expo-constants'; -import Animated, { - useAnimatedStyle, - interpolate, - Extrapolate, - useSharedValue, - withTiming, - runOnJS, - withRepeat, - FadeIn, -} from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { useTraktContext } from '../../contexts/TraktContext'; import { logger } from '../../utils/logger'; @@ -30,25 +20,12 @@ import { TMDBService } from '../../services/tmdbService'; const { width, height } = Dimensions.get('window'); -// Ultra-optimized animation constants -const PARALLAX_FACTOR = 0.3; -const SCALE_FACTOR = 1.02; -const FADE_THRESHOLD = 200; - // Types - streamlined interface HeroSectionProps { metadata: any; bannerImage: string | null; loadingBanner: boolean; logoLoadError: boolean; - scrollY: Animated.SharedValue; - heroHeight: Animated.SharedValue; - heroOpacity: Animated.SharedValue; - logoOpacity: Animated.SharedValue; - buttonsOpacity: Animated.SharedValue; - buttonsTranslateY: Animated.SharedValue; - watchProgressOpacity: Animated.SharedValue; - watchProgressWidth: Animated.SharedValue; watchProgress: { currentTime: number; duration: number; @@ -70,7 +47,6 @@ interface HeroSectionProps { groupedEpisodes?: { [seasonNumber: number]: any[] }; } -// Ultra-optimized ActionButtons Component - minimal re-renders const ActionButtons = React.memo(({ handleShowStreams, toggleLibrary, @@ -79,7 +55,6 @@ const ActionButtons = React.memo(({ id, navigation, playButtonText, - animatedStyle, isWatched, watchProgress, groupedEpisodes @@ -91,205 +66,56 @@ const ActionButtons = React.memo(({ id: string; navigation: any; playButtonText: string; - animatedStyle: any; isWatched: boolean; watchProgress: any; groupedEpisodes?: { [seasonNumber: number]: any[] }; }) => { const { currentTheme } = useTheme(); - // Memoized navigation handler - 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; - } - } 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]); - - // Determine play button style and text based on watched status - const playButtonStyle = useMemo(() => { - if (isWatched && type === 'movie') { - // Only movies get the dark watched style for "Watch Again" - return [styles.actionButton, styles.playButton, styles.watchedPlayButton]; - } - // All other buttons (Resume, Play SxxEyy, regular Play) get white background - return [styles.actionButton, styles.playButton]; - }, [isWatched, type]); - - const playButtonTextStyle = useMemo(() => { - if (isWatched && type === 'movie') { - // Only movies get white text for "Watch Again" - return [styles.playButtonText, styles.watchedPlayButtonText]; - } - // All other buttons get black text - return styles.playButtonText; - }, [isWatched, type]); - - const finalPlayButtonText = useMemo(() => { - // For movies, handle watched state - if (type === 'movie') { - return isWatched ? 'Watch Again' : playButtonText; - } - - // For series, validate next episode existence for both watched and resume cases - if (type === 'series' && watchProgress?.episodeId && groupedEpisodes) { - let seasonNum: number | null = null; - let episodeNum: number | null = null; - - const parts = watchProgress.episodeId.split(':'); - - if (parts.length === 3) { - // Format: showId:season:episode - seasonNum = parseInt(parts[1], 10); - episodeNum = parseInt(parts[2], 10); - } else if (parts.length === 2) { - // Format: season:episode (no show id) - seasonNum = parseInt(parts[0], 10); - episodeNum = parseInt(parts[1], 10); - } else { - // Try pattern s1e2 - const match = watchProgress.episodeId.match(/s(\d+)e(\d+)/i); - if (match) { - seasonNum = parseInt(match[1], 10); - episodeNum = parseInt(match[2], 10); - } - } - - if (seasonNum !== null && episodeNum !== null && !isNaN(seasonNum) && !isNaN(episodeNum)) { - if (isWatched) { - // For watched episodes, check if next episode exists - const nextEpisode = episodeNum + 1; - const currentSeasonEpisodes = groupedEpisodes[seasonNum] || []; - const nextEpisodeExists = currentSeasonEpisodes.some(ep => - ep.episode_number === nextEpisode - ); - - if (nextEpisodeExists) { - // Show the NEXT episode number only if it exists - const seasonStr = seasonNum.toString().padStart(2, '0'); - const episodeStr = nextEpisode.toString().padStart(2, '0'); - return `Play S${seasonStr}E${episodeStr}`; - } else { - // If next episode doesn't exist, show generic text - return 'Completed'; - } - } else { - // For non-watched episodes, check if current episode exists - const currentSeasonEpisodes = groupedEpisodes[seasonNum] || []; - const currentEpisodeExists = currentSeasonEpisodes.some(ep => - ep.episode_number === episodeNum - ); - - if (currentEpisodeExists) { - // Current episode exists, use original button text - return playButtonText; - } else { - // Current episode doesn't exist, fallback to generic play - return 'Play'; - } - } - } - - // Fallback label if parsing fails - return isWatched ? 'Play Next Episode' : playButtonText; - } - - // Default fallback for non-series or missing data - return isWatched ? 'Play' : playButtonText; - }, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]); - return ( - - + { - if (isWatched) { - return type === 'movie' ? 'replay' : 'play-arrow'; - } - return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow'; - })()} - size={24} - color={isWatched && type === 'movie' ? "#fff" : "#000"} + name="play-arrow" + size={20} + color={isWatched ? "#fff" : "#000"} /> - {finalPlayButtonText} - - - - {Platform.OS === 'ios' ? ( - - ) : ( - - )} - - - {inLibrary ? 'Saved' : 'Save'} + + {playButtonText} - - {type === 'series' && ( - - {Platform.OS === 'ios' ? ( - - ) : ( - - )} - - - )} - + + + {Platform.OS === 'ios' ? ( + + ) : ( + + )} + + + ); }); -// Enhanced WatchProgress Component with Trakt integration and watched status const WatchProgressDisplay = React.memo(({ watchProgress, type, - getEpisodeDetails, - animatedStyle, + getEpisodeDetails, isWatched }: { watchProgress: { @@ -302,324 +128,75 @@ const WatchProgressDisplay = React.memo(({ } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; - animatedStyle: any; isWatched: boolean; }) => { const { currentTheme } = useTheme(); - const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext(); - // State to trigger refresh after manual sync - const [refreshTrigger, setRefreshTrigger] = useState(0); - const [isSyncing, setIsSyncing] = useState(false); + if (!watchProgress || watchProgress.duration <= 0) return null; - // Animated values for enhanced effects - const completionGlow = useSharedValue(0); - const celebrationScale = useSharedValue(1); - const progressPulse = useSharedValue(1); - const progressBoxOpacity = useSharedValue(0); - const progressBoxScale = useSharedValue(0.8); - const progressBoxTranslateY = useSharedValue(20); - const syncRotation = useSharedValue(0); + const progressPercentage = Math.min((watchProgress.currentTime / watchProgress.duration) * 100, 100); + const isCompleted = progressPercentage >= 90; - // Animate the sync icon when syncing - useEffect(() => { - if (isSyncing) { - syncRotation.value = withRepeat( - withTiming(360, { duration: 1000 }), - -1, // Infinite repeats - false // No reverse - ); - } else { - syncRotation.value = 0; - } - }, [isSyncing, syncRotation]); + const formatTime = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + }; - // Handle manual Trakt sync - const handleTraktSync = useMemo(() => async () => { - if (isTraktAuthenticated && forceSyncTraktProgress) { - logger.log('[HeroSection] Manual Trakt sync requested'); - setIsSyncing(true); - try { - const success = await forceSyncTraktProgress(); - logger.log(`[HeroSection] Manual Trakt sync ${success ? 'successful' : 'failed'}`); - - // Force component to re-render after a short delay to update sync status - if (success) { - setTimeout(() => { - setRefreshTrigger(prev => prev + 1); - setIsSyncing(false); - }, 500); - } else { - setIsSyncing(false); - } - } catch (error) { - logger.error('[HeroSection] Manual Trakt sync error:', error); - setIsSyncing(false); - } - } - }, [isTraktAuthenticated, forceSyncTraktProgress, setRefreshTrigger]); - - // Sync rotation animation style - const syncIconStyle = useAnimatedStyle(() => ({ - transform: [{ rotate: `${syncRotation.value}deg` }], - })); + const remainingTime = watchProgress.duration - watchProgress.currentTime; + const episodeDetails = watchProgress?.episodeId ? getEpisodeDetails(watchProgress.episodeId) : null; - // Memoized progress calculation with Trakt integration - const progressData = useMemo(() => { - // If content is fully watched, show watched status instead of progress - if (isWatched) { - 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}` : ''}`; - } - } - - const watchedDate = watchProgress?.lastUpdated - ? new Date(watchProgress.lastUpdated).toLocaleDateString() - : new Date().toLocaleDateString(); - - // Determine if watched via Trakt or local - const watchedViaTrakt = isTraktAuthenticated && - watchProgress?.traktProgress !== undefined && - watchProgress.traktProgress >= 95; - - return { - progressPercent: 100, - formattedTime: watchedDate, - episodeInfo, - displayText: watchedViaTrakt ? 'Watched on Trakt' : 'Watched', - syncStatus: isTraktAuthenticated && watchProgress?.traktSynced ? '' : '', // Clean look for watched - isTraktSynced: watchProgress?.traktSynced && isTraktAuthenticated, - isWatched: true - }; - } - - if (!watchProgress || watchProgress.duration === 0) return null; - - // Determine which progress to show - prioritize Trakt if available and authenticated - let progressPercent; - let isUsingTraktProgress = false; - - if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) { - progressPercent = watchProgress.traktProgress; - isUsingTraktProgress = true; - } else { - 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}` : ''}`; - } - } - - // Enhanced display text with Trakt integration - let displayText = progressPercent >= 85 ? 'Watched' : `${Math.round(progressPercent)}% watched`; - let syncStatus = ''; - - // Show Trakt sync status if user is authenticated - if (isTraktAuthenticated) { - if (isUsingTraktProgress) { - syncStatus = ' • Using Trakt progress'; - if (watchProgress.traktSynced) { - syncStatus = ' • Synced with Trakt'; - } - } else if (watchProgress.traktSynced) { - syncStatus = ' • Synced with Trakt'; - // If we have specific Trakt progress that differs from local, mention it - if (watchProgress.traktProgress !== undefined && - Math.abs(progressPercent - watchProgress.traktProgress) > 5) { - displayText = `${Math.round(progressPercent)}% watched (${Math.round(watchProgress.traktProgress)}% on Trakt)`; - } - } else { - // Do not show "Sync pending" label anymore; leave status empty. - syncStatus = ''; - } - } - - return { - progressPercent, - formattedTime, - episodeInfo, - displayText, - syncStatus, - isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated, - isWatched: false - }; - }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated, isWatched, refreshTrigger]); - - // Trigger appearance and completion animations - useEffect(() => { - if (progressData) { - // Smooth entrance animation for the glassmorphic box - progressBoxOpacity.value = withTiming(1, { duration: 400 }); - progressBoxScale.value = withTiming(1, { duration: 400 }); - progressBoxTranslateY.value = withTiming(0, { duration: 400 }); - - if (progressData.isWatched || (progressData.progressPercent && progressData.progressPercent >= 85)) { - // Celebration animation sequence - celebrationScale.value = withRepeat( - withTiming(1.05, { duration: 200 }), - 2, - true - ); - - // Glow effect - completionGlow.value = withRepeat( - withTiming(1, { duration: 1500 }), - -1, - true - ); - } else { - // Subtle progress pulse for ongoing content - progressPulse.value = withRepeat( - withTiming(1.02, { duration: 2000 }), - -1, - true - ); - } - } else { - // Hide animation when no progress data - progressBoxOpacity.value = withTiming(0, { duration: 300 }); - progressBoxScale.value = withTiming(0.8, { duration: 300 }); - progressBoxTranslateY.value = withTiming(20, { duration: 300 }); - } - }, [progressData]); - - // Animated styles for enhanced effects - const celebrationAnimatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: celebrationScale.value }], - })); - - const glowAnimatedStyle = useAnimatedStyle(() => ({ - opacity: interpolate(completionGlow.value, [0, 1], [0.3, 0.8], Extrapolate.CLAMP), - })); - - const progressPulseStyle = useAnimatedStyle(() => ({ - transform: [{ scale: progressPulse.value }], - })); - - const progressBoxAnimatedStyle = useAnimatedStyle(() => ({ - opacity: progressBoxOpacity.value, - transform: [ - { scale: progressBoxScale.value }, - { translateY: progressBoxTranslateY.value } - ], - })); - - if (!progressData) return null; - - const isCompleted = progressData.isWatched || progressData.progressPercent >= 85; - return ( - - {/* Glass morphism background with entrance animation */} - - {Platform.OS === 'ios' ? ( - - ) : ( - - )} + + + {Platform.OS === 'android' && } - {/* Enhanced progress bar with glow effects */} - - - {/* Background glow for completed content */} - {isCompleted && ( - - )} + + + - - - {/* Shimmer effect for active progress */} - {!isCompleted && progressData.progressPercent > 0 && ( - + {watchProgress.traktSynced && ( + + + + + )} - - - {/* Enhanced text container with better typography */} + + - - {progressData.displayText} + + {isCompleted ? 'Completed' : `${Math.round(progressPercentage)}% watched`} - - + - - {progressData.episodeInfo} • Last watched {progressData.formattedTime} - + {!isCompleted && ( + + {formatTime(remainingTime)} remaining + + )} - {/* Trakt sync status with enhanced styling */} - {progressData.syncStatus && ( - - - - {progressData.syncStatus} - - - {/* Enhanced manual Trakt sync button - moved inline */} - {isTraktAuthenticated && forceSyncTraktProgress && ( - - - - - - - - )} - - )} + {episodeDetails && ( + + S{episodeDetails.seasonNumber}E{episodeDetails.episodeNumber} • {episodeDetails.episodeName} + + )} + - - + ); }); @@ -628,13 +205,6 @@ const HeroSection: React.FC = ({ bannerImage, loadingBanner, logoLoadError, - scrollY, - heroHeight, - heroOpacity, - logoOpacity, - buttonsOpacity, - buttonsTranslateY, - watchProgressOpacity, watchProgress, type, getEpisodeDetails, @@ -649,249 +219,96 @@ const HeroSection: React.FC = ({ groupedEpisodes, }) => { const { currentTheme } = useTheme(); - const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); + const [imageLoadError, setImageLoadError] = useState(false); - // Enhanced state for smooth image loading - const [imageError, setImageError] = useState(false); - const [imageLoaded, setImageLoaded] = useState(false); - const imageOpacity = useSharedValue(1); - const imageLoadOpacity = useSharedValue(0); - const shimmerOpacity = useSharedValue(0.3); - - // Memoized image source - const imageSource = useMemo(() => - bannerImage || metadata.banner || metadata.poster - , [bannerImage, metadata.banner, metadata.poster]); - - // Start shimmer animation for loading state - useEffect(() => { - if (!imageLoaded && imageSource) { - // Start shimmer animation - shimmerOpacity.value = withRepeat( - withTiming(0.8, { duration: 1200 }), - -1, - true - ); - } else { - // Stop shimmer when loaded - shimmerOpacity.value = withTiming(0.3, { duration: 300 }); - } - }, [imageLoaded, imageSource]); - - // Reset loading state when image source changes - useEffect(() => { - if (imageSource) { - setImageLoaded(false); - imageLoadOpacity.value = 0; - } - }, [imageSource]); - - // Enhanced image handlers with smooth transitions - const handleImageError = () => { - 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); - } - })(); - }; - - const handleImageLoad = () => { - 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(() => ({ - height: heroHeight.value, - opacity: heroOpacity.value, - }), []); - - const logoAnimatedStyle = useAnimatedStyle(() => { - // Determine if progress bar should be shown - const hasProgress = watchProgress && watchProgress.duration > 0; - - // Scale down logo when progress bar is present - const logoScale = hasProgress ? 0.85 : 1; - - return { - opacity: logoOpacity.value, - transform: [ - { - translateY: interpolate( - scrollY.value, - [0, 100], - [0, -20], - Extrapolate.CLAMP - ) - }, - { scale: withTiming(logoScale, { duration: 300 }) } - ] - }; - }, [watchProgress]); - - const watchProgressAnimatedStyle = useAnimatedStyle(() => ({ - opacity: watchProgressOpacity.value, - }), []); - - // Enhanced backdrop with smooth loading animation - const backdropImageStyle = useAnimatedStyle(() => { - 'worklet'; - const translateY = scrollY.value * PARALLAX_FACTOR; - const scale = 1 + (scrollY.value * 0.0001); // Micro scale effect - - return { - opacity: imageOpacity.value * imageLoadOpacity.value, - transform: [ - { translateY: -Math.min(translateY, 100) }, // Cap translation - { scale: Math.min(scale, SCALE_FACTOR) } // Cap scale - ], - }; - }, []); - - // Simplified buttons animation - const buttonsAnimatedStyle = useAnimatedStyle(() => ({ - opacity: buttonsOpacity.value, - transform: [{ - translateY: interpolate( - buttonsTranslateY.value, - [0, 20], - [0, 20], - Extrapolate.CLAMP - ) - }] - }), []); - - // Ultra-optimized genre rendering with smooth animation - const genreElements = useMemo(() => { - if (!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[]) => ( - - - {genreName} - - {index < array.length - 1 && ( - - )} - - )); - }, [metadata.genres, currentTheme.colors.text]); - - // Memoized play button text - const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]); - - // Calculate if content is watched (>=85% progress) - check both local and Trakt progress const isWatched = useMemo(() => { if (!watchProgress) return false; - - // Check Trakt progress first if available and user is authenticated - if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) { - const traktWatched = watchProgress.traktProgress >= 95; - logger.log(`[HeroSection] Trakt authenticated: ${isTraktAuthenticated}, Trakt progress: ${watchProgress.traktProgress}%, Watched: ${traktWatched}`); - return traktWatched; - } - - // Fall back to local progress - if (watchProgress.duration === 0) return false; - const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; - const localWatched = progressPercent >= 85; - logger.log(`[HeroSection] Local progress: ${progressPercent.toFixed(1)}%, Watched: ${localWatched}`); - return localWatched; - }, [watchProgress, isTraktAuthenticated]); - + return (watchProgress.currentTime / watchProgress.duration) >= 0.9; + }, [watchProgress]); + + const playButtonText = getPlayButtonText(); + + const handleImageLoad = () => { + setImageLoadError(false); + }; + + const handleImageError = () => { + setImageLoadError(true); + logger.warn(`[HeroSection] Banner image failed to load: ${bannerImage}`); + }; + return ( - - {/* Optimized Background */} - - - {/* Loading placeholder for smooth transition */} - {((imageSource && !imageLoaded) || loadingBanner) && ( - - - - )} - - {/* Enhanced Background Image with smooth loading */} - {imageSource && !loadingBanner && ( - + {/* Background Image */} + {bannerImage && !imageLoadError && ( + )} - - {/* Simplified Gradient */} + + {/* Gradient Overlay */} - {/* Optimized Title/Logo */} - - - {metadata.logo && !logoLoadError ? ( - { - runOnJS(setLogoLoadError)(true); - }} - /> - ) : ( - - {metadata.name} - - )} - + {/* Logo or Title */} + + {metadata.logo && !logoLoadError ? ( + { + logger.warn(`[HeroSection] Logo failed to load: ${metadata.logo}`); + setLogoLoadError(true); + }} + /> + ) : ( + + {metadata.name} + + )} - - {/* Enhanced Watch Progress with Trakt integration */} - 0 && ( + + {metadata.genres.slice(0, 3).map((genre: string, index: number) => ( + + + {genre} + + {index < Math.min(metadata.genres.length - 1, 2) && ( + + )} + + ))} + + )} + + {/* Watch Progress */} + - - {/* Optimized Genres */} - {genreElements && ( - - {genreElements} - - )} - - {/* Optimized Action Buttons */} - = ({ id={id} navigation={navigation} playButtonText={playButtonText} - animatedStyle={buttonsAnimatedStyle} isWatched={isWatched} watchProgress={watchProgress} groupedEpisodes={groupedEpisodes} /> - + ); }; -// Ultra-optimized styles const styles = StyleSheet.create({ heroSection: { width: '100%', @@ -1156,153 +571,152 @@ const styles = StyleSheet.create({ borderRadius: 25, backgroundColor: 'rgba(255,255,255,0.15)', }, - watchedIndicator: { - position: 'absolute', - top: 4, - right: 4, - backgroundColor: 'rgba(0,0,0,0.6)', - borderRadius: 8, - width: 16, - height: 16, - alignItems: 'center', - justifyContent: 'center', - }, - watchedPlayButton: { - backgroundColor: '#1e1e1e', - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.3)', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 4, - }, - watchedPlayButtonText: { - color: '#fff', - fontWeight: '700', - marginLeft: 6, - fontSize: 15, - }, - // Enhanced progress indicator styles - progressShimmer: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - borderRadius: 2, - backgroundColor: 'rgba(255,255,255,0.1)', - }, - completionGlow: { - position: 'absolute', - top: -2, - left: -2, - right: -2, - bottom: -2, - borderRadius: 4, - backgroundColor: 'rgba(0,255,136,0.2)', - }, - completionIndicator: { - position: 'absolute', - right: 4, - top: -6, - bottom: -6, - width: 16, - height: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, - completionGradient: { - width: 16, - height: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, - sparkleContainer: { - position: 'absolute', - top: -10, - left: 0, - right: 0, - bottom: -10, - borderRadius: 2, - }, - sparkle: { - position: 'absolute', - width: 8, - height: 8, - borderRadius: 4, - alignItems: 'center', - justifyContent: 'center', - }, - progressInfoMain: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginBottom: 2, - }, - watchProgressMainText: { - fontSize: 11, - fontWeight: '600', - textAlign: 'center', - }, - watchProgressSubText: { - fontSize: 9, - textAlign: 'center', - opacity: 0.8, - marginBottom: 1, - }, - syncStatusContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginTop: 2, - width: '100%', - flexWrap: 'wrap', - }, - syncStatusText: { - fontSize: 9, - marginLeft: 4, - fontWeight: '500', - }, - traktSyncButtonEnhanced: { - position: 'absolute', - top: 8, - right: 8, - width: 24, - height: 24, - borderRadius: 12, - overflow: 'hidden', - }, - traktSyncButtonInline: { - marginLeft: 8, - width: 20, - height: 20, - borderRadius: 10, - overflow: 'hidden', - }, - syncButtonGradient: { - width: 24, - height: 24, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - }, - syncButtonGradientInline: { - width: 20, - height: 20, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - }, - traktIndicatorGradient: { - width: 16, - height: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, + watchedIndicator: { + position: 'absolute', + top: 4, + right: 4, + backgroundColor: 'rgba(0,0,0,0.6)', + borderRadius: 8, + width: 16, + height: 16, + alignItems: 'center', + justifyContent: 'center', + }, + watchedPlayButton: { + backgroundColor: '#1e1e1e', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.3)', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 4, + }, + watchedPlayButtonText: { + color: '#fff', + fontWeight: '700', + marginLeft: 6, + fontSize: 15, + }, + progressShimmer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + borderRadius: 2, + backgroundColor: 'rgba(255,255,255,0.1)', + }, + completionGlow: { + position: 'absolute', + top: -2, + left: -2, + right: -2, + bottom: -2, + borderRadius: 4, + backgroundColor: 'rgba(0,255,136,0.2)', + }, + completionIndicator: { + position: 'absolute', + right: 4, + top: -6, + bottom: -6, + width: 16, + height: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + completionGradient: { + width: 16, + height: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + sparkleContainer: { + position: 'absolute', + top: -10, + left: 0, + right: 0, + bottom: -10, + borderRadius: 2, + }, + sparkle: { + position: 'absolute', + width: 8, + height: 8, + borderRadius: 4, + alignItems: 'center', + justifyContent: 'center', + }, + progressInfoMain: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 2, + }, + watchProgressMainText: { + fontSize: 11, + fontWeight: '600', + textAlign: 'center', + }, + watchProgressSubText: { + fontSize: 9, + textAlign: 'center', + opacity: 0.8, + marginBottom: 1, + }, + syncStatusContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginTop: 2, + width: '100%', + flexWrap: 'wrap', + }, + syncStatusText: { + fontSize: 9, + marginLeft: 4, + fontWeight: '500', + }, + traktSyncButtonEnhanced: { + position: 'absolute', + top: 8, + right: 8, + width: 24, + height: 24, + borderRadius: 12, + overflow: 'hidden', + }, + traktSyncButtonInline: { + marginLeft: 8, + width: 20, + height: 20, + borderRadius: 10, + overflow: 'hidden', + }, + syncButtonGradient: { + width: 24, + height: 24, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, + syncButtonGradientInline: { + width: 20, + height: 20, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + }, + traktIndicatorGradient: { + width: 16, + height: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, }); export default React.memo(HeroSection); \ No newline at end of file diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index c2aa2ba..089c655 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -1,12 +1,7 @@ import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import Animated, { - FadeIn, - FadeOut, - SlideInRight, - SlideOutRight, -} from 'react-native-reanimated'; +// Removed react-native-reanimated imports import { styles } from '../utils/playerStyles'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; @@ -89,9 +84,7 @@ export const SubtitleModals: React.FC = ({ return ( <> {/* Backdrop */} - = ({ onPress={handleClose} activeOpacity={1} /> - + {/* Side Menu */} - = ({ - + ); }; @@ -539,4 +530,4 @@ export const SubtitleModals: React.FC = ({ ); }; -export default SubtitleModals; \ No newline at end of file +export default SubtitleModals; \ No newline at end of file diff --git a/src/hooks/useMetadataAnimations.ts b/src/hooks/useMetadataAnimations.ts deleted file mode 100644 index 62937e7..0000000 --- a/src/hooks/useMetadataAnimations.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { useEffect } from 'react'; -import { Dimensions } from 'react-native'; -import { - useSharedValue, - withTiming, - withSpring, - Easing, - useAnimatedScrollHandler, - runOnUI, - cancelAnimation, -} from 'react-native-reanimated'; - -const { width, height } = Dimensions.get('window'); - -// Highly optimized animation configurations -const fastSpring = { - damping: 15, - mass: 0.8, - stiffness: 150, -}; - -const ultraFastSpring = { - damping: 12, - mass: 0.6, - stiffness: 200, -}; - -// Ultra-optimized easing functions -const easings = { - 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) => { - // Consolidated entrance animations - start with visible values for Android compatibility - const screenOpacity = useSharedValue(1); - const contentOpacity = useSharedValue(1); - - // Combined hero animations - const heroOpacity = useSharedValue(1); - const heroScale = useSharedValue(1); // Start at 1 for Android compatibility - const heroHeightValue = useSharedValue(height * 0.5); - - // Combined UI element animations - const uiElementsOpacity = useSharedValue(1); - const uiElementsTranslateY = useSharedValue(0); - - // Progress animation - simplified to single value - const progressOpacity = useSharedValue(0); - - // Scroll values - minimal - const scrollY = useSharedValue(0); - const headerProgress = useSharedValue(0); // Single value for all header animations - - // Static header elements Y for performance - const staticHeaderElementsY = useSharedValue(0); - - // Ultra-fast entrance sequence - batch animations for better performance - useEffect(() => { - // Batch all entrance animations to run simultaneously with safety - const enterAnimations = () => { - 'worklet'; - - try { - // Start with slightly reduced values and animate to full visibility - screenOpacity.value = withTiming(1, { - duration: 250, - easing: easings.fast - }); - - heroOpacity.value = withTiming(1, { - duration: 300, - easing: easings.fast - }); - - heroScale.value = withSpring(1, ultraFastSpring); - - uiElementsOpacity.value = withTiming(1, { - duration: 400, - easing: easings.natural - }); - - uiElementsTranslateY.value = withSpring(0, fastSpring); - - contentOpacity.value = withTiming(1, { - duration: 350, - easing: easings.fast - }); - } catch (error) { - // Silently handle any animation errors - console.warn('Animation error in enterAnimations:', error); - } - }; - - // Use runOnUI for better performance with error handling - try { - runOnUI(enterAnimations)(); - } catch (error) { - console.warn('Failed to run enter animations:', error); - } - }, []); - - // Optimized watch progress animation with safety - useEffect(() => { - const hasProgress = watchProgress && watchProgress.duration > 0; - - const updateProgress = () => { - 'worklet'; - - try { - progressOpacity.value = withTiming(hasProgress ? 1 : 0, { - duration: hasProgress ? 200 : 150, - easing: easings.fast - }); - } catch (error) { - console.warn('Animation error in updateProgress:', error); - } - }; - - try { - runOnUI(updateProgress)(); - } catch (error) { - console.warn('Failed to run progress animation:', error); - } - }, [watchProgress]); - - // Cleanup function to cancel animations - useEffect(() => { - return () => { - try { - cancelAnimation(screenOpacity); - cancelAnimation(contentOpacity); - cancelAnimation(heroOpacity); - cancelAnimation(heroScale); - cancelAnimation(uiElementsOpacity); - cancelAnimation(uiElementsTranslateY); - cancelAnimation(progressOpacity); - cancelAnimation(scrollY); - cancelAnimation(headerProgress); - cancelAnimation(staticHeaderElementsY); - } catch (error) { - console.warn('Error canceling animations:', error); - } - }; - }, []); - - // Ultra-optimized scroll handler with minimal calculations and safety - const scrollHandler = useAnimatedScrollHandler({ - onScroll: (event) => { - 'worklet'; - - try { - const rawScrollY = event.contentOffset.y; - scrollY.value = rawScrollY; - - // Single calculation for header threshold - const threshold = height * 0.4 - safeAreaTop; - const progress = rawScrollY > threshold ? 1 : 0; - - // Use single progress value for all header animations - if (headerProgress.value !== progress) { - headerProgress.value = withTiming(progress, { - duration: progress ? 200 : 150, - easing: easings.ultraFast - }); - } - } catch (error) { - console.warn('Animation error in scroll handler:', error); - } - }, - }); - - return { - // Optimized shared values - reduced count - screenOpacity, - contentOpacity, - heroOpacity, - heroScale, - uiElementsOpacity, - uiElementsTranslateY, - progressOpacity, - scrollY, - headerProgress, - - // Direct shared value references for compatibility - heroHeight: heroHeightValue, - logoOpacity: uiElementsOpacity, - buttonsOpacity: uiElementsOpacity, - buttonsTranslateY: uiElementsTranslateY, - contentTranslateY: uiElementsTranslateY, - watchProgressOpacity: progressOpacity, - watchProgressWidth: progressOpacity, // Reuse for width animation - headerOpacity: headerProgress, - headerElementsY: staticHeaderElementsY, - headerElementsOpacity: headerProgress, - - // Functions - scrollHandler, - animateLogo: () => {}, // Simplified - no separate logo animation - }; -}; \ No newline at end of file diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 9fe19c8..6cb1c1d 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -13,12 +13,13 @@ import { ActivityIndicator, Platform, ScrollView, + TVEventHandler, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { Image } from 'expo-image'; -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +// Removed react-native-reanimated import import { LinearGradient } from 'expo-linear-gradient'; import { catalogService } from '../services/catalogService'; import type { StreamingContent } from '../services/catalogService'; @@ -203,7 +204,7 @@ const SkeletonLoader = () => { const LibraryScreen = () => { const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; - const { width } = useWindowDimensions(); + const { width, height } = useWindowDimensions(); const [loading, setLoading] = useState(true); const [libraryItems, setLibraryItems] = useState([]); const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all'); @@ -212,6 +213,22 @@ const LibraryScreen = () => { const insets = useSafeAreaInsets(); const { currentTheme } = useTheme(); + // TV-optimized grid calculations + const isTV = Platform.isTV || width > 1200; + const getNumColumns = () => { + if (isTV) { + if (width >= 1920) return 6; // 4K TVs + if (width >= 1280) return 5; // HD TVs + return 4; // Smaller TVs + } + return 2; // Mobile/tablet + }; + + const numColumns = getNumColumns(); + const itemSpacing = isTV ? 24 : 16; + const containerPadding = isTV ? 48 : 12; + const itemWidth = (width - (containerPadding * 2) - (itemSpacing * (numColumns - 1))) / numColumns; + // Trakt integration const { isAuthenticated: traktAuthenticated, @@ -270,6 +287,51 @@ const LibraryScreen = () => { }; }, []); + // TV Event Handler for remote control navigation + useEffect(() => { + if (!isTV) return; + + const handleTVEvent = (evt: any) => { + if (evt && evt.eventType === 'focus') { + // Handle focus events for TV navigation + console.log('TV Focus Event:', evt); + } else if (evt && evt.eventType === 'blur') { + // Handle blur events + console.log('TV Blur Event:', evt); + } else if (evt && evt.eventType === 'select') { + // Handle select/enter button press + console.log('TV Select Event:', evt); + } else if (evt && evt.eventType === 'longSelect') { + // Handle long press on select button + console.log('TV Long Select Event:', evt); + } else if (evt && evt.eventType === 'left') { + // Handle left arrow navigation + console.log('TV Left Event:', evt); + } else if (evt && evt.eventType === 'right') { + // Handle right arrow navigation + console.log('TV Right Event:', evt); + } else if (evt && evt.eventType === 'up') { + // Handle up arrow navigation + console.log('TV Up Event:', evt); + } else if (evt && evt.eventType === 'down') { + // Handle down arrow navigation + console.log('TV Down Event:', evt); + } else if (evt && evt.eventType === 'playPause') { + // Handle play/pause button + console.log('TV Play/Pause Event:', evt); + } else if (evt && evt.eventType === 'menu') { + // Handle menu button - could show filters or options + console.log('TV Menu Event:', evt); + } + }; + + const subscription = TVEventHandler.addListener(handleTVEvent); + + return () => { + subscription?.remove(); + }; + }, [isTV]); + const filteredItems = libraryItems.filter(item => { if (filter === 'all') return true; if (filter === 'movies') return item.type === 'movie'; @@ -328,15 +390,37 @@ const LibraryScreen = () => { return folders.filter(folder => folder.itemCount > 0); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); - const itemWidth = (width - 48) / 2; // 2 items per row with padding + // Use the TV-optimized itemWidth from above instead of hardcoded calculation const renderItem = ({ item }: { item: LibraryItem }) => ( navigation.navigate('Metadata', { id: item.id, type: item.type })} activeOpacity={0.7} + // TV optimizations + hasTVPreferredFocus={false} + tvParallaxProperties={{ + enabled: true, + shiftDistanceX: 2.0, + shiftDistanceY: 2.0, + tiltAngle: 0.05, + magnification: 1.1, + }} > - + { style={styles.posterGradient} > 1920 ? 18 : width > 1280 ? 16 : 15, // Responsive font size + } + ]} numberOfLines={2} > {item.name} {item.lastWatched && ( - + 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size + } + ]}> {item.lastWatched} )} @@ -365,7 +460,11 @@ const LibraryScreen = () => { 1920 ? 6 : 4, // Larger progress bar for TV + } ]} /> @@ -374,11 +473,17 @@ const LibraryScreen = () => { 1920 ? 18 : width > 1280 ? 16 : 14} // Responsive icon size color={currentTheme.colors.white} style={{ marginRight: 4 }} /> - Series + 1920 ? 12 : width > 1280 ? 11 : 10, // Responsive font size + } + ]}>Series )} @@ -388,31 +493,69 @@ const LibraryScreen = () => { // Render individual Trakt collection folder const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => ( { setSelectedTraktFolder(folder.id); loadAllCollections(); // Load all collections when entering a specific folder }} activeOpacity={0.7} + // TV optimizations + hasTVPreferredFocus={false} + tvParallaxProperties={{ + enabled: true, + shiftDistanceX: 2.0, + shiftDistanceY: 2.0, + tiltAngle: 0.05, + magnification: 1.1, + }} > - + 1920 ? 80 : width > 1280 ? 70 : 60} // Responsive icon size color={currentTheme.colors.white} - style={{ marginBottom: 12 }} + style={{ marginBottom: width > 1920 ? 16 : 12 }} /> - + 1920 ? 22 : width > 1280 ? 20 : 18, // Responsive font size + } + ]}> {folder.name} - + 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size + } + ]}> {folder.itemCount} items - + 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size + } + ]}> {folder.description} @@ -859,14 +1002,28 @@ const LibraryScreen = () => { return renderItem({ item: item as LibraryItem }); }} keyExtractor={item => item.id} - numColumns={2} - contentContainerStyle={styles.listContainer} + numColumns={numColumns} + contentContainerStyle={[ + styles.listContainer, + { + paddingHorizontal: containerPadding, + paddingVertical: width > 1920 ? 24 : width > 1280 ? 20 : 16, + paddingBottom: width > 1920 ? 120 : 90, + } + ]} showsVerticalScrollIndicator={false} - columnWrapperStyle={styles.columnWrapper} - initialNumToRender={6} - maxToRenderPerBatch={6} + columnWrapperStyle={numColumns > 1 ? [ + styles.columnWrapper, + { + marginBottom: width > 1920 ? 24 : width > 1280 ? 20 : 16, + } + ] : undefined} + initialNumToRender={numColumns * 3} + maxToRenderPerBatch={numColumns * 2} windowSize={5} removeClippedSubviews={Platform.OS === 'android'} + // TV optimizations + getItemLayout={undefined} // Let FlatList calculate for TV focus /> ); }; @@ -1017,7 +1174,7 @@ const styles = StyleSheet.create({ paddingBottom: 90, }, columnWrapper: { - justifyContent: 'space-between', + justifyContent: 'flex-start', marginBottom: 16, }, skeletonContainer: { @@ -1248,4 +1405,4 @@ const styles = StyleSheet.create({ }, }); -export default LibraryScreen; \ No newline at end of file +export default LibraryScreen; \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index cd2933d..1b7042f 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -7,6 +7,7 @@ import { ActivityIndicator, Dimensions, TouchableOpacity, + ScrollView, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRoute, useNavigation } from '@react-navigation/native'; @@ -21,13 +22,6 @@ import { MovieContent } from '../components/metadata/MovieContent'; import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection'; import { RatingsSection } from '../components/metadata/RatingsSection'; import { RouteParams, Episode } from '../types/metadata'; -import Animated, { - useAnimatedStyle, - interpolate, - Extrapolate, - useSharedValue, - withTiming, -} from 'react-native-reanimated'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; @@ -38,7 +32,6 @@ import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScre import HeroSection from '../components/metadata/HeroSection'; import FloatingHeader from '../components/metadata/FloatingHeader'; import MetadataDetails from '../components/metadata/MetadataDetails'; -import { useMetadataAnimations } from '../hooks/useMetadataAnimations'; import { useMetadataAssets } from '../hooks/useMetadataAssets'; import { useWatchProgress } from '../hooks/useWatchProgress'; import { TraktService, TraktPlaybackItem } from '../services/traktService'; @@ -59,7 +52,7 @@ const MetadataScreen: React.FC = () => { const [isContentReady, setIsContentReady] = useState(false); const [showCastModal, setShowCastModal] = useState(false); const [selectedCastMember, setSelectedCastMember] = useState(null); - const transitionOpacity = useSharedValue(1); + // Removed animation state const { metadata, @@ -84,7 +77,6 @@ const MetadataScreen: React.FC = () => { // 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); // Fetch and log Trakt progress data when entering the screen useEffect(() => { @@ -192,11 +184,10 @@ const MetadataScreen: React.FC = () => { useEffect(() => { if (isReady) { setIsContentReady(true); - transitionOpacity.value = withTiming(1, { duration: 50 }); + // Removed animation logic } else if (!isReady && isContentReady) { - setIsContentReady(false); - transitionOpacity.value = 0; - } + setIsContentReady(false); + } }, [isReady, isContentReady]); // Optimized callback functions with reduced dependencies @@ -318,19 +309,7 @@ const MetadataScreen: React.FC = () => { setShowCastModal(true); }, []); - // Ultra-optimized animated styles - minimal calculations - const containerStyle = useAnimatedStyle(() => ({ - opacity: animations.screenOpacity.value, - }), []); - - const contentStyle = useAnimatedStyle(() => ({ - opacity: animations.contentOpacity.value, - transform: [{ translateY: animations.uiElementsTranslateY.value }] - }), []); - - const transitionStyle = useAnimatedStyle(() => ({ - opacity: transitionOpacity.value, - }), []); + // Removed animated styles // Memoized error component for performance const ErrorComponent = useMemo(() => { @@ -377,7 +356,7 @@ const MetadataScreen: React.FC = () => { return ( @@ -390,19 +369,14 @@ const MetadataScreen: React.FC = () => { logoLoadError={assetData.logoLoadError} handleBack={handleBack} handleToggleLibrary={handleToggleLibrary} - headerElementsY={animations.headerElementsY} inLibrary={inLibrary} - headerOpacity={animations.headerOpacity} - headerElementsOpacity={animations.headerElementsOpacity} safeAreaTop={safeAreaTop} setLogoLoadError={assetData.setLogoLoadError} /> - { bannerImage={assetData.bannerImage} loadingBanner={assetData.loadingBanner} logoLoadError={assetData.logoLoadError} - scrollY={animations.scrollY} - heroHeight={animations.heroHeight} - heroOpacity={animations.heroOpacity} - logoOpacity={animations.logoOpacity} - buttonsOpacity={animations.buttonsOpacity} - buttonsTranslateY={animations.buttonsTranslateY} - watchProgressOpacity={animations.watchProgressOpacity} - watchProgressWidth={animations.watchProgressWidth} watchProgress={watchProgressData.watchProgress} type={type as 'movie' | 'series'} getEpisodeDetails={watchProgressData.getEpisodeDetails} @@ -436,7 +402,7 @@ const MetadataScreen: React.FC = () => { /> {/* Main Content - Optimized */} - + { ) : ( metadata && )} - - + + )} diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index e81f18a..42b18c8 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -13,14 +13,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withSpring, - withTiming, - FadeInDown, - FadeInUp, -} from 'react-native-reanimated'; +// Removed react-native-reanimated imports import { useTheme } from '../contexts/ThemeContext'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; @@ -77,18 +70,14 @@ const OnboardingScreen = () => { const navigation = useNavigation>(); const [currentIndex, setCurrentIndex] = useState(0); const flatListRef = useRef(null); - const progressValue = useSharedValue(0); - - const animatedProgressStyle = useAnimatedStyle(() => ({ - width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`), - })); + // Removed animated progress values const handleNext = () => { if (currentIndex < onboardingData.length - 1) { const nextIndex = currentIndex + 1; setCurrentIndex(nextIndex); flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true }); - progressValue.value = (nextIndex + 1) / onboardingData.length; + // Removed progress animation } else { handleGetStarted(); } @@ -125,22 +114,16 @@ const OnboardingScreen = () => { start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} > - + - + - + {item.title} @@ -150,7 +133,7 @@ const OnboardingScreen = () => { {item.description} - + ); }; @@ -188,11 +171,10 @@ const OnboardingScreen = () => { {/* Progress Bar */} - @@ -370,4 +352,4 @@ const styles = StyleSheet.create({ }, }); -export default OnboardingScreen; \ No newline at end of file +export default OnboardingScreen; \ No newline at end of file