feat: 支持长按右键/D键二倍速播放与视频AB循环

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 02:00:05 +08:00
co-authored by Cursor
parent bcf1e6e349
commit 11d9d35002
8 changed files with 431 additions and 37 deletions
+75 -5
View File
@@ -1,13 +1,18 @@
import { memo } from "react";
import type { AbLoop } from "../types.ts";
interface ProgressBarProps {
progress: number;
onSeek: (ratio: number) => void;
abLoop?: AbLoop;
duration?: number;
}
export const ProgressBar = memo(function ProgressBar({
progress,
onSeek,
abLoop,
duration = 0,
}: ProgressBarProps) {
const seekFromClientX = (clientX: number, element: HTMLElement): void => {
const rect = element.getBoundingClientRect();
@@ -15,9 +20,21 @@ export const ProgressBar = memo(function ProgressBar({
onSeek((clientX - rect.left) / rect.width);
};
const hasAbLoop = Boolean(
abLoop && (abLoop.a !== null || abLoop.b !== null) && duration > 0,
);
const startRatio =
hasAbLoop && abLoop
? Math.max(0, Math.min(1, (abLoop.a ?? 0) / duration))
: 0;
const endRatio =
hasAbLoop && abLoop
? Math.max(startRatio, Math.min(1, (abLoop.b ?? duration) / duration))
: 1;
return (
<div
className="relative h-1.5 rounded-full bg-white/25 overflow-hidden cursor-pointer touch-none"
className="relative h-1.5 cursor-pointer touch-none select-none"
onClick={(e) => {
seekFromClientX(e.clientX, e.currentTarget as HTMLElement);
}}
@@ -41,10 +58,63 @@ export const ProgressBar = memo(function ProgressBar({
}
}}
>
<div
className="origin-left h-full bg-white will-change-transform"
style={{ transform: `scaleX(${progress || 0})` }}
/>
{/* 轨道主体 */}
<div className="relative h-full w-full rounded-full bg-white/25 overflow-hidden">
{/* AB 循环区间底色高亮 */}
{hasAbLoop && (
<div
className="absolute top-0 bottom-0 bg-amber-400/40"
style={{
left: `${startRatio * 100}%`,
width: `${Math.max(0, endRatio - startRatio) * 100}%`,
}}
/>
)}
{/* 播放进度填充 */}
<div
className={`origin-left h-full will-change-transform ${
hasAbLoop ? "bg-amber-300" : "bg-white"
}`}
style={{ transform: `scaleX(${progress || 0})` }}
/>
{/* AB 循环区间顶部与底部亮边 */}
{hasAbLoop && (
<div
className="pointer-events-none absolute top-0 bottom-0 border-y border-amber-300/90 bg-amber-300/20"
style={{
left: `${startRatio * 100}%`,
width: `${Math.max(0, endRatio - startRatio) * 100}%`,
}}
/>
)}
</div>
{/* A / B 端点指示标记 */}
{hasAbLoop && (
<>
<div
className="pointer-events-none absolute top-1/2 -translate-y-1/2 -translate-x-1/2 flex flex-col items-center"
style={{ left: `${startRatio * 100}%` }}
>
<span className="mb-2.5 rounded bg-amber-400 px-1 py-[1px] text-[9px] font-bold leading-none text-black shadow">
A
</span>
<div className="h-3 w-[3px] rounded-full bg-amber-400 shadow-[0_0_4px_rgba(251,191,36,0.9)]" />
</div>
<div
className="pointer-events-none absolute top-1/2 -translate-y-1/2 -translate-x-1/2 flex flex-col items-center"
style={{ left: `${endRatio * 100}%` }}
>
<span className="mb-2.5 rounded bg-amber-400 px-1 py-[1px] text-[9px] font-bold leading-none text-black shadow">
B
</span>
<div className="h-3 w-[3px] rounded-full bg-amber-400 shadow-[0_0_4px_rgba(251,191,36,0.9)]" />
</div>
</>
)}
</div>
);
});