diff --git a/App.tsx b/App.tsx index 82d898f6..1b2a225f 100644 --- a/App.tsx +++ b/App.tsx @@ -8,7 +8,8 @@ import React, { useState, useEffect } from 'react'; import { View, - StyleSheet + StyleSheet, + I18nManager } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -44,6 +45,11 @@ Sentry.init({ // spotlight: __DEV__, }); +// Force LTR layout to prevent RTL issues when Arabic is set as system language +// This ensures posters and UI elements remain visible and properly positioned +I18nManager.allowRTL(false); +I18nManager.forceRTL(false); + // This fixes many navigation layout issues by using native screen containers enableScreens(true); diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index d2c29c6a..885e7e20 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -7,7 +7,6 @@ import { TouchableOpacity, Platform, InteractionManager, - StatusBar, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; @@ -28,6 +27,7 @@ import Animated, { } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { useTraktContext } from '../../contexts/TraktContext'; +import { useSettings } from '../../hooks/useSettings'; import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; import TrailerService from '../../services/trailerService'; @@ -311,7 +311,8 @@ const WatchProgressDisplay = memo(({ type, getEpisodeDetails, animatedStyle, - isWatched + isWatched, + isTrailerPlaying }: { watchProgress: { currentTime: number; @@ -325,6 +326,7 @@ const WatchProgressDisplay = memo(({ getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; isWatched: boolean; + isTrailerPlaying: boolean; }) => { const { currentTheme } = useTheme(); const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext(); @@ -398,8 +400,8 @@ const WatchProgressDisplay = memo(({ } const watchedDate = watchProgress?.lastUpdated - ? new Date(watchProgress.lastUpdated).toLocaleDateString() - : new Date().toLocaleDateString(); + ? new Date(watchProgress.lastUpdated).toLocaleDateString('en-US') + : new Date().toLocaleDateString('en-US'); // Determine if watched via Trakt or local const watchedViaTrakt = isTraktAuthenticated && @@ -429,7 +431,7 @@ const WatchProgressDisplay = memo(({ } else { progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; } - const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString(); + const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString('en-US'); let episodeInfo = ''; if (type === 'series' && watchProgress.episodeId) { @@ -534,6 +536,9 @@ const WatchProgressDisplay = memo(({ })); if (!progressData) return null; + + // Hide watch progress when trailer is playing + if (isTrailerPlaying) return null; const isCompleted = progressData.isWatched || progressData.progressPercent >= 85; @@ -688,6 +693,7 @@ const HeroSection: React.FC = memo(({ }) => { const { currentTheme } = useTheme(); const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); + const { settings } = useSettings(); // Performance optimization: Refs for avoiding re-renders const interactionComplete = useRef(false); @@ -703,13 +709,17 @@ const HeroSection: React.FC = memo(({ const [isTrailerPlaying, setIsTrailerPlaying] = useState(false); const [trailerReady, setTrailerReady] = useState(false); const [trailerPreloaded, setTrailerPreloaded] = useState(false); - const [isTrailerFullscreen, setIsTrailerFullscreen] = useState(false); const imageOpacity = useSharedValue(1); const imageLoadOpacity = useSharedValue(0); const shimmerOpacity = useSharedValue(0.3); const trailerOpacity = useSharedValue(0); const thumbnailOpacity = useSharedValue(1); + // Animation values for trailer unmute effects + const actionButtonsOpacity = useSharedValue(1); + const titleCardTranslateY = useSharedValue(0); + const genreOpacity = useSharedValue(1); + // Performance optimization: Cache theme colors const themeColors = useMemo(() => ({ black: currentTheme.colors.black, @@ -747,13 +757,6 @@ const HeroSection: React.FC = memo(({ trailerOpacity.value = withTiming(0, { duration: 300 }); thumbnailOpacity.value = withTiming(1, { duration: 300 }); }, [trailerOpacity, thumbnailOpacity]); - - // Handle trailer fullscreen toggle - const handleTrailerFullscreenToggle = useCallback(() => { - setIsTrailerFullscreen(true); - // Hide status bar when entering fullscreen - StatusBar.setHidden(true, 'slide'); - }, []); // Memoized image source const imageSource = useMemo(() => @@ -772,10 +775,10 @@ const HeroSection: React.FC = memo(({ return () => timer.cancel(); }, []); - // Fetch trailer URL when component mounts + // Fetch trailer URL when component mounts (only if trailers are enabled) useEffect(() => { const fetchTrailer = async () => { - if (!metadata?.name || !metadata?.year) return; + if (!metadata?.name || !metadata?.year || !settings?.showTrailers) return; setTrailerLoading(true); setTrailerError(false); @@ -800,7 +803,7 @@ const HeroSection: React.FC = memo(({ }; fetchTrailer(); - }, [metadata?.name, metadata?.year]); + }, [metadata?.name, metadata?.year, settings?.showTrailers]); // Optimized shimmer animation for loading state useEffect(() => { @@ -898,7 +901,7 @@ const HeroSection: React.FC = memo(({ // Simplified buttons animation const buttonsAnimatedStyle = useAnimatedStyle(() => ({ - opacity: buttonsOpacity.value, + opacity: buttonsOpacity.value * actionButtonsOpacity.value, transform: [{ translateY: interpolate( buttonsTranslateY.value, @@ -909,6 +912,16 @@ const HeroSection: React.FC = memo(({ }] }), []); + // Title card animation for lowering position when trailer is unmuted + const titleCardAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: titleCardTranslateY.value }] + }), []); + + // Genre animation for hiding when trailer is unmuted + const genreAnimatedStyle = useAnimatedStyle(() => ({ + opacity: genreOpacity.value + }), []); + // Optimized genre rendering with lazy loading and memory management const genreElements = useMemo(() => { if (!shouldLoadSecondaryData || !metadata?.genres?.length) return null; @@ -960,9 +973,6 @@ const HeroSection: React.FC = memo(({ imageLoadOpacity.value = 0; shimmerOpacity.value = 0.3; interactionComplete.current = false; - - // Restore status bar when component unmounts - StatusBar.setHidden(false, 'slide'); }; }, []); @@ -980,38 +990,6 @@ const HeroSection: React.FC = memo(({ } }); - // Don't render hero content when trailer is in fullscreen - if (isTrailerFullscreen) { - return ( - - {/* Back button for fullscreen mode */} - { - setIsTrailerFullscreen(false); - // Restore status bar when exiting fullscreen - StatusBar.setHidden(false, 'slide'); - }} - activeOpacity={0.7} - > - - - - {trailerUrl && ( - - )} - - ); - } - return ( {/* Optimized Background */} @@ -1047,7 +1025,7 @@ const HeroSection: React.FC = memo(({ )} {/* Hidden preload trailer player - loads in background */} - {shouldLoadSecondaryData && trailerUrl && !trailerLoading && !trailerError && !trailerPreloaded && ( + {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && !trailerPreloaded && ( = memo(({ )} {/* Visible trailer player - rendered on top with fade transition */} - {shouldLoadSecondaryData && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && ( + {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && ( @@ -1084,7 +1062,7 @@ const HeroSection: React.FC = memo(({ )} {/* Unmute button for trailer */} - {trailerReady && trailerUrl && ( + {settings?.showTrailers && trailerReady && trailerUrl && ( = memo(({ zIndex: 10, opacity: trailerOpacity }}> - - setTrailerMuted(!trailerMuted)} - activeOpacity={0.7} - style={styles.trailerControlButton} - > - - - - {/* Fullscreen button */} - - - - + { + setTrailerMuted(!trailerMuted); + if (trailerMuted) { + // When unmuting, hide action buttons, genre, and lower title card more + actionButtonsOpacity.value = withTiming(0, { duration: 300 }); + genreOpacity.value = withTiming(0, { duration: 300 }); + titleCardTranslateY.value = withTiming(60, { duration: 300 }); + } else { + // When muting, show action buttons, genre, and restore title card position + actionButtonsOpacity.value = withTiming(1, { duration: 300 }); + genreOpacity.value = withTiming(1, { duration: 300 }); + titleCardTranslateY.value = withTiming(0, { duration: 300 }); + } + }} + activeOpacity={0.7} + style={{ + padding: 8, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + borderRadius: 20, + }} + > + + )} @@ -1161,11 +1141,9 @@ const HeroSection: React.FC = memo(({ style={styles.bottomFadeGradient} pointerEvents="none" /> - + {/* Optimized Title/Logo */} - + {shouldLoadSecondaryData && metadata.logo && !logoLoadError ? ( = memo(({ )} - + {/* Enhanced Watch Progress with Trakt integration */} = memo(({ getEpisodeDetails={getEpisodeDetails} animatedStyle={watchProgressAnimatedStyle} isWatched={isWatched} + isTrailerPlaying={isTrailerPlaying} /> {/* Optimized genre display with lazy loading */} {shouldLoadSecondaryData && genreElements && ( - + {genreElements} - + )} {/* Optimized Action Buttons */} @@ -1228,20 +1207,6 @@ const styles = StyleSheet.create({ backgroundColor: '#000', overflow: 'hidden', }, - fullscreenTrailerContainer: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - zIndex: 9999, - backgroundColor: '#000', - }, - fullscreenTrailerPlayer: { - flex: 1, - width: '100%', - height: '100%', - }, absoluteFill: { position: 'absolute', top: 0, @@ -1263,15 +1228,6 @@ const styles = StyleSheet.create({ textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 3, }, - fullscreenBackButton: { - position: 'absolute', - top: Platform.OS === 'android' ? 40 : 50, - left: isTablet ? 32 : 16, - zIndex: 10, - padding: 8, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - borderRadius: 20, - }, heroGradient: { flex: 1, @@ -1786,15 +1742,6 @@ const styles = StyleSheet.create({ opacity: 0.8, marginBottom: 1, }, - trailerControlsContainer: { - flexDirection: 'row', - gap: 10, - }, - trailerControlButton: { - padding: 8, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - borderRadius: 20, - }, }); export default HeroSection; \ No newline at end of file diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index ff998824..5a0ccd29 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -10,7 +10,7 @@ import { Episode } from '../../types/metadata'; import { tmdbService } from '../../services/tmdbService'; import { storageService } from '../../services/storageService'; import { useFocusEffect } from '@react-navigation/native'; -import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft, withTiming, withSpring, useSharedValue, useAnimatedStyle, Easing } from 'react-native-reanimated'; +import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft } from 'react-native-reanimated'; import { TraktService } from '../../services/traktService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -53,30 +53,9 @@ export const SeriesContent: React.FC = ({ // Add state for season view mode (persists for current show across navigation) const [seasonViewMode, setSeasonViewMode] = useState<'posters' | 'text'>('posters'); - // Animated values for view mode transitions - const posterViewOpacity = useSharedValue(1); - const textViewOpacity = useSharedValue(0); - const posterViewTranslateX = useSharedValue(0); - const textViewTranslateX = useSharedValue(50); - const posterViewScale = useSharedValue(1); - const textViewScale = useSharedValue(0.95); - - // Animated styles for view transitions - const posterViewAnimatedStyle = useAnimatedStyle(() => ({ - opacity: posterViewOpacity.value, - transform: [ - { translateX: posterViewTranslateX.value }, - { scale: posterViewScale.value } - ], - })); - - const textViewAnimatedStyle = useAnimatedStyle(() => ({ - opacity: textViewOpacity.value, - transform: [ - { translateX: textViewTranslateX.value }, - { scale: textViewScale.value } - ], - })); + // View mode state (no animations) + const [posterViewVisible, setPosterViewVisible] = useState(true); + const [textViewVisible, setTextViewVisible] = useState(false); // Add refs for the scroll views const seasonScrollViewRef = useRef(null); @@ -102,28 +81,20 @@ export const SeriesContent: React.FC = ({ loadViewModePreference(); }, [metadata?.id]); - // Initialize animated values based on current view mode + // Initialize view mode visibility based on current view mode useEffect(() => { if (seasonViewMode === 'text') { - // Initialize text view as visible - posterViewOpacity.value = 0; - posterViewTranslateX.value = -60; - posterViewScale.value = 0.95; - textViewOpacity.value = 1; - textViewTranslateX.value = 0; - textViewScale.value = 1; + setPosterViewVisible(false); + setTextViewVisible(true); } else { - // Initialize poster view as visible - posterViewOpacity.value = 1; - posterViewTranslateX.value = 0; - posterViewScale.value = 1; - textViewOpacity.value = 0; - textViewTranslateX.value = 50; - textViewScale.value = 0.95; + setPosterViewVisible(true); + setTextViewVisible(false); } }, [seasonViewMode]); - // Save view mode preference when it changes + + + // Update view mode without animations const updateViewMode = (newMode: 'posters' | 'text') => { setSeasonViewMode(newMode); if (metadata?.id) { @@ -132,73 +103,6 @@ export const SeriesContent: React.FC = ({ }); } }; - - // Animate view mode transition - const animateViewModeTransition = (newMode: 'posters' | 'text') => { - if (newMode === 'text') { - // Animate to text view with spring animations for smoother feel - posterViewOpacity.value = withTiming(0, { - duration: 250, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) - }); - posterViewTranslateX.value = withSpring(-60, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - posterViewScale.value = withSpring(0.95, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - - textViewOpacity.value = withTiming(1, { - duration: 300, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) - }); - textViewTranslateX.value = withSpring(0, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - textViewScale.value = withSpring(1, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - } else { - // Animate to poster view with spring animations - textViewOpacity.value = withTiming(0, { - duration: 250, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) - }); - textViewTranslateX.value = withSpring(60, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - textViewScale.value = withSpring(0.95, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - - posterViewOpacity.value = withTiming(1, { - duration: 300, - easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) - }); - posterViewTranslateX.value = withSpring(0, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - posterViewScale.value = withSpring(1, { - damping: 20, - stiffness: 200, - mass: 0.8 - }); - } - }; // Add refs for the scroll views @@ -452,7 +356,6 @@ export const SeriesContent: React.FC = ({ ]} onPress={() => { const newMode = seasonViewMode === 'posters' ? 'text' : 'posters'; - animateViewModeTransition(newMode); updateViewMode(newMode); console.log('[SeriesContent] View mode changed to:', newMode, 'Current ref value:', seasonViewMode); }} @@ -497,11 +400,9 @@ export const SeriesContent: React.FC = ({ // Text-only view console.log('[SeriesContent] Rendering text view for season:', season, 'View mode ref:', seasonViewMode); return ( - = ({ Season {season} - + ); } // Poster view (current implementation) console.log('[SeriesContent] Rendering poster view for season:', season, 'View mode ref:', seasonViewMode); return ( - = ({ > Season {season} - - - ); - }} + + + ); + }} keyExtractor={season => season.toString()} /> diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index facb03fc..b4baa497 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -64,6 +64,8 @@ export interface AppSettings { posterSize: 'small' | 'medium' | 'large'; // Predefined sizes posterBorderRadius: number; // 0-20 range for border radius postersPerRow: number; // 3-6 range for number of posters per row + // Trailer settings + showTrailers: boolean; // Enable/disable trailer playback in hero section } export const DEFAULT_SETTINGS: AppSettings = { @@ -98,10 +100,12 @@ export const DEFAULT_SETTINGS: AppSettings = { themeId: 'default', customThemes: [], useDominantBackgroundColor: true, - // Home screen poster defaults + // Home screen poster customization posterSize: 'medium', posterBorderRadius: 12, postersPerRow: 4, + // Trailer settings + showTrailers: true, // Enable trailers by default }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 9e027449..24c2de43 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -530,6 +530,20 @@ const SettingsScreen: React.FC = () => { onPress={() => navigation.navigate('PlayerSettings')} isTablet={isTablet} /> + ( + updateSetting('showTrailers', value)} + trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} + thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'} + /> + )} + isTablet={isTablet} + />