client revised

This commit is contained in:
rgrgogu
2026-08-05 04:32:29 +08:00
parent cd16e996e5
commit 882dfbe67a
52 changed files with 1946 additions and 2302 deletions
@@ -31,6 +31,7 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const poster = thumbnailUrl ?? content?.thumbnail_url ?? undefined;
const vidRef = useRef(null);
const wrapRef = useRef(null);
const [playing, setPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
@@ -38,7 +39,13 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const [muted, setMuted] = useState(false);
const [volume, setVolume] = useState(1);
const [overlayVisible,setOverlayVisible]= useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
// Native Fullscreen API works on desktop (all browsers) and Android Chrome.
// iOS Safari doesn't support Fullscreen API on arbitrary elements at all, so
// pseudoFullscreen is a CSS-only (fixed inset-0) fallback that keeps our
// custom controls instead of falling back to native <video> fullscreen.
const [nativeFullscreen, setNativeFullscreen] = useState(false);
const [pseudoFullscreen, setPseudoFullscreen] = useState(false);
const isFullscreen = nativeFullscreen || pseudoFullscreen;
// True from the moment `src` is set until the browser has actually
// buffered enough to render a frame (or stalls mid-playback) — closes the
// gap between "token resolved" (the `loading` from useAssetPreviewSrc
@@ -104,6 +111,13 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
else { v.pause(); setPlaying(false); setOverlayVisible(true); }
}, []);
// Safari doesn't focus a <div tabIndex> on click, which silently kills
// keyboard shortcuts afterwards — force it explicitly.
const handleAreaClick = useCallback(() => {
wrapRef.current?.focus({ preventScroll: true });
togglePlay();
}, [togglePlay]);
const restart = () => {
const v = vidRef.current;
if (!v) return;
@@ -134,10 +148,24 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
};
const toggleFullscreen = () => {
const el = document.getElementById("video-block-wrap");
// Uses wrapRef instead of getElementById so this works correctly even if
// an admin page ever renders more than one VideoBlock at once (an id
// lookup would always grab the first match, toggling the wrong player).
const el = wrapRef.current;
if (!el) return;
if (document.fullscreenElement) document.exitFullscreen();
else el.requestFullscreen?.();
if (isFullscreen) {
if (document.fullscreenElement) document.exitFullscreen();
else if (document.webkitFullscreenElement) document.webkitExitFullscreen();
else setPseudoFullscreen(false);
} else if (el.requestFullscreen) {
el.requestFullscreen();
} else if (el.webkitRequestFullscreen) {
// Desktop Safari — supports element fullscreen via the prefixed API.
el.webkitRequestFullscreen();
} else {
// iOS Safari has no element-level Fullscreen API at all.
setPseudoFullscreen(true);
}
};
// Ignore keystrokes aimed at the seek/volume range inputs so arrow keys
@@ -145,7 +173,10 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const handleKeyDown = useCallback((e) => {
if (e.target.tagName === "INPUT") return;
const v = vidRef.current;
switch (e.key) {
// Fold single-char keys to lowercase so Shift/Caps-Lock doesn't break
// letter shortcuts (e.g. Shift+F reporting as "F", not "f").
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
switch (key) {
case " ":
e.preventDefault();
togglePlay();
@@ -166,20 +197,49 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
e.preventDefault();
toggleFullscreen();
break;
case "Escape":
if (isFullscreen) {
e.preventDefault();
toggleFullscreen();
}
break;
default: break;
}
}, [togglePlay]);
}, [togglePlay, toggleMute, toggleFullscreen, isFullscreen]);
// #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
// 16/9 + max-height constraints so the video fills the screen properly.
useEffect(() => {
const onChange = () => setIsFullscreen(!!document.fullscreenElement);
// Safari (incl. desktop) still fires the prefixed webkitfullscreenchange
// event for element-level fullscreen, so both are listened for.
const onChange = () => {
const active = !!(document.fullscreenElement || document.webkitFullscreenElement);
setNativeFullscreen(active);
// Safari doesn't focus a <div tabIndex> on click, so keyboard
// shortcuts silently stop working after entering fullscreen unless we
// explicitly refocus the wrapper here.
if (active) wrapRef.current?.focus({ preventScroll: true });
};
document.addEventListener("fullscreenchange", onChange);
return () => document.removeEventListener("fullscreenchange", onChange);
document.addEventListener("webkitfullscreenchange", onChange);
return () => {
document.removeEventListener("fullscreenchange", onChange);
document.removeEventListener("webkitfullscreenchange", onChange);
};
}, []);
// CSS pseudo-fullscreen fallback (iOS Safari) — lock page scroll and
// reclaim keyboard focus while it's active, restore on exit/unmount.
useEffect(() => {
if (!pseudoFullscreen) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
wrapRef.current?.focus({ preventScroll: true });
return () => { document.body.style.overflow = prevOverflow; };
}, [pseudoFullscreen]);
// ── Asset picker handler ──────────────────────────────────────────────────
//
// Saves all metadata needed by the client VideoBlock at render time.
@@ -221,16 +281,17 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
) : src ? (
<div
id="video-block-wrap"
ref={wrapRef}
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"}`}
className={`overflow-hidden bg-card outline-none focus-visible:ring-2 focus-visible:ring-ring ${isFullscreen ? "fixed inset-0 z-[100] flex flex-col" : "rounded-lg border"}`}
>
{/* ── Video area ── */}
<div
className={`relative w-full bg-black cursor-pointer group ${isFullscreen ? "flex-1 min-h-0" : ""}`}
style={isFullscreen ? undefined : { aspectRatio: "16/9", maxHeight: "calc(100vh - 260px)" }}
onClick={togglePlay}
onClick={handleAreaClick}
>
<video
ref={vidRef}