diff --git a/package.json b/package.json index d731b1d..d5a2e60 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "test:config": "pnpm run build && node --test dist/config/config.test.js", - "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js", + "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-service.test.js dist/core/services/startup-lifecycle-runtime-deps-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js", "test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js", "generate:config-example": "pnpm run build && node dist/generate-config-example.js", "start": "pnpm run build && electron . --start", diff --git a/src/core/services/startup-lifecycle-runtime-deps-service.test.ts b/src/core/services/startup-lifecycle-runtime-deps-service.test.ts new file mode 100644 index 0000000..bd7ce7d --- /dev/null +++ b/src/core/services/startup-lifecycle-runtime-deps-service.test.ts @@ -0,0 +1,74 @@ +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", + ]); +}); diff --git a/src/core/services/startup-lifecycle-runtime-deps-service.ts b/src/core/services/startup-lifecycle-runtime-deps-service.ts new file mode 100644 index 0000000..3da81c3 --- /dev/null +++ b/src/core/services/startup-lifecycle-runtime-deps-service.ts @@ -0,0 +1,55 @@ +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, + }; +} diff --git a/src/main.ts b/src/main.ts index 66930cc..0f5eaaf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -207,6 +207,10 @@ import { createOverlayWindowRuntimeDepsService, createVisibleOverlayVisibilityDepsRuntimeService, } from "./core/services/overlay-runtime-deps-service"; +import { + createStartupAppReadyDepsRuntimeService, + createStartupAppShutdownDepsRuntimeService, +} from "./core/services/startup-lifecycle-runtime-deps-service"; import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { @@ -502,158 +506,162 @@ if ( printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), logNoRunningInstance: () => appLogger.logNoRunningInstance(), onReady: async () => { - await runAppReadyRuntimeService({ - loadSubtitlePosition: () => loadSubtitlePosition(), - resolveKeybindings: () => { - keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS); - }, - createMpvClient: () => { - mpvClient = new MpvIpcClient( - mpvSocketPath, - createMpvIpcClientDepsRuntimeService({ - getResolvedConfig: () => getResolvedConfig(), - autoStartOverlay, - setOverlayVisible: (visible) => setOverlayVisible(visible), - shouldBindVisibleOverlayToMpvSubVisibility: () => - shouldBindVisibleOverlayToMpvSubVisibility(), - isVisibleOverlayVisible: () => visibleOverlayVisible, - getReconnectTimer: () => reconnectTimer, - setReconnectTimer: (timer) => { - reconnectTimer = timer; + await runAppReadyRuntimeService( + createStartupAppReadyDepsRuntimeService({ + loadSubtitlePosition: () => loadSubtitlePosition(), + resolveKeybindings: () => { + keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS); + }, + createMpvClient: () => { + mpvClient = new MpvIpcClient( + mpvSocketPath, + createMpvIpcClientDepsRuntimeService({ + getResolvedConfig: () => getResolvedConfig(), + autoStartOverlay, + setOverlayVisible: (visible) => setOverlayVisible(visible), + shouldBindVisibleOverlayToMpvSubVisibility: () => + shouldBindVisibleOverlayToMpvSubVisibility(), + isVisibleOverlayVisible: () => visibleOverlayVisible, + getReconnectTimer: () => reconnectTimer, + setReconnectTimer: (timer) => { + reconnectTimer = timer; + }, + getCurrentSubText: () => currentSubText, + setCurrentSubText: (text) => { + currentSubText = text; + }, + setCurrentSubAssText: (text) => { + currentSubAssText = text; + }, + getSubtitleTimingTracker: () => subtitleTimingTracker, + subtitleWsBroadcast: (text) => { + subtitleWsService.broadcast(text); + }, + getOverlayWindowsCount: () => getOverlayWindows().length, + tokenizeSubtitle: (text) => tokenizeSubtitle(text), + broadcastToOverlayWindows: (channel, ...args) => { + broadcastToOverlayWindows(channel, ...args); + }, + updateCurrentMediaPath: (mediaPath) => { + updateCurrentMediaPath(mediaPath); + }, + updateMpvSubtitleRenderMetrics: (patch) => { + updateMpvSubtitleRenderMetrics(patch); + }, + getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, + setPreviousSecondarySubVisibility: (value) => { + previousSecondarySubVisibility = value; + }, + showMpvOsd: (text) => { + showMpvOsd(text); + }, + }), + ); + }, + reloadConfig: () => { + configService.reloadConfig(); + }, + getResolvedConfig: () => getResolvedConfig(), + getConfigWarnings: () => configService.getWarnings(), + logConfigWarning: (warning) => appLogger.logConfigWarning(warning), + initRuntimeOptionsManager: () => { + runtimeOptionsManager = createRuntimeOptionsManagerRuntimeService({ + getAnkiConfig: () => configService.getConfig().ankiConnect, + applyAnkiPatch: (patch) => { + if (ankiIntegration) { + ankiIntegration.applyRuntimeConfigPatch(patch); + } }, - getCurrentSubText: () => currentSubText, - setCurrentSubText: (text) => { - currentSubText = text; + onOptionsChanged: () => { + broadcastRuntimeOptionsChanged(); + refreshOverlayShortcuts(); }, - setCurrentSubAssText: (text) => { - currentSubAssText = text; - }, - getSubtitleTimingTracker: () => subtitleTimingTracker, - subtitleWsBroadcast: (text) => { - subtitleWsService.broadcast(text); - }, - getOverlayWindowsCount: () => getOverlayWindows().length, - tokenizeSubtitle: (text) => tokenizeSubtitle(text), - broadcastToOverlayWindows: (channel, ...args) => { - broadcastToOverlayWindows(channel, ...args); - }, - updateCurrentMediaPath: (mediaPath) => { - updateCurrentMediaPath(mediaPath); - }, - updateMpvSubtitleRenderMetrics: (patch) => { - updateMpvSubtitleRenderMetrics(patch); - }, - getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, - setPreviousSecondarySubVisibility: (value) => { - previousSecondarySubVisibility = value; - }, - showMpvOsd: (text) => { - showMpvOsd(text); + }); + }, + setSecondarySubMode: (mode) => { + secondarySubMode = mode; + }, + defaultSecondarySubMode: "hover", + defaultWebsocketPort: DEFAULT_CONFIG.websocket.port, + hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(), + startSubtitleWebsocket: (port) => { + subtitleWsService.start(port, () => currentSubText); + }, + log: (message) => appLogger.logInfo(message), + createMecabTokenizerAndCheck: async () => + createMecabTokenizerAndCheckRuntimeService({ + createMecabTokenizer: () => new MecabTokenizer(), + setMecabTokenizer: (tokenizer) => { + mecabTokenizer = tokenizer; }, }), - ); - }, - reloadConfig: () => { - configService.reloadConfig(); - }, - getResolvedConfig: () => getResolvedConfig(), - getConfigWarnings: () => configService.getWarnings(), - logConfigWarning: (warning) => appLogger.logConfigWarning(warning), - initRuntimeOptionsManager: () => { - runtimeOptionsManager = createRuntimeOptionsManagerRuntimeService({ - getAnkiConfig: () => configService.getConfig().ankiConnect, - applyAnkiPatch: (patch) => { - if (ankiIntegration) { - ankiIntegration.applyRuntimeConfigPatch(patch); - } - }, - onOptionsChanged: () => { - broadcastRuntimeOptionsChanged(); - refreshOverlayShortcuts(); - }, - }); - }, - setSecondarySubMode: (mode) => { - secondarySubMode = mode; - }, - defaultSecondarySubMode: "hover", - defaultWebsocketPort: DEFAULT_CONFIG.websocket.port, - hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(), - startSubtitleWebsocket: (port) => { - subtitleWsService.start(port, () => currentSubText); - }, - log: (message) => appLogger.logInfo(message), - createMecabTokenizerAndCheck: async () => - createMecabTokenizerAndCheckRuntimeService({ - createMecabTokenizer: () => new MecabTokenizer(), - setMecabTokenizer: (tokenizer) => { - mecabTokenizer = tokenizer; - }, - }), - createSubtitleTimingTracker: () => - createSubtitleTimingTrackerRuntimeService({ - createSubtitleTimingTracker: () => new SubtitleTimingTracker(), - setSubtitleTimingTracker: (tracker) => { - subtitleTimingTracker = tracker; - }, - }), - loadYomitanExtension: async () => { - await loadYomitanExtension(); - }, - texthookerOnlyMode, - shouldAutoInitializeOverlayRuntimeFromConfig: () => - shouldAutoInitializeOverlayRuntimeFromConfig(), - initializeOverlayRuntime: () => initializeOverlayRuntime(), - handleInitialArgs: () => handleInitialArgs(), - }); + createSubtitleTimingTracker: () => + createSubtitleTimingTrackerRuntimeService({ + createSubtitleTimingTracker: () => new SubtitleTimingTracker(), + setSubtitleTimingTracker: (tracker) => { + subtitleTimingTracker = tracker; + }, + }), + loadYomitanExtension: async () => { + await loadYomitanExtension(); + }, + texthookerOnlyMode, + shouldAutoInitializeOverlayRuntimeFromConfig: () => + shouldAutoInitializeOverlayRuntimeFromConfig(), + initializeOverlayRuntime: () => initializeOverlayRuntime(), + handleInitialArgs: () => handleInitialArgs(), + }), + ); }, onWillQuitCleanup: () => { - runAppShutdownRuntimeService({ - unregisterAllGlobalShortcuts: () => { - globalShortcut.unregisterAll(); - }, - stopSubtitleWebsocket: () => { - subtitleWsService.stop(); - }, - stopTexthookerService: () => { - texthookerService.stop(); - }, - destroyYomitanParserWindow: () => { - if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) { - yomitanParserWindow.destroy(); - } - yomitanParserWindow = null; - }, - clearYomitanParserPromises: () => { - yomitanParserReadyPromise = null; - yomitanParserInitPromise = null; - }, - stopWindowTracker: () => { - if (windowTracker) { - windowTracker.stop(); - } - }, - destroyMpvSocket: () => { - if (mpvClient && mpvClient.socket) { - mpvClient.socket.destroy(); - } - }, - clearReconnectTimer: () => { - if (reconnectTimer) { - clearTimeout(reconnectTimer); - } - }, - destroySubtitleTimingTracker: () => { - if (subtitleTimingTracker) { - subtitleTimingTracker.destroy(); - } - }, - destroyAnkiIntegration: () => { - if (ankiIntegration) { - ankiIntegration.destroy(); - } - }, - }); + runAppShutdownRuntimeService( + createStartupAppShutdownDepsRuntimeService({ + unregisterAllGlobalShortcuts: () => { + globalShortcut.unregisterAll(); + }, + stopSubtitleWebsocket: () => { + subtitleWsService.stop(); + }, + stopTexthookerService: () => { + texthookerService.stop(); + }, + destroyYomitanParserWindow: () => { + if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) { + yomitanParserWindow.destroy(); + } + yomitanParserWindow = null; + }, + clearYomitanParserPromises: () => { + yomitanParserReadyPromise = null; + yomitanParserInitPromise = null; + }, + stopWindowTracker: () => { + if (windowTracker) { + windowTracker.stop(); + } + }, + destroyMpvSocket: () => { + if (mpvClient && mpvClient.socket) { + mpvClient.socket.destroy(); + } + }, + clearReconnectTimer: () => { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + }, + destroySubtitleTimingTracker: () => { + if (subtitleTimingTracker) { + subtitleTimingTracker.destroy(); + } + }, + destroyAnkiIntegration: () => { + if (ankiIntegration) { + ankiIntegration.destroy(); + } + }, + }), + ); }, shouldRestoreWindowsOnActivate: () => overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,