fix(player): dock the big-view control bar in its own strip (UAT follow-up)
The overlaid, auto-hiding bar fought YouTube's seek bar for the same bottom pixels: while ours showed it covered YT's seek + buttons, and reaching them meant moving off the area and waiting for our bar to fade — a fiddly, "random"- feeling dance keyed on mouse movement. Now the bar is DOCKED and always visible: the video area is inset by the bar's height (a wrapper around the mount, since YT.Player replaces the mount element with its iframe and would drop a class set on it), so the bar sits in its own strip and never overlaps the video. YouTube's seek/controls stay reachable by hovering the video's bottom edge, just above ours. No auto-hide, no guessing, no fight. Verified in Chrome: iframe.bottom == bar.top (no overlap), bar opacity 1.
This commit is contained in:
@@ -197,10 +197,6 @@ export default function PlayerModal({
|
||||
// history layer only while active). Mirrors the Esc ladder in the keydown handler below.
|
||||
useBackToExit(isFullscreen, exitFullscreen);
|
||||
useBackToExit(maximized, () => setMaximized(false));
|
||||
// The big-view control bar + exit auto-hide when the pointer rests (like a native player) and
|
||||
// reappear on movement; always shown while in the modal card.
|
||||
const [controlsVisible, setControlsVisible] = useState(true);
|
||||
const controlsHideTimerRef = useRef<number | undefined>(undefined);
|
||||
// Volume survives the modal, the next video and a reload: a YT.Player is created per video and
|
||||
// always starts at full volume. Per-account, like the Plex player's prefs. Level 0 IS the muted
|
||||
// state (that's how YouTube's own control behaves), so there's no separate `muted` flag to
|
||||
@@ -589,27 +585,6 @@ export default function PlayerModal({
|
||||
return () => el.removeEventListener("wheel", onWheel);
|
||||
}, []);
|
||||
|
||||
// Auto-hide the big-view control bar after the pointer rests; any movement over the stage reveals
|
||||
// it again. Only active in the big views (maximise / our fullscreen); the modal keeps it shown.
|
||||
useEffect(() => {
|
||||
if (!maximized && !isFullscreen) {
|
||||
setControlsVisible(true);
|
||||
return;
|
||||
}
|
||||
const reveal = () => {
|
||||
setControlsVisible(true);
|
||||
window.clearTimeout(controlsHideTimerRef.current);
|
||||
controlsHideTimerRef.current = window.setTimeout(() => setControlsVisible(false), 2600);
|
||||
};
|
||||
reveal();
|
||||
const el = stageRef.current;
|
||||
el?.addEventListener("mousemove", reveal);
|
||||
return () => {
|
||||
el?.removeEventListener("mousemove", reveal);
|
||||
window.clearTimeout(controlsHideTimerRef.current);
|
||||
};
|
||||
}, [maximized, isFullscreen]);
|
||||
|
||||
// Track the browser's fullscreen state. WE never request it any more (F maximises via CSS), so
|
||||
// this is YouTube's own fullscreen — entered from its control bar, left by its button or Esc.
|
||||
useEffect(() => {
|
||||
@@ -855,10 +830,20 @@ export default function PlayerModal({
|
||||
>
|
||||
{/* Hide the iframe entirely on error so YouTube's own error screen can't bleed
|
||||
through our overlay. */}
|
||||
{/* In the big views the video area is inset by the control bar's height (`bottom-14`) so
|
||||
the bar below never overlaps it. The inset lives on THIS wrapper, not on `mountRef`:
|
||||
YT.Player REPLACES the mount element with its iframe (width/height 100% of the parent),
|
||||
which would drop any class on mountRef itself — the iframe then sizes to this wrapper. */}
|
||||
<div
|
||||
ref={mountRef}
|
||||
className={`w-full h-full ${playerError != null ? "invisible" : ""}`}
|
||||
/>
|
||||
className={
|
||||
maximized || isFullscreen ? "absolute inset-x-0 top-0 bottom-14" : "w-full h-full"
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={mountRef}
|
||||
className={`w-full h-full ${playerError != null ? "invisible" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
{/* Interaction layer over the CENTRE of the video: catches click (play/pause) + wheel
|
||||
volume (the wheel event bubbles from here to the modal's listener — over the bare
|
||||
cross-origin iframe it would be swallowed) and stops the iframe stealing keyboard focus.
|
||||
@@ -881,21 +866,18 @@ export default function PlayerModal({
|
||||
/>
|
||||
)}
|
||||
{/* Our own control bar for the big views (maximise / our fullscreen), where the card's
|
||||
controls are off-screen. It's IN the stage, so it stays visible in true fullscreen too
|
||||
(a <body> portal would not). Auto-hides on idle; stopPropagation so a click on it doesn't
|
||||
toggle playback. The exit button also re-arms our shortcuts (pull focus back off the
|
||||
iframe). Add-to-playlist / download are hidden in true fullscreen — their popovers portal
|
||||
to <body>, outside the fullscreen element, so they'd open invisibly. */}
|
||||
controls are off-screen. It's IN the stage (so it survives true fullscreen, where a
|
||||
<body> portal would not) and DOCKED in its own strip: the video area above is inset by
|
||||
this bar's height (see mountRef's `bottom-14`), so the bar NEVER overlaps YouTube's seek
|
||||
bar — that stays reachable by hovering the video's bottom edge, just above ours. Always
|
||||
visible (no auto-hide) so there's no guessing whether it will appear and no fight with the
|
||||
seek bar over the same pixels. stopPropagation so a click here doesn't toggle playback;
|
||||
the exit button also re-arms our shortcuts. Add-to-playlist / download are hidden in true
|
||||
fullscreen — their popovers portal to <body>, outside the fullscreen element. */}
|
||||
{(maximized || isFullscreen) && (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseEnter={() => {
|
||||
setControlsVisible(true);
|
||||
window.clearTimeout(controlsHideTimerRef.current);
|
||||
}}
|
||||
className={`absolute inset-x-0 bottom-0 z-menu flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 px-3 pb-3 pt-10 text-sm text-white bg-gradient-to-t from-black/85 via-black/45 to-transparent transition-opacity duration-200 ${
|
||||
controlsVisible ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
className="absolute inset-x-0 bottom-0 z-menu flex h-14 items-center justify-center gap-x-3 overflow-x-auto border-t border-white/10 bg-neutral-950/95 px-3 text-sm text-white"
|
||||
>
|
||||
{hasQueue && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user