tg-bot-ts/src/ui/poll/index.ts

105 lines
No EOL
3.5 KiB
TypeScript

import { EmbedBuilder } from "discord.js";
import { PollState, VoteEntry, Nation } from "@types";
import { Config } from "@systems/config";
import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types";
import path from "path";
import fs from "fs";
import { Runtime } from "@systems/runtime";
// ─── Runtime ──────────────────────────────────────────────────────────────────
Runtime.phase("restore", () => restoreLayout(), { name: "PollUI.restoreLayout" });
// ─── Layout registry ──────────────────────────────────────────────────────────
const _layouts = new Map<string, PollLayout>();
let _activeLayout: PollLayout;
export function registerLayout(layout: PollLayout): void {
_layouts.set(layout.name, layout);
if (!_activeLayout) _activeLayout = layout; // first registered = default
}
function isPollLayout(obj: any): obj is PollLayout {
return obj?.name &&
obj?.description &&
typeof obj?.buildEmbed === "function" &&
typeof obj?.formatRow === "function" &&
typeof obj?.buildContext === "function";
}
export function discoverLayouts(): void {
const layoutsDir = path.join(__dirname, "layouts");
if (!fs.existsSync(layoutsDir)) return;
const files = fs.readdirSync(layoutsDir)
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
.sort(); // consistent order — default.ts loads before side-by-side.ts
for (const file of files) {
try {
const mod = require(path.join(layoutsDir, file));
for (const exported of Object.values(mod)) {
if (isPollLayout(exported)) {
registerLayout(exported);
console.log(`[PollUI] Registered layout: ${(exported as PollLayout).name}`);
}
}
} catch (err) {
console.error(`[PollUI] Failed to load layout ${file}:`, err);
}
}
}
function restoreLayout() {
const savedLayout = Config.get({ section: "poll", key: "layout" });
if (savedLayout && _layouts.has(savedLayout)) {
_activeLayout = _layouts.get(savedLayout)!;
console.log(`[PollUI] Restored layout: ${savedLayout}`);
}
}
// Auto-discover at module load time
Config.load();
discoverLayouts();
restoreLayout();
// ─── Dispatcher ───────────────────────────────────────────────────────────────
function activeLayout(): PollLayout {
return _activeLayout;
}
export const PollUI = {
buildEmbed(state: PollState, options?: PollEmbedOptions): EmbedBuilder {
return activeLayout().buildEmbed(state, options);
},
formatRow(entry: VoteEntry, context: PollRowContext): string {
return activeLayout().formatRow(entry, context);
},
buildContext(
entries: VoteEntry[],
nation: Nation,
options?: { showNationEmoji?: boolean }
): PollRowContext {
return activeLayout().buildContext(entries, nation, options);
},
setLayout(name: string): boolean {
const layout = _layouts.get(name);
if (!layout) return false;
_activeLayout = layout;
return true;
},
layouts(): { name: string; description: string }[] {
return [..._layouts.values()].map((l) => ({
name: l.name,
description: l.description,
}));
},
register: registerLayout,
};