Compare commits

...

2 Commits

Author SHA1 Message Date
Fredrik Burmester
5358c1e1d5 fix: double tap works indipendant of controls showing 2025-11-17 07:25:20 +01:00
Fredrik Burmester
cd7a7b0e0e feat: double tap to seek 2025-11-16 22:13:35 +01:00
7 changed files with 88 additions and 65 deletions

View File

@@ -17,7 +17,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
const disabled = useMemo( const disabled = useMemo(
() => () =>
pluginSettings?.enableHorizontalSwipeSkip?.locked === true && pluginSettings?.enableDoubleTapSkip?.locked === true &&
pluginSettings?.enableLeftSideBrightnessSwipe?.locked === true && pluginSettings?.enableLeftSideBrightnessSwipe?.locked === true &&
pluginSettings?.enableRightSideVolumeSwipe?.locked === true, pluginSettings?.enableRightSideVolumeSwipe?.locked === true,
[pluginSettings], [pluginSettings],
@@ -35,13 +35,13 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
subtitle={t( subtitle={t(
"home.settings.gesture_controls.horizontal_swipe_skip_description", "home.settings.gesture_controls.horizontal_swipe_skip_description",
)} )}
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked} disabled={pluginSettings?.enableDoubleTapSkip?.locked}
> >
<Switch <Switch
value={settings.enableHorizontalSwipeSkip} value={settings.enableDoubleTapSkip}
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked} disabled={pluginSettings?.enableDoubleTapSkip?.locked}
onValueChange={(enableHorizontalSwipeSkip) => onValueChange={(enableDoubleTapSkip) =>
updateSettings({ enableHorizontalSwipeSkip }) updateSettings({ enableDoubleTapSkip })
} }
/> />
</ListItem> </ListItem>

View File

@@ -145,7 +145,7 @@ export const GestureOverlay = ({
}); });
const handleSkipForward = useCallback(() => { const handleSkipForward = useCallback(() => {
if (!settings.enableHorizontalSwipeSkip) return; if (!settings.enableDoubleTapSkip) return;
lightHaptic(); lightHaptic();
// Defer all actions to avoid useInsertionEffect warning // Defer all actions to avoid useInsertionEffect warning
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -153,7 +153,7 @@ export const GestureOverlay = ({
showFeedback("play-forward", `+${settings.forwardSkipTime}s`); showFeedback("play-forward", `+${settings.forwardSkipTime}s`);
}); });
}, [ }, [
settings.enableHorizontalSwipeSkip, settings.enableDoubleTapSkip,
settings.forwardSkipTime, settings.forwardSkipTime,
lightHaptic, lightHaptic,
onSkipForward, onSkipForward,
@@ -161,7 +161,7 @@ export const GestureOverlay = ({
]); ]);
const handleSkipBackward = useCallback(() => { const handleSkipBackward = useCallback(() => {
if (!settings.enableHorizontalSwipeSkip) return; if (!settings.enableDoubleTapSkip) return;
lightHaptic(); lightHaptic();
// Defer all actions to avoid useInsertionEffect warning // Defer all actions to avoid useInsertionEffect warning
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -169,7 +169,7 @@ export const GestureOverlay = ({
showFeedback("play-back", `-${settings.rewindSkipTime}s`); showFeedback("play-back", `-${settings.rewindSkipTime}s`);
}); });
}, [ }, [
settings.enableHorizontalSwipeSkip, settings.enableDoubleTapSkip,
settings.rewindSkipTime, settings.rewindSkipTime,
lightHaptic, lightHaptic,
onSkipBackward, onSkipBackward,
@@ -237,8 +237,8 @@ export const GestureOverlay = ({
const { handleTouchStart, handleTouchMove, handleTouchEnd } = const { handleTouchStart, handleTouchMove, handleTouchEnd } =
useGestureDetection({ useGestureDetection({
onSwipeLeft: handleSkipBackward, onDoubleTapLeft: handleSkipBackward,
onSwipeRight: handleSkipForward, onDoubleTapRight: handleSkipForward,
onVerticalDragStart: handleVerticalDragStart, onVerticalDragStart: handleVerticalDragStart,
onVerticalDragMove: handleVerticalDragMove, onVerticalDragMove: handleVerticalDragMove,
onVerticalDragEnd: handleVerticalDragEnd, onVerticalDragEnd: handleVerticalDragEnd,
@@ -247,29 +247,30 @@ export const GestureOverlay = ({
screenHeight, screenHeight,
}); });
// If controls are visible, act like the old tap overlay // Background overlay when controls are visible
if (showControls) { const controlsOverlay = showControls && (
return ( <Pressable
<Pressable onPress={onToggleControls}
onPress={onToggleControls} style={{
style={{ position: "absolute",
position: "absolute", width: screenWidth,
width: screenWidth, height: screenHeight,
height: screenHeight, backgroundColor: "black",
backgroundColor: "black", left: 0,
left: 0, right: 0,
right: 0, top: 0,
top: 0, bottom: 0,
bottom: 0, opacity: 0.75,
opacity: 0.75, }}
}} />
/> );
);
}
return ( return (
<> <>
{/* Gesture detection area */} {/* Controls overlay when visible */}
{controlsOverlay}
{/* Gesture detection area - always present */}
<Pressable <Pressable
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}

View File

@@ -4,8 +4,8 @@ import type { GestureResponderEvent } from "react-native";
export interface SwipeGestureOptions { export interface SwipeGestureOptions {
minDistance?: number; minDistance?: number;
maxDuration?: number; maxDuration?: number;
onSwipeLeft?: () => void; onDoubleTapLeft?: () => void;
onSwipeRight?: () => void; onDoubleTapRight?: () => void;
onVerticalDragStart?: (side: "left" | "right", initialY: number) => void; onVerticalDragStart?: (side: "left" | "right", initialY: number) => void;
onVerticalDragMove?: ( onVerticalDragMove?: (
side: "left" | "right", side: "left" | "right",
@@ -21,8 +21,8 @@ export interface SwipeGestureOptions {
export const useGestureDetection = ({ export const useGestureDetection = ({
minDistance = 50, minDistance = 50,
maxDuration = 800, maxDuration = 800,
onSwipeLeft, onDoubleTapLeft,
onSwipeRight, onDoubleTapRight,
onVerticalDragStart, onVerticalDragStart,
onVerticalDragMove, onVerticalDragMove,
onVerticalDragEnd, onVerticalDragEnd,
@@ -39,6 +39,11 @@ export const useGestureDetection = ({
const gestureType = useRef<"none" | "horizontal" | "vertical">("none"); const gestureType = useRef<"none" | "horizontal" | "vertical">("none");
const shouldIgnoreTouch = useRef(false); const shouldIgnoreTouch = useRef(false);
// Double tap detection refs
const lastTapTime = useRef(0);
const lastTapPosition = useRef({ x: 0, y: 0 });
const doubleTapTimeWindow = 300; // 300ms window for double tap
const handleTouchStart = useCallback( const handleTouchStart = useCallback(
(event: GestureResponderEvent) => { (event: GestureResponderEvent) => {
const startY = event.nativeEvent.pageY; const startY = event.nativeEvent.pageY;
@@ -102,9 +107,6 @@ export const useGestureDetection = ({
isDragging.current = true; isDragging.current = true;
dragSide.current = side; dragSide.current = side;
onVerticalDragStart?.(side, touchStartPosition.current.y); onVerticalDragStart?.(side, touchStartPosition.current.y);
} else if (absX > absY && absX > 10) {
// Horizontal gesture - mark for discrete swipe
gestureType.current = "horizontal";
} }
} }
@@ -144,8 +146,8 @@ export const useGestureDetection = ({
const touchDuration = touchEndTime - touchStartTime.current; const touchDuration = touchEndTime - touchStartTime.current;
const deltaX = touchEndPosition.x - touchStartPosition.current.x; const deltaX = touchEndPosition.x - touchStartPosition.current.x;
const deltaY = touchEndPosition.y - touchStartPosition.current.y; const deltaY = touchEndPosition.y - touchStartPosition.current.y;
const absX = Math.abs(deltaX); const _absX = Math.abs(deltaX);
const absY = Math.abs(deltaY); const _absY = Math.abs(deltaY);
const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// End vertical drag if we were dragging // End vertical drag if we were dragging
@@ -169,25 +171,43 @@ export const useGestureDetection = ({
return; return;
} }
// Handle discrete horizontal swipes (for skip) only if it was marked as horizontal // Check if it's a tap (short duration and small movement)
if ( if (
gestureType.current === "horizontal" &&
hasMovedEnough.current &&
absX > absY &&
totalDistance > minDistance
) {
if (deltaX > 0) {
onSwipeRight?.();
} else {
onSwipeLeft?.();
}
} else if (
!hasMovedEnough.current && !hasMovedEnough.current &&
touchDuration < 300 && touchDuration < 300 &&
totalDistance < 10 totalDistance < 10
) { ) {
// It's a tap - short duration and small movement const currentTime = Date.now();
onTap?.(); const tapX = touchEndPosition.x;
const tapY = touchEndPosition.y;
// Check for double tap
const timeSinceLastTap = currentTime - lastTapTime.current;
const distanceFromLastTap = Math.sqrt(
(tapX - lastTapPosition.current.x) ** 2 +
(tapY - lastTapPosition.current.y) ** 2,
);
if (
timeSinceLastTap <= doubleTapTimeWindow &&
distanceFromLastTap < 50
) {
// It's a double tap
const isLeftSide = tapX < screenWidth / 2;
if (isLeftSide) {
onDoubleTapLeft?.();
} else {
onDoubleTapRight?.();
}
// Reset last tap to prevent triple tap
lastTapTime.current = 0;
lastTapPosition.current = { x: 0, y: 0 };
} else {
// It's a single tap - execute immediately
onTap?.();
lastTapTime.current = currentTime;
lastTapPosition.current = { x: tapX, y: tapY };
}
} }
hasMovedEnough.current = false; hasMovedEnough.current = false;
@@ -196,10 +216,12 @@ export const useGestureDetection = ({
[ [
maxDuration, maxDuration,
minDistance, minDistance,
onSwipeLeft, onDoubleTapLeft,
onSwipeRight, onDoubleTapRight,
onVerticalDragEnd, onVerticalDragEnd,
onTap, onTap,
doubleTapTimeWindow,
screenWidth,
], ],
); );

View File

@@ -89,8 +89,8 @@
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "التحكم بالإيماءات", "gesture_controls_title": "التحكم بالإيماءات",
"horizontal_swipe_skip": "السحب الأفقي للتخطي", "horizontal_swipe_skip": "النقر المزدوج للتخطي",
"horizontal_swipe_skip_description": "اسحب لليسار/لليمين عندما تكون عناصر التحكم مخفية للتخطي", "horizontal_swipe_skip_description": "انقر نقرًا مزدوجًا على الجانب الأيسر/الأيمن عندما تكون عناصر التحكم مخفية للتخطي",
"left_side_brightness": "التحكم في السطوع من الجانب الأيسر", "left_side_brightness": "التحكم في السطوع من الجانب الأيسر",
"left_side_brightness_description": "اسحب لأعلى/لأسفل على الجانب الأيسر لضبط السطوع", "left_side_brightness_description": "اسحب لأعلى/لأسفل على الجانب الأيسر لضبط السطوع",
"right_side_volume": "التحكم في مستوى الصوت من الجانب الأيمن", "right_side_volume": "التحكم في مستوى الصوت من الجانب الأيمن",

View File

@@ -106,8 +106,8 @@
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gesture Controls", "gesture_controls_title": "Gesture Controls",
"horizontal_swipe_skip": "Horizontal Swipe to Skip", "horizontal_swipe_skip": "Double Tap to Skip",
"horizontal_swipe_skip_description": "Swipe left/right when controls are hidden to skip", "horizontal_swipe_skip_description": "Double tap left/right side when controls are hidden to skip",
"left_side_brightness": "Left Side Brightness Control", "left_side_brightness": "Left Side Brightness Control",
"left_side_brightness_description": "Swipe up/down on left side to adjust brightness", "left_side_brightness_description": "Swipe up/down on left side to adjust brightness",
"right_side_volume": "Right Side Volume Control", "right_side_volume": "Right Side Volume Control",

View File

@@ -94,8 +94,8 @@
}, },
"gesture_controls": { "gesture_controls": {
"gesture_controls_title": "Gesztusvezérlés", "gesture_controls_title": "Gesztusvezérlés",
"horizontal_swipe_skip": "Vízszintes Húzás Ugráshoz", "horizontal_swipe_skip": "Dupla Érintés Ugráshoz",
"horizontal_swipe_skip_description": "Ha a vezérlők el vannak rejtve, húzd balra vagy jobbra az ugráshoz.", "horizontal_swipe_skip_description": "Dupla érintés bal/jobb oldalon ha a vezérlők el vannak rejtve az ugráshoz.",
"left_side_brightness": "Fényerő a Bal Oldalon", "left_side_brightness": "Fényerő a Bal Oldalon",
"left_side_brightness_description": "Húzd felfelé vagy lefelé a bal oldalon a fényerő állításához", "left_side_brightness_description": "Húzd felfelé vagy lefelé a bal oldalon a fényerő állításához",
"right_side_volume": "Fényerő a Jobb Oldalon", "right_side_volume": "Fényerő a Jobb Oldalon",

View File

@@ -175,7 +175,7 @@ export type Settings = {
vlcOutlineOpacity?: number; vlcOutlineOpacity?: number;
vlcIsBold?: boolean; vlcIsBold?: boolean;
// Gesture controls // Gesture controls
enableHorizontalSwipeSkip: boolean; enableDoubleTapSkip: boolean;
enableLeftSideBrightnessSwipe: boolean; enableLeftSideBrightnessSwipe: boolean;
enableRightSideVolumeSwipe: boolean; enableRightSideVolumeSwipe: boolean;
usePopularPlugin: boolean; usePopularPlugin: boolean;
@@ -239,7 +239,7 @@ export const defaultValues: Settings = {
vlcOutlineOpacity: undefined, vlcOutlineOpacity: undefined,
vlcIsBold: undefined, vlcIsBold: undefined,
// Gesture controls // Gesture controls
enableHorizontalSwipeSkip: true, enableDoubleTapSkip: true,
enableLeftSideBrightnessSwipe: true, enableLeftSideBrightnessSwipe: true,
enableRightSideVolumeSwipe: true, enableRightSideVolumeSwipe: true,
usePopularPlugin: true, usePopularPlugin: true,