diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index c5a52d5..df8a6b3 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet, Modal } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; @@ -51,6 +52,7 @@ const getVideoResizeMode = (resizeMode: ResizeModeType) => { const AndroidVideoPlayer: React.FC = () => { const navigation = useNavigation(); + const insets = useSafeAreaInsets(); const route = useRoute>(); const { @@ -188,6 +190,12 @@ const AndroidVideoPlayer: React.FC = () => { const [showErrorModal, setShowErrorModal] = useState(false); const [errorDetails, setErrorDetails] = useState(''); const errorTimeoutRef = useRef(null); + + // Pause overlay state + const [showPauseOverlay, setShowPauseOverlay] = useState(false); + const pauseOverlayTimerRef = useRef(null); + const pauseOverlayOpacity = useRef(new Animated.Value(0)).current; + const pauseOverlayTranslateY = useRef(new Animated.Value(12)).current; // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); const metadataResult = useMetadata({ id: id || 'placeholder', type: (type as any) }); @@ -715,10 +723,21 @@ const AndroidVideoPlayer: React.FC = () => { // Navigate immediately without delay ScreenOrientation.unlockAsync().then(() => { + // On iOS, explicitly return to portrait to avoid sticking in landscape + if (Platform.OS === 'ios') { + setTimeout(() => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {}); + }, 50); + } disableImmersiveMode(); navigation.goBack(); }).catch(() => { - // Fallback: navigate even if orientation unlock fails + // Fallback: still try to restore portrait then navigate + if (Platform.OS === 'ios') { + setTimeout(() => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {}); + }, 50); + } disableImmersiveMode(); navigation.goBack(); }); @@ -1150,6 +1169,57 @@ const AndroidVideoPlayer: React.FC = () => { } }; + // Handle paused overlay after 5 seconds of being paused + useEffect(() => { + if (paused) { + if (pauseOverlayTimerRef.current) { + clearTimeout(pauseOverlayTimerRef.current); + } + pauseOverlayTimerRef.current = setTimeout(() => { + setShowPauseOverlay(true); + pauseOverlayOpacity.setValue(0); + pauseOverlayTranslateY.setValue(12); + Animated.parallel([ + Animated.timing(pauseOverlayOpacity, { + toValue: 1, + duration: 550, + useNativeDriver: true, + }), + Animated.timing(pauseOverlayTranslateY, { + toValue: 0, + duration: 450, + useNativeDriver: true, + }) + ]).start(); + }, 5000); + } else { + if (pauseOverlayTimerRef.current) { + clearTimeout(pauseOverlayTimerRef.current); + pauseOverlayTimerRef.current = null; + } + if (showPauseOverlay) { + Animated.parallel([ + Animated.timing(pauseOverlayOpacity, { + toValue: 0, + duration: 220, + useNativeDriver: true, + }), + Animated.timing(pauseOverlayTranslateY, { + toValue: 8, + duration: 220, + useNativeDriver: true, + }) + ]).start(() => setShowPauseOverlay(false)); + } + } + return () => { + if (pauseOverlayTimerRef.current) { + clearTimeout(pauseOverlayTimerRef.current); + pauseOverlayTimerRef.current = null; + } + }; + }, [paused]); + useEffect(() => { isMounted.current = true; return () => { @@ -1589,6 +1659,68 @@ const AndroidVideoPlayer: React.FC = () => { buffered={buffered} formatTime={formatTime} /> + + {showPauseOverlay && ( + + {/* Strong horizontal fade from left side */} + + + + + + You're watching + + {title} + + {!!year && ( + + {`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`} + + )} + {!!episodeTitle && ( + + {episodeTitle} + + )} + {!!metadata?.description && ( + + {metadata.description} + + )} + + + )} { + const insets = useSafeAreaInsets(); const route = useRoute>(); const { streamProvider, uri, headers, forceVlc } = route.params as any; @@ -209,6 +211,12 @@ const VideoPlayer: React.FC = () => { const controlsTimeout = useRef(null); const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false); + // Pause overlay state + const [showPauseOverlay, setShowPauseOverlay] = useState(false); + const pauseOverlayTimerRef = useRef(null); + const pauseOverlayOpacity = useRef(new Animated.Value(0)).current; + const pauseOverlayTranslateY = useRef(new Animated.Value(12)).current; + // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); const metadataResult = useMetadata({ @@ -756,6 +764,13 @@ const VideoPlayer: React.FC = () => { logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError); } + // On iOS, explicitly return to portrait to avoid sticking in landscape + if (Platform.OS === 'ios') { + setTimeout(() => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {}); + }, 50); + } + // Disable immersive mode disableImmersiveMode(); @@ -1073,6 +1088,57 @@ const VideoPlayer: React.FC = () => { } }; + // Handle paused overlay after 5 seconds of being paused + useEffect(() => { + if (paused) { + if (pauseOverlayTimerRef.current) { + clearTimeout(pauseOverlayTimerRef.current); + } + pauseOverlayTimerRef.current = setTimeout(() => { + setShowPauseOverlay(true); + pauseOverlayOpacity.setValue(0); + pauseOverlayTranslateY.setValue(12); + Animated.parallel([ + Animated.timing(pauseOverlayOpacity, { + toValue: 1, + duration: 550, + useNativeDriver: true, + }), + Animated.timing(pauseOverlayTranslateY, { + toValue: 0, + duration: 450, + useNativeDriver: true, + }) + ]).start(); + }, 5000); + } else { + if (pauseOverlayTimerRef.current) { + clearTimeout(pauseOverlayTimerRef.current); + pauseOverlayTimerRef.current = null; + } + if (showPauseOverlay) { + Animated.parallel([ + Animated.timing(pauseOverlayOpacity, { + toValue: 0, + duration: 220, + useNativeDriver: true, + }), + Animated.timing(pauseOverlayTranslateY, { + toValue: 8, + duration: 220, + useNativeDriver: true, + }) + ]).start(() => setShowPauseOverlay(false)); + } + } + return () => { + if (pauseOverlayTimerRef.current) { + clearTimeout(pauseOverlayTimerRef.current); + pauseOverlayTimerRef.current = null; + } + }; + }, [paused]); + useEffect(() => { isMounted.current = true; return () => { @@ -1516,6 +1582,68 @@ const VideoPlayer: React.FC = () => { formatTime={formatTime} /> + {showPauseOverlay && ( + + {/* Strong horizontal fade from left side */} + + + + + + You're watching + + {title} + + {!!year && ( + + {`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`} + + )} + {!!episodeTitle && ( + + {episodeTitle} + + )} + {!!metadata?.description && ( + + {metadata.description} + + )} + + + )} + { // For iOS specifically if (Platform.OS === 'ios') { StatusBar.setHidden(false); + // Ensure portrait when coming back to Home on iOS + try { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); + } catch {} } }; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 5d8dd80..5478d89 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -18,7 +18,7 @@ import { } from 'react-native'; import * as ScreenOrientation from 'expo-screen-orientation'; -import { useRoute, useNavigation } from '@react-navigation/native'; +import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -1047,6 +1047,16 @@ export const StreamsScreen = () => { } }, [settings.preferredPlayer, settings.useExternalPlayer, navigateToPlayer]); + // Ensure portrait when returning to this screen on iOS + useFocusEffect( + useCallback(() => { + if (Platform.OS === 'ios') { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {}); + } + return () => {}; + }, []) + ); + // Autoplay effect - triggers immediately when streams are available and autoplay is enabled useEffect(() => { if (