import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, ListMusic, Lock, Plus, Search, X } from "lucide-react";
import { qk } from "../lib/queryKeys";
import { api, isIntrinsicReadOnly, type PlexCard, type PlexCollection } from "../lib/api";
import { formatRuntime } from "../lib/format";
import { notifyActionError } from "../lib/actionErrors";
import { useDebounced } from "../lib/useDebounced";
import { LoadingState, StateMessage } from "./ui/QueryState";
import PosterTile from "./PosterTile";
import PlexLibraryPicker from "./PlexLibraryPicker";
import NavGrid from "./ui/NavGrid";
import { Button } from "./ui/Button";
import { IconButton } from "./ui/IconButton";
import { Input } from "./ui/Input";
import { SURFACE } from "./ui/surfaces";
// A collection tile's sub-line: item count · total runtime (each dropped when unknown, e.g. show-only
// collections have no movie runtime).
function collectionMeta(c: PlexCollection, t: TFunction, lang: string): string {
const parts: string[] = [];
if (c.child_count != null) parts.push(t("plex.browse.items", { count: c.child_count }));
const rt = formatRuntime(c.total_runtime_s, lang);
if (rt) parts.push(rt);
return parts.join(" · ");
}
// 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
// union), so this is a pure front-end surface. Collection tiles show a poster collage of their top
// films (PosterTile), falling back to a title-hash colour; playlists have no server art yet, so they
// take the colour fallback.
const GRID = "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
// A grid of the user's Plex playlists; a tile opens that playlist's view.
export function PlexPlaylistsBrowse({ onOpen }: { onOpen: (id: number) => void }) {
const { t } = useTranslation();
const q = useQuery({ queryKey: qk.plexPlaylists(), queryFn: () => api.plexPlaylists() });
const playlists = q.data?.playlists ?? [];
if (q.isError && !q.data) {
return (
q.refetch()}>
{t("common.loadError")}
);
}
if (q.isLoading) return ;
if (playlists.length === 0)
return
{t("plex.playlist.none")}
;
return (
{playlists.map((p, i) => (
}
onClick={() => onOpen(p.id)}
className="h-28"
/>
))}
);
}
// The "New collection" panel (admin, ask B-route): name (rejected if it duplicates an existing one —
// Plex conflates same-named collections) + a search to pick the FIRST title, since Plex can't create an
// empty collection. Creating opens the new collection's detail page (onCreated → onPick).
function NewCollectionPanel({
existingNames,
onCreated,
onClose,
}: {
existingNames: Set;
onCreated: (rk: string) => void;
onClose: () => void;
}) {
const { t } = useTranslation();
const [name, setName] = useState("");
const [term, setTerm] = useState("");
const [busy, setBusy] = useState(false);
const trimmed = name.trim();
// Advisory only: `existingNames` is the search-scoped, all-library collection union, whereas the
// backend enforces uniqueness PER library (Plex only conflates same-named collections within one
// section). So a match here is a hint, not a hard block — it must not gate creation, or a valid
// same-name collection in another library would be un-creatable. The backend's 409 is authoritative.
const dupHint = trimmed.length > 0 && existingNames.has(trimmed.toLowerCase());
const nameOk = trimmed.length > 0;
async function create(card: PlexCard) {
if (!nameOk || busy) return;
setBusy(true);
try {
const col = await api.plexCreateCollection("", trimmed, card.id); // backend derives the library
onCreated(col.id);
} catch (e) {
// The backend is authoritative on per-library name uniqueness — a taken name comes back
// structured, and so localized (see localizeDetail).
notifyActionError(e, t("plex.collection.editFailed"));
setBusy(false);
}
}
return (