import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; import type { RuntimeOptionId, RuntimeOptionValue, SubtitlePosition, SubsyncManualRunRequest, SubsyncResult, } from '../../types'; import { IPC_CHANNELS, type OverlayHostedModal } from '../../shared/ipc/contracts'; import { parseMpvCommand, parseOptionalForwardingOptions, parseOverlayHostedModal, parseRuntimeOptionDirection, parseRuntimeOptionId, parseRuntimeOptionValue, parseSubtitlePosition, parseSubsyncManualRunRequest, } from '../../shared/ipc/validators'; export interface IpcServiceDeps { getInvisibleWindow: () => WindowLike | null; isVisibleOverlayVisible: () => boolean; setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void; onOverlayModalClosed: (modal: OverlayHostedModal) => void; openYomitanSettings: () => void; quitApp: () => void; toggleDevTools: () => void; getVisibleOverlayVisibility: () => boolean; toggleVisibleOverlay: () => void; getInvisibleOverlayVisibility: () => boolean; tokenizeCurrentSubtitle: () => Promise; getCurrentSubtitleRaw: () => string; getCurrentSubtitleAss: () => string; getMpvSubtitleRenderMetrics: () => unknown; getSubtitlePosition: () => unknown; getSubtitleStyle: () => unknown; saveSubtitlePosition: (position: SubtitlePosition) => void; getMecabStatus: () => { available: boolean; enabled: boolean; path: string | null; }; setMecabEnabled: (enabled: boolean) => void; handleMpvCommand: (command: Array) => void; getKeybindings: () => unknown; getConfiguredShortcuts: () => unknown; getSecondarySubMode: () => unknown; getCurrentSecondarySub: () => string; focusMainWindow: () => void; runSubsyncManual: (request: SubsyncManualRunRequest) => Promise; getAnkiConnectStatus: () => boolean; getRuntimeOptions: () => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown; reportOverlayContentBounds: (payload: unknown) => void; getAnilistStatus: () => unknown; clearAnilistToken: () => void; openAnilistSetup: () => void; getAnilistQueueStatus: () => unknown; retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>; appendClipboardVideoToQueue: () => { ok: boolean; message: string }; } interface WindowLike { isDestroyed: () => boolean; focus: () => void; setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void; webContents: { toggleDevTools: () => void; }; } interface MecabTokenizerLike { getStatus: () => { available: boolean; enabled: boolean; path: string | null; }; setEnabled: (enabled: boolean) => void; } interface MpvClientLike { currentSecondarySubText?: string; } interface IpcMainRegistrar { on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => void; handle: (channel: string, listener: (event: unknown, ...args: unknown[]) => unknown) => void; } export interface IpcDepsRuntimeOptions { getInvisibleWindow: () => WindowLike | null; getMainWindow: () => WindowLike | null; getVisibleOverlayVisibility: () => boolean; getInvisibleOverlayVisibility: () => boolean; onOverlayModalClosed: (modal: OverlayHostedModal) => void; openYomitanSettings: () => void; quitApp: () => void; toggleVisibleOverlay: () => void; tokenizeCurrentSubtitle: () => Promise; getCurrentSubtitleRaw: () => string; getCurrentSubtitleAss: () => string; getMpvSubtitleRenderMetrics: () => unknown; getSubtitlePosition: () => unknown; getSubtitleStyle: () => unknown; saveSubtitlePosition: (position: SubtitlePosition) => void; getMecabTokenizer: () => MecabTokenizerLike | null; handleMpvCommand: (command: Array) => void; getKeybindings: () => unknown; getConfiguredShortcuts: () => unknown; getSecondarySubMode: () => unknown; getMpvClient: () => MpvClientLike | null; focusMainWindow: () => void; runSubsyncManual: (request: SubsyncManualRunRequest) => Promise; getAnkiConnectStatus: () => boolean; getRuntimeOptions: () => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown; reportOverlayContentBounds: (payload: unknown) => void; getAnilistStatus: () => unknown; clearAnilistToken: () => void; openAnilistSetup: () => void; getAnilistQueueStatus: () => unknown; retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>; appendClipboardVideoToQueue: () => { ok: boolean; message: string }; } export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps { return { getInvisibleWindow: () => options.getInvisibleWindow(), isVisibleOverlayVisible: options.getVisibleOverlayVisibility, setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => { const invisibleWindow = options.getInvisibleWindow(); if (!invisibleWindow || invisibleWindow.isDestroyed()) return; invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions); }, onOverlayModalClosed: options.onOverlayModalClosed, openYomitanSettings: options.openYomitanSettings, quitApp: options.quitApp, toggleDevTools: () => { const mainWindow = options.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed()) return; mainWindow.webContents.toggleDevTools(); }, getVisibleOverlayVisibility: options.getVisibleOverlayVisibility, toggleVisibleOverlay: options.toggleVisibleOverlay, getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility, tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle, getCurrentSubtitleRaw: options.getCurrentSubtitleRaw, getCurrentSubtitleAss: options.getCurrentSubtitleAss, getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics, getSubtitlePosition: options.getSubtitlePosition, getSubtitleStyle: options.getSubtitleStyle, saveSubtitlePosition: options.saveSubtitlePosition, getMecabStatus: () => { const mecabTokenizer = options.getMecabTokenizer(); return mecabTokenizer ? mecabTokenizer.getStatus() : { available: false, enabled: false, path: null }; }, setMecabEnabled: (enabled) => { const mecabTokenizer = options.getMecabTokenizer(); if (!mecabTokenizer) return; mecabTokenizer.setEnabled(enabled); }, handleMpvCommand: options.handleMpvCommand, getKeybindings: options.getKeybindings, getConfiguredShortcuts: options.getConfiguredShortcuts, getSecondarySubMode: options.getSecondarySubMode, getCurrentSecondarySub: () => options.getMpvClient()?.currentSecondarySubText || '', focusMainWindow: () => { const mainWindow = options.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed()) return; mainWindow.focus(); }, runSubsyncManual: options.runSubsyncManual, getAnkiConnectStatus: options.getAnkiConnectStatus, getRuntimeOptions: options.getRuntimeOptions, setRuntimeOption: options.setRuntimeOption, cycleRuntimeOption: options.cycleRuntimeOption, reportOverlayContentBounds: options.reportOverlayContentBounds, getAnilistStatus: options.getAnilistStatus, clearAnilistToken: options.clearAnilistToken, openAnilistSetup: options.openAnilistSetup, getAnilistQueueStatus: options.getAnilistQueueStatus, retryAnilistQueueNow: options.retryAnilistQueueNow, appendClipboardVideoToQueue: options.appendClipboardVideoToQueue, }; } export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar = ipcMain): void { ipc.on( IPC_CHANNELS.command.setIgnoreMouseEvents, (event: unknown, ignore: unknown, options: unknown = {}) => { if (typeof ignore !== 'boolean') return; const parsedOptions = parseOptionalForwardingOptions(options); const senderWindow = BrowserWindow.fromWebContents((event as IpcMainEvent).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, parsedOptions); } } }, ); ipc.on(IPC_CHANNELS.command.overlayModalClosed, (_event: unknown, modal: unknown) => { const parsedModal = parseOverlayHostedModal(modal); if (!parsedModal) return; deps.onOverlayModalClosed(parsedModal); }); ipc.on(IPC_CHANNELS.command.openYomitanSettings, () => { deps.openYomitanSettings(); }); ipc.on(IPC_CHANNELS.command.quitApp, () => { deps.quitApp(); }); ipc.on(IPC_CHANNELS.command.toggleDevTools, () => { deps.toggleDevTools(); }); ipc.handle(IPC_CHANNELS.request.getOverlayVisibility, () => { return deps.getVisibleOverlayVisibility(); }); ipc.on(IPC_CHANNELS.command.toggleOverlay, () => { deps.toggleVisibleOverlay(); }); ipc.handle(IPC_CHANNELS.request.getVisibleOverlayVisibility, () => { return deps.getVisibleOverlayVisibility(); }); ipc.handle(IPC_CHANNELS.request.getInvisibleOverlayVisibility, () => { return deps.getInvisibleOverlayVisibility(); }); ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => { return await deps.tokenizeCurrentSubtitle(); }); ipc.handle(IPC_CHANNELS.request.getCurrentSubtitleRaw, () => { return deps.getCurrentSubtitleRaw(); }); ipc.handle(IPC_CHANNELS.request.getCurrentSubtitleAss, () => { return deps.getCurrentSubtitleAss(); }); ipc.handle(IPC_CHANNELS.request.getMpvSubtitleRenderMetrics, () => { return deps.getMpvSubtitleRenderMetrics(); }); ipc.handle(IPC_CHANNELS.request.getSubtitlePosition, () => { return deps.getSubtitlePosition(); }); ipc.handle(IPC_CHANNELS.request.getSubtitleStyle, () => { return deps.getSubtitleStyle(); }); ipc.on(IPC_CHANNELS.command.saveSubtitlePosition, (_event: unknown, position: unknown) => { const parsedPosition = parseSubtitlePosition(position); if (!parsedPosition) return; deps.saveSubtitlePosition(parsedPosition); }); ipc.handle(IPC_CHANNELS.request.getMecabStatus, () => { return deps.getMecabStatus(); }); ipc.on(IPC_CHANNELS.command.setMecabEnabled, (_event: unknown, enabled: unknown) => { if (typeof enabled !== 'boolean') return; deps.setMecabEnabled(enabled); }); ipc.on(IPC_CHANNELS.command.mpvCommand, (_event: unknown, command: unknown) => { const parsedCommand = parseMpvCommand(command); if (!parsedCommand) return; deps.handleMpvCommand(parsedCommand); }); ipc.handle(IPC_CHANNELS.request.getKeybindings, () => { return deps.getKeybindings(); }); ipc.handle(IPC_CHANNELS.request.getConfigShortcuts, () => { return deps.getConfiguredShortcuts(); }); ipc.handle(IPC_CHANNELS.request.getSecondarySubMode, () => { return deps.getSecondarySubMode(); }); ipc.handle(IPC_CHANNELS.request.getCurrentSecondarySub, () => { return deps.getCurrentSecondarySub(); }); ipc.handle(IPC_CHANNELS.request.focusMainWindow, () => { deps.focusMainWindow(); }); ipc.handle(IPC_CHANNELS.request.runSubsyncManual, async (_event, request: unknown) => { const parsedRequest = parseSubsyncManualRunRequest(request); if (!parsedRequest) { return { ok: false, message: 'Invalid subsync manual request payload' }; } return await deps.runSubsyncManual(parsedRequest); }); ipc.handle(IPC_CHANNELS.request.getAnkiConnectStatus, () => { return deps.getAnkiConnectStatus(); }); ipc.handle(IPC_CHANNELS.request.getRuntimeOptions, () => { return deps.getRuntimeOptions(); }); ipc.handle(IPC_CHANNELS.request.setRuntimeOption, (_event, id: unknown, value: unknown) => { const parsedId = parseRuntimeOptionId(id); if (!parsedId) { return { ok: false, error: 'Invalid runtime option id' }; } const parsedValue = parseRuntimeOptionValue(value); if (parsedValue === null) { return { ok: false, error: 'Invalid runtime option value payload' }; } return deps.setRuntimeOption(parsedId, parsedValue); }); ipc.handle(IPC_CHANNELS.request.cycleRuntimeOption, (_event, id: unknown, direction: unknown) => { const parsedId = parseRuntimeOptionId(id); if (!parsedId) { return { ok: false, error: 'Invalid runtime option id' }; } const parsedDirection = parseRuntimeOptionDirection(direction); if (!parsedDirection) { return { ok: false, error: 'Invalid runtime option cycle direction' }; } return deps.cycleRuntimeOption(parsedId, parsedDirection); }); ipc.on(IPC_CHANNELS.command.reportOverlayContentBounds, (_event: unknown, payload: unknown) => { deps.reportOverlayContentBounds(payload); }); ipc.handle(IPC_CHANNELS.request.getAnilistStatus, () => { return deps.getAnilistStatus(); }); ipc.handle(IPC_CHANNELS.request.clearAnilistToken, () => { deps.clearAnilistToken(); return { ok: true }; }); ipc.handle(IPC_CHANNELS.request.openAnilistSetup, () => { deps.openAnilistSetup(); return { ok: true }; }); ipc.handle(IPC_CHANNELS.request.getAnilistQueueStatus, () => { return deps.getAnilistQueueStatus(); }); ipc.handle(IPC_CHANNELS.request.retryAnilistNow, async () => { return await deps.retryAnilistQueueNow(); }); ipc.handle(IPC_CHANNELS.request.appendClipboardVideoToQueue, () => { return deps.appendClipboardVideoToQueue(); }); }