- Channel-row tags show only what's attached; a '+' opens a per-channel picker. Clicking a tag filters the feed by it; a 'Your tags' sidebar widget makes that filter visible/clearable. - Tag manager dialog (add/rename/delete; delete only confirms when the tag is in use), reachable from the Channel manager and the feed sidebar; hovering a tag's count lists its channels, each a link that focuses it in the Channel manager (DataTable gains an external filter input). - Video cards get a reset action that clears all watch state (incl. an in-progress position). - api.req() now raises the error dialog on 5xx and 400/409/422 with the server's reason.
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
// Per-user customization of the filter sidebar: order of the widget cards, which are
|
|
// collapsed, and which are hidden. Persisted to localStorage and the server-side
|
|
// `preferences` blob so it follows the account.
|
|
|
|
// `show`, `sort` and `content` moved to the feed toolbar (above the cards); they are no
|
|
// longer sidebar widgets. normalizeLayout drops them from any persisted layout automatically.
|
|
export type WidgetId = "date" | "language" | "topic" | "tags";
|
|
|
|
export const ALL_WIDGETS: WidgetId[] = ["date", "language", "topic", "tags"];
|
|
|
|
export const WIDGET_TITLES: Record<WidgetId, string> = {
|
|
date: "Upload date",
|
|
language: "Language",
|
|
topic: "Topic",
|
|
tags: "Tags",
|
|
};
|
|
|
|
export interface SidebarLayout {
|
|
order: WidgetId[];
|
|
collapsed: Partial<Record<WidgetId, boolean>>;
|
|
hidden: Partial<Record<WidgetId, boolean>>;
|
|
}
|
|
|
|
export const DEFAULT_LAYOUT: SidebarLayout = {
|
|
order: [...ALL_WIDGETS],
|
|
collapsed: {},
|
|
hidden: {},
|
|
};
|
|
|
|
const KEY = "subfeed.sidebarLayout";
|
|
|
|
// Tolerate stale/partial data: keep only known widgets and append any that are missing
|
|
// (e.g. a widget added in a later version) so nothing silently disappears.
|
|
export function normalizeLayout(raw: unknown): SidebarLayout {
|
|
const r = (raw ?? {}) as Partial<SidebarLayout>;
|
|
const order: WidgetId[] = Array.isArray(r.order)
|
|
? (r.order.filter((x) => ALL_WIDGETS.includes(x as WidgetId)) as WidgetId[])
|
|
: [];
|
|
for (const w of ALL_WIDGETS) if (!order.includes(w)) order.push(w);
|
|
return {
|
|
order,
|
|
collapsed: { ...(r.collapsed ?? {}) },
|
|
hidden: { ...(r.hidden ?? {}) },
|
|
};
|
|
}
|
|
|
|
export function loadLayout(): SidebarLayout {
|
|
try {
|
|
return normalizeLayout(JSON.parse(localStorage.getItem(KEY) || "{}"));
|
|
} catch {
|
|
return DEFAULT_LAYOUT;
|
|
}
|
|
}
|
|
|
|
export function saveLayoutLocal(l: SidebarLayout): void {
|
|
localStorage.setItem(KEY, JSON.stringify(l));
|
|
}
|