feat(plex): Collections Phase 2 — admin collection editing (write-back to Plex)
Admins can curate Plex collections from a movie's info page ("Collections"
button → PlexCollectionEditor dialog): create a collection (seeded with the
movie), add/remove the movie to/from editable collections, delete a collection,
and "take over" an existing plain Plex collection (mark it editable). All writes
go to Plex (POST/PUT/DELETE via new PlexClient methods) and are reflected on
every client; a targeted single-collection re-sync (sync.resync_collection /
delete_collection_local) updates the local mirror without the full ~4-min sync.
Gating (per the design decisions): editing is ADMIN-ONLY (collections are shared
library-wide); only plain manual collections are editable — smart + external
auto-lists (IMDb/TMDb/…) are always read-only (can_edit = editable && !smart &&
source=="collection"). New admin endpoints under /api/plex/collections
(create/items add+remove/rename/delete/editable). Verified end-to-end incl. the
live Plex write API (create/add/rename/remove/delete all 200, self-cleaned) and
the editor UI (create + delete with confirm) on localdev.
This commit is contained in:
parent
4b053a55da
commit
8291f06525
12 changed files with 602 additions and 13 deletions
|
|
@ -5,6 +5,7 @@ import { useDismiss } from "../lib/useDismiss";
|
|||
import {
|
||||
Check,
|
||||
ExternalLink,
|
||||
FolderPlus,
|
||||
Play,
|
||||
RotateCcw,
|
||||
SlidersHorizontal,
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
X,
|
||||
} from "lucide-react";
|
||||
import { api, type PlexFilters, type PlexItemDetail } from "../lib/api";
|
||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
||||
|
||||
// The rich "media info" surface for a Plex item — poster/art, IMDb score + link, content rating,
|
||||
// genres, director, cast (with photos), summary. Reused in TWO places: a full-page view opened from
|
||||
|
|
@ -30,6 +32,8 @@ type Props = {
|
|||
onFilter?: (patch: Partial<PlexFilters>) => void;
|
||||
// Play a sibling title from a collection strip (opens its player).
|
||||
onPlayItem?: (id: string) => void;
|
||||
// The item's library key — enables the admin "Manage collections" editor (page variant, movies).
|
||||
library?: string;
|
||||
};
|
||||
|
||||
// A metadata value that filters the grid when clicked (if onFilter is wired), else plain text.
|
||||
|
|
@ -67,6 +71,7 @@ export default function PlexInfo({
|
|||
onStateChange,
|
||||
onFilter,
|
||||
onPlayItem,
|
||||
library,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
|
|
@ -88,6 +93,8 @@ export default function PlexInfo({
|
|||
return next;
|
||||
});
|
||||
};
|
||||
const isAdmin = (qc.getQueryData<{ role?: string }>(["me"])?.role) === "admin";
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [customizing, setCustomizing] = useState(false);
|
||||
const custBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const custMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -360,6 +367,15 @@ export default function PlexInfo({
|
|||
{t("plex.info.clearResume")}
|
||||
</button>
|
||||
)}
|
||||
{isAdmin && detail.kind === "movie" && library && (
|
||||
<button
|
||||
onClick={() => setEditorOpen(true)}
|
||||
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
{t("plex.collEditor.manage")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -488,6 +504,15 @@ export default function PlexInfo({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
{editorOpen && library && (
|
||||
<PlexCollectionEditor
|
||||
item={{ id: detail.id, title: detail.title }}
|
||||
library={library}
|
||||
memberOf={(detail.collections ?? []).map((c) => c.id)}
|
||||
onClose={() => setEditorOpen(false)}
|
||||
onChanged={() => onStateChange?.()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue