feat: 支持长按右键/D键二倍速播放与视频AB循环
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,8 @@ 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;
|
||||
@@ -11,6 +13,10 @@ interface UseNavigationProps {
|
||||
prevImg?: () => void;
|
||||
nextImg?: () => void;
|
||||
togglePlay: () => void;
|
||||
onFastForwardChange?: (active: boolean) => void;
|
||||
onSetAbStart?: () => void;
|
||||
onSetAbEnd?: () => void;
|
||||
onClearAbLoop?: () => void;
|
||||
}
|
||||
|
||||
export function useNavigation({
|
||||
@@ -21,9 +27,25 @@ export function useNavigation({
|
||||
prevImg,
|
||||
nextImg,
|
||||
togglePlay,
|
||||
onFastForwardChange,
|
||||
onSetAbStart,
|
||||
onSetAbEnd,
|
||||
onClearAbLoop,
|
||||
}: UseNavigationProps): void {
|
||||
const router = useRouter();
|
||||
const wheelCooldownRef = useRef<number>(0);
|
||||
const rightPressKeyRef = useRef<string | null>(null);
|
||||
const longPressActiveRef = useRef<boolean>(false);
|
||||
const longPressTimerRef = useRef<number | null>(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(() => {
|
||||
@@ -61,9 +83,28 @@ export function useNavigation({
|
||||
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 &&
|
||||
@@ -75,8 +116,9 @@ export function useNavigation({
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const code = e.code;
|
||||
|
||||
if (key === "arrowup" || key === "w") {
|
||||
if (key === "arrowup" || key === "w" || code === "KeyW") {
|
||||
e.preventDefault();
|
||||
const now = performance.now();
|
||||
if (now - wheelCooldownRef.current < 700) return;
|
||||
@@ -84,7 +126,7 @@ export function useNavigation({
|
||||
wheelCooldownRef.current = now;
|
||||
router.push(`/aweme/${neighbors.prev.aweme_id}`);
|
||||
}
|
||||
} else if (key === "arrowdown" || key === "s") {
|
||||
} else if (key === "arrowdown" || key === "s" || code === "KeyS") {
|
||||
e.preventDefault();
|
||||
const now = performance.now();
|
||||
if (now - wheelCooldownRef.current < 700) return;
|
||||
@@ -92,7 +134,7 @@ export function useNavigation({
|
||||
wheelCooldownRef.current = now;
|
||||
router.push(`/aweme/${neighbors.next.aweme_id}`);
|
||||
}
|
||||
} else if (key === "arrowleft" || key === "a") {
|
||||
} else if (key === "arrowleft" || key === "a" || code === "KeyA") {
|
||||
e.preventDefault();
|
||||
if (isVideo) {
|
||||
const v = videoRef?.current;
|
||||
@@ -102,26 +144,115 @@ export function useNavigation({
|
||||
} else {
|
||||
prevImg?.();
|
||||
}
|
||||
} else if (key === "arrowright" || key === "d") {
|
||||
} else if (key === "arrowright" || key === "d" || code === "KeyD") {
|
||||
e.preventDefault();
|
||||
if (isVideo) {
|
||||
const v = videoRef?.current;
|
||||
if (v && v.duration) {
|
||||
v.currentTime = Math.min(v.duration, v.currentTime + 5);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
nextImg?.();
|
||||
}
|
||||
} else if (key === " ") {
|
||||
}, 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);
|
||||
return () => window.removeEventListener("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
|
||||
|
||||
Reference in New Issue
Block a user