refactor runtime deps wiring and docs/config updates

This commit is contained in:
2026-02-10 02:44:35 -08:00
parent 1c69452356
commit 579661fbef
35 changed files with 372 additions and 1042 deletions

View File

@@ -1,48 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
AnkiJimakuIpcDepsRuntimeOptions,
createAnkiJimakuIpcDepsRuntimeService,
} from "./anki-jimaku-ipc-deps-runtime-service";
test("createAnkiJimakuIpcDepsRuntimeService returns passthrough runtime options", async () => {
const calls: string[] = [];
const options = {
patchAnkiConnectEnabled: () => calls.push("patch"),
getResolvedConfig: () => ({ ankiConnect: undefined }),
getRuntimeOptionsManager: () => null,
getSubtitleTimingTracker: () => null,
getMpvClient: () => null,
getAnkiIntegration: () => null,
setAnkiIntegration: () => calls.push("set-integration"),
showDesktopNotification: () => calls.push("notify"),
createFieldGroupingCallback: () => async () => ({
keepNoteId: 0,
deleteNoteId: 0,
deleteDuplicate: false,
cancelled: true,
}),
broadcastRuntimeOptionsChanged: () => calls.push("broadcast"),
getFieldGroupingResolver: () => null,
setFieldGroupingResolver: () => calls.push("set-resolver"),
parseMediaInfo: () => ({ mediaPath: null, baseName: null, episode: null }),
getCurrentMediaPath: () => "/tmp/a.mp4",
jimakuFetchJson: async () => ({ ok: true, data: [] }),
getJimakuMaxEntryResults: () => 100,
getJimakuLanguagePreference: () => "prefer-japanese",
resolveJimakuApiKey: async () => "abc",
isRemoteMediaPath: () => false,
downloadToFile: async () => ({ ok: true, path: "/tmp/a.srt" }),
} as unknown as AnkiJimakuIpcDepsRuntimeOptions;
const runtime = createAnkiJimakuIpcDepsRuntimeService(options);
runtime.patchAnkiConnectEnabled(true);
runtime.broadcastRuntimeOptionsChanged();
runtime.setFieldGroupingResolver(null);
assert.deepEqual(calls, ["patch", "broadcast", "set-resolver"]);
assert.equal(runtime.getCurrentMediaPath(), "/tmp/a.mp4");
assert.equal(runtime.getJimakuMaxEntryResults(), 100);
assert.equal(await runtime.resolveJimakuApiKey(), "abc");
});

View File

@@ -1,32 +0,0 @@
import {
AnkiJimakuIpcRuntimeOptions,
} from "./anki-jimaku-runtime-service";
export type AnkiJimakuIpcDepsRuntimeOptions = AnkiJimakuIpcRuntimeOptions;
export function createAnkiJimakuIpcDepsRuntimeService(
options: AnkiJimakuIpcDepsRuntimeOptions,
): AnkiJimakuIpcRuntimeOptions {
return {
patchAnkiConnectEnabled: options.patchAnkiConnectEnabled,
getResolvedConfig: options.getResolvedConfig,
getRuntimeOptionsManager: options.getRuntimeOptionsManager,
getSubtitleTimingTracker: options.getSubtitleTimingTracker,
getMpvClient: options.getMpvClient,
getAnkiIntegration: options.getAnkiIntegration,
setAnkiIntegration: options.setAnkiIntegration,
showDesktopNotification: options.showDesktopNotification,
createFieldGroupingCallback: options.createFieldGroupingCallback,
broadcastRuntimeOptionsChanged: options.broadcastRuntimeOptionsChanged,
getFieldGroupingResolver: options.getFieldGroupingResolver,
setFieldGroupingResolver: options.setFieldGroupingResolver,
parseMediaInfo: options.parseMediaInfo,
getCurrentMediaPath: options.getCurrentMediaPath,
jimakuFetchJson: options.jimakuFetchJson,
getJimakuMaxEntryResults: options.getJimakuMaxEntryResults,
getJimakuLanguagePreference: options.getJimakuLanguagePreference,
resolveJimakuApiKey: options.resolveJimakuApiKey,
isRemoteMediaPath: options.isRemoteMediaPath,
downloadToFile: options.downloadToFile,
};
}

View File

@@ -73,15 +73,12 @@ export {
getOverlayWindowsRuntimeService,
setOverlayDebugVisualizationEnabledRuntimeService,
} from "./overlay-broadcast-runtime-service";
export { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
export { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service";
export { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service";
export { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service";
export { createAnkiJimakuIpcDepsRuntimeService } from "./anki-jimaku-ipc-deps-runtime-service";
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service";
export { createSubsyncRuntimeDepsService } from "./subsync-deps-runtime-service";
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-runtime-service";
export { createOverlayVisibilityFacadeDepsRuntimeService } from "./overlay-visibility-facade-deps-runtime-service";
export { createMpvCommandIpcDepsRuntimeService } from "./mpv-command-ipc-deps-runtime-service";
export { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service";
export { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
@@ -90,26 +87,8 @@ export {
createInvisibleOverlayVisibilityDepsRuntimeService,
createOverlayWindowRuntimeDepsService,
createVisibleOverlayVisibilityDepsRuntimeService,
} from "./overlay-runtime-deps-service";
export {
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
} from "./overlay-shortcut-runtime-deps-service";
export {
createCopyCurrentSubtitleDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMineSentenceCardDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
} from "./mining-runtime-deps-service";
export {
createGlobalShortcutRegistrationDepsRuntimeService,
createSecondarySubtitleCycleDepsRuntimeService,
createYomitanSettingsWindowDepsRuntimeService,
runOverlayShortcutLocalFallbackRuntimeService,
} from "./shortcut-ui-runtime-deps-service";
} from "./overlay-deps-runtime-service";
export { runOverlayShortcutLocalFallbackRuntimeService } from "./shortcut-ui-deps-runtime-service";
export { createStartupLifecycleHooksRuntimeService } from "./startup-lifecycle-hooks-runtime-service";
export { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-runtime-service";
export { createAppLoggingRuntimeService } from "./app-logging-runtime-service";
@@ -122,3 +101,4 @@ export { runStartupBootstrapRuntimeService } from "./startup-bootstrap-runtime-s
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runtime-service";
export { updateInvisibleOverlayVisibilityService, updateVisibleOverlayVisibilityService } from "./overlay-visibility-service";
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-runtime-service";
export { createOverlayManagerService } from "./overlay-manager-service";

View File

@@ -1,65 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createCopyCurrentSubtitleDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMineSentenceCardDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
} from "./mining-runtime-deps-service";
test("mining runtime deps builders preserve references", () => {
const showMpvOsd = (_text: string) => {};
const writeClipboardText = (_text: string) => {};
const readClipboardText = () => "x";
const logError = (_message: string, _err: unknown) => {};
const subtitleTimingTracker = null;
const ankiIntegration = null;
const mpvClient = null;
const multiCopy = createHandleMultiCopyDigitDepsRuntimeService({
subtitleTimingTracker,
writeClipboardText,
showMpvOsd,
});
const copyCurrent = createCopyCurrentSubtitleDepsRuntimeService({
subtitleTimingTracker,
writeClipboardText,
showMpvOsd,
});
const updateLast = createUpdateLastCardFromClipboardDepsRuntimeService({
ankiIntegration,
readClipboardText,
showMpvOsd,
});
const fieldGrouping = createTriggerFieldGroupingDepsRuntimeService({
ankiIntegration,
showMpvOsd,
});
const markAudio = createMarkLastCardAsAudioCardDepsRuntimeService({
ankiIntegration,
showMpvOsd,
});
const mineCard = createMineSentenceCardDepsRuntimeService({
ankiIntegration,
mpvClient,
showMpvOsd,
});
const mineDigit = createHandleMineSentenceDigitDepsRuntimeService({
subtitleTimingTracker,
ankiIntegration,
getCurrentSecondarySubText: () => undefined,
showMpvOsd,
logError,
});
assert.equal(multiCopy.writeClipboardText, writeClipboardText);
assert.equal(copyCurrent.showMpvOsd, showMpvOsd);
assert.equal(updateLast.readClipboardText, readClipboardText);
assert.equal(fieldGrouping.ankiIntegration, ankiIntegration);
assert.equal(markAudio.showMpvOsd, showMpvOsd);
assert.equal(mineCard.mpvClient, mpvClient);
assert.equal(mineDigit.logError, logError);
});

View File

@@ -1,107 +0,0 @@
import {
copyCurrentSubtitleService,
handleMineSentenceDigitService,
handleMultiCopyDigitService,
markLastCardAsAudioCardService,
mineSentenceCardService,
triggerFieldGroupingService,
updateLastCardFromClipboardService,
} from "./mining-runtime-service";
export function createHandleMultiCopyDigitDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof handleMultiCopyDigitService>[1]["subtitleTimingTracker"];
writeClipboardText: Parameters<typeof handleMultiCopyDigitService>[1]["writeClipboardText"];
showMpvOsd: Parameters<typeof handleMultiCopyDigitService>[1]["showMpvOsd"];
},
): Parameters<typeof handleMultiCopyDigitService>[1] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
writeClipboardText: options.writeClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createCopyCurrentSubtitleDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof copyCurrentSubtitleService>[0]["subtitleTimingTracker"];
writeClipboardText: Parameters<typeof copyCurrentSubtitleService>[0]["writeClipboardText"];
showMpvOsd: Parameters<typeof copyCurrentSubtitleService>[0]["showMpvOsd"];
},
): Parameters<typeof copyCurrentSubtitleService>[0] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
writeClipboardText: options.writeClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createUpdateLastCardFromClipboardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof updateLastCardFromClipboardService>[0]["ankiIntegration"];
readClipboardText: Parameters<typeof updateLastCardFromClipboardService>[0]["readClipboardText"];
showMpvOsd: Parameters<typeof updateLastCardFromClipboardService>[0]["showMpvOsd"];
},
): Parameters<typeof updateLastCardFromClipboardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
readClipboardText: options.readClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createTriggerFieldGroupingDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof triggerFieldGroupingService>[0]["ankiIntegration"];
showMpvOsd: Parameters<typeof triggerFieldGroupingService>[0]["showMpvOsd"];
},
): Parameters<typeof triggerFieldGroupingService>[0] {
return {
ankiIntegration: options.ankiIntegration,
showMpvOsd: options.showMpvOsd,
};
}
export function createMarkLastCardAsAudioCardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof markLastCardAsAudioCardService>[0]["ankiIntegration"];
showMpvOsd: Parameters<typeof markLastCardAsAudioCardService>[0]["showMpvOsd"];
},
): Parameters<typeof markLastCardAsAudioCardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
showMpvOsd: options.showMpvOsd,
};
}
export function createMineSentenceCardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof mineSentenceCardService>[0]["ankiIntegration"];
mpvClient: Parameters<typeof mineSentenceCardService>[0]["mpvClient"];
showMpvOsd: Parameters<typeof mineSentenceCardService>[0]["showMpvOsd"];
},
): Parameters<typeof mineSentenceCardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
mpvClient: options.mpvClient,
showMpvOsd: options.showMpvOsd,
};
}
export function createHandleMineSentenceDigitDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof handleMineSentenceDigitService>[1]["subtitleTimingTracker"];
ankiIntegration: Parameters<typeof handleMineSentenceDigitService>[1]["ankiIntegration"];
getCurrentSecondarySubText: Parameters<typeof handleMineSentenceDigitService>[1]["getCurrentSecondarySubText"];
showMpvOsd: Parameters<typeof handleMineSentenceDigitService>[1]["showMpvOsd"];
logError: Parameters<typeof handleMineSentenceDigitService>[1]["logError"];
},
): Parameters<typeof handleMineSentenceDigitService>[1] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
ankiIntegration: options.ankiIntegration,
getCurrentSecondarySubText: options.getCurrentSecondarySubText,
showMpvOsd: options.showMpvOsd,
logError: options.logError,
};
}

View File

@@ -1,51 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
test("createMpvIpcClientDepsRuntimeService returns passthrough dep object", async () => {
const marker = {
getResolvedConfig: () => ({ auto_start_overlay: false } as never),
autoStartOverlay: true,
setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null,
setReconnectTimer: () => {},
getCurrentSubText: () => "x",
setCurrentSubText: () => {},
setCurrentSubAssText: () => {},
getSubtitleTimingTracker: () => null,
subtitleWsBroadcast: () => {},
getOverlayWindowsCount: () => 0,
tokenizeSubtitle: async () => ({ text: "x", tokens: [], mergedTokens: [] }),
broadcastToOverlayWindows: () => {},
updateCurrentMediaPath: () => {},
updateMpvSubtitleRenderMetrics: () => {},
getMpvSubtitleRenderMetrics: () => ({
subPos: 100,
subFontSize: 40,
subScale: 1,
subMarginY: 0,
subMarginX: 0,
subFont: "sans",
subSpacing: 0,
subBold: false,
subItalic: false,
subBorderSize: 0,
subShadowOffset: 0,
subAssOverride: "yes",
subScaleByWindow: true,
subUseMargins: true,
osdHeight: 720,
osdDimensions: null,
}),
setPreviousSecondarySubVisibility: () => {},
showMpvOsd: () => {},
};
const deps = createMpvIpcClientDepsRuntimeService(marker);
assert.equal(deps.autoStartOverlay, true);
assert.equal(deps.getCurrentSubText(), "x");
assert.equal(deps.getOverlayWindowsCount(), 0);
assert.equal(deps.shouldBindVisibleOverlayToMpvSubVisibility(), true);
});

View File

@@ -1,61 +0,0 @@
import {
MpvIpcClientDeps,
} from "./mpv-service";
import { Config, MpvSubtitleRenderMetrics, SubtitleData } from "../../types";
interface SubtitleTimingTrackerLike {
recordSubtitle: (text: string, start: number, end: number) => void;
}
export interface MpvClientDepsRuntimeOptions {
getResolvedConfig: () => Config;
autoStartOverlay: boolean;
setOverlayVisible: (visible: boolean) => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
getCurrentSubText: () => string;
setCurrentSubText: (text: string) => void;
setCurrentSubAssText: (text: string) => void;
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null;
subtitleWsBroadcast: (text: string) => void;
getOverlayWindowsCount: () => number;
tokenizeSubtitle: (text: string) => Promise<SubtitleData>;
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
updateCurrentMediaPath: (mediaPath: unknown) => void;
updateMpvSubtitleRenderMetrics: (
patch: Partial<MpvSubtitleRenderMetrics>,
) => void;
getMpvSubtitleRenderMetrics: () => MpvSubtitleRenderMetrics;
setPreviousSecondarySubVisibility: (value: boolean | null) => void;
showMpvOsd: (text: string) => void;
}
export function createMpvIpcClientDepsRuntimeService(
options: MpvClientDepsRuntimeOptions,
): MpvIpcClientDeps {
return {
getResolvedConfig: options.getResolvedConfig,
autoStartOverlay: options.autoStartOverlay,
setOverlayVisible: options.setOverlayVisible,
shouldBindVisibleOverlayToMpvSubVisibility:
options.shouldBindVisibleOverlayToMpvSubVisibility,
isVisibleOverlayVisible: options.isVisibleOverlayVisible,
getReconnectTimer: options.getReconnectTimer,
setReconnectTimer: options.setReconnectTimer,
getCurrentSubText: options.getCurrentSubText,
setCurrentSubText: options.setCurrentSubText,
setCurrentSubAssText: options.setCurrentSubAssText,
getSubtitleTimingTracker: options.getSubtitleTimingTracker,
subtitleWsBroadcast: options.subtitleWsBroadcast,
getOverlayWindowsCount: options.getOverlayWindowsCount,
tokenizeSubtitle: options.tokenizeSubtitle,
broadcastToOverlayWindows: options.broadcastToOverlayWindows,
updateCurrentMediaPath: options.updateCurrentMediaPath,
updateMpvSubtitleRenderMetrics: options.updateMpvSubtitleRenderMetrics,
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
setPreviousSecondarySubVisibility: options.setPreviousSecondarySubVisibility,
showMpvOsd: options.showMpvOsd,
};
}

View File

@@ -5,7 +5,7 @@ import {
createInvisibleOverlayVisibilityDepsRuntimeService,
createOverlayWindowRuntimeDepsService,
createVisibleOverlayVisibilityDepsRuntimeService,
} from "./overlay-runtime-deps-service";
} from "./overlay-deps-runtime-service";
test("createOverlayWindowRuntimeDepsService maps runtime state providers", () => {
let visible = true;

View File

@@ -0,0 +1,42 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createOverlayManagerService } from "./overlay-manager-service";
test("overlay manager initializes with empty windows and hidden overlays", () => {
const manager = createOverlayManagerService();
assert.equal(manager.getMainWindow(), null);
assert.equal(manager.getInvisibleWindow(), null);
assert.equal(manager.getVisibleOverlayVisible(), false);
assert.equal(manager.getInvisibleOverlayVisible(), false);
assert.deepEqual(manager.getOverlayWindows(), []);
});
test("overlay manager stores window references and returns stable window order", () => {
const manager = createOverlayManagerService();
const visibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
const invisibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
manager.setMainWindow(visibleWindow);
manager.setInvisibleWindow(invisibleWindow);
assert.equal(manager.getMainWindow(), visibleWindow);
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow]);
});
test("overlay manager excludes destroyed windows", () => {
const manager = createOverlayManagerService();
manager.setMainWindow({ isDestroyed: () => true } as unknown as Electron.BrowserWindow);
manager.setInvisibleWindow({ isDestroyed: () => false } as unknown as Electron.BrowserWindow);
assert.equal(manager.getOverlayWindows().length, 1);
});
test("overlay manager stores visibility state", () => {
const manager = createOverlayManagerService();
manager.setVisibleOverlayVisible(true);
manager.setInvisibleOverlayVisible(true);
assert.equal(manager.getVisibleOverlayVisible(), true);
assert.equal(manager.getInvisibleOverlayVisible(), true);
});

View File

@@ -0,0 +1,49 @@
import { BrowserWindow } from "electron";
export interface OverlayManagerService {
getMainWindow: () => BrowserWindow | null;
setMainWindow: (window: BrowserWindow | null) => void;
getInvisibleWindow: () => BrowserWindow | null;
setInvisibleWindow: (window: BrowserWindow | null) => void;
getVisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
getInvisibleOverlayVisible: () => boolean;
setInvisibleOverlayVisible: (visible: boolean) => void;
getOverlayWindows: () => BrowserWindow[];
}
export function createOverlayManagerService(): OverlayManagerService {
let mainWindow: BrowserWindow | null = null;
let invisibleWindow: BrowserWindow | null = null;
let visibleOverlayVisible = false;
let invisibleOverlayVisible = false;
return {
getMainWindow: () => mainWindow,
setMainWindow: (window) => {
mainWindow = window;
},
getInvisibleWindow: () => invisibleWindow,
setInvisibleWindow: (window) => {
invisibleWindow = window;
},
getVisibleOverlayVisible: () => visibleOverlayVisible,
setVisibleOverlayVisible: (visible) => {
visibleOverlayVisible = visible;
},
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
setInvisibleOverlayVisible: (visible) => {
invisibleOverlayVisible = visible;
},
getOverlayWindows: () => {
const windows: BrowserWindow[] = [];
if (mainWindow && !mainWindow.isDestroyed()) {
windows.push(mainWindow);
}
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
windows.push(invisibleWindow);
}
return windows;
},
};
}

View File

@@ -1,52 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
} from "./overlay-shortcut-runtime-deps-service";
test("createOverlayShortcutRuntimeDepsService returns callable runtime deps", async () => {
const calls: string[] = [];
const deps = createOverlayShortcutRuntimeDepsService({
showMpvOsd: () => calls.push("showMpvOsd"),
openRuntimeOptions: () => calls.push("openRuntimeOptions"),
openJimaku: () => calls.push("openJimaku"),
markAudioCard: async () => {
calls.push("markAudioCard");
},
copySubtitleMultiple: () => calls.push("copySubtitleMultiple"),
copySubtitle: () => calls.push("copySubtitle"),
toggleSecondarySub: () => calls.push("toggleSecondarySub"),
updateLastCardFromClipboard: async () => {
calls.push("updateLastCardFromClipboard");
},
triggerFieldGrouping: async () => {
calls.push("triggerFieldGrouping");
},
triggerSubsync: async () => {
calls.push("triggerSubsync");
},
mineSentence: async () => {
calls.push("mineSentence");
},
mineSentenceMultiple: () => calls.push("mineSentenceMultiple"),
});
deps.copySubtitle();
await deps.mineSentence();
deps.mineSentenceMultiple(2);
assert.deepEqual(calls, ["copySubtitle", "mineSentence", "mineSentenceMultiple"]);
});
test("createOverlayShortcutLifecycleDepsRuntimeService returns lifecycle passthrough", () => {
const deps = createOverlayShortcutLifecycleDepsRuntimeService({
getConfiguredShortcuts: () => ({ actions: [] } as never),
getOverlayHandlers: () => ({} as never),
cancelPendingMultiCopy: () => {},
cancelPendingMineSentenceMultiple: () => {},
});
assert.ok(deps.getConfiguredShortcuts());
assert.ok(deps.getOverlayHandlers());
});

View File

@@ -1,59 +0,0 @@
import {
OverlayShortcutLifecycleDeps,
} from "./overlay-shortcut-lifecycle-service";
import {
OverlayShortcutRuntimeDeps,
} from "./overlay-shortcut-runtime-service";
export interface OverlayShortcutRuntimeDepsOptions {
showMpvOsd: (text: string) => void;
openRuntimeOptions: () => void;
openJimaku: () => void;
markAudioCard: () => Promise<void>;
copySubtitleMultiple: (timeoutMs: number) => void;
copySubtitle: () => void;
toggleSecondarySub: () => void;
updateLastCardFromClipboard: () => Promise<void>;
triggerFieldGrouping: () => Promise<void>;
triggerSubsync: () => Promise<void>;
mineSentence: () => Promise<void>;
mineSentenceMultiple: (timeoutMs: number) => void;
}
export interface OverlayShortcutLifecycleDepsOptions {
getConfiguredShortcuts: OverlayShortcutLifecycleDeps["getConfiguredShortcuts"];
getOverlayHandlers: OverlayShortcutLifecycleDeps["getOverlayHandlers"];
cancelPendingMultiCopy: () => void;
cancelPendingMineSentenceMultiple: () => void;
}
export function createOverlayShortcutRuntimeDepsService(
options: OverlayShortcutRuntimeDepsOptions,
): OverlayShortcutRuntimeDeps {
return {
showMpvOsd: options.showMpvOsd,
openRuntimeOptions: options.openRuntimeOptions,
openJimaku: options.openJimaku,
markAudioCard: options.markAudioCard,
copySubtitleMultiple: options.copySubtitleMultiple,
copySubtitle: options.copySubtitle,
toggleSecondarySub: options.toggleSecondarySub,
updateLastCardFromClipboard: options.updateLastCardFromClipboard,
triggerFieldGrouping: options.triggerFieldGrouping,
triggerSubsync: options.triggerSubsync,
mineSentence: options.mineSentence,
mineSentenceMultiple: options.mineSentenceMultiple,
};
}
export function createOverlayShortcutLifecycleDepsRuntimeService(
options: OverlayShortcutLifecycleDepsOptions,
): OverlayShortcutLifecycleDeps {
return {
getConfiguredShortcuts: options.getConfiguredShortcuts,
getOverlayHandlers: options.getOverlayHandlers,
cancelPendingMultiCopy: options.cancelPendingMultiCopy,
cancelPendingMineSentenceMultiple:
options.cancelPendingMineSentenceMultiple,
};
}

View File

@@ -1,46 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createOverlayVisibilityFacadeDepsRuntimeService } from "./overlay-visibility-facade-deps-runtime-service";
test("createOverlayVisibilityFacadeDepsRuntimeService returns working deps object", () => {
let visible = false;
let invisible = true;
let mpvSubVisible: boolean | null = null;
let syncCalls = 0;
const deps = createOverlayVisibilityFacadeDepsRuntimeService({
getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => invisible,
setVisibleOverlayVisibleState: (nextVisible) => {
visible = nextVisible;
},
setInvisibleOverlayVisibleState: (nextVisible) => {
invisible = nextVisible;
},
updateVisibleOverlayVisibility: () => {},
updateInvisibleOverlayVisibility: () => {},
syncInvisibleOverlayMousePassthrough: () => {
syncCalls += 1;
},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isMpvConnected: () => true,
setMpvSubVisibility: (nextVisible) => {
mpvSubVisible = nextVisible;
},
});
assert.equal(deps.getVisibleOverlayVisible(), false);
assert.equal(deps.getInvisibleOverlayVisible(), true);
deps.setVisibleOverlayVisibleState(true);
deps.setInvisibleOverlayVisibleState(false);
deps.syncInvisibleOverlayMousePassthrough();
deps.setMpvSubVisibility(false);
assert.equal(visible, true);
assert.equal(invisible, false);
assert.equal(syncCalls, 1);
assert.equal(mpvSubVisible, false);
assert.equal(deps.shouldBindVisibleOverlayToMpvSubVisibility(), true);
assert.equal(deps.isMpvConnected(), true);
});

View File

@@ -1,35 +0,0 @@
import {
OverlayVisibilityFacadeDeps,
} from "./overlay-visibility-facade-service";
export interface OverlayVisibilityFacadeDepsRuntimeOptions {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisibleState: (nextVisible: boolean) => void;
setInvisibleOverlayVisibleState: (nextVisible: boolean) => void;
updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isMpvConnected: () => boolean;
setMpvSubVisibility: (mpvSubVisible: boolean) => void;
}
export function createOverlayVisibilityFacadeDepsRuntimeService(
options: OverlayVisibilityFacadeDepsRuntimeOptions,
): OverlayVisibilityFacadeDeps {
return {
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisibleState: options.setVisibleOverlayVisibleState,
setInvisibleOverlayVisibleState: options.setInvisibleOverlayVisibleState,
updateVisibleOverlayVisibility: options.updateVisibleOverlayVisibility,
updateInvisibleOverlayVisibility: options.updateInvisibleOverlayVisibility,
syncInvisibleOverlayMousePassthrough:
options.syncInvisibleOverlayMousePassthrough,
shouldBindVisibleOverlayToMpvSubVisibility:
options.shouldBindVisibleOverlayToMpvSubVisibility,
isMpvConnected: options.isMpvConnected,
setMpvSubVisibility: options.setMpvSubVisibility,
};
}

View File

@@ -1,35 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createGlobalShortcutRegistrationDepsRuntimeService,
createSecondarySubtitleCycleDepsRuntimeService,
createYomitanSettingsWindowDepsRuntimeService,
runOverlayShortcutLocalFallbackRuntimeService,
} from "./shortcut-ui-runtime-deps-service";
} from "./shortcut-ui-deps-runtime-service";
function makeOptions() {
return {
yomitanExt: null,
getYomitanSettingsWindow: () => null,
setYomitanSettingsWindow: () => {},
shortcuts: {
toggleVisibleOverlayGlobal: "Ctrl+Shift+O",
toggleInvisibleOverlayGlobal: "Ctrl+Alt+O",
},
onToggleVisibleOverlay: () => {},
onToggleInvisibleOverlay: () => {},
onOpenYomitanSettings: () => {},
isDev: false,
getMainWindow: () => null,
getSecondarySubMode: () => "hover" as const,
setSecondarySubMode: () => {},
getLastSecondarySubToggleAtMs: () => 0,
setLastSecondarySubToggleAtMs: () => {},
broadcastSecondarySubMode: () => {},
showMpvOsd: () => {},
getConfiguredShortcuts: () => ({
toggleVisibleOverlayGlobal: null,
toggleInvisibleOverlayGlobal: null,
@@ -63,17 +39,6 @@ function makeOptions() {
};
}
test("shortcut ui deps builders return expected adapters", () => {
const options = makeOptions();
const yomitan = createYomitanSettingsWindowDepsRuntimeService(options);
const globalShortcuts = createGlobalShortcutRegistrationDepsRuntimeService(options);
const secondary = createSecondarySubtitleCycleDepsRuntimeService(options);
assert.equal(yomitan.yomitanExt, null);
assert.equal(typeof globalShortcuts.onOpenYomitanSettings, "function");
assert.equal(secondary.getSecondarySubMode(), "hover");
});
test("runOverlayShortcutLocalFallbackRuntimeService delegates and returns boolean", () => {
const options = {
...makeOptions(),

View File

@@ -0,0 +1,24 @@
import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
export interface ShortcutUiRuntimeDepsOptions {
getConfiguredShortcuts: () => ConfiguredShortcuts;
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
shortcutMatcher: (
input: Electron.Input,
accelerator: string,
allowWhenRegistered?: boolean,
) => boolean;
}
export function runOverlayShortcutLocalFallbackRuntimeService(
input: Electron.Input,
options: ShortcutUiRuntimeDepsOptions,
): boolean {
return runOverlayShortcutLocalFallback(
input,
options.getConfiguredShortcuts(),
options.shortcutMatcher,
options.getOverlayShortcutFallbackHandlers(),
);
}

View File

@@ -1,83 +0,0 @@
import { Extension } from "electron";
import { SecondarySubMode } from "../../types";
import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { CycleSecondarySubModeDeps } from "./secondary-subtitle-service";
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
import { OpenYomitanSettingsWindowOptions } from "./yomitan-settings-service";
import { RegisterGlobalShortcutsServiceOptions } from "./shortcut-service";
export interface ShortcutUiRuntimeDepsOptions {
yomitanExt: Extension | null;
getYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["getExistingWindow"];
setYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["setWindow"];
shortcuts: RegisterGlobalShortcutsServiceOptions["shortcuts"];
onToggleVisibleOverlay: () => void;
onToggleInvisibleOverlay: () => void;
onOpenYomitanSettings: () => void;
isDev: boolean;
getMainWindow: RegisterGlobalShortcutsServiceOptions["getMainWindow"];
getSecondarySubMode: () => SecondarySubMode;
setSecondarySubMode: (mode: SecondarySubMode) => void;
getLastSecondarySubToggleAtMs: () => number;
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
showMpvOsd: (text: string) => void;
getConfiguredShortcuts: () => ConfiguredShortcuts;
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
shortcutMatcher: (
input: Electron.Input,
accelerator: string,
allowWhenRegistered?: boolean,
) => boolean;
}
export function createYomitanSettingsWindowDepsRuntimeService(
options: ShortcutUiRuntimeDepsOptions,
): OpenYomitanSettingsWindowOptions {
return {
yomitanExt: options.yomitanExt,
getExistingWindow: options.getYomitanSettingsWindow,
setWindow: options.setYomitanSettingsWindow,
};
}
export function createGlobalShortcutRegistrationDepsRuntimeService(
options: ShortcutUiRuntimeDepsOptions,
): RegisterGlobalShortcutsServiceOptions {
return {
shortcuts: options.shortcuts,
onToggleVisibleOverlay: options.onToggleVisibleOverlay,
onToggleInvisibleOverlay: options.onToggleInvisibleOverlay,
onOpenYomitanSettings: options.onOpenYomitanSettings,
isDev: options.isDev,
getMainWindow: options.getMainWindow,
};
}
export function createSecondarySubtitleCycleDepsRuntimeService(
options: ShortcutUiRuntimeDepsOptions,
): CycleSecondarySubModeDeps {
return {
getSecondarySubMode: options.getSecondarySubMode,
setSecondarySubMode: options.setSecondarySubMode,
getLastSecondarySubToggleAtMs: options.getLastSecondarySubToggleAtMs,
setLastSecondarySubToggleAtMs: options.setLastSecondarySubToggleAtMs,
broadcastSecondarySubMode: options.broadcastSecondarySubMode,
showMpvOsd: options.showMpvOsd,
};
}
export function runOverlayShortcutLocalFallbackRuntimeService(
input: Electron.Input,
options: ShortcutUiRuntimeDepsOptions,
): boolean {
return runOverlayShortcutLocalFallback(
input,
options.getConfiguredShortcuts(),
options.shortcutMatcher,
options.getOverlayShortcutFallbackHandlers(),
);
}

View File

@@ -7,10 +7,6 @@ import {
AppShutdownRuntimeDeps,
runAppShutdownRuntimeService,
} from "./app-shutdown-runtime-service";
import {
createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService,
} from "./startup-lifecycle-runtime-deps-service";
type StartupLifecycleHookDeps = Pick<
AppLifecycleDepsRuntimeOptions,
@@ -29,14 +25,10 @@ export function createStartupLifecycleHooksRuntimeService(
): StartupLifecycleHookDeps {
return {
onReady: async () => {
await runAppReadyRuntimeService(
createStartupAppReadyDepsRuntimeService(options.appReadyDeps),
);
await runAppReadyRuntimeService(options.appReadyDeps);
},
onWillQuitCleanup: () => {
runAppShutdownRuntimeService(
createStartupAppShutdownDepsRuntimeService(options.appShutdownDeps),
);
runAppShutdownRuntimeService(options.appShutdownDeps);
},
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
restoreWindowsOnActivate: options.restoreWindowsOnActivate,

View File

@@ -1,74 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService,
} from "./startup-lifecycle-runtime-deps-service";
test("createStartupAppReadyDepsRuntimeService preserves runtime deps behavior", async () => {
const calls: string[] = [];
const deps = createStartupAppReadyDepsRuntimeService({
loadSubtitlePosition: () => calls.push("loadSubtitlePosition"),
resolveKeybindings: () => calls.push("resolveKeybindings"),
createMpvClient: () => calls.push("createMpvClient"),
reloadConfig: () => calls.push("reloadConfig"),
getResolvedConfig: () => ({
secondarySub: { defaultMode: "hover" },
websocket: { enabled: "auto", port: 1234 },
}),
getConfigWarnings: () => [],
logConfigWarning: () => {},
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
setSecondarySubMode: () => calls.push("setSecondarySubMode"),
defaultSecondarySubMode: "hover",
defaultWebsocketPort: 8765,
hasMpvWebsocketPlugin: () => true,
startSubtitleWebsocket: () => calls.push("startSubtitleWebsocket"),
log: () => calls.push("log"),
createMecabTokenizerAndCheck: async () => {
calls.push("createMecab");
},
createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"),
loadYomitanExtension: async () => {
calls.push("loadYomitan");
},
texthookerOnlyMode: false,
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
initializeOverlayRuntime: () => calls.push("initOverlayRuntime"),
handleInitialArgs: () => calls.push("handleInitialArgs"),
});
deps.loadSubtitlePosition();
await deps.createMecabTokenizerAndCheck();
deps.handleInitialArgs();
assert.equal(deps.defaultWebsocketPort, 8765);
assert.equal(deps.defaultSecondarySubMode, "hover");
assert.deepEqual(calls, ["loadSubtitlePosition", "createMecab", "handleInitialArgs"]);
});
test("createStartupAppShutdownDepsRuntimeService preserves shutdown handlers", () => {
const calls: string[] = [];
const deps = createStartupAppShutdownDepsRuntimeService({
unregisterAllGlobalShortcuts: () => calls.push("unregisterAllGlobalShortcuts"),
stopSubtitleWebsocket: () => calls.push("stopSubtitleWebsocket"),
stopTexthookerService: () => calls.push("stopTexthookerService"),
destroyYomitanParserWindow: () => calls.push("destroyYomitanParserWindow"),
clearYomitanParserPromises: () => calls.push("clearYomitanParserPromises"),
stopWindowTracker: () => calls.push("stopWindowTracker"),
destroyMpvSocket: () => calls.push("destroyMpvSocket"),
clearReconnectTimer: () => calls.push("clearReconnectTimer"),
destroySubtitleTimingTracker: () => calls.push("destroySubtitleTimingTracker"),
destroyAnkiIntegration: () => calls.push("destroyAnkiIntegration"),
});
deps.stopSubtitleWebsocket();
deps.clearReconnectTimer();
deps.destroyAnkiIntegration();
assert.deepEqual(calls, [
"stopSubtitleWebsocket",
"clearReconnectTimer",
"destroyAnkiIntegration",
]);
});

View File

@@ -1,55 +0,0 @@
import {
AppReadyRuntimeDeps,
} from "./app-ready-runtime-service";
import {
AppShutdownRuntimeDeps,
} from "./app-shutdown-runtime-service";
export type StartupAppReadyDepsRuntimeOptions = AppReadyRuntimeDeps;
export type StartupAppShutdownDepsRuntimeOptions = AppShutdownRuntimeDeps;
export function createStartupAppReadyDepsRuntimeService(
options: StartupAppReadyDepsRuntimeOptions,
): AppReadyRuntimeDeps {
return {
loadSubtitlePosition: options.loadSubtitlePosition,
resolveKeybindings: options.resolveKeybindings,
createMpvClient: options.createMpvClient,
reloadConfig: options.reloadConfig,
getResolvedConfig: options.getResolvedConfig,
getConfigWarnings: options.getConfigWarnings,
logConfigWarning: options.logConfigWarning,
initRuntimeOptionsManager: options.initRuntimeOptionsManager,
setSecondarySubMode: options.setSecondarySubMode,
defaultSecondarySubMode: options.defaultSecondarySubMode,
defaultWebsocketPort: options.defaultWebsocketPort,
hasMpvWebsocketPlugin: options.hasMpvWebsocketPlugin,
startSubtitleWebsocket: options.startSubtitleWebsocket,
log: options.log,
createMecabTokenizerAndCheck: options.createMecabTokenizerAndCheck,
createSubtitleTimingTracker: options.createSubtitleTimingTracker,
loadYomitanExtension: options.loadYomitanExtension,
texthookerOnlyMode: options.texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig:
options.shouldAutoInitializeOverlayRuntimeFromConfig,
initializeOverlayRuntime: options.initializeOverlayRuntime,
handleInitialArgs: options.handleInitialArgs,
};
}
export function createStartupAppShutdownDepsRuntimeService(
options: StartupAppShutdownDepsRuntimeOptions,
): AppShutdownRuntimeDeps {
return {
unregisterAllGlobalShortcuts: options.unregisterAllGlobalShortcuts,
stopSubtitleWebsocket: options.stopSubtitleWebsocket,
stopTexthookerService: options.stopTexthookerService,
destroyYomitanParserWindow: options.destroyYomitanParserWindow,
clearYomitanParserPromises: options.clearYomitanParserPromises,
stopWindowTracker: options.stopWindowTracker,
destroyMpvSocket: options.destroyMpvSocket,
clearReconnectTimer: options.clearReconnectTimer,
destroySubtitleTimingTracker: options.destroySubtitleTimingTracker,
destroyAnkiIntegration: options.destroyAnkiIntegration,
};
}