diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index d40f25b6..c4e10838 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -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(null); const exoPlayerRef = useRef(null); const pinchRef = useRef(null); + const lockOverlayRef = useRef(null); + const [isLocked, setIsLocked] = useState(false); const tracksHook = usePlayerTracks(); const [currentStreamUrl, setCurrentStreamUrl] = useState(uri); @@ -1050,29 +1053,31 @@ const AndroidVideoPlayer: React.FC = () => { controlsVisible={playerState.showControls} controlsExtraOffset={100} /> - { - 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} - /> + + { + 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} + /> + {/* Buffering Indicator (Visible when controls are hidden) */} {playerState.isBuffering && !playerState.showControls && ( @@ -1081,58 +1086,73 @@ const AndroidVideoPlayer: React.FC = () => { )} - { - 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); + + { + 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()} + /> + + + { + 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} /> void; isBuffering?: boolean; imdbId?: string; + onLock?: () => void; } export const PlayerControls: React.FC = ({ @@ -120,6 +121,7 @@ export const PlayerControls: React.FC = ({ onEnterPictureInPicture, isBuffering = false, imdbId, + onLock, }) => { const { currentTheme } = useTheme(); const { settings } = useSettings(); @@ -680,6 +682,16 @@ export const PlayerControls: React.FC = ({ )} + + {/* Lock Button */} + {onLock && ( + + + + )} diff --git a/src/components/player/modals/LockOverlay.tsx b/src/components/player/modals/LockOverlay.tsx new file mode 100644 index 00000000..57b2008f --- /dev/null +++ b/src/components/player/modals/LockOverlay.tsx @@ -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(({ onHideControls, onShowControls }, ref) => { + const [isLocked, setIsLocked] = useState(false); + const isButtonVisibleRef = useRef(false); + const buttonOpacity = useRef(new Animated.Value(0)).current; + const hideTimerRef = useRef(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 ( + + + + + + Unlock + + + + ); +}); + +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', + }, +});