siftlode/frontend/src/lib/urlState.ts

83 lines
3.1 KiB
TypeScript
Raw Normal View History

// Serialize feed filters to/from a compact, human-readable URL query string, so a
// pasted URL reproduces exactly what's on screen (handy for "here's the URL, I see
// this bug"). Only non-default values are emitted to keep URLs clean.
import type { FeedFilters } from "./api";
const KEYS = [
"q",
"show",
"sort",
"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.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("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));
}
/** Reflect the current filters into the address bar without adding history entries. */
export function syncUrl(f: FeedFilters): void {
const qs = filtersToParams(f).toString();
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
window.history.replaceState(null, "", url);
}