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:
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ListMusic } from "lucide-react";
|
||||
import { ListMusic, Lock } from "lucide-react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api, type PlexCollection } from "../lib/api";
|
||||
import { formatRuntime } from "../lib/format";
|
||||
@@ -20,6 +20,12 @@ function collectionMeta(c: PlexCollection, t: TFunction): string {
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
// A read-only collection (a smart or external auto-list — imdb/tmdb/…) can never be edited; mark it up
|
||||
// front on the tile, mirroring the badge on the info page.
|
||||
function isReadOnlyCollection(c: PlexCollection): boolean {
|
||||
return !!c.smart || (!!c.source && c.source !== "collection");
|
||||
}
|
||||
|
||||
// The Playlist / Collection browse surfaces reached from the header pill (Piece 2): a grid of tiles,
|
||||
// each a way INTO that area — a playlist tile opens the playlist view, a collection tile filters the
|
||||
// title grid by that collection. Both reuse the existing list endpoints (plexPlaylists / plexCollections
|
||||
@@ -100,6 +106,14 @@ export function PlexCollectionsBrowse({ onPick }: { onPick: (id: string, title:
|
||||
title={c.title}
|
||||
art={c.art}
|
||||
subtitle={collectionMeta(c, t)}
|
||||
topRight={
|
||||
isReadOnlyCollection(c) ? (
|
||||
<Lock
|
||||
className="h-3.5 w-3.5 text-white/80 drop-shadow"
|
||||
aria-label={t("plex.collection.readOnly")}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
onClick={() => onPick(c.id, c.title)}
|
||||
className="h-28"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user