fix(plex): seek tooltip tracks cursor under scale; tidy the tuning menu

- The seek-bar hover tooltip positioned itself with a px `left` derived
  from the scaled getBoundingClientRect, so inside the transform:scale
  player it got re-scaled and drifted right of the cursor (only 0 was
  right). Position it by fraction (% of the bar) instead — it now sits
  under the cursor at any point, like the volume tooltip already did.
- The tuning (gear) menu tabs wrapped to two lines at the +25% scale.
  Widen the menu, keep tab labels on one line (whitespace-nowrap), and
  shorten the labels: "Sync offset" → "Offsets", "Subtitle style" →
  "Subtitles" (all three languages).
This commit is contained in:
npeter83 2026-07-09 01:32:04 +02:00
parent 23070279a3
commit 2659991d84
4 changed files with 17 additions and 14 deletions

View file

@ -860,8 +860,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeMarker?.type, activeMarker?.start_s, prefs.autoSkipIntro, prefs.autoSkipCredits, prefs.autoSkipDelay, doSkip]); }, [activeMarker?.type, activeMarker?.start_s, prefs.autoSkipIntro, prefs.autoSkipCredits, prefs.autoSkipDelay, doSkip]);
// Seekbar hover: a timestamp tooltip that follows the cursor. // Seekbar hover: a timestamp tooltip that follows the cursor. Store the position as a FRACTION
const [hover, setHover] = useState<{ x: number; t: number } | null>(null); // (0..1), not pixels: the bar sits inside the transform:scale(1.25) subtree, so a px `left` would be
// re-scaled and drift right of the cursor (only 0 stayed correct). A % `left` is relative to the
// bar's own width and scales cleanly.
const [hover, setHover] = useState<{ frac: number; t: number } | null>(null);
// Seekbar scrubbing: the absolute time under the dragging finger. While non-null the head/thumb // Seekbar scrubbing: the absolute time under the dragging finger. While non-null the head/thumb
// preview this position; the real seek fires once on release (avoids restarting the session on // preview this position; the real seek fires once on release (avoids restarting the session on
// every out-of-buffer step of the drag). A plain click is a down+up at one spot → seeks there. // every out-of-buffer step of the drag). A plain click is a down+up at one spot → seeks there.
@ -1037,9 +1040,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
}} }}
onPointerMove={(e) => { onPointerMove={(e) => {
const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - r.left, r.width)); const frac = Math.max(0, Math.min((e.clientX - r.left) / r.width, 1));
const tt = (x / r.width) * duration; const tt = frac * duration;
setHover({ x, t: tt }); setHover({ frac, t: tt });
if (e.buttons & 1) setScrub(tt); // dragging with the primary button held if (e.buttons & 1) setScrub(tt); // dragging with the primary button held
}} }}
onPointerUp={() => { onPointerUp={() => {
@ -1054,7 +1057,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
{hover && duration > 0 && ( {hover && duration > 0 && (
<div <div
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded bg-black/90 px-1.5 py-0.5 text-[11px] tabular-nums text-white" className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded bg-black/90 px-1.5 py-0.5 text-[11px] tabular-nums text-white"
style={{ left: `${hover.x}px` }} style={{ left: `${hover.frac * 100}%` }}
> >
{fmt(hover.t)} {fmt(hover.t)}
</div> </div>
@ -1236,14 +1239,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
ref={menuRef} ref={menuRef}
onWheel={(e) => e.stopPropagation()} onWheel={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="glass-menu absolute bottom-full right-0 mb-2 w-72 rounded-xl p-2 text-sm text-white shadow-2xl" className="glass-menu 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"> <div className="mb-1.5 flex gap-1">
{settingsTabs.map((tab) => ( {settingsTabs.map((tab) => (
<button <button
key={tab.key} key={tab.key}
onClick={() => setSettingsTab(tab.key)} onClick={() => setSettingsTab(tab.key)}
className={`flex-1 rounded px-2 py-1 text-[11px] ${ className={`flex-1 whitespace-nowrap rounded px-2 py-1 text-[11px] ${
activeTab === tab.key ? "bg-white/15 text-accent" : "text-white/70 hover:bg-white/10" activeTab === tab.key ? "bg-white/15 text-accent" : "text-white/70 hover:bg-white/10"
}`} }`}
> >

View file

@ -126,14 +126,14 @@
"fineSeek": "Feines Springen" "fineSeek": "Feines Springen"
}, },
"settings": "Einstellungen", "settings": "Einstellungen",
"sync": "Synchronität", "sync": "Offsets",
"subOffset": "Untertitel-Verzögerung", "subOffset": "Untertitel-Verzögerung",
"audioOffset": "Audio-Verzögerung", "audioOffset": "Audio-Verzögerung",
"playback": "Wiedergabe", "playback": "Wiedergabe",
"seekStep": "Sprungweite", "seekStep": "Sprungweite",
"fineSeek": "Feines Springen (Strg)", "fineSeek": "Feines Springen (Strg)",
"autoHide": "Steuerung ausblenden", "autoHide": "Steuerung ausblenden",
"subStyle": "Untertitel-Stil", "subStyle": "Untertitel",
"subSize": "Schriftgröße", "subSize": "Schriftgröße",
"subPos": "Position", "subPos": "Position",
"subColor": "Textfarbe", "subColor": "Textfarbe",

View file

@ -126,14 +126,14 @@
"fineSeek": "Fine seek" "fineSeek": "Fine seek"
}, },
"settings": "Settings", "settings": "Settings",
"sync": "Sync offset", "sync": "Offsets",
"subOffset": "Subtitle delay", "subOffset": "Subtitle delay",
"audioOffset": "Audio delay", "audioOffset": "Audio delay",
"playback": "Playback", "playback": "Playback",
"seekStep": "Seek step", "seekStep": "Seek step",
"fineSeek": "Fine seek (Ctrl)", "fineSeek": "Fine seek (Ctrl)",
"autoHide": "Auto-hide controls", "autoHide": "Auto-hide controls",
"subStyle": "Subtitle style", "subStyle": "Subtitles",
"subSize": "Text size", "subSize": "Text size",
"subPos": "Position", "subPos": "Position",
"subColor": "Text color", "subColor": "Text color",

View file

@ -126,14 +126,14 @@
"fineSeek": "Finom léptetés" "fineSeek": "Finom léptetés"
}, },
"settings": "Beállítások", "settings": "Beállítások",
"sync": "Szinkron-eltolás", "sync": "Eltolások",
"subOffset": "Felirat-eltolás", "subOffset": "Felirat-eltolás",
"audioOffset": "Hang-eltolás", "audioOffset": "Hang-eltolás",
"playback": "Lejátszás", "playback": "Lejátszás",
"seekStep": "Léptetés", "seekStep": "Léptetés",
"fineSeek": "Finom léptetés (Ctrl)", "fineSeek": "Finom léptetés (Ctrl)",
"autoHide": "Vezérlők elrejtése", "autoHide": "Vezérlők elrejtése",
"subStyle": "Felirat stílusa", "subStyle": "Feliratok",
"subSize": "Szövegméret", "subSize": "Szövegméret",
"subPos": "Pozíció", "subPos": "Pozíció",
"subColor": "Szövegszín", "subColor": "Szövegszín",