From 94dfa6a34b0d51a8bc09137041d2080375a4e78f Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 21 Jul 2026 22:29:40 +0200 Subject: [PATCH] fix(player): Escape coordination, focused-Space, light-theme menus (U-C, U-3.1.4, U-3.3.12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three player/overlay interaction fixes (PlexPlayer changes share a file, hence one commit): - U-C (Escape double-close): useDismiss's Escape now stopPropagation()s (its document listener bubbles before a player's window listener, so one Escape closes only the popover, not the popover AND the player). Modal exports modalCount(); both players' Escape defers when a Modal is open above them, so Escape closes the dialog, not the player under it and then the dialog. - U-3.1.4: PlexPlayer's Space now defers to a focused BUTTON/A (e.g. "Skip intro") instead of toggling playback — matching PlayerModal. - U-3.3.12: the audio/subtitle menus used theme-adaptive glass-menu + forced text-white (white-on-white in light theme); they now use the player's own explicit dark panel (border-white/15 + bg-neutral-900/95), readable in both themes. --- frontend/src/components/Modal.tsx | 7 +++++++ frontend/src/components/PlayerModal.tsx | 4 ++++ frontend/src/components/PlexPlayer.tsx | 13 +++++++++++-- frontend/src/lib/useDismiss.ts | 8 +++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 85906b1d..99b5b6bb 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -8,6 +8,13 @@ import { useBackToClose } from "../lib/history"; let modalStack: number[] = []; let nextModalId = 1; +/** How many s are currently open. The players (which use their own window-level Escape + * listeners, not this stack) check it so their Escape defers to a Modal opened above them — + * otherwise one Escape closes the player UNDER the dialog and then the dialog too (U-C). */ +export function modalCount(): number { + return modalStack.length; +} + // Small centered modal shell (portaled to ): backdrop + ESC + scroll-lock close. export default function Modal({ title, diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index e90e0716..1fc3d1d5 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -23,6 +23,7 @@ import { } from "lucide-react"; import Avatar from "./Avatar"; import AddToPlaylist from "./AddToPlaylist"; +import { modalCount } from "./Modal"; import DownloadButton from "./DownloadButton"; import { api, type Video } from "../lib/api"; import { @@ -375,6 +376,9 @@ export default function PlayerModal({ if (e.key === "Escape") { // In fullscreen, let the browser's Esc exit fullscreen only — don't also close the modal. if (document.fullscreenElement) return; + // Defer to any Modal opened above the player (e.g. the download dialog): its Escape closes + // it, ours would otherwise also fire and close the player underneath it (U-C). + if (modalCount() > 0) return; onClose(); return; } diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index 8e9fccd4..f445d0d2 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -39,6 +39,7 @@ import { LS, useAccountPersistedObject } from "../lib/storage"; import { useDismiss } from "../lib/useDismiss"; import { useScrollFade } from "../lib/useScrollFade"; import { useBackToClose } from "../lib/history"; +import { modalCount } from "./Modal"; // The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page. const PlexInfo = lazy(() => import("./PlexInfo")); @@ -809,6 +810,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { wake(); // keyboard use also reveals the controls switch (e.key) { case " ": + // Let Space activate a focused button/link natively (e.g. "Skip intro"); only toggle + // playback when the focus isn't on a control — matching PlayerModal's guard. + if (tag === "BUTTON" || tag === "A") return; + e.preventDefault(); + togglePlay(); + break; case "k": e.preventDefault(); togglePlay(); @@ -861,6 +868,8 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { else handleBack(); break; case "Escape": + // Defer to any Modal opened above the player — let its Escape close it, not ours (U-C). + if (modalCount() > 0) break; if (skipProgressRef.current != null) cancelAutoSkipRef.current(); else if (menuOpenRef.current) { setMenuOpen(false); @@ -1332,7 +1341,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { style={tracksFade.style} onClick={(e) => e.stopPropagation()} onWheel={(e) => e.stopPropagation()} - className="glass-menu absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto no-scrollbar rounded-xl p-2 text-sm text-white shadow-2xl" + className="border border-white/15 bg-neutral-900/95 absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto no-scrollbar rounded-xl p-2 text-sm text-white shadow-2xl" > {detail.audio_streams.length > 1 && ( <> @@ -1395,7 +1404,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { ref={menuRef} onWheel={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} - className="glass-menu absolute bottom-full right-0 mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl" + className="border border-white/15 bg-neutral-900/95 absolute bottom-full right-0 mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl" >
{settingsTabs.map((tab) => ( diff --git a/frontend/src/lib/useDismiss.ts b/frontend/src/lib/useDismiss.ts index 58c7fbe1..9845d2f7 100644 --- a/frontend/src/lib/useDismiss.ts +++ b/frontend/src/lib/useDismiss.ts @@ -17,7 +17,13 @@ export function useDismiss( if (!list.some((r) => r.current?.contains(target))) onClose(); }; const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); + if (e.key === "Escape") { + // Stop the same Escape from also reaching a player's window-level listener underneath — + // this document listener bubbles first, so one Escape closes only this popover, not the + // popover AND the player behind it. (U-C quick-fix.) + e.stopPropagation(); + onClose(); + } }; document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey);