// Serialize feed filters to/from a compact, human-readable URL query string. Filters are // NOT kept in the address bar during normal use (localStorage is the single source of // truth); this serializer powers the explicit "Share view" link and the one-time hydration // when someone opens such a link. Only non-default values are emitted to keep URLs clean. import type { FeedFilters } from "./api"; const KEYS = [ "q", "show", "sort", "scope", "normal", "shorts", "live", "tags", "tagmode", "channel", "maxage", "from", "to", "mindur", "maxdur", ] as const; export function filtersToParams(f: FeedFilters): URLSearchParams { const p = new URLSearchParams(); if (f.q) p.set("q", f.q); if (f.show && f.show !== "unwatched") p.set("show", f.show); if (f.sort && f.sort !== "newest") p.set("sort", f.sort); if (f.scope === "all") p.set("scope", "all"); if (!f.includeNormal) p.set("normal", "0"); if (f.includeShorts) p.set("shorts", "1"); if (f.includeLive) p.set("live", "1"); if (f.tags.length) p.set("tags", f.tags.join(",")); if (f.tagMode === "and") p.set("tagmode", "and"); if (f.channelId) p.set("channel", f.channelId); if (f.maxAgeDays) p.set("maxage", String(f.maxAgeDays)); if (f.dateFrom) p.set("from", f.dateFrom); if (f.dateTo) p.set("to", f.dateTo); if (f.minDuration != null) p.set("mindur", String(f.minDuration)); if (f.maxDuration != null) p.set("maxdur", String(f.maxDuration)); return p; } /** Merge URL params onto a base filter set (typically the defaults). */ export function paramsToFilters(params: URLSearchParams, base: FeedFilters): FeedFilters { const f: FeedFilters = { ...base }; const num = (v: string | null): number | undefined => { if (v == null || v === "") return undefined; const n = Number(v); return Number.isFinite(n) ? n : undefined; }; if (params.has("q")) f.q = params.get("q") ?? ""; if (params.has("show")) f.show = params.get("show") || "unwatched"; if (params.has("sort")) f.sort = params.get("sort") || "newest"; if (params.has("scope")) f.scope = params.get("scope") === "all" ? "all" : "my"; if (params.has("normal")) f.includeNormal = params.get("normal") !== "0"; if (params.has("shorts")) f.includeShorts = params.get("shorts") === "1"; if (params.has("live")) f.includeLive = params.get("live") === "1"; if (params.has("tags")) f.tags = (params.get("tags") ?? "") .split(",") .map((s) => Number(s)) .filter((n) => Number.isInteger(n)); if (params.has("tagmode")) f.tagMode = params.get("tagmode") === "and" ? "and" : "or"; f.channelId = params.get("channel") || undefined; f.maxAgeDays = num(params.get("maxage")); f.dateFrom = params.get("from") || undefined; f.dateTo = params.get("to") || undefined; f.minDuration = num(params.get("mindur")); f.maxDuration = num(params.get("maxdur")); return f; } /** True if the query string carries any filter param we recognize. */ export function hasFilterParams(params: URLSearchParams): boolean { return KEYS.some((k) => params.has(k)); } // The single source of truth for valid page ids; `Page` and the runtime validator both // derive from it, so adding a page is a one-line change (no parallel allowlists to keep in // sync). "feed" is the default/fallback. export const PAGES = [ "feed", "channels", "stats", "playlists", "settings", "scheduler", "notifications", ] as const; export type Page = (typeof PAGES)[number]; /** Narrow an arbitrary string to a known Page (else null). */ export function isPage(p: string | null | undefined): p is Page { return !!p && (PAGES as readonly string[]).includes(p); } export function readPage(): Page { const p = new URLSearchParams(window.location.search).get("page"); return isPage(p) ? p : "feed"; } /** Build a shareable absolute URL that reproduces the current filters (+ page). Used by the * opt-in "Share view" action — filters are not otherwise written to the address bar. */ export function shareUrl(f: FeedFilters, page: Page = "feed"): string { const params = filtersToParams(f); if (page !== "feed") params.set("page", page); const qs = params.toString(); return `${window.location.origin}${window.location.pathname}${qs ? `?${qs}` : ""}`; } /** Strip any filter/page query params from the address bar (after hydrating from a share * link), leaving a clean URL without touching history. */ export function stripUrlParams(): void { if (window.location.search) { window.history.replaceState(null, "", window.location.pathname); } }