tier plans missing

This commit is contained in:
rgrgogu
2026-08-01 23:02:46 +08:00
parent 510a883974
commit 1315796412
20 changed files with 616 additions and 79 deletions
@@ -46,6 +46,8 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
// blank black box with no indication anything was happening, especially
// on large/slow-loading files.
const [mediaLoading, setMediaLoading] = useState(true);
const [hoverProgress, setHoverProgress] = useState(null); // { x, time }
const previewVidRef = useRef(null);
// Reset player when video changes
useEffect(() => {
@@ -138,6 +140,36 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
else el.requestFullscreen?.();
};
// Ignore keystrokes aimed at the seek/volume range inputs so arrow keys
// there keep their native behavior instead of double-seeking.
const handleKeyDown = useCallback((e) => {
if (e.target.tagName === "INPUT") return;
const v = vidRef.current;
switch (e.key) {
case " ":
e.preventDefault();
togglePlay();
break;
case "ArrowRight":
e.preventDefault();
if (v && v.duration) v.currentTime = Math.min(v.currentTime + 5, v.duration);
break;
case "ArrowLeft":
e.preventDefault();
if (v) v.currentTime = Math.max(v.currentTime - 5, 0);
break;
case "m":
e.preventDefault();
toggleMute();
break;
case "f":
e.preventDefault();
toggleFullscreen();
break;
default: break;
}
}, [togglePlay]);
// #video-block-wrap now wraps both the video area and the controls bar
// below it (previously just the video), so fullscreen no longer drops the
// seek bar / volume / fullscreen button. isFullscreen also relaxes the
@@ -189,7 +221,9 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
) : src ? (
<div
id="video-block-wrap"
className={`overflow-hidden bg-card ${isFullscreen ? "fixed inset-0 flex flex-col" : "rounded-lg border"}`}
tabIndex={0}
onKeyDown={handleKeyDown}
className={`overflow-hidden bg-card outline-none focus-visible:ring-2 focus-visible:ring-ring ${isFullscreen ? "fixed inset-0 flex flex-col" : "rounded-lg border"}`}
>
{/* ── Video area ── */}
@@ -251,7 +285,19 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
<div className="px-3 pt-2.5 pb-3 flex flex-col gap-2">
{/* Progress bar */}
<div className="relative h-1 bg-border rounded-full cursor-pointer">
<div
className="relative h-1 bg-border rounded-full cursor-pointer"
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const pct = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
const time = pct * (vidRef.current?.duration ?? 0);
setHoverProgress({ x: e.clientX - rect.left, time });
if (previewVidRef.current && isFinite(time) && time >= 0) {
previewVidRef.current.currentTime = time;
}
}}
onMouseLeave={() => setHoverProgress(null)}
>
<div
className="h-full bg-foreground rounded-full transition-[width] duration-100"
style={{ width: `${progress}%` }}
@@ -263,6 +309,31 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
aria-label="Seek"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
{/* Scrub preview — hidden video seeked to hover position, no canvas/sprite needed */}
{hoverProgress && (
<div
className="absolute bottom-3 flex flex-col items-center pointer-events-none z-30"
style={{ left: `${hoverProgress.x}px`, transform: "translateX(-50%)" }}
>
<div className="rounded-md overflow-hidden border border-white/20 shadow-xl bg-black" style={{ width: 160, height: 90 }}>
<video
ref={previewVidRef}
src={src}
preload="auto"
muted
playsInline
disablePictureInPicture
disableRemotePlayback
onContextMenu={(e) => e.preventDefault()}
className="w-full h-full object-cover"
/>
</div>
<span className="text-white text-xs mt-1 font-medium tabular-nums drop-shadow bg-black/70 px-1.5 py-0.5 rounded">
{fmtTime(hoverProgress.time)}
</span>
</div>
)}
</div>
{/* Button row */}