feat(plex): subtitles rise above the control bar while it shows

On an iPad the bar covered the text whenever it was up: the browser places a
native cue by its line, which nothing animates. The track now stays loaded as
hidden and PlexSubtitles draws the active cues, measured against the bar and
lifted above it in 200 ms, back down when it hides. The viewer's size, colour,
background and outline settings style the text directly. Also fixes a test
typing slip from the search pill change.
This commit is contained in:
2026-10-08 02:59:13 +02:00
parent 5db6dc8ee6
commit 75093458c9
8 changed files with 333 additions and 58 deletions
+17 -10
View File
@@ -45,7 +45,6 @@ import {
DEFAULT_PREFS,
fmt,
subOrdForLang,
subShadowCss,
type PlexPlayerPrefs,
} from "../lib/plexPlayerPrefs";
import { Ctrl, PanelHead } from "./plexPlayerControls";
@@ -84,6 +83,7 @@ import { LS, useAccountPersistedObject } from "../lib/storage";
import { useDismiss } from "../lib/useDismiss";
import { useScrollFade } from "../lib/useScrollFade";
import { useBackToClose } from "../lib/history";
import PlexSubtitles from "./PlexSubtitles";
import { lockBodyScroll, trapTab } from "./ui/dialogFocus";
import {
pushLayer,
@@ -150,6 +150,8 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
const videoRef = useRef<HTMLVideoElement | null>(null);
const wrapRef = useRef<HTMLDivElement>(null);
// The bottom band, which the subtitles keep above while it shows.
const bottomBarRef = useRef<HTMLDivElement>(null);
// A control that goes while it holds the focus — Next onto the last item or a series finale —
// hands it to its neighbour instead of <body>. The player is portaled outside the
// page scroller, whose keeper does not watch it.
@@ -418,7 +420,6 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
detail,
sessionK,
subOffset: prefs.subOffset,
subPos: prefs.subPos,
restartSession,
setAudioTrack: (track: number) => engine.setAudioTrack(track),
patchPrefs,
@@ -1130,12 +1131,6 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
}),
}}
>
{/* Per-user subtitle appearance (font size / colour / background). ::cue is global, but this
player owns the only <video> on screen. */}
<style>{`video::cue{font-size:${prefs.subSize}%;color:${prefs.subColor};background-color:${prefs.subBg};${subShadowCss(
prefs.subShadow,
prefs.subShadowColor,
)}}`}</style>
<video
ref={(el) => {
videoRef.current = el;
@@ -1151,8 +1146,8 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
onClick={onPictureClick}
playsInline
>
{/* Text subtitles as native tracks (same-origin → sent with the session cookie). The effect
above flips modes; the browser fetches a track's VTT the first time it's shown. */}
{/* Text subtitles as a native track (same-origin → sent with the session cookie), kept
`hidden`: the browser fetches and parses the VTT, PlexSubtitles below draws the cues. */}
{selectedSub && subShowable && (
<track
// One track = the selected sub only (no multi-track overlap). The key (see usePlexTracks)
@@ -1168,6 +1163,17 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
/>
)}
</video>
<PlexSubtitles
video={videoEl}
pos={prefs.subPos}
size={prefs.subSize}
color={prefs.subColor}
background={prefs.subBg}
shadowPx={prefs.subShadow}
shadowColor={prefs.subShadowColor}
avoid={uiVisible}
avoidRef={bottomBarRef}
/>
{/* Loading / buffering / error overlay. `!ready` = initial session load; `buffering` = a
mid-playback stall or in-flight seek (see the state); `loadError` surfaces a fatal failure so
@@ -1331,6 +1337,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
{/* Bottom controls */}
<div
ref={bottomBarRef}
{...holdWhileHovered}
className={`absolute bottom-0 inset-x-0 px-4 pb-3 pt-3 bg-linear-to-t from-black/80 to-transparent transition-opacity ${
uiVisible