some ui changes for player, and improved orientation afetr player closes for ios

This commit is contained in:
tapframe 2025-08-13 12:25:23 +05:30
parent 77029294aa
commit 7a172f03d4
4 changed files with 276 additions and 2 deletions

View file

@ -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<RouteProp<RootStackParamList, 'Player'>>();
const {
@ -188,6 +190,12 @@ const AndroidVideoPlayer: React.FC = () => {
const [showErrorModal, setShowErrorModal] = useState(false);
const [errorDetails, setErrorDetails] = useState<string>('');
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Pause overlay state
const [showPauseOverlay, setShowPauseOverlay] = useState(false);
const pauseOverlayTimerRef = useRef<NodeJS.Timeout | null>(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 && (
<Animated.View
pointerEvents="none"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: pauseOverlayOpacity,
}}
>
{/* Strong horizontal fade from left side */}
<View style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: screenDimensions.width * 0.7 }}>
<LinearGradient
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
colors={[ 'rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)' ]}
locations={[0, 1]}
style={StyleSheet.absoluteFill}
/>
</View>
<LinearGradient
colors={[
'rgba(0,0,0,0.6)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.0)'
]}
locations={[0, 0.3, 0.6, 1]}
style={StyleSheet.absoluteFill}
/>
<Animated.View style={{
position: 'absolute',
left: 24 + (Platform.OS === 'ios' ? insets.left : 0),
right: 24 + (Platform.OS === 'ios' ? insets.right : 0),
bottom: 110 + (Platform.OS === 'ios' ? insets.bottom : 0),
transform: [{ translateY: pauseOverlayTranslateY }]
}}>
<Text style={{ color: '#B8B8B8', fontSize: 18, marginBottom: 8 }}>You're watching</Text>
<Text style={{ color: '#FFFFFF', fontSize: 48, fontWeight: '800', marginBottom: 10 }} numberOfLines={1}>
{title}
</Text>
{!!year && (
<Text style={{ color: '#CCCCCC', fontSize: 18, marginBottom: 8 }} numberOfLines={1}>
{`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`}
</Text>
)}
{!!episodeTitle && (
<Text style={{ color: '#FFFFFF', fontSize: 20, fontWeight: '600', marginBottom: 8 }} numberOfLines={1}>
{episodeTitle}
</Text>
)}
{!!metadata?.description && (
<Text style={{ color: '#D6D6D6', fontSize: 18, lineHeight: 24 }} numberOfLines={3}>
{metadata.description}
</Text>
)}
</Animated.View>
</Animated.View>
)}
<CustomSubtitles
key={customSubtitleVersion}

View file

@ -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 { VLCPlayer } from 'react-native-vlc-media-player';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { RootStackParamList, RootStackNavigationProp } from '../../navigation/AppNavigator';
@ -41,6 +42,7 @@ import axios from 'axios';
import { stremioService } from '../../services/stremioService';
const VideoPlayer: React.FC = () => {
const insets = useSafeAreaInsets();
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
const { streamProvider, uri, headers, forceVlc } = route.params as any;
@ -209,6 +211,12 @@ const VideoPlayer: React.FC = () => {
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
// Pause overlay state
const [showPauseOverlay, setShowPauseOverlay] = useState(false);
const pauseOverlayTimerRef = useRef<NodeJS.Timeout | null>(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 && (
<Animated.View
pointerEvents="none"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: pauseOverlayOpacity,
}}
>
{/* Strong horizontal fade from left side */}
<View style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: screenDimensions.width * 0.7 }}>
<LinearGradient
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
colors={[ 'rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)' ]}
locations={[0, 1]}
style={StyleSheet.absoluteFill}
/>
</View>
<LinearGradient
colors={[
'rgba(0,0,0,0.6)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.0)'
]}
locations={[0, 0.3, 0.6, 1]}
style={StyleSheet.absoluteFill}
/>
<Animated.View style={{
position: 'absolute',
left: 24 + (Platform.OS === 'ios' ? insets.left : 0),
right: 24 + (Platform.OS === 'ios' ? insets.right : 0),
bottom: 110 + (Platform.OS === 'ios' ? insets.bottom : 0),
transform: [{ translateY: pauseOverlayTranslateY }]
}}>
<Text style={{ color: '#B8B8B8', fontSize: 18, marginBottom: 8 }}>You're watching</Text>
<Text style={{ color: '#FFFFFF', fontSize: 48, fontWeight: '800', marginBottom: 10 }} numberOfLines={1}>
{title}
</Text>
{!!year && (
<Text style={{ color: '#CCCCCC', fontSize: 18, marginBottom: 8 }} numberOfLines={1}>
{`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`}
</Text>
)}
{!!episodeTitle && (
<Text style={{ color: '#FFFFFF', fontSize: 20, fontWeight: '600', marginBottom: 8 }} numberOfLines={1}>
{episodeTitle}
</Text>
)}
{!!metadata?.description && (
<Text style={{ color: '#D6D6D6', fontSize: 18, lineHeight: 24 }} numberOfLines={3}>
{metadata.description}
</Text>
)}
</Animated.View>
</Animated.View>
)}
<CustomSubtitles
useCustomSubtitles={useCustomSubtitles}
currentSubtitle={currentSubtitle}

View file

@ -374,6 +374,10 @@ const HomeScreen = () => {
// 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 {}
}
};

View file

@ -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 (