feat(core): add module scaffolding and provider registries

This commit is contained in:
kyasuda
2026-02-10 13:16:01 -08:00
committed by sudacode
parent 531f8027bd
commit 09e142279a
19 changed files with 822 additions and 0 deletions

21
src/core/action-bus.ts Normal file
View File

@@ -0,0 +1,21 @@
export type ActionWithType = { type: string };
export type ActionHandler<TAction extends ActionWithType> = (
action: TAction,
) => void | Promise<void>;
export class ActionBus<TAction extends ActionWithType> {
private handlers = new Map<string, ActionHandler<TAction>>();
register(type: TAction["type"], handler: ActionHandler<TAction>): void {
this.handlers.set(type, handler);
}
async dispatch(action: TAction): Promise<void> {
const handler = this.handlers.get(action.type);
if (!handler) {
throw new Error(`No handler registered for action: ${action.type}`);
}
await handler(action);
}
}

16
src/core/actions.ts Normal file
View File

@@ -0,0 +1,16 @@
export type AppAction =
| { type: "overlay.toggleVisible" }
| { type: "overlay.toggleInvisible" }
| { type: "overlay.setVisible"; visible: boolean }
| { type: "overlay.setInvisibleVisible"; visible: boolean }
| { type: "overlay.openSettings" }
| { type: "subtitle.copyCurrent" }
| { type: "subtitle.copyMultiplePrompt"; timeoutMs: number }
| { type: "anki.mineSentence" }
| { type: "anki.mineSentenceMultiplePrompt"; timeoutMs: number }
| { type: "anki.updateLastCardFromClipboard" }
| { type: "anki.markAudioCard" }
| { type: "kiku.triggerFieldGrouping" }
| { type: "subsync.triggerFromConfig" }
| { type: "secondarySub.toggleMode" }
| { type: "runtimeOptions.openPalette" };

45
src/core/app-context.ts Normal file
View File

@@ -0,0 +1,45 @@
import {
AnkiConnectConfig,
JimakuApiResponse,
JimakuDownloadQuery,
JimakuDownloadResult,
JimakuEntry,
JimakuFileEntry,
JimakuFilesQuery,
JimakuMediaInfo,
JimakuSearchQuery,
RuntimeOptionState,
SubsyncManualRunRequest,
SubsyncMode,
SubsyncResult,
} from "../types";
export interface RuntimeOptionsModuleContext {
getAnkiConfig: () => AnkiConnectConfig;
applyAnkiPatch: (patch: Partial<AnkiConnectConfig>) => void;
onOptionsChanged: (options: RuntimeOptionState[]) => void;
}
export interface AppContext {
runtimeOptions?: RuntimeOptionsModuleContext;
jimaku?: {
getMediaInfo: () => JimakuMediaInfo;
searchEntries: (
query: JimakuSearchQuery,
) => Promise<JimakuApiResponse<JimakuEntry[]>>;
listFiles: (
query: JimakuFilesQuery,
) => Promise<JimakuApiResponse<JimakuFileEntry[]>>;
downloadFile: (
query: JimakuDownloadQuery,
) => Promise<JimakuDownloadResult>;
};
subsync?: {
getDefaultMode: () => SubsyncMode;
openManualPicker: () => Promise<void>;
runAuto: () => Promise<SubsyncResult>;
runManual: (request: SubsyncManualRunRequest) => Promise<SubsyncResult>;
showOsd: (message: string) => void;
runWithSpinner: <T>(task: () => Promise<T>, label?: string) => Promise<T>;
};
}

View File

@@ -0,0 +1,36 @@
import { SubminerModule } from "./module";
export class ModuleRegistry<TContext = unknown> {
private readonly modules: SubminerModule<TContext>[] = [];
register(module: SubminerModule<TContext>): void {
if (this.modules.some((existing) => existing.id === module.id)) {
throw new Error(`Module already registered: ${module.id}`);
}
this.modules.push(module);
}
async initAll(context: TContext): Promise<void> {
for (const module of this.modules) {
if (module.init) {
await module.init(context);
}
}
}
async startAll(): Promise<void> {
for (const module of this.modules) {
if (module.start) {
await module.start();
}
}
}
async stopAll(): Promise<void> {
for (const module of [...this.modules].reverse()) {
if (module.stop) {
await module.stop();
}
}
}
}

6
src/core/module.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface SubminerModule<TContext = unknown> {
id: string;
init?: (context: TContext) => void | Promise<void>;
start?: () => void | Promise<void>;
stop?: () => void | Promise<void>;
}