feat(plex): active-filter chip row above the grid (R13 S2)

Plex now shows the same at-a-glance "what's narrowing this" row the feed has,
instead of the narrowing being visible only inside the collapsed sidebar. New
usePlexActiveFilters is the Plex analogue of useActiveFilters — it turns the Plex
state (expanded filters + watch-state + sort) into the source-agnostic
ActiveFilter[] the SHARED ActiveFilterChips renders. One removable chip per
dimension (genres/ratings/people emit one each), plus Clear-all resetting filters
+ watch-state + sort. Labels reuse the existing plex.* i18n keys; no new strings.

Lives in components (not lib) since it reads the Plex provider — lib stays
provider-free like useActiveFilters. Gate green: tsc/eslint-0/prettier/knip.
Self-E2E'd: a sort=year chip renders, removing it resets sort to title and hides
the row.
This commit is contained in:
2026-08-14 00:56:03 +02:00
parent 002ef0ab84
commit 5e7169e064
2 changed files with 132 additions and 2 deletions
+14 -2
View File
@@ -9,7 +9,7 @@ import {
type ReactNode,
} from "react";
import { qk } from "../lib/queryKeys";
import { healPlexSort } from "../lib/plexSort";
import { healPlexSort, PLEX_SORT_FALLBACK } from "../lib/plexSort";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import {
@@ -59,6 +59,8 @@ import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
import PlexCollectionEditor from "./PlexCollectionEditor";
import { LoadingState, StateMessage } from "./QueryState";
import { PageToolbar } from "./PageShell";
import ActiveFilterChips from "./ActiveFilterChips";
import { usePlexActiveFilters } from "./usePlexActiveFilters";
import { usePlex, usePlexActions } from "./PlexProvider";
// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
@@ -89,8 +91,16 @@ export default function PlexBrowse() {
// search term (q) comes from the shared header box; the rail hands off a playlist to open via the
// shared `playlistOpen` slot, which we consume into a history subview then clear.
const { q, scope, show, sort, filters, playlistOpen: openPlaylist } = usePlex();
const { setScope, setFilters, setPlaylistOpen } = usePlexActions();
const { setScope, setShow, setSort, setFilters, setPlaylistOpen } = usePlexActions();
const dq = useDebounced(q.trim(), 350);
// Plex's active-filter chip row (above the grid), the shared feed idiom: one removable chip per
// narrowing, plus a Clear-all that resets filters + watch-state + sort to their defaults.
const plexChips = usePlexActiveFilters();
const clearAllFilters = () => {
setFilters(EMPTY_PLEX_FILTERS);
setShow("all");
setSort(PLEX_SORT_FALLBACK);
};
// The provider holds sort scope-agnostically; heal it against THIS scope's options so a movie-only
// key (e.g. duration) left over from before a switch to shows doesn't ask for an unavailable sort.
const sortState = healPlexSort(sort, scope);
@@ -333,6 +343,8 @@ export default function PlexBrowse() {
</div>
</PageToolbar>
<ActiveFilterChips filters={plexChips} onClearAll={clearAllFilters} />
{personCards.length > 0 && (
<div className="mb-4 flex flex-wrap gap-3">
{personCards.map((p) => (