feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
This commit is contained in:
parent
eed517b475
commit
690e17611c
10 changed files with 568 additions and 9 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Fragment, lazy, Suspense, 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";
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ArrowLeft,
|
||||
Clock,
|
||||
Download,
|
||||
Info,
|
||||
Keyboard,
|
||||
Maximize,
|
||||
Minimize,
|
||||
|
|
@ -22,6 +23,9 @@ import {
|
|||
} from "lucide-react";
|
||||
import { api, type PlexMarker } from "../lib/api";
|
||||
|
||||
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
|
||||
const PlexInfo = lazy(() => import("./PlexInfo"));
|
||||
|
||||
// The Plex module's rich, full-page player. Plays the LOCAL file: direct-playable files stream raw
|
||||
// (native <video>), remux files stream via the seek-restart HLS session (hls.js). The visible
|
||||
// timeline is ABSOLUTE (0…duration); the HLS session is relative to its start offset, so we map
|
||||
|
|
@ -77,6 +81,10 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const helpOpenRef = useRef(false);
|
||||
helpOpenRef.current = helpOpen;
|
||||
// "I" toggles a rich info overlay (poster/cast/ratings) over the running video.
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const infoOpenRef = useRef(false);
|
||||
infoOpenRef.current = infoOpen;
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
useEffect(() => {
|
||||
const iv = window.setInterval(() => setNow(new Date()), 15000);
|
||||
|
|
@ -409,14 +417,20 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
case "H":
|
||||
setHelpOpen((v) => !v);
|
||||
break;
|
||||
case "i":
|
||||
case "I":
|
||||
setInfoOpen((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 if (infoOpenRef.current) setInfoOpen(false);
|
||||
else onClose();
|
||||
break;
|
||||
case "Escape":
|
||||
if (helpOpenRef.current) setHelpOpen(false);
|
||||
else if (infoOpenRef.current) setInfoOpen(false);
|
||||
else if (!document.fullscreenElement) onClose();
|
||||
break;
|
||||
}
|
||||
|
|
@ -637,6 +651,9 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<Ctrl label={t("plex.player.infoBtn")} onClick={() => setInfoOpen((v) => !v)}>
|
||||
<Info className="w-5 h-5" />
|
||||
</Ctrl>
|
||||
<Ctrl label={t("plex.player.help")} onClick={() => setHelpOpen((v) => !v)}>
|
||||
<Keyboard className="w-5 h-5" />
|
||||
</Ctrl>
|
||||
|
|
@ -686,6 +703,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
[["↑", "↓"], "plex.player.keys.volume"],
|
||||
[["M"], "plex.player.keys.mute"],
|
||||
[["F"], "plex.player.keys.fullscreen"],
|
||||
[["I"], "plex.player.keys.info"],
|
||||
[["⌫", "Esc"], "plex.player.keys.back"],
|
||||
[["H"], "plex.player.keys.help"],
|
||||
] as [string[], string][]
|
||||
|
|
@ -708,6 +726,26 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rich info overlay (poster/cast/ratings) — "i" or the info button; video keeps playing. */}
|
||||
{infoOpen && detail && (
|
||||
<div
|
||||
className="absolute inset-0 z-20 grid place-items-center overflow-y-auto bg-black/70 p-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setInfoOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="my-auto w-full max-w-3xl rounded-2xl border border-white/15 bg-neutral-900/95 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<PlexInfo detail={detail} variant="overlay" onClose={() => setInfoOpen(false)} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue