// Serialize the Channels-manager content state (active tab + column sort + the column-filter map) to/ // from the address bar so a manager view is bookmarkable/shareable — the analogue of the feed's // urlState and Plex's plexUrlState. Keys are NAMESPACED (`ctab`/`csort`/`cf`) so they're // channels-unique (like `?v=`/`?pl`/`pscope`): no other provider ever reads or clears them, and the // feed's URL-stripping can't delete them out from under the manager. The tab, sort and filters are // always written, their defaults too: a page opened without them takes this browser's remembered view, // which may not be the one bookmarked. The name search (`cq`) is part of the view like the rest (a // bookmark of a searched list opens searched), but nothing remembers it, so only a term is written. import { decodeSortParamOrNone, encodeSortParamOrNone, type SortState } from "./columnSort"; import { type FilterMap } from "./tableColumns"; import { decodeFilterMap, encodeFilterMap } from "./filterMapParam"; // The content state this mirrors. `view` is the raw tab string (the provider clamps it); `sort` is the // raw provider SortState (healed against live columns only at the point of use), so the user's real // preference round-trips through the URL. export interface ChannelsUrlState { view: string; sort: SortState; filters: FilterMap; search: string; } export const CHANNELS_KEYS = ["ctab", "csort", "cf", "cq"] as const; const KEYS = CHANNELS_KEYS; const VIEWS = ["subscribed", "discovery", "hidden", "blocked"]; // Sort ⇄ `key:dir` (e.g. `subscribers:desc`, `none` unsorted) via the shared codec in columnSort. The // filter map rides as one JSON value (lib/filterMapParam), an empty one as `cf=`. /** The address-bar delta for the manager content state: each namespaced key mapped to its emitted * value, or null to delete it (only the search's, when cleared). Pass `null` to clear * ALL manager keys — used when the app is off the Channels page (localStorage still holds the state; it * re-mirrors on return). Feed the result into urlWithParams(). */ export function channelsParamUpdates( state: ChannelsUrlState | null, ): Record { if (!state) return { ctab: null, csort: null, cf: null, cq: null }; return { ctab: state.view || "subscribed", csort: encodeSortParamOrNone(state.sort), cf: encodeFilterMap(state.filters) ?? "", cq: state.search.trim() || null, }; } /** True if the query string carries any manager content key we recognize (a shared manager link). */ export function hasChannelsParams(params: URLSearchParams): boolean { return KEYS.some((k) => params.has(k)); } /** Merge the manager content keys from the URL over a base state (the localStorage-seeded values); an * absent or invalid key keeps the base value, so a partial link (only `?ctab=`) is honoured. */ export function channelsParamsToState( params: URLSearchParams, base: ChannelsUrlState, ): ChannelsUrlState { const tab = params.get("ctab"); const linkedSort = decodeSortParamOrNone(params.get("csort")); return { view: tab && VIEWS.includes(tab) ? tab : base.view, sort: linkedSort === undefined ? base.sort : linkedSort, filters: params.has("cf") ? decodeFilterMap(params.get("cf") ?? "") : base.filters, search: params.get("cq") ?? base.search, }; }