feat(plex): restore Collections filter as a cross-library union picker
The searchable Collections picker was dropped from the sidebar when browsing went unified (movies+shows across libraries) because collections are per-library and there's no single selected library. Bring it back library-agnostic: GET /api/plex/collections `library` is now optional — without it the endpoint unions collections across all enabled libraries (with it, the old single-library path the collection editor uses). The sidebar re-adds the searchable list (chip when one is active). Its query key is ["plex-collections","union",<search>] so a search term equal to a library plex_key can't collide with the editor's ["plex-collections",<key>], while the shared prefix keeps editor invalidation refreshing the picker.
This commit is contained in:
parent
b3af04a997
commit
5759deac20
3 changed files with 72 additions and 16 deletions
|
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronRight, Film, Layers, ListMusic, Plus, SlidersHorizontal, Tv2, X } from "lucide-react";
|
||||
import { api, EMPTY_PLEX_FILTERS, plexFilterCount, type PlexFilters } from "../lib/api";
|
||||
import { useDebounced } from "../lib/useDebounced";
|
||||
|
||||
// The Plex module's left filter column (mirrors the feed Sidebar's shell). Movie libraries get the
|
||||
// full metadata filter set (genre / rating / year / duration / added / content rating + the
|
||||
|
|
@ -76,8 +77,21 @@ export default function PlexSidebar({
|
|||
});
|
||||
const facets = facetsQ.data;
|
||||
|
||||
// (Collection filtering: the active-collection chip is set from an item info page's "Browse
|
||||
// collection"; there's no per-library picker in the unified cross-library scope.)
|
||||
// Collections picker (searchable; library-agnostic UNION across all enabled libraries, since the
|
||||
// unified scope has no single library). Only fetched while no collection is active (else the chip
|
||||
// shows instead). Applying one sets the collection filter; the actual filtering already flows through
|
||||
// /library + /facets by rating_key.
|
||||
const [collSearch, setCollSearch] = useState("");
|
||||
const collSearchDeb = useDebounced(collSearch.trim(), 300);
|
||||
const collectionsQ = useQuery({
|
||||
// "union" disambiguates from the editor's per-library ["plex-collections", <plex_key>] key (a search
|
||||
// term equal to a plex_key would otherwise collide); the shared prefix keeps editor invalidation working.
|
||||
queryKey: ["plex-collections", "union", collSearchDeb],
|
||||
queryFn: () => api.plexCollections(undefined, collSearchDeb || undefined),
|
||||
enabled: !filters.collection,
|
||||
});
|
||||
const collections = collectionsQ.data?.collections ?? [];
|
||||
|
||||
const fCount = plexFilterCount(filters);
|
||||
const activeCount = fCount + (show !== "all" ? 1 : 0) + (sort !== "title" ? 1 : 0);
|
||||
const anyActive = activeCount > 0;
|
||||
|
|
@ -257,18 +271,44 @@ export default function PlexSidebar({
|
|||
</Section>
|
||||
)}
|
||||
|
||||
{/* Collection — active-collection chip only (set from an item's info page "Browse
|
||||
collection"). The searchable picker is per-library, so it's omitted in the unified scope. */}
|
||||
{filters.collection && (
|
||||
<Section label={t("plex.filter.collection")}>
|
||||
{/* Collection — the active one shows as a chip; otherwise a searchable picker over the union
|
||||
of all enabled libraries' collections (also settable from an item info page's "Browse
|
||||
collection"). */}
|
||||
<Section label={t("plex.filter.collection")}>
|
||||
{filters.collection ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<RemovableChip
|
||||
label={filters.collectionTitle || filters.collection}
|
||||
onRemove={() => patch({ collection: null, collectionTitle: null })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
value={collSearch}
|
||||
onChange={(e) => setCollSearch(e.target.value)}
|
||||
placeholder={t("plex.filter.collectionSearch")}
|
||||
className="mb-1.5 w-full rounded-lg border border-border bg-card px-2 py-1 text-sm focus:border-accent focus:outline-none"
|
||||
/>
|
||||
<div className="flex max-h-56 flex-col gap-0.5 overflow-y-auto">
|
||||
{collections.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
onClick={() => patch({ collection: c.id, collectionTitle: c.title })}
|
||||
className="glass-hover flex items-center gap-2 rounded-lg px-2 py-1 text-left text-sm"
|
||||
>
|
||||
<Layers className="w-3.5 h-3.5 shrink-0 text-muted" />
|
||||
<span className="flex-1 truncate">{c.title}</span>
|
||||
<span className="text-xs text-muted">{c.child_count}</span>
|
||||
</button>
|
||||
))}
|
||||
{collections.length === 0 && (
|
||||
<span className="px-2 py-1 text-xs text-muted">{t("plex.filter.collectionEmpty")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* IMDb rating min */}
|
||||
<Section label={t("plex.filter.rating")}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue