mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-28 20:03:34 +00:00
test
This commit is contained in:
parent
67648ea6db
commit
2a3c504c67
4 changed files with 643 additions and 4 deletions
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "nuvio",
|
"name": "nuvio",
|
||||||
"version": "0.6.0-beta.6",
|
"version": "0.6.0-beta.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@adrianso/react-native-device-brightness": "^1.2.7",
|
||||||
"@backpackapp-io/react-native-toast": "^0.14.0",
|
"@backpackapp-io/react-native-toast": "^0.14.0",
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
|
@ -63,11 +64,13 @@
|
||||||
"react-native-paper": "^5.13.1",
|
"react-native-paper": "^5.13.1",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
|
"react-native-screen-brightness": "^2.0.0-alpha",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "15.8.0",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-vlc-media-player": "^1.0.87",
|
"react-native-vlc-media-player": "^1.0.87",
|
||||||
|
"react-native-volume-manager": "^2.0.8",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-wheel-color-picker": "^1.3.1"
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
|
|
@ -12329,6 +12338,12 @@
|
||||||
"react-native": "*"
|
"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": {
|
"node_modules/react-native-screens": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
|
"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"
|
"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": {
|
"node_modules/react-native-web": {
|
||||||
"version": "0.19.13",
|
"version": "0.19.13",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
"ios": "expo run:ios"
|
"ios": "expo run:ios"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@adrianso/react-native-device-brightness": "^1.2.7",
|
||||||
"@backpackapp-io/react-native-toast": "^0.14.0",
|
"@backpackapp-io/react-native-toast": "^0.14.0",
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
|
@ -63,11 +64,13 @@
|
||||||
"react-native-paper": "^5.13.1",
|
"react-native-paper": "^5.13.1",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
|
"react-native-screen-brightness": "^2.0.0-alpha",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "15.8.0",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-vlc-media-player": "^1.0.87",
|
"react-native-vlc-media-player": "^1.0.87",
|
||||||
|
"react-native-volume-manager": "^2.0.8",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-wheel-color-picker": "^1.3.1"
|
"react-native-wheel-color-picker": "^1.3.1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video';
|
import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
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 RNImmersiveMode from 'react-native-immersive-mode';
|
||||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||||
import { storageService } from '../../services/storageService';
|
import { storageService } from '../../services/storageService';
|
||||||
|
|
@ -38,6 +38,8 @@ import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||||
import { SourcesModal } from './modals/SourcesModal';
|
import { SourcesModal } from './modals/SourcesModal';
|
||||||
import { stremioService } from '../../services/stremioService';
|
import { stremioService } from '../../services/stremioService';
|
||||||
import axios from 'axios';
|
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
|
// Map VLC resize modes to react-native-video resize modes
|
||||||
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
||||||
|
|
@ -214,6 +216,18 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [errorDetails, setErrorDetails] = useState<string>('');
|
const [errorDetails, setErrorDetails] = useState<string>('');
|
||||||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
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
|
// iOS startup timing diagnostics
|
||||||
const loadStartAtRef = useRef<number | null>(null);
|
const loadStartAtRef = useRef<number | null>(null);
|
||||||
const firstFrameAtRef = 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 resetZoom = () => {
|
||||||
const targetZoom = is16by9Content ? 1.1 : 1;
|
const targetZoom = is16by9Content ? 1.1 : 1;
|
||||||
setZoomScale(targetZoom);
|
setZoomScale(targetZoom);
|
||||||
|
|
@ -385,10 +507,31 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
||||||
setScreenDimensions(screen);
|
setScreenDimensions(screen);
|
||||||
});
|
});
|
||||||
const initializePlayer = () => {
|
const initializePlayer = async () => {
|
||||||
StatusBar.setHidden(true, 'none');
|
StatusBar.setHidden(true, 'none');
|
||||||
enableImmersiveMode();
|
enableImmersiveMode();
|
||||||
startOpeningAnimation();
|
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();
|
initializePlayer();
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -1767,6 +1910,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
if (errorTimeoutRef.current) {
|
if (errorTimeoutRef.current) {
|
||||||
clearTimeout(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}
|
onPress={toggleControls}
|
||||||
activeOpacity={1}
|
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
|
<PinchGestureHandler
|
||||||
ref={pinchRef}
|
ref={pinchRef}
|
||||||
onGestureEvent={onPinchGestureEvent}
|
onGestureEvent={onPinchGestureEvent}
|
||||||
|
|
@ -2196,7 +2379,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
|
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
|
||||||
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
||||||
rate={1.0}
|
rate={1.0}
|
||||||
volume={1.0}
|
volume={volume}
|
||||||
muted={false}
|
muted={false}
|
||||||
repeat={false}
|
repeat={false}
|
||||||
playInBackground={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}
|
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 */}
|
{/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { VLCPlayer } from 'react-native-vlc-media-player';
|
import { VLCPlayer } from 'react-native-vlc-media-player';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList, RootStackNavigationProp } from '../../navigation/AppNavigator';
|
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 RNImmersiveMode from 'react-native-immersive-mode';
|
||||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||||
import { storageService } from '../../services/storageService';
|
import { storageService } from '../../services/storageService';
|
||||||
|
|
@ -40,6 +40,8 @@ import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||||
import { SourcesModal } from './modals/SourcesModal';
|
import { SourcesModal } from './modals/SourcesModal';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { stremioService } from '../../services/stremioService';
|
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 VideoPlayer: React.FC = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
@ -236,6 +238,18 @@ const VideoPlayer: React.FC = () => {
|
||||||
const castDetailsOpacity = useRef(new Animated.Value(0)).current;
|
const castDetailsOpacity = useRef(new Animated.Value(0)).current;
|
||||||
const castDetailsScale = useRef(new Animated.Value(0.95)).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)
|
// Get metadata to access logo (only if we have a valid id)
|
||||||
const shouldLoadMetadata = Boolean(id && type);
|
const shouldLoadMetadata = Boolean(id && type);
|
||||||
const metadataResult = useMetadata({
|
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(() => {
|
useEffect(() => {
|
||||||
if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
|
if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
|
||||||
const styles = calculateVideoStyles(
|
const styles = calculateVideoStyles(
|
||||||
|
|
@ -405,6 +536,27 @@ const VideoPlayer: React.FC = () => {
|
||||||
StatusBar.setHidden(true, 'none');
|
StatusBar.setHidden(true, 'none');
|
||||||
enableImmersiveMode();
|
enableImmersiveMode();
|
||||||
startOpeningAnimation();
|
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();
|
initializePlayer();
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -1556,6 +1708,12 @@ const VideoPlayer: React.FC = () => {
|
||||||
if (errorTimeoutRef.current) {
|
if (errorTimeoutRef.current) {
|
||||||
clearTimeout(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}
|
onPress={toggleControls}
|
||||||
activeOpacity={1}
|
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
|
<PinchGestureHandler
|
||||||
ref={pinchRef}
|
ref={pinchRef}
|
||||||
onGestureEvent={onPinchGestureEvent}
|
onGestureEvent={onPinchGestureEvent}
|
||||||
|
|
@ -2398,6 +2590,124 @@ const VideoPlayer: React.FC = () => {
|
||||||
controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 126 : 106}
|
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 */}
|
{/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue