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:
@@ -252,6 +252,37 @@ export const EMPTY_PLEX_FILTERS: PlexFilters = {
|
||||
keywords: [],
|
||||
collections: [],
|
||||
};
|
||||
// Coerce an opaque persisted/saved filter blob to a full PlexFilters: fill defaults for any missing
|
||||
// array/scalar, and migrate the legacy single `collection`/`collectionTitle` into the multi-select
|
||||
// `collections`/`collectionTitles`. Shared by PlexProvider (localStorage seed) and the saved-views
|
||||
// provider (a DB view blob). Not a migration — an unknown field a newer build dropped just falls away.
|
||||
export function coercePlexFilters(raw: Record<string, unknown>): PlexFilters {
|
||||
const {
|
||||
sortDir: _drop,
|
||||
collection,
|
||||
collectionTitle,
|
||||
...rest
|
||||
} = raw as Record<string, unknown> & {
|
||||
collection?: string | null;
|
||||
collectionTitle?: string | null;
|
||||
};
|
||||
const merged: PlexFilters = { ...EMPTY_PLEX_FILTERS, ...(rest as Partial<PlexFilters>) };
|
||||
if (collection && !merged.collections.length) {
|
||||
merged.collections = [collection];
|
||||
if (collectionTitle) merged.collectionTitles = { [collection]: collectionTitle };
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// A stable signature of a PlexFilters set (only non-default keys; display-only titles excluded), so a
|
||||
// saved view can be matched against the live filters regardless of key order — the Plex analogue of the
|
||||
// feed's filtersToParams. Reuses appendPlexFilters so the signature can't drift from the wire params.
|
||||
export function plexFiltersSignature(f: PlexFilters): string {
|
||||
const u = new URLSearchParams();
|
||||
appendPlexFilters(u, f);
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
export function plexFilterCount(f: PlexFilters): number {
|
||||
return (
|
||||
f.genres.length +
|
||||
|
||||
Reference in New Issue
Block a user