// Serialize the Plex-browse content state (library scope + watch-state + sort + metadata filters) to/ // from the address bar so a Plex-browse view is bookmarkable/shareable — the analogue of the feed's // urlState. Keys are NAMESPACED (`pscope`/`psort`/`pshow`/`pf`) rather than reusing the feed's bare // `scope`/`sort`/`show`: FeedFiltersProvider actively STRIPS its own KEYS from the URL whenever the app // is off the feed page, so a shared key would be deleted out from under the Plex page. The namespaced // keys are Plex-unique (like `?v=`/`?pl`), so no other provider ever reads or clears them. Only non- // default values are emitted, so URLs stay clean. import { parsePlexFilters, plexFiltersToParams, type PlexFilters } from "./api"; import { decodeSortParam, encodeSortParam, type SortState } from "./columnSort"; import { PLEX_SORT_FALLBACK } from "./plexSort"; // The content state this mirrors. `sort` is the RAW provider SortState (scope-healed only at the point of // use), so the user's real preference round-trips through the URL. export interface PlexUrlState { scope: string; sort: SortState; show: string; filters: PlexFilters; } const KEYS = ["pscope", "psort", "pshow", "pf"] as const; const SCOPES = ["movie", "show", "both"]; const SHOWS = ["all", "unwatched", "in_progress", "watched"]; // Sort ⇄ `key:dir` (e.g. `year:desc`) via the shared codec in columnSort; the Plex default (`title:asc`) // emits nothing. function encodeSort(s: SortState): string | null { if (!s || (s.key === PLEX_SORT_FALLBACK?.key && s.dir === PLEX_SORT_FALLBACK?.dir)) return null; return encodeSortParam(s); } /** The address-bar delta for the Plex content state: each namespaced key mapped to its emitted value, * or null to delete it (so a control back at its default clears the key). Pass `null` to clear ALL Plex * keys — used when the app is off the Plex page (localStorage still holds the state; it re-mirrors on * return). Feed the result into urlWithParams(). */ export function plexParamUpdates(state: PlexUrlState | null): Record { if (!state) return { pscope: null, psort: null, pshow: null, pf: null }; const pf = plexFiltersToParams(state.filters).toString(); return { pscope: state.scope && state.scope !== "both" ? state.scope : null, psort: encodeSort(state.sort), pshow: state.show && state.show !== "all" ? state.show : null, pf: pf || null, }; } /** True if the query string carries any Plex content key we recognize (a shared Plex-browse link). */ export function hasPlexParams(params: URLSearchParams): boolean { return KEYS.some((k) => params.has(k)); } /** Merge the Plex content keys from the URL over a base state (the localStorage-seeded defaults); an * absent or invalid key keeps the base value, so a partial link (only `?psort=`) is honoured. */ export function plexParamsToState(params: URLSearchParams, base: PlexUrlState): PlexUrlState { const scope = params.get("pscope"); const show = params.get("pshow"); const psort = params.get("psort"); const sort = psort != null ? (decodeSortParam(psort) ?? base.sort) : base.sort; return { scope: scope && SCOPES.includes(scope) ? scope : base.scope, sort, show: show && SHOWS.includes(show) ? show : base.show, filters: params.has("pf") ? parsePlexFilters(new URLSearchParams(params.get("pf") ?? "")) : base.filters, }; }