merge: Plex player HTPC polish (Stop, keyboard help, time-remaining + clock, scroll-restore) — v0.24.0
This commit is contained in:
commit
eed517b475
7 changed files with 165 additions and 7 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.23.2
|
||||
0.24.0
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { lazy, Suspense, useEffect, useRef, type CSSProperties } from "react";
|
||||
import { lazy, Suspense, useEffect, useLayoutEffect, useRef, type CSSProperties } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowLeft, CheckCircle2, Play } from "lucide-react";
|
||||
|
|
@ -34,6 +34,19 @@ export default function PlexBrowse({ q, library, show, sort }: Props) {
|
|||
const dq = useDebounced(q.trim(), 350);
|
||||
const sub = useHistorySubview<Sub>({ kind: "grid" });
|
||||
|
||||
// Opening a show/player replaces the grid entirely (the component returns early below), so the
|
||||
// scroll container resets to the top. Remember where the grid was scrolled when leaving it, and
|
||||
// restore it when we come back — so the browser/mouse Back from the player lands on the same card
|
||||
// instead of the top of the library. The scroller is App's <main> (the page's overflow-y-auto).
|
||||
const scrollRef = useRef(0);
|
||||
const scroller = () => document.querySelector("main");
|
||||
useLayoutEffect(() => {
|
||||
if (sub.view.kind === "grid" && scrollRef.current) {
|
||||
const el = scroller();
|
||||
if (el) el.scrollTop = scrollRef.current;
|
||||
}
|
||||
}, [sub.view.kind]);
|
||||
|
||||
const browseQ = useInfiniteQuery({
|
||||
queryKey: ["plex-browse", library, dq, sort, show],
|
||||
enabled: !!library && sub.view.kind === "grid",
|
||||
|
|
@ -66,6 +79,7 @@ export default function PlexBrowse({ q, library, show, sort }: Props) {
|
|||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
function onCard(card: PlexCard) {
|
||||
scrollRef.current = scroller()?.scrollTop ?? 0; // remember grid position for the trip back
|
||||
if (card.type === "show") sub.open({ kind: "show", id: card.id });
|
||||
else sub.open({ kind: "player", id: card.id });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Fragment, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Hls from "hls.js";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
Download,
|
||||
Keyboard,
|
||||
Maximize,
|
||||
Minimize,
|
||||
Pause,
|
||||
|
|
@ -13,8 +15,10 @@ import {
|
|||
Settings,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
Square,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api, type PlexMarker } from "../lib/api";
|
||||
|
||||
|
|
@ -69,6 +73,15 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
audioRef.current = audioOrd;
|
||||
subRef.current = subOrd;
|
||||
menuOpenRef.current = menuOpen;
|
||||
// Keyboard-shortcut cheat sheet (toggled with "h") + a live wall clock for the top bar.
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const helpOpenRef = useRef(false);
|
||||
helpOpenRef.current = helpOpen;
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
useEffect(() => {
|
||||
const iv = window.setInterval(() => setNow(new Date()), 15000);
|
||||
return () => window.clearInterval(iv);
|
||||
}, []);
|
||||
|
||||
durationRef.current = duration;
|
||||
|
||||
|
|
@ -392,8 +405,19 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
return !m;
|
||||
});
|
||||
break;
|
||||
case "h":
|
||||
case "H":
|
||||
setHelpOpen((v) => !v);
|
||||
break;
|
||||
case "Backspace":
|
||||
// HTPC-style "stop & back to the feed" — mirrors the mouse Back button.
|
||||
e.preventDefault();
|
||||
if (helpOpenRef.current) setHelpOpen(false);
|
||||
else onClose();
|
||||
break;
|
||||
case "Escape":
|
||||
if (!document.fullscreenElement) onClose();
|
||||
if (helpOpenRef.current) setHelpOpen(false);
|
||||
else if (!document.fullscreenElement) onClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
@ -453,6 +477,11 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Live wall clock — handy on a lean-back / HTPC screen. */}
|
||||
<div className="ml-auto flex items-center gap-1.5 text-sm tabular-nums text-white/80 shrink-0">
|
||||
<Clock className="w-4 h-4" />
|
||||
{now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -505,6 +534,9 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
<Ctrl label={t("plex.player.restart")} onClick={() => seekTo(0)}>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</Ctrl>
|
||||
<Ctrl label={t("plex.player.stop")} onClick={onClose}>
|
||||
<Square className="w-5 h-5" />
|
||||
</Ctrl>
|
||||
{detail?.kind === "episode" && (
|
||||
<>
|
||||
<Ctrl label={t("plex.player.prev")} onClick={() => go(detail?.prev_id)} disabled={!detail?.prev_id}>
|
||||
|
|
@ -546,6 +578,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
|
||||
<div className="text-xs tabular-nums text-white/90">
|
||||
{fmt(abs)} / {fmt(duration)}
|
||||
<span className="ml-1.5 text-white/55">-{fmt(Math.max(0, duration - abs))}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
|
@ -604,6 +637,9 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<Ctrl label={t("plex.player.help")} onClick={() => setHelpOpen((v) => !v)}>
|
||||
<Keyboard className="w-5 h-5" />
|
||||
</Ctrl>
|
||||
<Ctrl label={t("plex.player.download")} onClick={download}>
|
||||
<Download className="w-5 h-5" />
|
||||
</Ctrl>
|
||||
|
|
@ -613,6 +649,65 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keyboard-shortcut cheat sheet (toggle with "h" or the keyboard button). */}
|
||||
{helpOpen && (
|
||||
<div
|
||||
className="absolute inset-0 z-20 grid place-items-center bg-black/60 p-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setHelpOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-[min(92vw,26rem)] rounded-2xl border border-white/15 bg-neutral-900/95 p-5 text-white shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Keyboard className="w-5 h-5" /> {t("plex.player.help")}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setHelpOpen(false)}
|
||||
className="p-1 rounded-lg hover:bg-white/10"
|
||||
aria-label={t("plex.player.back")}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<dl className="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5 text-sm">
|
||||
{(
|
||||
[
|
||||
[["Space", "K"], "plex.player.keys.playPause"],
|
||||
[["←", "→"], "plex.player.keys.seek"],
|
||||
...(detail?.kind === "episode"
|
||||
? [[["⇧ ←", "⇧ →"], "plex.player.keys.episode"] as [string[], string]]
|
||||
: []),
|
||||
[["↑", "↓"], "plex.player.keys.volume"],
|
||||
[["M"], "plex.player.keys.mute"],
|
||||
[["F"], "plex.player.keys.fullscreen"],
|
||||
[["⌫", "Esc"], "plex.player.keys.back"],
|
||||
[["H"], "plex.player.keys.help"],
|
||||
] as [string[], string][]
|
||||
).map(([keys, labelKey]) => (
|
||||
<Fragment key={labelKey}>
|
||||
<dt className="flex flex-wrap gap-1">
|
||||
{keys.map((k) => (
|
||||
<kbd
|
||||
key={k}
|
||||
className="rounded bg-white/15 px-1.5 py-0.5 text-xs font-mono whitespace-nowrap"
|
||||
>
|
||||
{k}
|
||||
</kbd>
|
||||
))}
|
||||
</dt>
|
||||
<dd className="text-white/80">{t(labelKey)}</dd>
|
||||
</Fragment>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,19 @@
|
|||
"subOff": "Aus",
|
||||
"errNotFound": "Diese Datei kann nicht abgespielt werden — die Medien sind auf dem Server nicht erreichbar (die Plex-Medieneinbindung fehlt oder die Pfadzuordnung ist falsch). Bitte den Administrator, die Plex-Konfiguration zu prüfen.",
|
||||
"errTranscode": "Das Format dieser Datei erfordert eine Transkodierung, die noch nicht unterstützt wird.",
|
||||
"errGeneric": "Die Wiedergabe konnte nicht gestartet werden. Bitte erneut versuchen."
|
||||
"errGeneric": "Die Wiedergabe konnte nicht gestartet werden. Bitte erneut versuchen.",
|
||||
"stop": "Stopp",
|
||||
"help": "Tastenkürzel",
|
||||
"keys": {
|
||||
"playPause": "Wiedergabe / Pause",
|
||||
"seek": "10 Sekunden spulen",
|
||||
"episode": "Vorherige / nächste Folge",
|
||||
"volume": "Lautstärke lauter / leiser",
|
||||
"mute": "Stumm",
|
||||
"fullscreen": "Vollbild",
|
||||
"back": "Stopp & zurück zum Feed",
|
||||
"help": "Diese Hilfe anzeigen"
|
||||
}
|
||||
},
|
||||
"playable": {
|
||||
"direct": "Spielt direkt im Browser",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,19 @@
|
|||
"subOff": "Off",
|
||||
"errNotFound": "This file can't be played — the media isn't reachable on the server (the Plex media mount is missing or the path map is wrong). Ask the admin to check the Plex configuration.",
|
||||
"errTranscode": "This file's format needs transcoding, which isn't supported yet.",
|
||||
"errGeneric": "Playback couldn't start. Please try again."
|
||||
"errGeneric": "Playback couldn't start. Please try again.",
|
||||
"stop": "Stop",
|
||||
"help": "Keyboard shortcuts",
|
||||
"keys": {
|
||||
"playPause": "Play / pause",
|
||||
"seek": "Seek 10 seconds",
|
||||
"episode": "Previous / next episode",
|
||||
"volume": "Volume up / down",
|
||||
"mute": "Mute",
|
||||
"fullscreen": "Fullscreen",
|
||||
"back": "Stop & back to feed",
|
||||
"help": "Show this help"
|
||||
}
|
||||
},
|
||||
"playable": {
|
||||
"direct": "Plays directly in the browser",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,19 @@
|
|||
"subOff": "Ki",
|
||||
"errNotFound": "Ez a fájl nem játszható le — a média nem érhető el a szerveren (hiányzik a Plex media-mount, vagy hibás az útvonal-leképezés). Kérd meg az adminisztrátort, hogy ellenőrizze a Plex beállításait.",
|
||||
"errTranscode": "Ennek a fájlnak a formátuma transzkódolást igényel, ami még nem támogatott.",
|
||||
"errGeneric": "A lejátszást nem sikerült elindítani. Próbáld újra."
|
||||
"errGeneric": "A lejátszást nem sikerült elindítani. Próbáld újra.",
|
||||
"stop": "Leállítás",
|
||||
"help": "Billentyűparancsok",
|
||||
"keys": {
|
||||
"playPause": "Lejátszás / szünet",
|
||||
"seek": "Tekerés 10 másodperc",
|
||||
"episode": "Előző / következő rész",
|
||||
"volume": "Hangerő fel / le",
|
||||
"mute": "Némítás",
|
||||
"fullscreen": "Teljes képernyő",
|
||||
"back": "Leállítás és vissza a feedre",
|
||||
"help": "Súgó megjelenítése"
|
||||
}
|
||||
},
|
||||
"playable": {
|
||||
"direct": "Közvetlenül játszható a böngészőben",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@ export interface ReleaseEntry {
|
|||
}
|
||||
|
||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||
{
|
||||
version: "0.24.0",
|
||||
date: "2026-07-05",
|
||||
summary: "Plex player polish: a Stop button, a keyboard help overlay, time-remaining and a clock, and Back returns you to your place in the library.",
|
||||
features: [
|
||||
"Plex player: added an explicit Stop button, a time-remaining readout next to the seek bar, and a live clock in the top corner (both fade with the controls).",
|
||||
"Plex player: press “H” (or the keyboard button) for an on-screen cheat sheet of all shortcuts.",
|
||||
"Plex player: Backspace now stops playback and returns to the library — the same as the mouse Back button — matching a typical HTPC remote.",
|
||||
],
|
||||
fixes: [
|
||||
"Plex library: pressing Back from the player now returns you to the exact spot you were scrolled to, instead of jumping to the top of the library.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.23.2",
|
||||
date: "2026-07-05",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue