Files
streamyfin/components/video-player/controls/hooks/useGestureDetection.ts
Fredrik Burmester 7b146e30bd
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
fix: disable gestures from top and bottom of screen because of interference with notification shade pull down (#1206)
2025-11-16 10:49:54 +01:00

212 lines
6.1 KiB
TypeScript

import { useCallback, useRef } from "react";
import type { GestureResponderEvent } from "react-native";
export interface SwipeGestureOptions {
minDistance?: number;
maxDuration?: number;
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
onVerticalDragStart?: (side: "left" | "right", initialY: number) => void;
onVerticalDragMove?: (
side: "left" | "right",
deltaY: number,
currentY: number,
) => void;
onVerticalDragEnd?: (side: "left" | "right") => void;
onTap?: () => void;
screenWidth?: number;
screenHeight?: number;
}
export const useGestureDetection = ({
minDistance = 50,
maxDuration = 800,
onSwipeLeft,
onSwipeRight,
onVerticalDragStart,
onVerticalDragMove,
onVerticalDragEnd,
onTap,
screenWidth = 400,
screenHeight = 800,
}: SwipeGestureOptions = {}) => {
const touchStartTime = useRef(0);
const touchStartPosition = useRef({ x: 0, y: 0 });
const lastTouchPosition = useRef({ x: 0, y: 0 });
const isDragging = useRef(false);
const dragSide = useRef<"left" | "right" | null>(null);
const hasMovedEnough = useRef(false);
const gestureType = useRef<"none" | "horizontal" | "vertical">("none");
const shouldIgnoreTouch = useRef(false);
const handleTouchStart = useCallback(
(event: GestureResponderEvent) => {
const startY = event.nativeEvent.pageY;
// Define exclusion zones (15% from top and bottom)
const topExclusionZone = screenHeight * 0.15;
const bottomExclusionZone = screenHeight * 0.85;
// Check if touch started in exclusion zones
if (startY < topExclusionZone || startY > bottomExclusionZone) {
shouldIgnoreTouch.current = true;
return;
}
shouldIgnoreTouch.current = false;
touchStartTime.current = Date.now();
touchStartPosition.current = {
x: event.nativeEvent.pageX,
y: startY,
};
lastTouchPosition.current = {
x: event.nativeEvent.pageX,
y: startY,
};
isDragging.current = false;
dragSide.current = null;
hasMovedEnough.current = false;
gestureType.current = "none";
},
[screenHeight],
);
const handleTouchMove = useCallback(
(event: GestureResponderEvent) => {
// Ignore touch if it started in exclusion zone
if (shouldIgnoreTouch.current) {
return;
}
const currentPosition = {
x: event.nativeEvent.pageX,
y: event.nativeEvent.pageY,
};
const deltaX = currentPosition.x - touchStartPosition.current.x;
const deltaY = currentPosition.y - touchStartPosition.current.y;
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Lower threshold for starting gestures - make it more sensitive
if (!hasMovedEnough.current && totalDistance > 8) {
hasMovedEnough.current = true;
// Determine gesture type based on initial movement direction
if (absY > absX && absY > 5) {
// Vertical gesture - start drag immediately
gestureType.current = "vertical";
const side =
touchStartPosition.current.x < screenWidth / 2 ? "left" : "right";
isDragging.current = true;
dragSide.current = side;
onVerticalDragStart?.(side, touchStartPosition.current.y);
} else if (absX > absY && absX > 10) {
// Horizontal gesture - mark for discrete swipe
gestureType.current = "horizontal";
}
}
// Continue vertical drag if already dragging
if (
isDragging.current &&
dragSide.current &&
gestureType.current === "vertical"
) {
const deltaFromStart = currentPosition.y - touchStartPosition.current.y;
onVerticalDragMove?.(
dragSide.current,
deltaFromStart,
currentPosition.y,
);
}
lastTouchPosition.current = currentPosition;
},
[onVerticalDragStart, onVerticalDragMove, screenWidth],
);
const handleTouchEnd = useCallback(
(event: GestureResponderEvent) => {
// Ignore touch if it started in exclusion zone
if (shouldIgnoreTouch.current) {
shouldIgnoreTouch.current = false;
return;
}
const touchEndTime = Date.now();
const touchEndPosition = {
x: event.nativeEvent.pageX,
y: event.nativeEvent.pageY,
};
const touchDuration = touchEndTime - touchStartTime.current;
const deltaX = touchEndPosition.x - touchStartPosition.current.x;
const deltaY = touchEndPosition.y - touchStartPosition.current.y;
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// End vertical drag if we were dragging
if (
isDragging.current &&
dragSide.current &&
gestureType.current === "vertical"
) {
onVerticalDragEnd?.(dragSide.current);
isDragging.current = false;
dragSide.current = null;
hasMovedEnough.current = false;
gestureType.current = "none";
return;
}
// Check if gesture is too long for discrete actions
if (touchDuration > maxDuration) {
hasMovedEnough.current = false;
gestureType.current = "none";
return;
}
// Handle discrete horizontal swipes (for skip) only if it was marked as horizontal
if (
gestureType.current === "horizontal" &&
hasMovedEnough.current &&
absX > absY &&
totalDistance > minDistance
) {
if (deltaX > 0) {
onSwipeRight?.();
} else {
onSwipeLeft?.();
}
} else if (
!hasMovedEnough.current &&
touchDuration < 300 &&
totalDistance < 10
) {
// It's a tap - short duration and small movement
onTap?.();
}
hasMovedEnough.current = false;
gestureType.current = "none";
},
[
maxDuration,
minDistance,
onSwipeLeft,
onSwipeRight,
onVerticalDragEnd,
onTap,
],
);
return {
handleTouchStart,
handleTouchMove,
handleTouchEnd,
};
};