From 0402b773edf08763b9604962e8327d544ba3bae1 Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 9 Feb 2026 19:42:21 -0800 Subject: [PATCH] refactor: extract core ipc handler registration service --- src/core/services/ipc-service.ts | 157 +++++++++++++++ src/main.ts | 333 +++++++++++-------------------- 2 files changed, 276 insertions(+), 214 deletions(-) create mode 100644 src/core/services/ipc-service.ts diff --git a/src/core/services/ipc-service.ts b/src/core/services/ipc-service.ts new file mode 100644 index 0000000..4801fdd --- /dev/null +++ b/src/core/services/ipc-service.ts @@ -0,0 +1,157 @@ +import { BrowserWindow, ipcMain, IpcMainEvent } from "electron"; + +export interface IpcServiceDeps { + getInvisibleWindow: () => BrowserWindow | null; + isVisibleOverlayVisible: () => boolean; + setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void; + onOverlayModalClosed: (modal: string) => void; + openYomitanSettings: () => void; + quitApp: () => void; + toggleDevTools: () => void; + getVisibleOverlayVisibility: () => boolean; + toggleVisibleOverlay: () => void; + getInvisibleOverlayVisibility: () => boolean; + tokenizeCurrentSubtitle: () => Promise; + getCurrentSubtitleAss: () => string; + getMpvSubtitleRenderMetrics: () => unknown; + getSubtitlePosition: () => unknown; + getSubtitleStyle: () => unknown; + saveSubtitlePosition: (position: unknown) => void; + getMecabStatus: () => { available: boolean; enabled: boolean; path: string | null }; + setMecabEnabled: (enabled: boolean) => void; + handleMpvCommand: (command: Array) => void; + getKeybindings: () => unknown; + getSecondarySubMode: () => unknown; + getCurrentSecondarySub: () => string; + runSubsyncManual: (request: unknown) => Promise; + getAnkiConnectStatus: () => boolean; + getRuntimeOptions: () => unknown; + setRuntimeOption: (id: string, value: unknown) => unknown; + cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown; +} + +export function registerIpcHandlersService(deps: IpcServiceDeps): void { + ipcMain.on( + "set-ignore-mouse-events", + ( + event: IpcMainEvent, + ignore: boolean, + options: { forward?: boolean } = {}, + ) => { + const senderWindow = BrowserWindow.fromWebContents(event.sender); + if (senderWindow && !senderWindow.isDestroyed()) { + const invisibleWindow = deps.getInvisibleWindow(); + if ( + senderWindow === invisibleWindow && + deps.isVisibleOverlayVisible() && + invisibleWindow && + !invisibleWindow.isDestroyed() + ) { + deps.setInvisibleIgnoreMouseEvents(true, { forward: true }); + } else { + senderWindow.setIgnoreMouseEvents(ignore, options); + } + } + }, + ); + + ipcMain.on("overlay:modal-closed", (_event: IpcMainEvent, modal: string) => { + deps.onOverlayModalClosed(modal); + }); + + ipcMain.on("open-yomitan-settings", () => { + deps.openYomitanSettings(); + }); + + ipcMain.on("quit-app", () => { + deps.quitApp(); + }); + + ipcMain.on("toggle-dev-tools", () => { + deps.toggleDevTools(); + }); + + ipcMain.handle("get-overlay-visibility", () => { + return deps.getVisibleOverlayVisibility(); + }); + + ipcMain.on("toggle-overlay", () => { + deps.toggleVisibleOverlay(); + }); + + ipcMain.handle("get-visible-overlay-visibility", () => { + return deps.getVisibleOverlayVisibility(); + }); + + ipcMain.handle("get-invisible-overlay-visibility", () => { + return deps.getInvisibleOverlayVisibility(); + }); + + ipcMain.handle("get-current-subtitle", async () => { + return await deps.tokenizeCurrentSubtitle(); + }); + + ipcMain.handle("get-current-subtitle-ass", () => { + return deps.getCurrentSubtitleAss(); + }); + + ipcMain.handle("get-mpv-subtitle-render-metrics", () => { + return deps.getMpvSubtitleRenderMetrics(); + }); + + ipcMain.handle("get-subtitle-position", () => { + return deps.getSubtitlePosition(); + }); + + ipcMain.handle("get-subtitle-style", () => { + return deps.getSubtitleStyle(); + }); + + ipcMain.on("save-subtitle-position", (_event: IpcMainEvent, position: unknown) => { + deps.saveSubtitlePosition(position); + }); + + ipcMain.handle("get-mecab-status", () => { + return deps.getMecabStatus(); + }); + + ipcMain.on("set-mecab-enabled", (_event: IpcMainEvent, enabled: boolean) => { + deps.setMecabEnabled(enabled); + }); + + ipcMain.on("mpv-command", (_event: IpcMainEvent, command: (string | number)[]) => { + deps.handleMpvCommand(command); + }); + + ipcMain.handle("get-keybindings", () => { + return deps.getKeybindings(); + }); + + ipcMain.handle("get-secondary-sub-mode", () => { + return deps.getSecondarySubMode(); + }); + + ipcMain.handle("get-current-secondary-sub", () => { + return deps.getCurrentSecondarySub(); + }); + + ipcMain.handle("subsync:run-manual", async (_event, request: unknown) => { + return await deps.runSubsyncManual(request); + }); + + ipcMain.handle("get-anki-connect-status", () => { + return deps.getAnkiConnectStatus(); + }); + + ipcMain.handle("runtime-options:get", () => { + return deps.getRuntimeOptions(); + }); + + ipcMain.handle("runtime-options:set", (_event, id: string, value: unknown) => { + return deps.setRuntimeOption(id, value); + }); + + ipcMain.handle("runtime-options:cycle", (_event, id: string, direction: 1 | -1) => { + return deps.cycleRuntimeOption(id, direction); + }); +} diff --git a/src/main.ts b/src/main.ts index 7d7060d..0617c9e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -130,6 +130,7 @@ import { SubtitleWebSocketService, } from "./core/services/subtitle-ws-service"; import { registerGlobalShortcutsService } from "./core/services/shortcut-service"; +import { registerIpcHandlersService } from "./core/services/ipc-service"; import { ConfigService, DEFAULT_CONFIG, @@ -3775,234 +3776,138 @@ function toggleOverlay(): void { toggleVisibleOverlay(); } -ipcMain.on( - "set-ignore-mouse-events", - ( - event: IpcMainEvent, - ignore: boolean, - options: { forward?: boolean } = {}, - ) => { - const senderWindow = BrowserWindow.fromWebContents(event.sender); - if (senderWindow && !senderWindow.isDestroyed()) { - if ( - senderWindow === invisibleWindow && - visibleOverlayVisible && - !invisibleWindow.isDestroyed() - ) { - invisibleWindow.setIgnoreMouseEvents(true, { forward: true }); - } else { - senderWindow.setIgnoreMouseEvents(ignore, options); - } - } - }, -); - -ipcMain.on( - "overlay:modal-closed", - (_event: IpcMainEvent, modal: OverlayHostedModal) => { - if (!restoreVisibleOverlayOnModalClose.has(modal)) return; - restoreVisibleOverlayOnModalClose.delete(modal); - if (restoreVisibleOverlayOnModalClose.size === 0) { - setVisibleOverlayVisible(false); - } - }, -); - -ipcMain.on("open-yomitan-settings", () => { - openYomitanSettings(); -}); - -ipcMain.on("quit-app", () => { - app.quit(); -}); - -ipcMain.on("toggle-dev-tools", () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.toggleDevTools(); +function handleOverlayModalClosed(modal: OverlayHostedModal): void { + if (!restoreVisibleOverlayOnModalClose.has(modal)) return; + restoreVisibleOverlayOnModalClose.delete(modal); + if (restoreVisibleOverlayOnModalClose.size === 0) { + setVisibleOverlayVisible(false); } -}); +} -ipcMain.handle("get-overlay-visibility", () => { - return visibleOverlayVisible; -}); - -ipcMain.on("toggle-overlay", () => { - toggleVisibleOverlay(); -}); - -ipcMain.handle("get-visible-overlay-visibility", () => { - return visibleOverlayVisible; -}); - -ipcMain.handle("get-invisible-overlay-visibility", () => { - return invisibleOverlayVisible; -}); - -ipcMain.handle("get-current-subtitle", async () => { - return await tokenizeSubtitle(currentSubText); -}); - -ipcMain.handle("get-current-subtitle-ass", () => { - return currentSubAssText; -}); - -ipcMain.handle("get-mpv-subtitle-render-metrics", () => { - return mpvSubtitleRenderMetrics; -}); - -ipcMain.handle("get-subtitle-position", () => { - return loadSubtitlePosition(); -}); - -ipcMain.handle("get-subtitle-style", () => { - const config = getResolvedConfig(); - return config.subtitleStyle ?? null; -}); - -ipcMain.on( - "save-subtitle-position", - (_event: IpcMainEvent, position: SubtitlePosition) => { - saveSubtitlePosition(position); - }, -); - -ipcMain.handle("get-mecab-status", () => { - if (mecabTokenizer) { - return mecabTokenizer.getStatus(); +function handleMpvCommandFromIpc(command: (string | number)[]): void { + const first = typeof command[0] === "string" ? command[0] : ""; + if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) { + triggerSubsyncFromConfig(); + return; } - return { available: false, enabled: false, path: null }; -}); -ipcMain.on("set-mecab-enabled", (_event: IpcMainEvent, enabled: boolean) => { - if (mecabTokenizer) { - mecabTokenizer.setEnabled(enabled); + if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) { + openRuntimeOptionsPalette(); + return; } -}); -ipcMain.on( - "mpv-command", - (_event: IpcMainEvent, command: (string | number)[]) => { - const first = typeof command[0] === "string" ? command[0] : ""; - if (first === SPECIAL_COMMANDS.SUBSYNC_TRIGGER) { - triggerSubsyncFromConfig(); - return; - } - - if (first === SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN) { - openRuntimeOptionsPalette(); - return; - } - - if (first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)) { - if (!runtimeOptionsManager) return; - const [, idToken, directionToken] = first.split(":"); - const id = idToken as RuntimeOptionId; - const direction: 1 | -1 = directionToken === "prev" ? -1 : 1; - const result = applyRuntimeOptionResult( - runtimeOptionsManager.cycleOption(id, direction), - ); - if (!result.ok && result.error) { - showMpvOsd(result.error); - } - return; - } - - if (mpvClient && mpvClient.connected) { - if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) { - mpvClient.replayCurrentSubtitle(); - } else if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) { - mpvClient.playNextSubtitle(); - } else { - mpvClient.send({ command }); - } - } - }, -); - -ipcMain.handle("get-keybindings", () => { - return keybindings; -}); - -ipcMain.handle("get-secondary-sub-mode", () => { - return secondarySubMode; -}); - -ipcMain.handle("get-current-secondary-sub", () => { - return mpvClient?.currentSecondarySubText || ""; -}); - -ipcMain.handle( - "subsync:run-manual", - async (_event, request: SubsyncManualRunRequest): Promise => { - if (subsyncInProgress) { - const busy = "Subsync already running"; - showMpvOsd(busy); - return { ok: false, message: busy }; - } - try { - subsyncInProgress = true; - const result = await runWithSubsyncSpinner(() => - runSubsyncManual(request), - ); - showMpvOsd(result.message); - return result; - } catch (error) { - const message = `Subsync failed: ${(error as Error).message}`; - showMpvOsd(message); - return { ok: false, message }; - } finally { - subsyncInProgress = false; - } - }, -); - -ipcMain.handle("get-anki-connect-status", () => { - return ankiIntegration !== null; -}); - -ipcMain.handle("runtime-options:get", (): RuntimeOptionState[] => { - return getRuntimeOptionsState(); -}); - -ipcMain.handle( - "runtime-options:set", - ( - _event, - id: RuntimeOptionId, - value: RuntimeOptionValue, - ): RuntimeOptionApplyResult => { - if (!runtimeOptionsManager) { - return { ok: false, error: "Runtime options manager unavailable" }; - } - const result = applyRuntimeOptionResult( - runtimeOptionsManager.setOptionValue(id, value), - ); - if (!result.ok && result.error) { - showMpvOsd(result.error); - } - return result; - }, -); - -ipcMain.handle( - "runtime-options:cycle", - ( - _event, - id: RuntimeOptionId, - direction: 1 | -1, - ): RuntimeOptionApplyResult => { - if (!runtimeOptionsManager) { - return { ok: false, error: "Runtime options manager unavailable" }; - } + if (first.startsWith(SPECIAL_COMMANDS.RUNTIME_OPTION_CYCLE_PREFIX)) { + if (!runtimeOptionsManager) return; + const [, idToken, directionToken] = first.split(":"); + const id = idToken as RuntimeOptionId; + const direction: 1 | -1 = directionToken === "prev" ? -1 : 1; const result = applyRuntimeOptionResult( runtimeOptionsManager.cycleOption(id, direction), ); if (!result.ok && result.error) { showMpvOsd(result.error); } + return; + } + + if (mpvClient && mpvClient.connected) { + if (first === SPECIAL_COMMANDS.REPLAY_SUBTITLE) { + mpvClient.replayCurrentSubtitle(); + } else if (first === SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE) { + mpvClient.playNextSubtitle(); + } else { + mpvClient.send({ command }); + } + } +} + +async function runSubsyncManualFromIpc( + request: SubsyncManualRunRequest, +): Promise { + if (subsyncInProgress) { + const busy = "Subsync already running"; + showMpvOsd(busy); + return { ok: false, message: busy }; + } + try { + subsyncInProgress = true; + const result = await runWithSubsyncSpinner(() => runSubsyncManual(request)); + showMpvOsd(result.message); + return result; + } catch (error) { + const message = `Subsync failed: ${(error as Error).message}`; + showMpvOsd(message); + return { ok: false, message }; + } finally { + subsyncInProgress = false; + } +} + +registerIpcHandlersService({ + getInvisibleWindow: () => invisibleWindow, + isVisibleOverlayVisible: () => visibleOverlayVisible, + setInvisibleIgnoreMouseEvents: (ignore, options) => { + if (!invisibleWindow || invisibleWindow.isDestroyed()) return; + invisibleWindow.setIgnoreMouseEvents(ignore, options); + }, + onOverlayModalClosed: (modal) => + handleOverlayModalClosed(modal as OverlayHostedModal), + openYomitanSettings: () => openYomitanSettings(), + quitApp: () => app.quit(), + toggleDevTools: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.toggleDevTools(); + } + }, + getVisibleOverlayVisibility: () => visibleOverlayVisible, + toggleVisibleOverlay: () => toggleVisibleOverlay(), + getInvisibleOverlayVisibility: () => invisibleOverlayVisible, + tokenizeCurrentSubtitle: () => tokenizeSubtitle(currentSubText), + getCurrentSubtitleAss: () => currentSubAssText, + getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, + getSubtitlePosition: () => loadSubtitlePosition(), + getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null, + saveSubtitlePosition: (position) => saveSubtitlePosition(position as SubtitlePosition), + getMecabStatus: () => + mecabTokenizer + ? mecabTokenizer.getStatus() + : { available: false, enabled: false, path: null }, + setMecabEnabled: (enabled) => { + if (mecabTokenizer) mecabTokenizer.setEnabled(enabled); + }, + handleMpvCommand: (command) => handleMpvCommandFromIpc(command), + getKeybindings: () => keybindings, + getSecondarySubMode: () => secondarySubMode, + getCurrentSecondarySub: () => mpvClient?.currentSecondarySubText || "", + runSubsyncManual: (request) => + runSubsyncManualFromIpc(request as SubsyncManualRunRequest), + getAnkiConnectStatus: () => ankiIntegration !== null, + getRuntimeOptions: () => getRuntimeOptionsState(), + setRuntimeOption: (id, value) => { + if (!runtimeOptionsManager) { + return { ok: false, error: "Runtime options manager unavailable" }; + } + const result = applyRuntimeOptionResult( + runtimeOptionsManager.setOptionValue(id as RuntimeOptionId, value as RuntimeOptionValue), + ); + if (!result.ok && result.error) { + showMpvOsd(result.error); + } return result; }, -); + cycleRuntimeOption: (id, direction) => { + if (!runtimeOptionsManager) { + return { ok: false, error: "Runtime options manager unavailable" }; + } + const result = applyRuntimeOptionResult( + runtimeOptionsManager.cycleOption(id as RuntimeOptionId, direction), + ); + if (!result.ok && result.error) { + showMpvOsd(result.error); + } + return result; + }, +}); /** * Create and show a desktop notification with robust icon handling.