feat(plex): Collection info page — hero, play/shuffle, and write-through editing (B)
Clicking a collection tile now opens a dedicated Collection info page (like the Plex-web
one) instead of just applying it as a filter. New GET /collections/{rk} returns the hero
aggregates (year range, most-restrictive member cert, total runtime) + the members ordered
by release year then title; a new PlexCollectionView renders the hero (art/title/meta/
summary+More) and the member grid, with a Play-all and Shuffle over the playable leaves
(reusing the player's existing queue + auto-advance).
For a non-external, admin-enabled collection the page offers rename / delete / add-title
(a search picker) / remove-item, all through the pre-existing admin endpoints that write
THROUGH to Plex (each edit awaits and re-fetches — no optimism, since a Plex write can 422).
An eligible collection shows "Enable editing" first (the DB editable flag). Read-only ones
(smart / imdb / tmdb / … auto-lists) are marked with a lock on both the info page AND the
browse tile, up front.
New `{ kind: "collection" }` sub-view wired through plexSubUrl (bookmarkable ?v=collection:rk),
PlexBrowse (render branch, Backspace back-nav, tile handler), with a round-trip test.
Verified live: tile→page, hero + chronological members (James Bond 1962→2021, PG-13, 55h),
Play/Shuffle, Enable editing → edit controls + remove-X, the add search (marks already-in
titles "Added"), and the read-only lock on external collections. Real Plex mutations wired
but not executed against the live account.
Gate: tsc/lint/knip/prettier clean, 424 vitest; backend ruff clean, 6 collections tests
(+ a detail-hero/ordering assertion).
This commit is contained in:
@@ -42,6 +42,7 @@ import { PlexInfoView, PlexPersonView, PlexSeasonView, PlexShowView } from "./pl
|
||||
// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
|
||||
const PlexPlayer = lazy(() => import("./PlexPlayer"));
|
||||
const PlexPlaylistView = lazy(() => import("./PlexPlaylistView"));
|
||||
const PlexCollectionView = lazy(() => import("./PlexCollectionView"));
|
||||
|
||||
// The Plex module's content area: browse/search the mirrored library as poster cards, drill from a
|
||||
// show into seasons/episodes. Library scope / watch-state / sort live in the left PlexSidebar; the
|
||||
@@ -173,6 +174,7 @@ export default function PlexBrowse() {
|
||||
"info",
|
||||
"person",
|
||||
"playlist",
|
||||
"collection",
|
||||
].includes(sub.view.kind)
|
||||
) {
|
||||
e.preventDefault();
|
||||
@@ -341,6 +343,20 @@ export default function PlexBrowse() {
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
if (sub.view.kind === "collection") {
|
||||
const cid = sub.view.id;
|
||||
return (
|
||||
<Suspense fallback={<p className="p-8 text-muted text-sm">{t("plex.loading")}</p>}>
|
||||
<PlexCollectionView
|
||||
collectionId={cid}
|
||||
onBack={sub.back}
|
||||
onPlay={(rk, queue) => sub.open({ kind: "player", id: rk, queue })}
|
||||
onOpenCard={onCard}
|
||||
onInfoCard={onInfo}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
if (sub.view.kind === "info") {
|
||||
const infoId = sub.view.id;
|
||||
return (
|
||||
@@ -431,11 +447,7 @@ export default function PlexBrowse() {
|
||||
if (sub.view.kind === "collections") {
|
||||
return (
|
||||
<div className="p-4 max-w-[1600px] mx-auto">
|
||||
<PlexCollectionsBrowse
|
||||
onPick={(id, title) =>
|
||||
applyFilter({ collections: [id], collectionTitles: { [id]: title } })
|
||||
}
|
||||
/>
|
||||
<PlexCollectionsBrowse onPick={(id) => sub.open({ kind: "collection", id })} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user