This commit is contained in:
tapframe 2025-09-12 23:45:42 +05:30
parent 67648ea6db
commit 2a3c504c67
4 changed files with 643 additions and 4 deletions

25
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "nuvio",
"version": "0.6.0-beta.6",
"dependencies": {
"@adrianso/react-native-device-brightness": "^1.2.7",
"@backpackapp-io/react-native-toast": "^0.14.0",
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4",
@ -63,11 +64,13 @@
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screen-brightness": "^2.0.0-alpha",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1"
},
@ -94,6 +97,12 @@
}
}
},
"node_modules/@adrianso/react-native-device-brightness": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@adrianso/react-native-device-brightness/-/react-native-device-brightness-1.2.7.tgz",
"integrity": "sha512-q3OTFGohAh04R8cBka7/eNXaeSD8bAZocMsifLIR3oDF5N9cNH2UVEjzJaFXW8DHFEsODpljpT+eyGWIhKlkow==",
"license": "MIT"
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@ -12329,6 +12338,12 @@
"react-native": "*"
}
},
"node_modules/react-native-screen-brightness": {
"version": "2.0.0-alpha",
"resolved": "https://registry.npmjs.org/react-native-screen-brightness/-/react-native-screen-brightness-2.0.0-alpha.tgz",
"integrity": "sha512-NdJPptcWiFwG9aypm0awYv9d/aoZSJetXPFkx6w/UuNX9SRr5YTKXMSND/2lwuSOw6uGU9mQ0f4dBuGZSNgzow==",
"license": "MIT"
},
"node_modules/react-native-screens": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
@ -12608,6 +12623,16 @@
"react-native-vector-icons": "^9.2.0"
}
},
"node_modules/react-native-volume-manager": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/react-native-volume-manager/-/react-native-volume-manager-2.0.8.tgz",
"integrity": "sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-web": {
"version": "0.19.13",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",

View file

@ -8,6 +8,7 @@
"ios": "expo run:ios"
},
"dependencies": {
"@adrianso/react-native-device-brightness": "^1.2.7",
"@backpackapp-io/react-native-toast": "^0.14.0",
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4",
@ -63,11 +64,13 @@
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screen-brightness": "^2.0.0-alpha",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1"
},

View file

@ -4,7 +4,7 @@ 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';
import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import { PinchGestureHandler, PanGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import RNImmersiveMode from 'react-native-immersive-mode';
import * as ScreenOrientation from 'expo-screen-orientation';
import { storageService } from '../../services/storageService';
@ -38,6 +38,8 @@ import CustomSubtitles from './subtitles/CustomSubtitles';
import { SourcesModal } from './modals/SourcesModal';
import { stremioService } from '../../services/stremioService';
import axios from 'axios';
import DeviceBrightness from '@adrianso/react-native-device-brightness';
import { VolumeManager } from 'react-native-volume-manager';
// Map VLC resize modes to react-native-video resize modes
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
@ -214,6 +216,18 @@ const AndroidVideoPlayer: React.FC = () => {
const [errorDetails, setErrorDetails] = useState<string>('');
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Volume and brightness controls
const [volume, setVolume] = useState(1.0);
const [brightness, setBrightness] = useState(1.0);
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const lastVolumeChange = useRef<number>(0);
const lastBrightnessChange = useRef<number>(0);
// iOS startup timing diagnostics
const loadStartAtRef = useRef<number | null>(null);
const firstFrameAtRef = useRef<number | null>(null);
@ -357,6 +371,114 @@ const AndroidVideoPlayer: React.FC = () => {
}
};
// Volume gesture handler (right side of screen)
const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
const { translationY, state } = event.nativeEvent;
const screenHeight = screenDimensions.height;
const sensitivity = 0.003; // Adjust sensitivity
if (state === State.ACTIVE) {
const deltaY = -translationY; // Invert for natural feel (up = increase)
const volumeChange = deltaY * sensitivity;
const newVolume = Math.max(0, Math.min(1, volume + volumeChange));
if (Math.abs(newVolume - volume) > 0.01) { // Only update if significant change
setVolume(newVolume);
lastVolumeChange.current = Date.now();
// Set device volume using VolumeManager
try {
await VolumeManager.setVolume(newVolume);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Device volume set to: ${newVolume}`);
}
} catch (error) {
logger.warn('[AndroidVideoPlayer] Error setting device volume:', error);
}
// Show overlay
if (!showVolumeOverlay) {
setShowVolumeOverlay(true);
Animated.timing(volumeOverlayOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
}
// Clear existing timeout
if (volumeOverlayTimeout.current) {
clearTimeout(volumeOverlayTimeout.current);
}
// Hide overlay after 2 seconds
volumeOverlayTimeout.current = setTimeout(() => {
Animated.timing(volumeOverlayOpacity, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowVolumeOverlay(false);
});
}, 2000);
}
}
};
// Brightness gesture handler (left side of screen)
const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
const { translationY, state } = event.nativeEvent;
const screenHeight = screenDimensions.height;
const sensitivity = 0.003; // Adjust sensitivity
if (state === State.ACTIVE) {
const deltaY = -translationY; // Invert for natural feel (up = increase)
const brightnessChange = deltaY * sensitivity;
const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange));
if (Math.abs(newBrightness - brightness) > 0.01) { // Only update if significant change
setBrightness(newBrightness);
lastBrightnessChange.current = Date.now();
// Set device brightness using DeviceBrightness
try {
await DeviceBrightness.setBrightnessLevel(newBrightness);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Device brightness set to: ${newBrightness}`);
}
} catch (error) {
logger.warn('[AndroidVideoPlayer] Error setting device brightness:', error);
}
// Show overlay
if (!showBrightnessOverlay) {
setShowBrightnessOverlay(true);
Animated.timing(brightnessOverlayOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
}
// Clear existing timeout
if (brightnessOverlayTimeout.current) {
clearTimeout(brightnessOverlayTimeout.current);
}
// Hide overlay after 2 seconds
brightnessOverlayTimeout.current = setTimeout(() => {
Animated.timing(brightnessOverlayOpacity, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowBrightnessOverlay(false);
});
}, 2000);
}
}
};
const resetZoom = () => {
const targetZoom = is16by9Content ? 1.1 : 1;
setZoomScale(targetZoom);
@ -385,10 +507,31 @@ const AndroidVideoPlayer: React.FC = () => {
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
setScreenDimensions(screen);
});
const initializePlayer = () => {
const initializePlayer = async () => {
StatusBar.setHidden(true, 'none');
enableImmersiveMode();
startOpeningAnimation();
// Initialize current volume and brightness levels
try {
const currentVolume = await VolumeManager.getVolume();
setVolume(currentVolume.volume);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Initial volume: ${currentVolume.volume}`);
}
} catch (error) {
logger.warn('[AndroidVideoPlayer] Error getting initial volume:', error);
}
try {
const currentBrightness = await DeviceBrightness.getBrightnessLevel();
setBrightness(currentBrightness);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Initial brightness: ${currentBrightness}`);
}
} catch (error) {
logger.warn('[AndroidVideoPlayer] Error getting initial brightness:', error);
}
};
initializePlayer();
return () => {
@ -1767,6 +1910,12 @@ const AndroidVideoPlayer: React.FC = () => {
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
}
if (volumeOverlayTimeout.current) {
clearTimeout(volumeOverlayTimeout.current);
}
if (brightnessOverlayTimeout.current) {
clearTimeout(brightnessOverlayTimeout.current);
}
};
}, []);
@ -2139,6 +2288,40 @@ const AndroidVideoPlayer: React.FC = () => {
onPress={toggleControls}
activeOpacity={1}
>
{/* Left side brightness gesture handler */}
<PanGestureHandler
onGestureEvent={onBrightnessGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-50, 50]}
shouldCancelWhenOutside={false}
>
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: screenDimensions.width * 0.3, // Left 30% of screen
height: screenDimensions.height,
zIndex: 10,
}} />
</PanGestureHandler>
{/* Right side volume gesture handler */}
<PanGestureHandler
onGestureEvent={onVolumeGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-50, 50]}
shouldCancelWhenOutside={false}
>
<View style={{
position: 'absolute',
top: 0,
right: 0,
width: screenDimensions.width * 0.3, // Right 30% of screen
height: screenDimensions.height,
zIndex: 10,
}} />
</PanGestureHandler>
<PinchGestureHandler
ref={pinchRef}
onGestureEvent={onPinchGestureEvent}
@ -2196,7 +2379,7 @@ const AndroidVideoPlayer: React.FC = () => {
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
rate={1.0}
volume={1.0}
volume={volume}
muted={false}
repeat={false}
playInBackground={false}
@ -2634,6 +2817,124 @@ const AndroidVideoPlayer: React.FC = () => {
controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 120 : 100}
/>
{/* Volume Overlay */}
{showVolumeOverlay && (
<Animated.View
style={{
position: 'absolute',
right: 20 + insets.right,
top: '50%',
transform: [{ translateY: -50 }],
opacity: volumeOverlayOpacity,
zIndex: 1000,
}}
>
<View style={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 12,
padding: 16,
alignItems: 'center',
minWidth: 80,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}}>
<MaterialIcons
name={volume === 0 ? "volume-off" : volume < 0.5 ? "volume-down" : "volume-up"}
size={24}
color="#FFFFFF"
style={{ marginBottom: 8 }}
/>
<View style={{
width: 4,
height: 60,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 2,
position: 'relative',
}}>
<View style={{
position: 'absolute',
bottom: 0,
left: 0,
width: 4,
height: `${volume * 100}%`,
backgroundColor: '#E50914',
borderRadius: 2,
}} />
</View>
<Text style={{
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 8,
}}>
{Math.round(volume * 100)}%
</Text>
</View>
</Animated.View>
)}
{/* Brightness Overlay */}
{showBrightnessOverlay && (
<Animated.View
style={{
position: 'absolute',
left: 20 + insets.left,
top: '50%',
transform: [{ translateY: -50 }],
opacity: brightnessOverlayOpacity,
zIndex: 1000,
}}
>
<View style={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 12,
padding: 16,
alignItems: 'center',
minWidth: 80,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}}>
<MaterialIcons
name={brightness < 0.3 ? "brightness-low" : brightness < 0.7 ? "brightness-medium" : "brightness-high"}
size={24}
color="#FFFFFF"
style={{ marginBottom: 8 }}
/>
<View style={{
width: 4,
height: 60,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 2,
position: 'relative',
}}>
<View style={{
position: 'absolute',
bottom: 0,
left: 0,
width: 4,
height: `${brightness * 100}%`,
backgroundColor: '#FFD700',
borderRadius: 2,
}} />
</View>
<Text style={{
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 8,
}}>
{Math.round(brightness * 100)}%
</Text>
</View>
</Animated.View>
)}
{/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}
</TouchableOpacity>
</Animated.View>

View file

@ -4,7 +4,7 @@ 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';
import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import { PinchGestureHandler, PanGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import RNImmersiveMode from 'react-native-immersive-mode';
import * as ScreenOrientation from 'expo-screen-orientation';
import { storageService } from '../../services/storageService';
@ -40,6 +40,8 @@ import CustomSubtitles from './subtitles/CustomSubtitles';
import { SourcesModal } from './modals/SourcesModal';
import axios from 'axios';
import { stremioService } from '../../services/stremioService';
import DeviceBrightness from '@adrianso/react-native-device-brightness';
import { VolumeManager } from 'react-native-volume-manager';
const VideoPlayer: React.FC = () => {
const insets = useSafeAreaInsets();
@ -236,6 +238,18 @@ const VideoPlayer: React.FC = () => {
const castDetailsOpacity = useRef(new Animated.Value(0)).current;
const castDetailsScale = useRef(new Animated.Value(0.95)).current;
// Volume and brightness controls
const [volume, setVolume] = useState(100); // VLC uses 0-100 range
const [brightness, setBrightness] = useState(1.0);
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const lastVolumeChange = useRef<number>(0);
const lastBrightnessChange = useRef<number>(0);
// Get metadata to access logo (only if we have a valid id)
const shouldLoadMetadata = Boolean(id && type);
const metadataResult = useMetadata({
@ -363,6 +377,123 @@ const VideoPlayer: React.FC = () => {
}
};
// Volume gesture handler (right side of screen)
const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
const { translationY, state } = event.nativeEvent;
const screenHeight = screenDimensions.height;
const sensitivity = 0.3; // Adjust sensitivity for VLC (0-100 range)
if (state === State.ACTIVE) {
const deltaY = -translationY; // Invert for natural feel (up = increase)
const volumeChange = deltaY * sensitivity;
const newVolume = Math.max(0, Math.min(100, volume + volumeChange));
if (Math.abs(newVolume - volume) > 1) { // Only update if significant change
setVolume(newVolume);
lastVolumeChange.current = Date.now();
// Set device volume using VolumeManager
try {
await VolumeManager.setVolume(newVolume / 100); // Convert to 0-1 range
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Device volume set to: ${newVolume / 100}`);
}
} catch (error) {
logger.warn('[VideoPlayer] Error setting device volume:', error);
}
// Set VLC volume as well
if (vlcRef.current) {
try {
vlcRef.current.setVolume(newVolume);
} catch (error) {
logger.warn('[VideoPlayer] Error setting VLC volume:', error);
}
}
// Show overlay
if (!showVolumeOverlay) {
setShowVolumeOverlay(true);
Animated.timing(volumeOverlayOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
}
// Clear existing timeout
if (volumeOverlayTimeout.current) {
clearTimeout(volumeOverlayTimeout.current);
}
// Hide overlay after 2 seconds
volumeOverlayTimeout.current = setTimeout(() => {
Animated.timing(volumeOverlayOpacity, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowVolumeOverlay(false);
});
}, 2000);
}
}
};
// Brightness gesture handler (left side of screen)
const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
const { translationY, state } = event.nativeEvent;
const screenHeight = screenDimensions.height;
const sensitivity = 0.003; // Adjust sensitivity
if (state === State.ACTIVE) {
const deltaY = -translationY; // Invert for natural feel (up = increase)
const brightnessChange = deltaY * sensitivity;
const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange));
if (Math.abs(newBrightness - brightness) > 0.01) { // Only update if significant change
setBrightness(newBrightness);
lastBrightnessChange.current = Date.now();
// Set device brightness using DeviceBrightness
try {
await DeviceBrightness.setBrightnessLevel(newBrightness);
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Device brightness set to: ${newBrightness}`);
}
} catch (error) {
logger.warn('[VideoPlayer] Error setting device brightness:', error);
}
// Show overlay
if (!showBrightnessOverlay) {
setShowBrightnessOverlay(true);
Animated.timing(brightnessOverlayOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
}
// Clear existing timeout
if (brightnessOverlayTimeout.current) {
clearTimeout(brightnessOverlayTimeout.current);
}
// Hide overlay after 2 seconds
brightnessOverlayTimeout.current = setTimeout(() => {
Animated.timing(brightnessOverlayOpacity, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowBrightnessOverlay(false);
});
}, 2000);
}
}
};
useEffect(() => {
if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
const styles = calculateVideoStyles(
@ -405,6 +536,27 @@ const VideoPlayer: React.FC = () => {
StatusBar.setHidden(true, 'none');
enableImmersiveMode();
startOpeningAnimation();
// Initialize current volume and brightness levels
try {
const currentVolume = await VolumeManager.getVolume();
setVolume(currentVolume.volume * 100); // Convert to 0-100 range for VLC
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Initial volume: ${currentVolume.volume * 100}`);
}
} catch (error) {
logger.warn('[VideoPlayer] Error getting initial volume:', error);
}
try {
const currentBrightness = await DeviceBrightness.getBrightnessLevel();
setBrightness(currentBrightness);
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Initial brightness: ${currentBrightness}`);
}
} catch (error) {
logger.warn('[VideoPlayer] Error getting initial brightness:', error);
}
};
initializePlayer();
return () => {
@ -1556,6 +1708,12 @@ const VideoPlayer: React.FC = () => {
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
}
if (volumeOverlayTimeout.current) {
clearTimeout(volumeOverlayTimeout.current);
}
if (brightnessOverlayTimeout.current) {
clearTimeout(brightnessOverlayTimeout.current);
}
};
}, []);
@ -1942,6 +2100,40 @@ const VideoPlayer: React.FC = () => {
onPress={toggleControls}
activeOpacity={1}
>
{/* Left side brightness gesture handler */}
<PanGestureHandler
onGestureEvent={onBrightnessGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-50, 50]}
shouldCancelWhenOutside={false}
>
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: screenDimensions.width * 0.3, // Left 30% of screen
height: screenDimensions.height,
zIndex: 10,
}} />
</PanGestureHandler>
{/* Right side volume gesture handler */}
<PanGestureHandler
onGestureEvent={onVolumeGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-50, 50]}
shouldCancelWhenOutside={false}
>
<View style={{
position: 'absolute',
top: 0,
right: 0,
width: screenDimensions.width * 0.3, // Right 30% of screen
height: screenDimensions.height,
zIndex: 10,
}} />
</PanGestureHandler>
<PinchGestureHandler
ref={pinchRef}
onGestureEvent={onPinchGestureEvent}
@ -2398,6 +2590,124 @@ const VideoPlayer: React.FC = () => {
controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 126 : 106}
/>
{/* Volume Overlay */}
{showVolumeOverlay && (
<Animated.View
style={{
position: 'absolute',
right: 20 + insets.right,
top: '50%',
transform: [{ translateY: -50 }],
opacity: volumeOverlayOpacity,
zIndex: 1000,
}}
>
<View style={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 12,
padding: 16,
alignItems: 'center',
minWidth: 80,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}}>
<MaterialIcons
name={volume === 0 ? "volume-off" : volume < 50 ? "volume-down" : "volume-up"}
size={24}
color="#FFFFFF"
style={{ marginBottom: 8 }}
/>
<View style={{
width: 4,
height: 60,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 2,
position: 'relative',
}}>
<View style={{
position: 'absolute',
bottom: 0,
left: 0,
width: 4,
height: `${volume}%`,
backgroundColor: '#E50914',
borderRadius: 2,
}} />
</View>
<Text style={{
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 8,
}}>
{volume}%
</Text>
</View>
</Animated.View>
)}
{/* Brightness Overlay */}
{showBrightnessOverlay && (
<Animated.View
style={{
position: 'absolute',
left: 20 + insets.left,
top: '50%',
transform: [{ translateY: -50 }],
opacity: brightnessOverlayOpacity,
zIndex: 1000,
}}
>
<View style={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 12,
padding: 16,
alignItems: 'center',
minWidth: 80,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}}>
<MaterialIcons
name={brightness < 0.3 ? "brightness-low" : brightness < 0.7 ? "brightness-medium" : "brightness-high"}
size={24}
color="#FFFFFF"
style={{ marginBottom: 8 }}
/>
<View style={{
width: 4,
height: 60,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 2,
position: 'relative',
}}>
<View style={{
position: 'absolute',
bottom: 0,
left: 0,
width: 4,
height: `${brightness * 100}%`,
backgroundColor: '#FFD700',
borderRadius: 2,
}} />
</View>
<Text style={{
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 8,
}}>
{Math.round(brightness * 100)}%
</Text>
</View>
</Animated.View>
)}
{/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}
</TouchableOpacity>
</Animated.View>