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(`/aweme/${neighbors.next.aweme_id}`); if (neighbors.prev) router.prefetch(`/aweme/${neighbors.prev.aweme_id}`); }, [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(`/aweme/${neighbors.next.aweme_id}`); } else if (dy < 0 && neighbors?.prev) { wheelCooldownRef.current = now; router.push(`/aweme/${neighbors.prev.aweme_id}`); } }; el.addEventListener("wheel", onWheel, { passive: false }); return () => el.removeEventListener("wheel", onWheel); }, [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(`/aweme/${neighbors.prev.aweme_id}`); } } 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(`/aweme/${neighbors.next.aweme_id}`); } } 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]); }