feat(plex): port the saved-views (personal views) UI to Plex (frontend)
Plex now has the same personal-views system as the feed — the rail header icon opens the quick-pick popover, the rail title opens the manager modal (list / create / rename / delete / reorder / set-default / apply), and the active view leads the chip row with a "modified" dot. One SavedViewsProvider serves both modules: it switches `source` (feed ⟷ plex) by the active page (both filter providers are global, so it reads whichever the page owns) and runs the same logic on per-source wiring — the live filters, apply, a stable signature (feed filtersToParams / plex plexFiltersSignature), coercion (asFilters / coercePlexFilters), and what to strip on save. Moved below PlexProvider in the tree so it can read plex state; resets sticky/modal on a source switch. The shared popover/modal/chip are unchanged; the modal describes a plex view's captured filters via a pure buildPlexActiveFilters (extracted from usePlexActiveFilters — no duplication). api.savedViews/createView take a source; `SavedView.filters` is now an opaque blob. New coercePlexFilters/plexFilters- Signature (also dedups PlexProvider's collection migration). Empty-state copy genericised (no longer says "feed"). A Plex view captures the rail filters (not watch-state/sort/scope). Verified end-to-end on localdev: create/apply/preview/default/delete on plex, feed views stay separate + unregressed (402 vitest), no console errors.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
|
||||
import { EMPTY_PLEX_FILTERS, type PlexFilters } from "../lib/api";
|
||||
import { coercePlexFilters, EMPTY_PLEX_FILTERS, type PlexFilters } from "../lib/api";
|
||||
import { parseSortState, type SortState } from "../lib/columnSort";
|
||||
import { parsePlexField, type PlexSearchField } from "../lib/plexSearch";
|
||||
import { PLEX_SORT_FALLBACK } from "../lib/plexSort";
|
||||
@@ -122,21 +122,11 @@ export function PlexProvider({ children }: { children: ReactNode }) {
|
||||
const [filtersRaw, setFiltersRaw] = useState<string>(readPlexFiltersRaw);
|
||||
const filters = useMemo<PlexFilters>(() => {
|
||||
try {
|
||||
// Drop the legacy `sortDir` — direction now lives in the sort SortState. Left here it would leak
|
||||
// into the facets query key and be re-persisted on every filter change. Also migrate the legacy
|
||||
// single `collection`/`collectionTitle` into the multi-select `collections`/`collectionTitles`.
|
||||
const {
|
||||
sortDir: _drop,
|
||||
collection: legacyCollection,
|
||||
collectionTitle: legacyTitle,
|
||||
...rest
|
||||
} = JSON.parse(filtersRaw);
|
||||
const merged: PlexFilters = { ...EMPTY_PLEX_FILTERS, ...rest };
|
||||
if (legacyCollection && !merged.collections.length) {
|
||||
merged.collections = [legacyCollection];
|
||||
if (legacyTitle) merged.collectionTitles = { [legacyCollection]: legacyTitle };
|
||||
}
|
||||
return merged;
|
||||
// coercePlexFilters fills defaults, drops the legacy `sortDir` (direction now lives in the sort
|
||||
// SortState — left here it would leak into the facets key and be re-persisted), and migrates the
|
||||
// legacy single `collection`/`collectionTitle` into `collections`/`collectionTitles`. Shared with
|
||||
// the saved-views provider so the localStorage seed and a DB view blob coerce identically.
|
||||
return coercePlexFilters(JSON.parse(filtersRaw));
|
||||
} catch {
|
||||
return EMPTY_PLEX_FILTERS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user