Files
douyin-archive/app/aweme/[awemeId]/hooks/useNavigation.ts
T

152 lines
4.5 KiB
TypeScript

import { useRouter } from "next/navigation";
import type { RefObject } from "react";
import { useEffect, useRef } from "react";
import type { Neighbors } from "../types.ts";
interface UseNavigationProps {
neighbors: Neighbors;
isVideo: boolean;
mediaContainerRef: RefObject<HTMLDivElement | null>;
videoRef?: RefObject<HTMLVideoElement | null>;
prevImg?: () => void;
nextImg?: () => void;
togglePlay: () => void;
}
export function useNavigation({
neighbors,
isVideo,
mediaContainerRef,
videoRef,
prevImg,
nextImg,
togglePlay,
}: UseNavigationProps): void {
const router = useRouter();
const wheelCooldownRef = useRef<number>(0);
// 预取路由
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]);
// 键盘快捷键
useEffect(() => {
const onKeyDown = (e: KeyboardEvent): void => {
const target = e.target as HTMLElement | null;
if (
target &&
(target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable)
) {
return;
}
const key = e.key.toLowerCase();
if (key === "arrowup" || key === "w") {
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") {
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") {
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") {
e.preventDefault();
if (isVideo) {
const v = videoRef?.current;
if (v && v.duration) {
v.currentTime = Math.min(v.duration, v.currentTime + 5);
}
} else {
nextImg?.();
}
} else if (key === " ") {
e.preventDefault();
togglePlay();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [isVideo, neighbors, router, videoRef, prevImg, nextImg, togglePlay]);
// 仅在无历史记录的独立新标签页首次进入时注入一次返回兜底,避免连续刷视频时无限叠加 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]);
}