mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Added double tap to Skip, Slide to seek
This commit is contained in:
parent
8b267fb6d7
commit
a318bd350b
4 changed files with 652 additions and 201 deletions
|
|
@ -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}
|
||||
/>
|
||||
|
||||
<PlayerControls
|
||||
|
|
|
|||
|
|
@ -807,6 +807,11 @@ const KSPlayerCore: React.FC = () => {
|
|||
brightness={brightness}
|
||||
controlsTimeout={controlsTimeout}
|
||||
resizeMode={resizeMode}
|
||||
skip={controls.skip}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
seekToTime={controls.seekToTime}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
|
||||
{/* UI Controls */}
|
||||
|
|
|
|||
|
|
@ -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<NodeJS.Timeout | null>;
|
||||
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<GestureControlsProps> = ({
|
||||
|
|
@ -36,7 +42,12 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
|
|||
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<GestureControlsProps> = ({
|
|||
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<NodeJS.Timeout | null>(null);
|
||||
const skipBackwardTimeoutRef = React.useRef<NodeJS.Timeout | null>(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) */}
|
||||
<LongPressGestureHandler
|
||||
ref={leftLongPressRef}
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
shouldCancelWhenOutside={false}
|
||||
>
|
||||
<View style={leftSideStyle} />
|
||||
</LongPressGestureHandler>
|
||||
|
||||
{/* Horizontal seek gesture - OUTERMOST wrapper, fails on vertical movement */}
|
||||
<PanGestureHandler
|
||||
ref={leftPanRef}
|
||||
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||
activeOffsetY={[-15, 15]}
|
||||
failOffsetX={[-60, 60]}
|
||||
shouldCancelWhenOutside={false}
|
||||
maxPointers={1}
|
||||
>
|
||||
<View style={leftSideStyle} />
|
||||
</PanGestureHandler>
|
||||
ref={horizontalSeekPanRef}
|
||||
onGestureEvent={(event: any) => {
|
||||
const { translationX, state } = event.nativeEvent;
|
||||
|
||||
<TapGestureHandler
|
||||
ref={leftTapRef}
|
||||
onActivated={toggleControls}
|
||||
shouldCancelWhenOutside={false}
|
||||
waitFor={[leftPanRef, leftLongPressRef]}
|
||||
>
|
||||
<View style={leftSideStyle} />
|
||||
</TapGestureHandler>
|
||||
|
||||
{/* Right side gestures - volume + tap + long press (flat structure) */}
|
||||
<LongPressGestureHandler
|
||||
ref={rightLongPressRef}
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
shouldCancelWhenOutside={false}
|
||||
>
|
||||
<View style={rightSideStyle} />
|
||||
</LongPressGestureHandler>
|
||||
|
||||
<PanGestureHandler
|
||||
ref={rightPanRef}
|
||||
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||
activeOffsetY={[-15, 15]}
|
||||
failOffsetX={[-60, 60]}
|
||||
shouldCancelWhenOutside={false}
|
||||
maxPointers={1}
|
||||
>
|
||||
<View style={rightSideStyle} />
|
||||
</PanGestureHandler>
|
||||
|
||||
<TapGestureHandler
|
||||
ref={rightTapRef}
|
||||
onActivated={toggleControls}
|
||||
shouldCancelWhenOutside={false}
|
||||
waitFor={[rightPanRef, rightLongPressRef]}
|
||||
>
|
||||
<View style={rightSideStyle} />
|
||||
</TapGestureHandler>
|
||||
|
||||
{/* Center area tap handler */}
|
||||
<TapGestureHandler
|
||||
ref={centerTapRef}
|
||||
onActivated={() => {
|
||||
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}
|
||||
>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: screenDimensions.height * 0.15,
|
||||
left: screenDimensions.width * 0.4,
|
||||
width: screenDimensions.width * 0.2,
|
||||
height: screenDimensions.height * 0.7,
|
||||
zIndex: 10,
|
||||
}} />
|
||||
</TapGestureHandler>
|
||||
<View style={gestureAreaStyle}>
|
||||
{/* Left side gestures */}
|
||||
<TapGestureHandler
|
||||
ref={leftDoubleTapRef}
|
||||
numberOfTaps={2}
|
||||
onActivated={handleLeftDoubleTap}
|
||||
>
|
||||
<View style={leftSideStyle}>
|
||||
<LongPressGestureHandler
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<PanGestureHandler
|
||||
ref={leftVerticalPanRef}
|
||||
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-20, 20]}
|
||||
maxPointers={1}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler
|
||||
waitFor={leftDoubleTapRef}
|
||||
onActivated={toggleControls}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill} />
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
</LongPressGestureHandler>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
|
||||
{/* Center area tap handler */}
|
||||
<TapGestureHandler
|
||||
onActivated={() => {
|
||||
if (showControls) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
hideControls();
|
||||
}, 0);
|
||||
if (controlsTimeout.current) {
|
||||
clearTimeout(controlsTimeout.current);
|
||||
}
|
||||
controlsTimeout.current = timeoutId;
|
||||
} else {
|
||||
toggleControls();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: screenDimensions.width * 0.4,
|
||||
width: screenDimensions.width * 0.2,
|
||||
height: '100%',
|
||||
}} />
|
||||
</TapGestureHandler>
|
||||
|
||||
{/* Right side gestures */}
|
||||
<TapGestureHandler
|
||||
ref={rightDoubleTapRef}
|
||||
numberOfTaps={2}
|
||||
onActivated={handleRightDoubleTap}
|
||||
>
|
||||
<View style={rightSideStyle}>
|
||||
<LongPressGestureHandler
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<PanGestureHandler
|
||||
ref={rightVerticalPanRef}
|
||||
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-20, 20]}
|
||||
maxPointers={1}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler
|
||||
waitFor={rightDoubleTapRef}
|
||||
onActivated={toggleControls}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill} />
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
</LongPressGestureHandler>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
|
||||
{/* Volume/Brightness Pill Overlay */}
|
||||
{(gestureControls.showVolumeOverlay || gestureControls.showBrightnessOverlay) && (
|
||||
|
|
@ -251,6 +366,99 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
|
|||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Skip Forward Overlay - Right side */}
|
||||
{showSkipForwardOverlay && (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
right: screenDimensions.width * 0.1,
|
||||
top: screenDimensions.height * 0.35,
|
||||
height: screenDimensions.height * 0.3,
|
||||
width: screenDimensions.width * 0.25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 2000,
|
||||
}}>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
}} />
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<MaterialIcons name="fast-forward" size={32} color="#FFFFFF" />
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 4,
|
||||
}}>
|
||||
+{skipAmount}s
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Skip Backward Overlay - Left side */}
|
||||
{showSkipBackwardOverlay && (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
left: screenDimensions.width * 0.1,
|
||||
top: screenDimensions.height * 0.35,
|
||||
height: screenDimensions.height * 0.3,
|
||||
width: screenDimensions.width * 0.25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 2000,
|
||||
}}>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
}} />
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<MaterialIcons name="fast-rewind" size={32} color="#FFFFFF" />
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 4,
|
||||
}}>
|
||||
-{skipAmount}s
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Horizontal Seek Preview Overlay */}
|
||||
{isHorizontalSeeking && formatTime && (
|
||||
<View style={localStyles.gestureIndicatorContainer}>
|
||||
<View style={localStyles.gestureIndicatorPill}>
|
||||
<View style={[localStyles.iconWrapper, { backgroundColor: 'rgba(59, 59, 59)' }]}>
|
||||
<MaterialIcons
|
||||
name={seekPreviewTime > (currentTime || 0) ? "fast-forward" : "fast-rewind"}
|
||||
size={24}
|
||||
color="rgba(255, 255, 255)"
|
||||
/>
|
||||
</View>
|
||||
<Text style={localStyles.gestureText}>
|
||||
{formatTime(seekPreviewTime)}
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: seekPreviewTime > (currentTime || 0) ? '#4CAF50' : '#FF5722',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
}}>
|
||||
{seekPreviewTime > (currentTime || 0) ? '+' : ''}
|
||||
{Math.round(seekPreviewTime - (currentTime || 0))}s
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<NodeJS.Timeout | null>;
|
||||
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<GestureControlsProps> = ({
|
||||
|
|
@ -36,7 +42,12 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
|
|||
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<GestureControlsProps> = ({
|
|||
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<NodeJS.Timeout | null>(null);
|
||||
const skipBackwardTimeoutRef = React.useRef<NodeJS.Timeout | null>(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 */}
|
||||
<LongPressGestureHandler
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
>
|
||||
<PanGestureHandler
|
||||
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-30, 30]}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
maxPointers={1}
|
||||
>
|
||||
<TapGestureHandler
|
||||
onActivated={toggleControls}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: screenDimensions.height * 0.15,
|
||||
left: 0,
|
||||
width: screenDimensions.width * 0.4,
|
||||
height: screenDimensions.height * 0.7,
|
||||
zIndex: 10,
|
||||
}} />
|
||||
</TapGestureHandler>
|
||||
</PanGestureHandler>
|
||||
</LongPressGestureHandler>
|
||||
{/* Horizontal seek gesture - OUTERMOST wrapper, fails on vertical movement */}
|
||||
<PanGestureHandler
|
||||
ref={horizontalSeekPanRef}
|
||||
onGestureEvent={(event: any) => {
|
||||
const { translationX, state } = event.nativeEvent;
|
||||
|
||||
{/* Right side gesture handler - volume + tap + long press */}
|
||||
<LongPressGestureHandler
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
>
|
||||
<PanGestureHandler
|
||||
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-30, 30]}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
maxPointers={1}
|
||||
>
|
||||
<TapGestureHandler
|
||||
onActivated={toggleControls}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: screenDimensions.height * 0.15,
|
||||
right: 0,
|
||||
width: screenDimensions.width * 0.4,
|
||||
height: screenDimensions.height * 0.7,
|
||||
zIndex: 10,
|
||||
}} />
|
||||
</TapGestureHandler>
|
||||
</PanGestureHandler>
|
||||
</LongPressGestureHandler>
|
||||
|
||||
{/* Center area tap handler */}
|
||||
<TapGestureHandler
|
||||
onActivated={() => {
|
||||
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={[]}
|
||||
>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: screenDimensions.height * 0.15,
|
||||
left: screenDimensions.width * 0.4,
|
||||
width: screenDimensions.width * 0.2,
|
||||
height: screenDimensions.height * 0.7,
|
||||
zIndex: 5,
|
||||
}} />
|
||||
</TapGestureHandler>
|
||||
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}
|
||||
>
|
||||
<View style={gestureAreaStyle}>
|
||||
{/* Left side gestures */}
|
||||
<TapGestureHandler
|
||||
ref={leftDoubleTapRef}
|
||||
numberOfTaps={2}
|
||||
onActivated={handleLeftDoubleTap}
|
||||
>
|
||||
<View style={leftSideStyle}>
|
||||
<LongPressGestureHandler
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<PanGestureHandler
|
||||
ref={leftVerticalPanRef}
|
||||
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-20, 20]}
|
||||
maxPointers={1}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler
|
||||
waitFor={leftDoubleTapRef}
|
||||
onActivated={toggleControls}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill} />
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
</LongPressGestureHandler>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
|
||||
{/* Center area tap handler */}
|
||||
<TapGestureHandler
|
||||
onActivated={() => {
|
||||
if (showControls) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
hideControls();
|
||||
}, 0);
|
||||
if (controlsTimeout.current) {
|
||||
clearTimeout(controlsTimeout.current);
|
||||
}
|
||||
controlsTimeout.current = timeoutId;
|
||||
} else {
|
||||
toggleControls();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: screenDimensions.width * 0.4,
|
||||
width: screenDimensions.width * 0.2,
|
||||
height: '100%',
|
||||
}} />
|
||||
</TapGestureHandler>
|
||||
|
||||
{/* Right side gestures */}
|
||||
<TapGestureHandler
|
||||
ref={rightDoubleTapRef}
|
||||
numberOfTaps={2}
|
||||
onActivated={handleRightDoubleTap}
|
||||
>
|
||||
<View style={rightSideStyle}>
|
||||
<LongPressGestureHandler
|
||||
onActivated={onLongPressActivated}
|
||||
onEnded={onLongPressEnd}
|
||||
onHandlerStateChange={onLongPressStateChange}
|
||||
minDurationMs={500}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<PanGestureHandler
|
||||
ref={rightVerticalPanRef}
|
||||
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-20, 20]}
|
||||
maxPointers={1}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler
|
||||
waitFor={rightDoubleTapRef}
|
||||
onActivated={toggleControls}
|
||||
>
|
||||
<View style={StyleSheet.absoluteFill} />
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
</LongPressGestureHandler>
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
|
||||
{/* Volume/Brightness Pill Overlay */}
|
||||
{(gestureControls.showVolumeOverlay || gestureControls.showBrightnessOverlay) && (
|
||||
<View style={localStyles.gestureIndicatorContainer}>
|
||||
<View style={[
|
||||
|
|
@ -219,6 +359,99 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
|
|||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Skip Forward Overlay - Right side */}
|
||||
{showSkipForwardOverlay && (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
right: screenDimensions.width * 0.1,
|
||||
top: screenDimensions.height * 0.35,
|
||||
height: screenDimensions.height * 0.3,
|
||||
width: screenDimensions.width * 0.25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 2000,
|
||||
}}>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
}} />
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<MaterialIcons name="fast-forward" size={32} color="#FFFFFF" />
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 4,
|
||||
}}>
|
||||
+{skipAmount}s
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Skip Backward Overlay - Left side */}
|
||||
{showSkipBackwardOverlay && (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
left: screenDimensions.width * 0.1,
|
||||
top: screenDimensions.height * 0.35,
|
||||
height: screenDimensions.height * 0.3,
|
||||
width: screenDimensions.width * 0.25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 2000,
|
||||
}}>
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
}} />
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<MaterialIcons name="fast-rewind" size={32} color="#FFFFFF" />
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 4,
|
||||
}}>
|
||||
-{skipAmount}s
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Horizontal Seek Preview Overlay */}
|
||||
{isHorizontalSeeking && formatTime && (
|
||||
<View style={localStyles.gestureIndicatorContainer}>
|
||||
<View style={localStyles.gestureIndicatorPill}>
|
||||
<View style={[localStyles.iconWrapper, { backgroundColor: 'rgba(59, 59, 59)' }]}>
|
||||
<MaterialIcons
|
||||
name={seekPreviewTime > (currentTime || 0) ? "fast-forward" : "fast-rewind"}
|
||||
size={18}
|
||||
color="rgba(255, 255, 255, 0.9)"
|
||||
/>
|
||||
</View>
|
||||
<Text style={localStyles.gestureText}>
|
||||
{formatTime(seekPreviewTime)}
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: seekPreviewTime > (currentTime || 0) ? '#4CAF50' : '#FF5722',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
}}>
|
||||
{seekPreviewTime > (currentTime || 0) ? '+' : ''}
|
||||
{Math.round(seekPreviewTime - (currentTime || 0))}s
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue