From 375c814bb6521878a108b31f370a0dc5d4cd2c72 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 26 Jul 2026 03:18:15 +0200 Subject: [PATCH] fix(player): dock the big-view control bar in its own strip (UAT follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/components/PlayerModal.tsx | 62 +++++++++---------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index e929005..157d8da 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -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(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. */}
+ className={ + maximized || isFullscreen ? "absolute inset-x-0 top-0 bottom-14" : "w-full h-full" + } + > +
+
{/* 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 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 , 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 + 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 , outside the fullscreen element. */} {(maximized || isFullscreen) && (
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 && ( <>