diff --git a/package.json b/package.json index 504e3ae..05a20d0 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/startup-lifecycle-runtime-deps-service.test.js dist/core/services/overlay-shortcut-runtime-deps-service.test.js dist/core/services/mining-runtime-deps-service.test.js dist/core/services/shortcut-ui-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/startup-lifecycle-hooks-runtime-service.test.js dist/core/services/overlay-shortcut-runtime-deps-service.test.js dist/core/services/mining-runtime-deps-service.test.js dist/core/services/shortcut-ui-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-hooks-runtime-service.test.ts b/src/core/services/startup-lifecycle-hooks-runtime-service.test.ts new file mode 100644 index 0000000..8911532 --- /dev/null +++ b/src/core/services/startup-lifecycle-hooks-runtime-service.test.ts @@ -0,0 +1,63 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createStartupLifecycleHooksRuntimeService } from "./startup-lifecycle-hooks-runtime-service"; + +test("createStartupLifecycleHooksRuntimeService wires app-ready and app-shutdown handlers", async () => { + const calls: string[] = []; + const hooks = createStartupLifecycleHooksRuntimeService({ + appReadyDeps: { + 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: () => calls.push("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("createMecabTokenizerAndCheck"); + }, + createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"), + loadYomitanExtension: async () => { + calls.push("loadYomitanExtension"); + }, + texthookerOnlyMode: false, + shouldAutoInitializeOverlayRuntimeFromConfig: () => false, + initializeOverlayRuntime: () => calls.push("initializeOverlayRuntime"), + handleInitialArgs: () => calls.push("handleInitialArgs"), + }, + appShutdownDeps: { + 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"), + }, + shouldRestoreWindowsOnActivate: () => true, + restoreWindowsOnActivate: () => calls.push("restoreWindowsOnActivate"), + }); + + await hooks.onReady(); + hooks.onWillQuitCleanup(); + assert.equal(hooks.shouldRestoreWindowsOnActivate(), true); + hooks.restoreWindowsOnActivate(); + + assert.ok(calls.includes("loadSubtitlePosition")); + assert.ok(calls.includes("handleInitialArgs")); + assert.ok(calls.includes("destroyAnkiIntegration")); + assert.ok(calls.includes("restoreWindowsOnActivate")); +}); diff --git a/src/core/services/startup-lifecycle-hooks-runtime-service.ts b/src/core/services/startup-lifecycle-hooks-runtime-service.ts new file mode 100644 index 0000000..75e4067 --- /dev/null +++ b/src/core/services/startup-lifecycle-hooks-runtime-service.ts @@ -0,0 +1,44 @@ +import { AppLifecycleDepsRuntimeOptions } from "./app-lifecycle-deps-runtime-service"; +import { + AppReadyRuntimeDeps, + runAppReadyRuntimeService, +} from "./app-ready-runtime-service"; +import { + AppShutdownRuntimeDeps, + runAppShutdownRuntimeService, +} from "./app-shutdown-runtime-service"; +import { + createStartupAppReadyDepsRuntimeService, + createStartupAppShutdownDepsRuntimeService, +} from "./startup-lifecycle-runtime-deps-service"; + +type StartupLifecycleHookDeps = Pick< + AppLifecycleDepsRuntimeOptions, + "onReady" | "onWillQuitCleanup" | "shouldRestoreWindowsOnActivate" | "restoreWindowsOnActivate" +>; + +export interface StartupLifecycleHooksRuntimeOptions { + appReadyDeps: AppReadyRuntimeDeps; + appShutdownDeps: AppShutdownRuntimeDeps; + shouldRestoreWindowsOnActivate: () => boolean; + restoreWindowsOnActivate: () => void; +} + +export function createStartupLifecycleHooksRuntimeService( + options: StartupLifecycleHooksRuntimeOptions, +): StartupLifecycleHookDeps { + return { + onReady: async () => { + await runAppReadyRuntimeService( + createStartupAppReadyDepsRuntimeService(options.appReadyDeps), + ); + }, + onWillQuitCleanup: () => { + runAppShutdownRuntimeService( + createStartupAppShutdownDepsRuntimeService(options.appShutdownDeps), + ); + }, + shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate, + restoreWindowsOnActivate: options.restoreWindowsOnActivate, + }; +} diff --git a/src/main.ts b/src/main.ts index 3300a58..723808e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -186,8 +186,6 @@ import { getOverlayWindowsRuntimeService, setOverlayDebugVisualizationEnabledRuntimeService, } from "./core/services/overlay-broadcast-runtime-service"; -import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-service"; -import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service"; import { createMpvIpcClientDepsRuntimeService } from "./core/services/mpv-client-deps-runtime-service"; import { createAppLifecycleDepsRuntimeService } from "./core/services/app-lifecycle-deps-runtime-service"; import { createCliCommandDepsRuntimeService } from "./core/services/cli-command-deps-runtime-service"; @@ -225,10 +223,7 @@ import { createYomitanSettingsWindowDepsRuntimeService, runOverlayShortcutLocalFallbackRuntimeService, } from "./core/services/shortcut-ui-runtime-deps-service"; -import { - createStartupAppReadyDepsRuntimeService, - createStartupAppShutdownDepsRuntimeService, -} from "./core/services/startup-lifecycle-runtime-deps-service"; +import { createStartupLifecycleHooksRuntimeService } from "./core/services/startup-lifecycle-hooks-runtime-service"; import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { @@ -525,172 +520,166 @@ const startupState = runStartupBootstrapRuntimeService({ handleCliCommand: (nextArgs, source) => handleCliCommand(nextArgs, source), printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), logNoRunningInstance: () => appLogger.logNoRunningInstance(), - onReady: async () => { - 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, ...channelArgs) => { - broadcastToOverlayWindows(channel, ...channelArgs); - }, - 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); - } + ...createStartupLifecycleHooksRuntimeService({ + appReadyDeps: { + 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; }, - onOptionsChanged: () => { - broadcastRuntimeOptionsChanged(); - refreshOverlayShortcuts(); + getCurrentSubText: () => currentSubText, + setCurrentSubText: (text) => { + currentSubText = 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; + setCurrentSubAssText: (text) => { + currentSubAssText = text; + }, + getSubtitleTimingTracker: () => subtitleTimingTracker, + subtitleWsBroadcast: (text) => { + subtitleWsService.broadcast(text); + }, + getOverlayWindowsCount: () => getOverlayWindows().length, + tokenizeSubtitle: (text) => tokenizeSubtitle(text), + broadcastToOverlayWindows: (channel, ...channelArgs) => { + broadcastToOverlayWindows(channel, ...channelArgs); + }, + updateCurrentMediaPath: (mediaPath) => { + updateCurrentMediaPath(mediaPath); + }, + updateMpvSubtitleRenderMetrics: (patch) => { + updateMpvSubtitleRenderMetrics(patch); + }, + getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, + setPreviousSecondarySubVisibility: (value) => { + previousSecondarySubVisibility = value; + }, + showMpvOsd: (text) => { + showMpvOsd(text); }, }), - createSubtitleTimingTracker: () => - createSubtitleTimingTrackerRuntimeService({ - createSubtitleTimingTracker: () => new SubtitleTimingTracker(), - setSubtitleTimingTracker: (tracker) => { - subtitleTimingTracker = tracker; - }, - }), - loadYomitanExtension: async () => { - await loadYomitanExtension(); - }, - texthookerOnlyMode, - shouldAutoInitializeOverlayRuntimeFromConfig: () => - shouldAutoInitializeOverlayRuntimeFromConfig(), - initializeOverlayRuntime: () => initializeOverlayRuntime(), - handleInitialArgs: () => handleInitialArgs(), - }), - ); - }, - onWillQuitCleanup: () => { - 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, - restoreWindowsOnActivate: () => { - createMainWindow(); - createInvisibleWindow(); - updateVisibleOverlayVisibility(); - updateInvisibleOverlayVisibility(); - }, + ); + }, + 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(), + }, + appShutdownDeps: { + 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, + restoreWindowsOnActivate: () => { + createMainWindow(); + createInvisibleWindow(); + updateVisibleOverlayVisibility(); + updateInvisibleOverlayVisibility(); + }, + }), })); }, });