feat(plex): player HTPC polish — Stop button, keyboard help, time-remaining + clock, scroll-restore
- Explicit Stop button (stops playback + returns to the library). - Keyboard cheat-sheet overlay toggled with 'H' (movie hides the episode-nav row). - Backspace = stop & back to feed (HTPC-remote convention); closes the help first. - Time-remaining readout beside the seek bar + a live wall clock in the top bar (both fade with the controls). - PlexBrowse restores the grid scroll position when returning from the player, so Back lands on the same card instead of the top of the library. - New plex.player i18n keys (stop/help/keys.*) in en/hu/de.
This commit is contained in:
parent
4993298838
commit
38c4bee869
7 changed files with 165 additions and 7 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue