mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
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
212 lines
6.1 KiB
TypeScript
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,
|
|
};
|
|
};
|