- A URL mirror names the view again on every entry Back or Forward lands on, so a change made under a dialog, a drawer or a sub-view is not undone by the next reload. - A view pushed from the entry a reload under a dialog started from no longer inherits its overlay mark; a reload under two overlays closes onto the page in one step. - The linked player reopens over a live search, gives itself up when its list moves first, and plays alone over an emptied playlist; a capped Files list keeps the link it cannot resolve. - The Feed press over a channel ends the search; leaving a search ends its asked mark; Files and Playlists searches end with their page; the feed always names its view and debounces its search. - Search-box keys are declared per module and kept out of the client log below TRACE. - Writers outside lib/ go through codecs; the URL contract scans computed keys, overlay writers and two-line reads, and holds every page to a bookmark round trip (Config, Activity, Playlists and the Files sort and viewer added).
118 lines
5.5 KiB
TypeScript
118 lines
5.5 KiB
TypeScript
import type { Me } from "../../lib/api";
|
|
import type { Page } from "../../lib/urlState";
|
|
import type { ModuleDef } from "../../lib/moduleDef";
|
|
import { homeModule } from "../../modules/home";
|
|
import { feedModule } from "../../modules/feed";
|
|
import { channelsModule } from "../../modules/channels";
|
|
import { playlistsModule } from "../../modules/playlists";
|
|
import { plexModule } from "../../modules/plex";
|
|
import { notificationsModule } from "../../modules/notifications";
|
|
import { filesModule } from "../../modules/files";
|
|
import { statsModule } from "../../modules/stats";
|
|
import { schedulerModule } from "../../modules/scheduler";
|
|
import { configModule } from "../../modules/config";
|
|
import { usersModule } from "../../modules/users";
|
|
import { activityModule } from "../../modules/activity";
|
|
import { settingsModule } from "../../modules/settings";
|
|
|
|
// ── The module registry ───────────────────────────────────────────────────────────────────────
|
|
// Every page, as each module defines itself (its folder's module.ts), in one ordered list: the nav
|
|
// rail, the palette, the `g` chords, the tab title, the reachable set and the shell's render all read
|
|
// from here, so a new or newly gated module shows up everywhere at once. Each definition's page and
|
|
// rail are lazy imports, so listing them here pulls no page into the boot chunk (chunkGuard holds it).
|
|
//
|
|
// Ordered in nav-rail order — this array IS the sequence the rail and the palette list. Home leads: it
|
|
// is the landing (see navigationInit.loadInitialPage). The feed takes the YouTube mark its own toolbar
|
|
// already uses, so it and Home sit apart by shape and not just by position.
|
|
const MODULES: readonly ModuleDef[] = [
|
|
homeModule,
|
|
feedModule,
|
|
channelsModule,
|
|
playlistsModule,
|
|
plexModule,
|
|
notificationsModule,
|
|
filesModule,
|
|
statsModule,
|
|
schedulerModule,
|
|
configModule,
|
|
usersModule,
|
|
activityModule,
|
|
];
|
|
|
|
// Settings is reachable (its own rail button, a `g` chord) but is not a primary module in the ordered
|
|
// nav list, so it lives beside the array rather than in it.
|
|
const ALL: readonly ModuleDef[] = [...MODULES, settingsModule];
|
|
|
|
// Every Page has a definition: the registry contract test pins this list against lib/urlState's ids.
|
|
export const REGISTERED_PAGES: readonly Page[] = ALL.map((m) => m.id);
|
|
|
|
const BY_ID = Object.fromEntries(ALL.map((m) => [m.id, m])) as Record<Page, ModuleDef>;
|
|
|
|
// Every module's address-bar keys, and those of every module but `page`: what a page switch clears and
|
|
// what a page opens without (NavigationProvider). Derived from the definitions, so a new module's keys
|
|
// are cleared the day it declares them.
|
|
const ALL_URL_KEYS: readonly string[] = ALL.flatMap((m) => m.urlKeys);
|
|
export function moduleUrlKeys(): readonly string[] {
|
|
return ALL_URL_KEYS;
|
|
}
|
|
/** Every module's address-bar keys that carry typed text — what the client log keeps out (main.tsx). */
|
|
export function typedUrlKeys(): readonly string[] {
|
|
return ALL.flatMap((m) => m.typedUrlKeys ?? []);
|
|
}
|
|
export function foreignUrlKeys(page: Page): readonly string[] {
|
|
return ALL.filter((m) => m.id !== page).flatMap((m) => m.urlKeys);
|
|
}
|
|
|
|
// Look up a module's definition by page id (every Page is covered, incl. settings).
|
|
export function moduleDef(id: Page): ModuleDef {
|
|
return BY_ID[id];
|
|
}
|
|
|
|
// The i18n key for each module's SHORT display name, and its rail label.
|
|
export const moduleLabelKey = Object.fromEntries(ALL.map((m) => [m.id, m.labelKey])) as Record<
|
|
Page,
|
|
string
|
|
>;
|
|
export const moduleRailKey = Object.fromEntries(ALL.map((m) => [m.id, m.railKey])) as Record<
|
|
Page,
|
|
string
|
|
>;
|
|
|
|
// Admin/system modules — rendered in their own section (below a divider) in the nav rail.
|
|
export const SYSTEM_PAGES: readonly Page[] = MODULES.filter((m) => m.system).map((m) => m.id);
|
|
|
|
// The ordered list of primary module pages available to THIS user, in nav-rail order. Availability
|
|
// is derived from the account via each module's `gate`, so the reachable set changes with the
|
|
// account state, not the UI.
|
|
export function moduleOrder(me: Me): Page[] {
|
|
return MODULES.filter((m) => !m.gate || m.gate(me)).map((m) => m.id);
|
|
}
|
|
|
|
/** The page this account is shown for `page`: itself when its gate lets the account in, else the
|
|
* feed (a link to an admin page, a Back onto one, an admin role taken away mid-session). Signed
|
|
* out, no page renders, so nothing is moved. */
|
|
export function reachablePage(page: Page, me: Me | null | undefined): Page {
|
|
const gate = moduleDef(page).gate;
|
|
return !me || !gate || gate(me) ? page : "feed";
|
|
}
|
|
|
|
// The i18n key for a page's human title (the browser tab): a module's optional `titleKey` is the
|
|
// longer, descriptive one (e.g. "Channel manager"), falling back to its short nav `labelKey`.
|
|
export function pageTitleKey(page: Page): string {
|
|
const def = moduleDef(page);
|
|
return def.titleKey ?? def.labelKey;
|
|
}
|
|
|
|
/** Fetch the code of these pages and of their side rails now, ahead of their first visit. Errors are
|
|
* swallowed: a chunk that fails to preload is simply fetched (and its failure surfaced) on visit. */
|
|
export function preloadPages(pages: Page[]): void {
|
|
for (const p of pages) {
|
|
const def = moduleDef(p);
|
|
def.Page.preload().catch(() => {});
|
|
def.Rail?.preload().catch(() => {});
|
|
}
|
|
}
|
|
|
|
// The modules' stores (lib/moduleDef `Store`), in registry order: ModuleStores mounts them at the root.
|
|
export const MODULE_STORES = ALL.flatMap((m) => (m.Store ? [m.Store] : []));
|