diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 3fe131e4..cd3bbd46 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -872,6 +872,11 @@ const AndroidVideoPlayer: React.FC = () => { volume={volume} controlsTimeout={controlsTimeout} resizeMode={playerState.resizeMode} + skip={controlsHook.skip} + currentTime={playerState.currentTime} + duration={playerState.duration} + seekToTime={controlsHook.seekToTime} + formatTime={formatTime} /> { brightness={brightness} controlsTimeout={controlsTimeout} resizeMode={resizeMode} + skip={controls.skip} + currentTime={currentTime} + duration={duration} + seekToTime={controls.seekToTime} + formatTime={formatTime} /> {/* UI Controls */} diff --git a/src/components/player/android/components/GestureControls.tsx b/src/components/player/android/components/GestureControls.tsx index 9216664a..3485daf3 100644 --- a/src/components/player/android/components/GestureControls.tsx +++ b/src/components/player/android/components/GestureControls.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { View, Text, StyleSheet, Animated } from 'react-native'; import { TapGestureHandler, @@ -22,6 +22,12 @@ interface GestureControlsProps { brightness: number; controlsTimeout: React.MutableRefObject; resizeMode?: string; + // New props for double-tap skip and horizontal seek + skip?: (seconds: number) => void; + currentTime?: number; + duration?: number; + seekToTime?: (seconds: number) => void; + formatTime?: (seconds: number) => string; } export const GestureControls: React.FC = ({ @@ -36,7 +42,12 @@ export const GestureControls: React.FC = ({ volume, brightness, controlsTimeout, - resizeMode = 'contain' + resizeMode = 'contain', + skip, + currentTime, + duration, + seekToTime, + formatTime, }) => { const getVolumeIcon = (value: number) => { @@ -52,128 +63,232 @@ export const GestureControls: React.FC = ({ return 'brightness-high'; }; - // Create refs for all gesture handlers to enable cross-referencing - const leftPanRef = React.useRef(null); - const rightPanRef = React.useRef(null); - const leftTapRef = React.useRef(null); - const rightTapRef = React.useRef(null); - const centerTapRef = React.useRef(null); - const leftLongPressRef = React.useRef(null); - const rightLongPressRef = React.useRef(null); + // Refs for gesture handlers + const leftDoubleTapRef = React.useRef(null); + const rightDoubleTapRef = React.useRef(null); + const horizontalSeekPanRef = React.useRef(null); + const leftVerticalPanRef = React.useRef(null); + const rightVerticalPanRef = React.useRef(null); - // Shared style for left side gesture area + // State for double-tap skip overlays + const [showSkipForwardOverlay, setShowSkipForwardOverlay] = useState(false); + const [showSkipBackwardOverlay, setShowSkipBackwardOverlay] = useState(false); + const [skipAmount, setSkipAmount] = useState(10); + + // State for horizontal seek + const [isHorizontalSeeking, setIsHorizontalSeeking] = useState(false); + const [seekPreviewTime, setSeekPreviewTime] = useState(0); + const [seekStartTime, setSeekStartTime] = useState(0); + + // Refs for overlay timeouts + const skipForwardTimeoutRef = React.useRef(null); + const skipBackwardTimeoutRef = React.useRef(null); + + // Cleanup timeouts on unmount + React.useEffect(() => { + return () => { + if (skipForwardTimeoutRef.current) clearTimeout(skipForwardTimeoutRef.current); + if (skipBackwardTimeoutRef.current) clearTimeout(skipBackwardTimeoutRef.current); + }; + }, []); + + // Double-tap handlers + const handleLeftDoubleTap = () => { + if (skip) { + skip(-10); + setSkipAmount(prev => { + const newAmount = showSkipBackwardOverlay ? prev + 10 : 10; + return newAmount; + }); + setShowSkipBackwardOverlay(true); + if (skipBackwardTimeoutRef.current) { + clearTimeout(skipBackwardTimeoutRef.current); + } + skipBackwardTimeoutRef.current = setTimeout(() => { + setShowSkipBackwardOverlay(false); + setSkipAmount(10); + }, 800); + } + }; + + const handleRightDoubleTap = () => { + if (skip) { + skip(10); + setSkipAmount(prev => { + const newAmount = showSkipForwardOverlay ? prev + 10 : 10; + return newAmount; + }); + setShowSkipForwardOverlay(true); + if (skipForwardTimeoutRef.current) { + clearTimeout(skipForwardTimeoutRef.current); + } + skipForwardTimeoutRef.current = setTimeout(() => { + setShowSkipForwardOverlay(false); + setSkipAmount(10); + }, 800); + } + }; + + // Shared styles for gesture areas (relative to parent container) const leftSideStyle = { + position: 'absolute' as const, + top: 0, + left: 0, + width: screenDimensions.width * 0.4, + height: '100%' as const, + }; + + const rightSideStyle = { + position: 'absolute' as const, + top: 0, + right: 0, + width: screenDimensions.width * 0.4, + height: '100%' as const, + }; + + // Full gesture area style + const gestureAreaStyle = { position: 'absolute' as const, top: screenDimensions.height * 0.15, left: 0, - width: screenDimensions.width * 0.4, - height: screenDimensions.height * 0.7, - zIndex: 10, - }; - - // Shared style for right side gesture area - const rightSideStyle = { - position: 'absolute' as const, - top: screenDimensions.height * 0.15, - right: 0, - width: screenDimensions.width * 0.4, + width: screenDimensions.width, height: screenDimensions.height * 0.7, zIndex: 10, }; return ( <> - {/* Left side gestures - brightness + tap + long press (flat structure) */} - - - - + {/* Horizontal seek gesture - OUTERMOST wrapper, fails on vertical movement */} - - + ref={horizontalSeekPanRef} + onGestureEvent={(event: any) => { + const { translationX, state } = event.nativeEvent; - - - - - {/* Right side gestures - volume + tap + long press (flat structure) */} - - - - - - - - - - - - - {/* Center area tap handler */} - { - if (showControls) { - const timeoutId = setTimeout(() => { - hideControls(); - }, 0); - if (controlsTimeout.current) { - clearTimeout(controlsTimeout.current); + if (state === State.ACTIVE) { + if (!isHorizontalSeeking && currentTime !== undefined) { + setIsHorizontalSeeking(true); + setSeekStartTime(currentTime); + } + + if (duration && duration > 0) { + const sensitivityFactor = duration > 3600 ? 120 : duration > 1800 ? 90 : 60; + const seekDelta = (translationX / screenDimensions.width) * sensitivityFactor; + const newTime = Math.max(0, Math.min(duration, seekStartTime + seekDelta)); + setSeekPreviewTime(newTime); } - controlsTimeout.current = timeoutId; - } else { - toggleControls(); } }} - shouldCancelWhenOutside={false} + onHandlerStateChange={(event: any) => { + const { state } = event.nativeEvent; + + if (state === State.END || state === State.CANCELLED) { + if (isHorizontalSeeking && seekToTime) { + seekToTime(seekPreviewTime); + } + setIsHorizontalSeeking(false); + } + }} + activeOffsetX={[-30, 30]} + failOffsetY={[-20, 20]} + maxPointers={1} > - - + + {/* Left side gestures */} + + + + + + + + + + + + + + + + + {/* Center area tap handler */} + { + if (showControls) { + const timeoutId = setTimeout(() => { + hideControls(); + }, 0); + if (controlsTimeout.current) { + clearTimeout(controlsTimeout.current); + } + controlsTimeout.current = timeoutId; + } else { + toggleControls(); + } + }} + > + + + + {/* Right side gestures */} + + + + + + + + + + + + + + + + + {/* Volume/Brightness Pill Overlay */} {(gestureControls.showVolumeOverlay || gestureControls.showBrightnessOverlay) && ( @@ -251,6 +366,99 @@ export const GestureControls: React.FC = ({ )} + + {/* Skip Forward Overlay - Right side */} + {showSkipForwardOverlay && ( + + + + + + +{skipAmount}s + + + + )} + + {/* Skip Backward Overlay - Left side */} + {showSkipBackwardOverlay && ( + + + + + + -{skipAmount}s + + + + )} + + {/* Horizontal Seek Preview Overlay */} + {isHorizontalSeeking && formatTime && ( + + + + (currentTime || 0) ? "fast-forward" : "fast-rewind"} + size={24} + color="rgba(255, 255, 255)" + /> + + + {formatTime(seekPreviewTime)} + + (currentTime || 0) ? '#4CAF50' : '#FF5722', + fontSize: 12, + fontWeight: '600', + marginLeft: 4, + }}> + {seekPreviewTime > (currentTime || 0) ? '+' : ''} + {Math.round(seekPreviewTime - (currentTime || 0))}s + + + + )} ); }; diff --git a/src/components/player/components/GestureControls.tsx b/src/components/player/components/GestureControls.tsx index b249e855..973965df 100644 --- a/src/components/player/components/GestureControls.tsx +++ b/src/components/player/components/GestureControls.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { View, Text, StyleSheet, Animated } from 'react-native'; import { TapGestureHandler, @@ -22,6 +22,12 @@ interface GestureControlsProps { brightness?: number; controlsTimeout: React.MutableRefObject; resizeMode?: string; + // New props for double-tap skip and horizontal seek + skip?: (seconds: number) => void; + currentTime?: number; + duration?: number; + seekToTime?: (seconds: number) => void; + formatTime?: (seconds: number) => string; } export const GestureControls: React.FC = ({ @@ -36,7 +42,12 @@ export const GestureControls: React.FC = ({ volume, brightness = 0.5, controlsTimeout, - resizeMode = 'contain' + resizeMode = 'contain', + skip, + currentTime, + duration, + seekToTime, + formatTime, }) => { const getVolumeIcon = (value: number) => { @@ -52,105 +63,234 @@ export const GestureControls: React.FC = ({ return 'brightness-high'; }; + // Refs for gesture handlers + const leftDoubleTapRef = React.useRef(null); + const rightDoubleTapRef = React.useRef(null); + const horizontalSeekPanRef = React.useRef(null); + const leftVerticalPanRef = React.useRef(null); + const rightVerticalPanRef = React.useRef(null); + + // State for double-tap skip overlays + const [showSkipForwardOverlay, setShowSkipForwardOverlay] = useState(false); + const [showSkipBackwardOverlay, setShowSkipBackwardOverlay] = useState(false); + const [skipAmount, setSkipAmount] = useState(10); + + // State for horizontal seek + const [isHorizontalSeeking, setIsHorizontalSeeking] = useState(false); + const [seekPreviewTime, setSeekPreviewTime] = useState(0); + const [seekStartTime, setSeekStartTime] = useState(0); + + // Refs for overlay timeouts + const skipForwardTimeoutRef = React.useRef(null); + const skipBackwardTimeoutRef = React.useRef(null); + + // Cleanup timeouts on unmount + React.useEffect(() => { + return () => { + if (skipForwardTimeoutRef.current) clearTimeout(skipForwardTimeoutRef.current); + if (skipBackwardTimeoutRef.current) clearTimeout(skipBackwardTimeoutRef.current); + }; + }, []); + + // Double-tap handlers + const handleLeftDoubleTap = () => { + if (skip) { + skip(-10); + setSkipAmount(prev => { + const newAmount = showSkipBackwardOverlay ? prev + 10 : 10; + return newAmount; + }); + setShowSkipBackwardOverlay(true); + if (skipBackwardTimeoutRef.current) { + clearTimeout(skipBackwardTimeoutRef.current); + } + skipBackwardTimeoutRef.current = setTimeout(() => { + setShowSkipBackwardOverlay(false); + setSkipAmount(10); + }, 800); + } + }; + + const handleRightDoubleTap = () => { + if (skip) { + skip(10); + setSkipAmount(prev => { + const newAmount = showSkipForwardOverlay ? prev + 10 : 10; + return newAmount; + }); + setShowSkipForwardOverlay(true); + if (skipForwardTimeoutRef.current) { + clearTimeout(skipForwardTimeoutRef.current); + } + skipForwardTimeoutRef.current = setTimeout(() => { + setShowSkipForwardOverlay(false); + setSkipAmount(10); + }, 800); + } + }; + + // Shared styles for gesture areas (relative to parent container) + const leftSideStyle = { + position: 'absolute' as const, + top: 0, + left: 0, + width: screenDimensions.width * 0.4, + height: '100%' as const, + }; + + const rightSideStyle = { + position: 'absolute' as const, + top: 0, + right: 0, + width: screenDimensions.width * 0.4, + height: '100%' as const, + }; + + // Full gesture area style + const gestureAreaStyle = { + position: 'absolute' as const, + top: screenDimensions.height * 0.15, + left: 0, + width: screenDimensions.width, + height: screenDimensions.height * 0.7, + zIndex: 10, + }; + return ( <> - {/* Left side gesture handler - brightness + tap + long press */} - - - - - - - + {/* Horizontal seek gesture - OUTERMOST wrapper, fails on vertical movement */} + { + const { translationX, state } = event.nativeEvent; - {/* Right side gesture handler - volume + tap + long press */} - - - - - - - - - {/* Center area tap handler */} - { - if (showControls) { - const timeoutId = setTimeout(() => { - hideControls(); - }, 0); - if (controlsTimeout.current) { - clearTimeout(controlsTimeout.current); + if (state === State.ACTIVE) { + if (!isHorizontalSeeking && currentTime !== undefined) { + setIsHorizontalSeeking(true); + setSeekStartTime(currentTime); + } + + if (duration && duration > 0) { + const sensitivityFactor = duration > 3600 ? 120 : duration > 1800 ? 90 : 60; + const seekDelta = (translationX / screenDimensions.width) * sensitivityFactor; + const newTime = Math.max(0, Math.min(duration, seekStartTime + seekDelta)); + setSeekPreviewTime(newTime); } - controlsTimeout.current = timeoutId; - } else { - toggleControls(); } }} - shouldCancelWhenOutside={false} - simultaneousHandlers={[]} - > - - + onHandlerStateChange={(event: any) => { + const { state } = event.nativeEvent; - {/* Volume/Brightness Pill Overlay - Compact top design */} + if (state === State.END || state === State.CANCELLED) { + if (isHorizontalSeeking && seekToTime) { + seekToTime(seekPreviewTime); + } + setIsHorizontalSeeking(false); + } + }} + activeOffsetX={[-30, 30]} + failOffsetY={[-20, 20]} + maxPointers={1} + > + + {/* Left side gestures */} + + + + + + + + + + + + + + + + + {/* Center area tap handler */} + { + if (showControls) { + const timeoutId = setTimeout(() => { + hideControls(); + }, 0); + if (controlsTimeout.current) { + clearTimeout(controlsTimeout.current); + } + controlsTimeout.current = timeoutId; + } else { + toggleControls(); + } + }} + > + + + + {/* Right side gestures */} + + + + + + + + + + + + + + + + + + + {/* Volume/Brightness Pill Overlay */} {(gestureControls.showVolumeOverlay || gestureControls.showBrightnessOverlay) && ( = ({ )} + + {/* Skip Forward Overlay - Right side */} + {showSkipForwardOverlay && ( + + + + + + +{skipAmount}s + + + + )} + + {/* Skip Backward Overlay - Left side */} + {showSkipBackwardOverlay && ( + + + + + + -{skipAmount}s + + + + )} + + {/* Horizontal Seek Preview Overlay */} + {isHorizontalSeeking && formatTime && ( + + + + (currentTime || 0) ? "fast-forward" : "fast-rewind"} + size={18} + color="rgba(255, 255, 255, 0.9)" + /> + + + {formatTime(seekPreviewTime)} + + (currentTime || 0) ? '#4CAF50' : '#FF5722', + fontSize: 12, + fontWeight: '600', + marginLeft: 4, + }}> + {seekPreviewTime > (currentTime || 0) ? '+' : ''} + {Math.round(seekPreviewTime - (currentTime || 0))}s + + + + )} ); };