mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 23:42:04 +00:00
Merge 1b2355d439 into 6a8f8ec606
This commit is contained in:
commit
ad354ce803
3 changed files with 229 additions and 73 deletions
|
|
@ -30,6 +30,7 @@ import { useSettings } from '../../hooks/useSettings';
|
|||
|
||||
// Shared Components
|
||||
import { GestureControls, PauseOverlay, SpeedActivatedOverlay } from './components';
|
||||
import { LockOverlay, LockOverlayRef } from './modals/LockOverlay';
|
||||
import LoadingOverlay from './modals/LoadingOverlay';
|
||||
import PlayerControls from './controls/PlayerControls';
|
||||
import { AudioTrackModal } from './modals/AudioTrackModal';
|
||||
|
|
@ -91,6 +92,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const mpvPlayerRef = useRef<MpvPlayerRef>(null);
|
||||
const exoPlayerRef = useRef<any>(null);
|
||||
const pinchRef = useRef(null);
|
||||
const lockOverlayRef = useRef<LockOverlayRef>(null);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const tracksHook = usePlayerTracks();
|
||||
|
||||
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
||||
|
|
@ -1050,29 +1053,31 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
controlsVisible={playerState.showControls}
|
||||
controlsExtraOffset={100}
|
||||
/>
|
||||
<GestureControls
|
||||
screenDimensions={playerState.screenDimensions}
|
||||
gestureControls={gestureControls}
|
||||
onLongPressActivated={speedControl.activateSpeedBoost}
|
||||
onLongPressEnd={speedControl.deactivateSpeedBoost}
|
||||
onLongPressStateChange={(e) => {
|
||||
const state = e.nativeEvent.state;
|
||||
if (state === 5 || state === 3 || state === 1) { // END, CANCELLED, FAILED
|
||||
speedControl.deactivateSpeedBoost();
|
||||
}
|
||||
}}
|
||||
toggleControls={toggleControls}
|
||||
showControls={playerState.showControls}
|
||||
hideControls={hideControls}
|
||||
volume={volume}
|
||||
controlsTimeout={controlsTimeout}
|
||||
resizeMode={playerState.resizeMode}
|
||||
skip={controlsHook.skip}
|
||||
currentTime={playerState.currentTime}
|
||||
duration={playerState.duration}
|
||||
seekToTime={controlsHook.seekToTime}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
<View pointerEvents={isLocked ? 'none' : 'box-none'} style={StyleSheet.absoluteFill}>
|
||||
<GestureControls
|
||||
screenDimensions={playerState.screenDimensions}
|
||||
gestureControls={gestureControls}
|
||||
onLongPressActivated={speedControl.activateSpeedBoost}
|
||||
onLongPressEnd={speedControl.deactivateSpeedBoost}
|
||||
onLongPressStateChange={(e) => {
|
||||
const state = e.nativeEvent.state;
|
||||
if (state === 5 || state === 3 || state === 1) { // END, CANCELLED, FAILED
|
||||
speedControl.deactivateSpeedBoost();
|
||||
}
|
||||
}}
|
||||
toggleControls={toggleControls}
|
||||
showControls={playerState.showControls}
|
||||
hideControls={hideControls}
|
||||
volume={volume}
|
||||
controlsTimeout={controlsTimeout}
|
||||
resizeMode={playerState.resizeMode}
|
||||
skip={controlsHook.skip}
|
||||
currentTime={playerState.currentTime}
|
||||
duration={playerState.duration}
|
||||
seekToTime={controlsHook.seekToTime}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Buffering Indicator (Visible when controls are hidden) */}
|
||||
{playerState.isBuffering && !playerState.showControls && (
|
||||
|
|
@ -1081,58 +1086,73 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
<PlayerControls
|
||||
showControls={playerState.showControls}
|
||||
fadeAnim={fadeAnim}
|
||||
paused={playerState.paused}
|
||||
title={title}
|
||||
episodeTitle={episodeTitle}
|
||||
season={season}
|
||||
episode={episode}
|
||||
quality={currentQuality || quality}
|
||||
year={year}
|
||||
streamProvider={currentStreamProvider || streamProvider}
|
||||
streamName={currentStreamName}
|
||||
currentTime={playerState.currentTime}
|
||||
duration={playerState.duration}
|
||||
zoomScale={1}
|
||||
currentResizeMode={playerState.resizeMode}
|
||||
ksAudioTracks={tracksHook.ksAudioTracks}
|
||||
selectedAudioTrack={tracksHook.computedSelectedAudioTrack}
|
||||
availableStreams={availableStreams}
|
||||
togglePlayback={controlsHook.togglePlayback}
|
||||
skip={controlsHook.skip}
|
||||
handleClose={handleClose}
|
||||
cycleAspectRatio={cycleResizeMode}
|
||||
cyclePlaybackSpeed={() => {
|
||||
const speeds = [0.5, 1, 1.25, 1.5, 2];
|
||||
const idx = speeds.indexOf(speedControl.playbackSpeed);
|
||||
const next = speeds[(idx + 1) % speeds.length];
|
||||
speedControl.setPlaybackSpeed(next);
|
||||
<View pointerEvents={isLocked ? 'none' : 'box-none'} style={StyleSheet.absoluteFill}>
|
||||
<PlayerControls
|
||||
showControls={playerState.showControls}
|
||||
fadeAnim={fadeAnim}
|
||||
paused={playerState.paused}
|
||||
title={title}
|
||||
episodeTitle={episodeTitle}
|
||||
season={season}
|
||||
episode={episode}
|
||||
quality={currentQuality || quality}
|
||||
year={year}
|
||||
streamProvider={currentStreamProvider || streamProvider}
|
||||
streamName={currentStreamName}
|
||||
currentTime={playerState.currentTime}
|
||||
duration={playerState.duration}
|
||||
zoomScale={1}
|
||||
currentResizeMode={playerState.resizeMode}
|
||||
ksAudioTracks={tracksHook.ksAudioTracks}
|
||||
selectedAudioTrack={tracksHook.computedSelectedAudioTrack}
|
||||
availableStreams={availableStreams}
|
||||
togglePlayback={controlsHook.togglePlayback}
|
||||
skip={controlsHook.skip}
|
||||
handleClose={handleClose}
|
||||
cycleAspectRatio={cycleResizeMode}
|
||||
cyclePlaybackSpeed={() => {
|
||||
const speeds = [0.5, 1, 1.25, 1.5, 2];
|
||||
const idx = speeds.indexOf(speedControl.playbackSpeed);
|
||||
const next = speeds[(idx + 1) % speeds.length];
|
||||
speedControl.setPlaybackSpeed(next);
|
||||
}}
|
||||
currentPlaybackSpeed={speedControl.playbackSpeed}
|
||||
setShowAudioModal={modals.setShowAudioModal}
|
||||
setShowSubtitleModal={modals.setShowSubtitleModal}
|
||||
setShowSpeedModal={modals.setShowSpeedModal}
|
||||
setShowSubmitIntroModal={modals.setShowSubmitIntroModal}
|
||||
isSubtitleModalOpen={modals.showSubtitleModal}
|
||||
setShowSourcesModal={modals.setShowSourcesModal}
|
||||
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
|
||||
onSliderValueChange={(val) => { playerState.isDragging.current = true; }}
|
||||
onSlidingStart={() => { playerState.isDragging.current = true; }}
|
||||
onSlidingComplete={(val) => {
|
||||
playerState.isDragging.current = false;
|
||||
controlsHook.seekToTime(val);
|
||||
}}
|
||||
buffered={playerState.buffered}
|
||||
formatTime={formatTime}
|
||||
playerBackend={useExoPlayer ? 'ExoPlayer' : 'MPV'}
|
||||
onSwitchToMPV={handleManualSwitchToMPV}
|
||||
useExoPlayer={useExoPlayer}
|
||||
canEnterPictureInPicture={canShowPipButton}
|
||||
onEnterPictureInPicture={handleEnterPictureInPicture}
|
||||
isBuffering={playerState.isBuffering}
|
||||
imdbId={resolvedImdbId}
|
||||
onLock={() => lockOverlayRef.current?.lock()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<LockOverlay
|
||||
ref={lockOverlayRef}
|
||||
onHideControls={() => {
|
||||
setIsLocked(true);
|
||||
playerState.setShowControls(false);
|
||||
}}
|
||||
currentPlaybackSpeed={speedControl.playbackSpeed}
|
||||
setShowAudioModal={modals.setShowAudioModal}
|
||||
setShowSubtitleModal={modals.setShowSubtitleModal}
|
||||
setShowSpeedModal={modals.setShowSpeedModal}
|
||||
setShowSubmitIntroModal={modals.setShowSubmitIntroModal}
|
||||
isSubtitleModalOpen={modals.showSubtitleModal}
|
||||
setShowSourcesModal={modals.setShowSourcesModal}
|
||||
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
|
||||
onSliderValueChange={(val) => { playerState.isDragging.current = true; }}
|
||||
onSlidingStart={() => { playerState.isDragging.current = true; }}
|
||||
onSlidingComplete={(val) => {
|
||||
playerState.isDragging.current = false;
|
||||
controlsHook.seekToTime(val);
|
||||
onShowControls={() => {
|
||||
setIsLocked(false);
|
||||
playerState.setShowControls(true);
|
||||
}}
|
||||
buffered={playerState.buffered}
|
||||
formatTime={formatTime}
|
||||
playerBackend={useExoPlayer ? 'ExoPlayer' : 'MPV'}
|
||||
onSwitchToMPV={handleManualSwitchToMPV}
|
||||
useExoPlayer={useExoPlayer}
|
||||
canEnterPictureInPicture={canShowPipButton}
|
||||
onEnterPictureInPicture={handleEnterPictureInPicture}
|
||||
isBuffering={playerState.isBuffering}
|
||||
imdbId={resolvedImdbId}
|
||||
/>
|
||||
|
||||
<SpeedActivatedOverlay
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ interface PlayerControlsProps {
|
|||
onEnterPictureInPicture?: () => void;
|
||||
isBuffering?: boolean;
|
||||
imdbId?: string;
|
||||
onLock?: () => void;
|
||||
}
|
||||
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||
|
|
@ -120,6 +121,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
onEnterPictureInPicture,
|
||||
isBuffering = false,
|
||||
imdbId,
|
||||
onLock,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
|
|
@ -680,6 +682,16 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
<PlayerEpisodesIcon width={24} height={24} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Lock Button */}
|
||||
{onLock && (
|
||||
<TouchableOpacity
|
||||
style={styles.iconButton}
|
||||
onPress={onLock}
|
||||
>
|
||||
<Ionicons name="lock-closed-outline" size={24} color="white" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
|
|
|||
124
src/components/player/modals/LockOverlay.tsx
Normal file
124
src/components/player/modals/LockOverlay.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import React, { useRef, useCallback, useImperativeHandle, forwardRef, useState } from 'react';
|
||||
import { View, TouchableOpacity, Animated, StyleSheet, Text } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
export interface LockOverlayRef {
|
||||
lock: () => void;
|
||||
}
|
||||
|
||||
interface LockOverlayProps {
|
||||
onHideControls: () => void;
|
||||
onShowControls: () => void;
|
||||
}
|
||||
|
||||
const UNLOCK_BUTTON_VISIBLE_MS = 5000;
|
||||
|
||||
export const LockOverlay = forwardRef<LockOverlayRef, LockOverlayProps>(({ onHideControls, onShowControls }, ref) => {
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const isButtonVisibleRef = useRef(false);
|
||||
const buttonOpacity = useRef(new Animated.Value(0)).current;
|
||||
const hideTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const clearHideTimer = useCallback(() => {
|
||||
if (hideTimerRef.current) {
|
||||
clearTimeout(hideTimerRef.current);
|
||||
hideTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hideButton = useCallback(() => {
|
||||
isButtonVisibleRef.current = false;
|
||||
Animated.timing(buttonOpacity, {
|
||||
toValue: 0,
|
||||
duration: 400,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [buttonOpacity]);
|
||||
|
||||
const showButton = useCallback(() => {
|
||||
clearHideTimer();
|
||||
isButtonVisibleRef.current = true;
|
||||
Animated.timing(buttonOpacity, {
|
||||
toValue: 1,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
hideTimerRef.current = setTimeout(hideButton, UNLOCK_BUTTON_VISIBLE_MS);
|
||||
}, [buttonOpacity, clearHideTimer, hideButton]);
|
||||
|
||||
const lock = useCallback(() => {
|
||||
setIsLocked(true);
|
||||
onHideControls();
|
||||
showButton();
|
||||
}, [showButton, onHideControls]);
|
||||
|
||||
const unlock = useCallback(() => {
|
||||
clearHideTimer();
|
||||
buttonOpacity.setValue(0);
|
||||
isButtonVisibleRef.current = false;
|
||||
setIsLocked(false);
|
||||
onShowControls();
|
||||
}, [buttonOpacity, clearHideTimer, onShowControls]);
|
||||
|
||||
useImperativeHandle(ref, () => ({ lock }), [lock]);
|
||||
|
||||
const handleScreenTap = useCallback(() => {
|
||||
if (isButtonVisibleRef.current) return;
|
||||
showButton();
|
||||
}, [showButton]);
|
||||
|
||||
if (!isLocked) return null;
|
||||
|
||||
return (
|
||||
<View style={styles.fullScreenBlocker}>
|
||||
<TouchableOpacity
|
||||
style={[StyleSheet.absoluteFill, { backgroundColor: 'transparent' }]}
|
||||
activeOpacity={1}
|
||||
onPress={handleScreenTap}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[styles.unlockButtonContainer, { opacity: buttonOpacity }]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.unlockButton}
|
||||
activeOpacity={0.8}
|
||||
onPress={unlock}
|
||||
>
|
||||
<Ionicons name="lock-open-outline" size={20} color="white" />
|
||||
<Text style={styles.unlockText}>Unlock</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fullScreenBlocker: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
zIndex: 100,
|
||||
backgroundColor: 'transparent',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
unlockButtonContainer: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
unlockButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 24,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.70)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
unlockText: {
|
||||
color: 'white',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue