mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
client revised
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user