Retire the standalone 'Saved views' sidebar panel. The saved-views feature now
lives in three lighter surfaces, coordinated by a new SavedViewsProvider (query +
CRUD + the active/sticky view + modal state):
- the filter panel's header ICON opens a quick-pick popover (apply a view, active
one checked, plus 'Manage views…');
- the header TITLE ('Filters') opens a new Personal Views modal — a master/detail
manager: left = the reorderable list + a save-current-filters input, right = the
selected view's name, its captured filters (shown as the same chips the toolbar
row uses), default toggle, apply, and delete;
- a SelectedViewChip leads the applied-filter row, naming the active view with a
warn-coloured 'modified' dot once the live filters diverge from it; clicking it
reopens the picker.
SidePanel/FilterRail gain optional iconExpanded + onTitleClick slots (feed-only,
hidden for the demo account); the collapsed tab keeps its plain pin behaviour.
SavedViewsWidget is deleted and dropped from the panel layout.
Verified live: icon popover lists HH + Manage; applying HH surfaces the chip and
switches scope; removing a filter lights the dirty dot; the title opens the
master/detail modal with the captured-filters summary. 374 vitest, tsc, eslint
(0 err).
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
// Per-user customization of a side panel's group cards: their order, which are collapsed, and
|
|
// which are hidden — the same "customize" affordance the feed filter rail had, generalized so
|
|
// every panel (feed / plex / playlists) has its own saved layout. Persisted to localStorage and
|
|
// the server-side `preferences` blob (schemaless), so it follows the account across devices.
|
|
import { LS, readAccount, writeAccount } from "./storage";
|
|
|
|
export type PanelId = "feed" | "plex" | "playlists";
|
|
|
|
export interface PanelLayout {
|
|
order: string[];
|
|
collapsed: Record<string, boolean>;
|
|
hidden: Record<string, boolean>;
|
|
}
|
|
|
|
// The default group order per panel. Group ids must match the ids each panel feeds to PanelGroups.
|
|
// Unknown/stale ids are dropped and any missing (e.g. a group added in a later version) are
|
|
// appended by normalizeLayout, so nothing silently disappears.
|
|
const PANEL_ORDER: Record<PanelId, string[]> = {
|
|
feed: ["date", "channel", "tags", "language", "topic"],
|
|
plex: [
|
|
"playlists",
|
|
"watch",
|
|
"people",
|
|
"collection",
|
|
"rating",
|
|
"genre",
|
|
"year",
|
|
"duration",
|
|
"added",
|
|
"contentrating",
|
|
],
|
|
playlists: ["manage", "list"],
|
|
};
|
|
|
|
// localStorage key per panel (feed keeps its original key for backward compatibility).
|
|
const LS_KEY: Record<PanelId, string> = {
|
|
feed: LS.sidebarLayout,
|
|
plex: LS.plexLayout,
|
|
playlists: LS.playlistsLayout,
|
|
};
|
|
|
|
// Key on the server `preferences` blob per panel (feed keeps its original key).
|
|
export const PREF_KEY: Record<PanelId, "sidebarLayout" | "plexLayout" | "playlistsLayout"> = {
|
|
feed: "sidebarLayout",
|
|
plex: "plexLayout",
|
|
playlists: "playlistsLayout",
|
|
};
|
|
|
|
export function defaultLayout(panel: PanelId): PanelLayout {
|
|
return { order: [...PANEL_ORDER[panel]], collapsed: {}, hidden: {} };
|
|
}
|
|
|
|
export function normalizeLayout(panel: PanelId, raw: unknown): PanelLayout {
|
|
const all = PANEL_ORDER[panel];
|
|
const r = (raw ?? {}) as Partial<PanelLayout>;
|
|
const order = Array.isArray(r.order) ? r.order.filter((x) => all.includes(x)) : [];
|
|
for (const id of all) if (!order.includes(id)) order.push(id);
|
|
return {
|
|
order,
|
|
collapsed: { ...(r.collapsed ?? {}) },
|
|
hidden: { ...(r.hidden ?? {}) },
|
|
};
|
|
}
|
|
|
|
export function loadLayout(panel: PanelId): PanelLayout {
|
|
return normalizeLayout(panel, readAccount<unknown>(LS_KEY[panel], {}));
|
|
}
|
|
|
|
export function saveLayoutLocal(panel: PanelId, l: PanelLayout): void {
|
|
writeAccount(LS_KEY[panel], l);
|
|
}
|