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; // 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] : []));