import { useRouter } from "next/navigation"; import type { RefObject } from "react"; import { useEffect, useRef } from "react"; import type { Neighbors } from "../types.ts"; const LONG_PRESS_DELAY_MS = 220; interface UseNavigationProps { neighbors: Neighbors; isVideo: boolean; mediaContainerRef: RefObject; videoRef?: RefObject; prevImg?: () => void; nextImg?: () => void; togglePlay: () => void; onFastForwardChange?: (active: boolean) => void; onSetAbStart?: () => void; onSetAbEnd?: () => void; onClearAbLoop?: () => void; } export function useNavigation({ neighbors, isVideo, mediaContainerRef, videoRef, prevImg, nextImg, togglePlay, onFastForwardChange, onSetAbStart, onSetAbEnd, onClearAbLoop, }: UseNavigationProps): void { const router = useRouter(); const wheelCooldownRef = useRef(0); const rightPressKeyRef = useRef(null); const longPressActiveRef = useRef(false); const longPressTimerRef = useRef(null); const onFastForwardChangeRef = useRef(onFastForwardChange); onFastForwardChangeRef.current = onFastForwardChange; const onSetAbStartRef = useRef(onSetAbStart); onSetAbStartRef.current = onSetAbStart; const onSetAbEndRef = useRef(onSetAbEnd); onSetAbEndRef.current = onSetAbEnd; const onClearAbLoopRef = useRef(onClearAbLoop); onClearAbLoopRef.current = onClearAbLoop; // 预取路由 useEffect(() => { if (!neighbors) return; if (neighbors.next) router.prefetch(neighbors.next.href); if (neighbors.prev) router.prefetch(neighbors.prev.href); }, [neighbors, router]); // 鼠标滚轮切换 useEffect(() => { const el = mediaContainerRef.current; if (!el) return; const onWheel = (e: WheelEvent): void => { if (e.ctrlKey) return; const now = performance.now(); if (now - wheelCooldownRef.current < 700) return; const dy = e.deltaY; if (Math.abs(dy) < 40) return; if ((dy > 0 && neighbors?.next) || (dy < 0 && neighbors?.prev)) { e.preventDefault(); } if (dy > 0 && neighbors?.next) { wheelCooldownRef.current = now; router.push(neighbors.next.href); } else if (dy < 0 && neighbors?.prev) { wheelCooldownRef.current = now; router.push(neighbors.prev.href); } }; el.addEventListener("wheel", onWheel, { passive: false }); return () => el.removeEventListener("wheel", onWheel); }, [neighbors, mediaContainerRef, router]); // 触屏上下滑切换(上滑下一条,下滑上一条)。只认明显的竖向手势, // 不影响图文的左右滑动;从按钮、链接、控制栏(data-no-swipe)上开始的触摸不处理。 useEffect(() => { const el = mediaContainerRef.current; if (!el) return; let start: { x: number; y: number; t: number } | null = null; let vertical = false; const onTouchStart = (e: TouchEvent): void => { const target = e.target as HTMLElement | null; if ( e.touches.length !== 1 || target?.closest( "button, a, input, textarea, select, [role='slider'], [data-no-swipe]", ) ) { start = null; return; } const touch = e.touches[0]; start = { x: touch.clientX, y: touch.clientY, t: performance.now() }; vertical = false; }; const onTouchMove = (e: TouchEvent): void => { if (!start) return; if (e.touches.length !== 1) { start = null; return; } const touch = e.touches[0]; const dx = touch.clientX - start.x; const dy = touch.clientY - start.y; if (!vertical && Math.abs(dy) > 12 && Math.abs(dy) > Math.abs(dx) * 1.5) { vertical = true; } // 竖向手势由我们处理,阻止浏览器下拉刷新 / 回弹 if (vertical && e.cancelable) e.preventDefault(); }; const onTouchEnd = (e: TouchEvent): void => { if (!start) return; const touch = e.changedTouches[0]; const dy = touch.clientY - start.y; const elapsed = performance.now() - start.t; const wasVertical = vertical; start = null; vertical = false; if (!wasVertical) return; // 快速轻扫,或者慢慢拖过屏幕高度的四分之一 const flick = Math.abs(dy) > 60 && elapsed < 800; const longDrag = Math.abs(dy) > window.innerHeight * 0.25; if (!flick && !longDrag) return; const now = performance.now(); if (now - wheelCooldownRef.current < 500) return; if (dy < 0 && neighbors?.next) { wheelCooldownRef.current = now; router.push(neighbors.next.href); } else if (dy > 0 && neighbors?.prev) { wheelCooldownRef.current = now; router.push(neighbors.prev.href); } }; const onTouchCancel = (): void => { start = null; vertical = false; }; el.addEventListener("touchstart", onTouchStart, { passive: true }); el.addEventListener("touchmove", onTouchMove, { passive: false }); el.addEventListener("touchend", onTouchEnd); el.addEventListener("touchcancel", onTouchCancel); return () => { el.removeEventListener("touchstart", onTouchStart); el.removeEventListener("touchmove", onTouchMove); el.removeEventListener("touchend", onTouchEnd); el.removeEventListener("touchcancel", onTouchCancel); }; }, [neighbors, mediaContainerRef, router]); // 键盘快捷键(支持向右箭头 / D 键区分短按与长按 2 倍速,以及视频 [ ] 设置 AB 循环) useEffect(() => { const clearLongPressTimer = (): void => { if (longPressTimerRef.current !== null) { window.clearTimeout(longPressTimerRef.current); longPressTimerRef.current = null; } }; const resetRightKeyPress = (): void => { clearLongPressTimer(); const wasLongPress = longPressActiveRef.current; rightPressKeyRef.current = null; longPressActiveRef.current = false; if (wasLongPress) { onFastForwardChangeRef.current?.(false); } }; const onKeyDown = (e: KeyboardEvent): void => { if (e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return; const target = e.target as HTMLElement | null; if ( target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) ) { return; } const key = e.key.toLowerCase(); const code = e.code; if (key === "arrowup" || key === "w" || code === "KeyW") { e.preventDefault(); const now = performance.now(); if (now - wheelCooldownRef.current < 700) return; if (neighbors?.prev) { wheelCooldownRef.current = now; router.push(neighbors.prev.href); } } else if (key === "arrowdown" || key === "s" || code === "KeyS") { e.preventDefault(); const now = performance.now(); if (now - wheelCooldownRef.current < 700) return; if (neighbors?.next) { wheelCooldownRef.current = now; router.push(neighbors.next.href); } } else if (key === "arrowleft" || key === "a" || code === "KeyA") { e.preventDefault(); if (isVideo) { const v = videoRef?.current; if (v && v.duration) { v.currentTime = Math.max(0, v.currentTime - 5); } } else { prevImg?.(); } } else if (key === "arrowright" || key === "d" || code === "KeyD") { e.preventDefault(); const normalizedKey = code === "KeyD" ? "d" : key; // 忽略系统长按重复触发的 keydown,交由定时器统一判定长按阈值 if (e.repeat || rightPressKeyRef.current !== null) return; rightPressKeyRef.current = normalizedKey; longPressActiveRef.current = false; clearLongPressTimer(); longPressTimerRef.current = window.setTimeout(() => { longPressTimerRef.current = null; if ( rightPressKeyRef.current !== null && !longPressActiveRef.current ) { longPressActiveRef.current = true; onFastForwardChangeRef.current?.(true); } }, LONG_PRESS_DELAY_MS); } else if ( isVideo && (code === "BracketLeft" || key === "[" || key === "【" || key === "「") ) { if (e.repeat) return; e.preventDefault(); onSetAbStartRef.current?.(); } else if ( isVideo && (code === "BracketRight" || key === "]" || key === "】" || key === "」") ) { if (e.repeat) return; e.preventDefault(); onSetAbEndRef.current?.(); } else if ( isVideo && (code === "Backslash" || key === "\\" || key === "、") ) { if (e.repeat) return; e.preventDefault(); onClearAbLoopRef.current?.(); } else if (key === " " || code === "Space") { if (e.repeat) return; e.preventDefault(); togglePlay(); } }; const onKeyUp = (e: KeyboardEvent): void => { const key = e.key.toLowerCase(); const code = e.code; const normalizedKey = code === "KeyD" ? "d" : key; if (normalizedKey !== "arrowright" && normalizedKey !== "d") return; if (rightPressKeyRef.current !== normalizedKey) return; e.preventDefault(); clearLongPressTimer(); const wasLongPress = longPressActiveRef.current; rightPressKeyRef.current = null; longPressActiveRef.current = false; if (wasLongPress) { onFastForwardChangeRef.current?.(false); return; } // 短按(单击):视频快进 5 秒,图文切换下一张图片 if (isVideo) { const v = videoRef?.current; if (v && v.duration) { v.currentTime = Math.min(v.duration, v.currentTime + 5); } } else { nextImg?.(); } }; const handleVisibilityChange = (): void => { if (document.hidden) resetRightKeyPress(); }; window.addEventListener("keydown", onKeyDown); window.addEventListener("keyup", onKeyUp); window.addEventListener("blur", resetRightKeyPress); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keyup", onKeyUp); window.removeEventListener("blur", resetRightKeyPress); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [isVideo, neighbors, router, videoRef, prevImg, nextImg, togglePlay]); // 组件卸载时确保清理长按定时器并恢复倍速 useEffect(() => { return () => { if (longPressTimerRef.current !== null) { window.clearTimeout(longPressTimerRef.current); longPressTimerRef.current = null; } if (longPressActiveRef.current) { longPressActiveRef.current = false; rightPressKeyRef.current = null; onFastForwardChangeRef.current?.(false); } }; }, []); // 仅在无历史记录的独立新标签页首次进入时注入一次返回兜底,避免连续刷视频时无限叠加 history 栈 useEffect(() => { const currentState = window.history.state as | { interceptBack?: boolean } | null | undefined; if (window.history.length <= 1 && !currentState?.interceptBack) { window.history.pushState({ ...currentState, interceptBack: true }, ""); } const handlePopState = (): void => { if (window.history.length <= 1) { window.close(); setTimeout(() => { if (!document.hidden) { router.push("/"); } }, 100); } }; window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); }; }, [router]); }