fix(player): Escape coordination, focused-Space, light-theme menus (U-C, U-3.1.4, U-3.3.12)

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.
This commit is contained in:
2026-07-21 22:29:40 +02:00
parent f590be2bef
commit 94dfa6a34b
4 changed files with 29 additions and 3 deletions
+11 -2
View File
@@ -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"
>
<div className="mb-1.5 flex gap-1">
{settingsTabs.map((tab) => (