diff --git a/src/components/player/android/components/GestureControls.tsx b/src/components/player/android/components/GestureControls.tsx deleted file mode 100644 index 3485daf3..00000000 --- a/src/components/player/android/components/GestureControls.tsx +++ /dev/null @@ -1,464 +0,0 @@ -import React, { useState } from 'react'; -import { View, Text, StyleSheet, Animated } from 'react-native'; -import { - TapGestureHandler, - PanGestureHandler, - LongPressGestureHandler, - State -} from 'react-native-gesture-handler'; -import { MaterialIcons } from '@expo/vector-icons'; -import { styles as localStyles } from '../../utils/playerStyles'; - -interface GestureControlsProps { - screenDimensions: { width: number, height: number }; - gestureControls: any; - onLongPressActivated: () => void; - onLongPressEnd: () => void; - onLongPressStateChange: (event: any) => void; - toggleControls: () => void; - showControls: boolean; - hideControls: () => void; - volume: number; - 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 = ({ - screenDimensions, - gestureControls, - onLongPressActivated, - onLongPressEnd, - onLongPressStateChange, - toggleControls, - showControls, - hideControls, - volume, - brightness, - controlsTimeout, - resizeMode = 'contain', - skip, - currentTime, - duration, - seekToTime, - formatTime, -}) => { - - const getVolumeIcon = (value: number) => { - if (value === 0) return 'volume-off'; - if (value < 0.3) return 'volume-mute'; - if (value < 0.6) return 'volume-down'; - return 'volume-up'; - }; - - const getBrightnessIcon = (value: number) => { - if (value < 0.3) return 'brightness-low'; - if (value < 0.7) return 'brightness-medium'; - 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 ( - <> - {/* Horizontal seek gesture - OUTERMOST wrapper, fails on vertical movement */} - { - const { translationX, state } = event.nativeEvent; - - 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); - } - } - }} - 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) && ( - - - - - - - - {gestureControls.showVolumeOverlay && volume === 0 - ? "Muted" - : `${Math.round((gestureControls.showVolumeOverlay ? volume : brightness) * 100)}%` - } - - - - )} - - {/* Aspect Ratio Overlay */} - {gestureControls.showResizeModeOverlay && ( - - - - - - - - {resizeMode.charAt(0).toUpperCase() + resizeMode.slice(1)} - - - - )} - - {/* 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 973965df..599a22af 100644 --- a/src/components/player/components/GestureControls.tsx +++ b/src/components/player/components/GestureControls.tsx @@ -92,40 +92,85 @@ export const GestureControls: React.FC = ({ }; }, []); + // Refs for tracking rapid seek state + const seekBaselineTime = React.useRef(null); + const gestureSkipAmount = React.useRef(0); + // Double-tap handlers const handleLeftDoubleTap = () => { - if (skip) { - skip(-10); - setSkipAmount(prev => { - const newAmount = showSkipBackwardOverlay ? prev + 10 : 10; - return newAmount; - }); + if (seekToTime && currentTime !== undefined) { + // If overlay is not visible, this is a new seek sequence + if (!showSkipBackwardOverlay) { + seekBaselineTime.current = currentTime; + gestureSkipAmount.current = 0; + } + + // Increment skip amount + gestureSkipAmount.current += 10; + const currentSkip = gestureSkipAmount.current; + + // Calculate target time based on locked baseline + const baseTime = seekBaselineTime.current !== null ? seekBaselineTime.current : currentTime; + const targetTime = Math.max(0, baseTime - currentSkip); + + // Execute seek + seekToTime(targetTime); + + // Update UI state + setSkipAmount(currentSkip); setShowSkipBackwardOverlay(true); + if (skipBackwardTimeoutRef.current) { clearTimeout(skipBackwardTimeoutRef.current); } skipBackwardTimeoutRef.current = setTimeout(() => { setShowSkipBackwardOverlay(false); setSkipAmount(10); + gestureSkipAmount.current = 0; + seekBaselineTime.current = null; }, 800); + } else if (skip) { + // Fallback if seekToTime not available + skip(-10); } }; const handleRightDoubleTap = () => { - if (skip) { - skip(10); - setSkipAmount(prev => { - const newAmount = showSkipForwardOverlay ? prev + 10 : 10; - return newAmount; - }); + if (seekToTime && currentTime !== undefined) { + // If overlay is not visible, this is a new seek sequence + if (!showSkipForwardOverlay) { + seekBaselineTime.current = currentTime; + gestureSkipAmount.current = 0; + } + + // Increment skip amount + gestureSkipAmount.current += 10; + const currentSkip = gestureSkipAmount.current; + + // Calculate target time based on locked baseline + const baseTime = seekBaselineTime.current !== null ? seekBaselineTime.current : currentTime; + const targetTime = baseTime + currentSkip; + // Note: duration check happens in seekToTime + + // Execute seek + seekToTime(targetTime); + + // Update UI state + setSkipAmount(currentSkip); setShowSkipForwardOverlay(true); + if (skipForwardTimeoutRef.current) { clearTimeout(skipForwardTimeoutRef.current); } skipForwardTimeoutRef.current = setTimeout(() => { setShowSkipForwardOverlay(false); setSkipAmount(10); + gestureSkipAmount.current = 0; + seekBaselineTime.current = null; }, 800); + } else if (skip) { + // Fallback + skip(10); } }; @@ -362,31 +407,12 @@ export const GestureControls: React.FC = ({ {/* Skip Forward Overlay - Right side */} {showSkipForwardOverlay && ( - - - - - + + + + + + +{skipAmount}s @@ -395,31 +421,12 @@ export const GestureControls: React.FC = ({ {/* Skip Backward Overlay - Left side */} {showSkipBackwardOverlay && ( - - - - - + + + + + + -{skipAmount}s diff --git a/src/components/player/hooks/usePlayerControls.ts b/src/components/player/hooks/usePlayerControls.ts index 16a5a180..f2561c08 100644 --- a/src/components/player/hooks/usePlayerControls.ts +++ b/src/components/player/hooks/usePlayerControls.ts @@ -37,30 +37,32 @@ export const usePlayerControls = (config: PlayerControlsConfig) => { setPaused(!paused); }, [paused, setPaused]); + const seekTimeoutRef = useRef(null); + const seekToTime = useCallback((rawSeconds: number) => { const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds)); - if (playerRef.current && duration > 0 && !isSeeking.current) { + if (playerRef.current && duration > 0) { if (DEBUG_MODE) logger.log(`[usePlayerControls] Seeking to ${timeInSeconds}`); isSeeking.current = true; - // iOS optimization: pause while seeking for smoother experience - + // Clear existing timeout to keep isSeeking true during rapid seeks + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + } // Actually perform the seek playerRef.current.seek(timeInSeconds); // Debounce the seeking state reset - setTimeout(() => { + seekTimeoutRef.current = setTimeout(() => { if (isMounted.current && isSeeking.current) { isSeeking.current = false; - // Resume if it was playing (iOS specific) - } }, 500); } - }, [duration, paused, setPaused, playerRef, isSeeking, isMounted]); + }, [duration, paused, playerRef, isSeeking, isMounted]); const skip = useCallback((seconds: number) => { seekToTime(currentTime + seconds); diff --git a/src/components/player/ios/components/GestureControls.tsx b/src/components/player/ios/components/GestureControls.tsx deleted file mode 100644 index 7ffe3af9..00000000 --- a/src/components/player/ios/components/GestureControls.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import React from 'react'; -import { View, Text, Animated } from 'react-native'; -import { - TapGestureHandler, - PanGestureHandler, - LongPressGestureHandler, -} from 'react-native-gesture-handler'; -import { MaterialIcons } from '@expo/vector-icons'; - -interface GestureControlsProps { - screenDimensions: { width: number, height: number }; - gestureControls: any; - onLongPressActivated: () => void; - onLongPressEnd: () => void; - onLongPressStateChange: (event: any) => void; - toggleControls: () => void; - showControls: boolean; - hideControls: () => void; - volume: number; - brightness: number; - controlsTimeout: React.MutableRefObject; -} - -export const GestureControls: React.FC = ({ - screenDimensions, - gestureControls, - onLongPressActivated, - onLongPressEnd, - onLongPressStateChange, - toggleControls, - showControls, - hideControls, - volume, - brightness, - controlsTimeout -}) => { - // Helper to get dimensions (using passed screenDimensions) - const getDimensions = () => screenDimensions; - - // 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); - - // Shared style for left side gesture area - const leftSideStyle = { - 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, - height: screenDimensions.height * 0.7, - zIndex: 10, - }; - - return ( - <> - {/* Left side gestures - brightness + tap + long press (flat structure) */} - - - - - - - - - - - - - {/* 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); - } - controlsTimeout.current = timeoutId; - } else { - toggleControls(); - } - }} - shouldCancelWhenOutside={false} - > - - - - {/* Volume Overlay */} - {gestureControls.showVolumeOverlay && ( - - - - - {/* Horizontal Dotted Progress Bar */} - - {/* Dotted background */} - - {Array.from({ length: 16 }, (_, i) => ( - - ))} - - - {/* Progress fill */} - - - - - {Math.round(volume)}% - - - - )} - - {/* Brightness Overlay */} - {gestureControls.showBrightnessOverlay && ( - - - - - {/* Horizontal Dotted Progress Bar */} - - {/* Dotted background */} - - {Array.from({ length: 16 }, (_, i) => ( - - ))} - - - {/* Progress fill */} - - - - - {Math.round(brightness * 100)}% - - - - )} - - ); -};