/* * SubMiner - All-in-one sentence mining overlay * Copyright (C) 2024 sudacode * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import type { SubtitleData, SubtitlePosition, MecabStatus, Keybinding, ElectronAPI, SecondarySubMode, SubtitleStyleConfig, JimakuMediaInfo, JimakuSearchQuery, JimakuFilesQuery, JimakuDownloadQuery, JimakuEntry, JimakuFileEntry, JimakuApiResponse, JimakuDownloadResult, SubsyncManualPayload, SubsyncManualRunRequest, SubsyncResult, ClipboardAppendResult, KikuFieldGroupingRequestData, KikuFieldGroupingChoice, KikuMergePreviewRequest, KikuMergePreviewResponse, RuntimeOptionApplyResult, RuntimeOptionId, RuntimeOptionState, RuntimeOptionValue, OverlayContentMeasurement, ShortcutsConfig, ConfigHotReloadPayload, } from './types'; import { IPC_CHANNELS } from './shared/ipc/contracts'; const overlayLayerArg = process.argv.find((arg) => arg.startsWith('--overlay-layer=')); const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length); const overlayLayer = overlayLayerFromArg === 'visible' || overlayLayerFromArg === 'modal' ? overlayLayerFromArg : null; type EmptyListener = () => void; type PayloadedListener = (payload: T) => void; function createQueuedIpcListener( channel: string, ): (listener: EmptyListener) => void { let count = 0; const listeners: EmptyListener[] = []; const dispatch = (): void => { if (listeners.length === 0) { count += 1; return; } for (const listener of listeners) { listener(); } }; ipcRenderer.on(channel, () => { dispatch(); }); return (listener: EmptyListener): void => { listeners.push(listener); while (count > 0) { count -= 1; listener(); } }; } function createQueuedIpcListenerWithPayload( channel: string, normalize: (payload: unknown) => T, ): (listener: PayloadedListener) => void { const pending: T[] = []; const listeners: PayloadedListener[] = []; const dispatch = (payload: T): void => { if (listeners.length === 0) { pending.push(payload); return; } for (const listener of listeners) { listener(payload); } }; ipcRenderer.on(channel, (_event: IpcRendererEvent, payloadArg: unknown) => { dispatch(normalize(payloadArg)); }); return (listener: PayloadedListener): void => { listeners.push(listener); while (pending.length > 0) { const payload = pending.shift(); listener(payload as T); } }; } const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen); const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen); const onSubsyncManualOpenEvent = createQueuedIpcListenerWithPayload( IPC_CHANNELS.event.subsyncOpenManual, (payload) => payload as SubsyncManualPayload, ); const onKikuFieldGroupingRequestEvent = createQueuedIpcListenerWithPayload( IPC_CHANNELS.event.kikuFieldGroupingRequest, (payload) => payload as KikuFieldGroupingRequestData, ); const electronAPI: ElectronAPI = { getOverlayLayer: () => overlayLayer, onSubtitle: (callback: (data: SubtitleData) => void) => { ipcRenderer.on(IPC_CHANNELS.event.subtitleSet, (_event: IpcRendererEvent, data: SubtitleData) => callback(data), ); }, onVisibility: (callback: (visible: boolean) => void) => { ipcRenderer.on( IPC_CHANNELS.event.subtitleVisibility, (_event: IpcRendererEvent, visible: boolean) => callback(visible), ); }, onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => { ipcRenderer.on( IPC_CHANNELS.event.subtitlePositionSet, (_event: IpcRendererEvent, position: SubtitlePosition | null) => { callback(position); }, ); }, getOverlayVisibility: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getVisibleOverlayVisibility), getCurrentSubtitle: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitle), getCurrentSubtitleRaw: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleRaw), getCurrentSubtitleAss: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleAss), onSubtitleAss: (callback: (assText: string) => void) => { ipcRenderer.on( IPC_CHANNELS.event.subtitleAssSet, (_event: IpcRendererEvent, assText: string) => { callback(assText); }, ); }, setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => { ipcRenderer.send(IPC_CHANNELS.command.setIgnoreMouseEvents, ignore, options); }, openYomitanSettings: () => { ipcRenderer.send(IPC_CHANNELS.command.openYomitanSettings); }, getSubtitlePosition: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getSubtitlePosition), saveSubtitlePosition: (position: SubtitlePosition) => { ipcRenderer.send(IPC_CHANNELS.command.saveSubtitlePosition, position); }, getMecabStatus: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getMecabStatus), setMecabEnabled: (enabled: boolean) => { ipcRenderer.send(IPC_CHANNELS.command.setMecabEnabled, enabled); }, sendMpvCommand: (command: (string | number)[]) => { ipcRenderer.send(IPC_CHANNELS.command.mpvCommand, command); }, getKeybindings: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getKeybindings), getConfiguredShortcuts: (): Promise> => ipcRenderer.invoke(IPC_CHANNELS.request.getConfigShortcuts), getJimakuMediaInfo: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.jimakuGetMediaInfo), jimakuSearchEntries: (query: JimakuSearchQuery): Promise> => ipcRenderer.invoke(IPC_CHANNELS.request.jimakuSearchEntries, query), jimakuListFiles: (query: JimakuFilesQuery): Promise> => ipcRenderer.invoke(IPC_CHANNELS.request.jimakuListFiles, query), jimakuDownloadFile: (query: JimakuDownloadQuery): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.jimakuDownloadFile, query), quitApp: () => { ipcRenderer.send(IPC_CHANNELS.command.quitApp); }, toggleDevTools: () => { ipcRenderer.send(IPC_CHANNELS.command.toggleDevTools); }, toggleOverlay: () => { ipcRenderer.send(IPC_CHANNELS.command.toggleOverlay); }, getAnkiConnectStatus: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getAnkiConnectStatus), setAnkiConnectEnabled: (enabled: boolean) => { ipcRenderer.send(IPC_CHANNELS.command.setAnkiConnectEnabled, enabled); }, clearAnkiConnectHistory: () => { ipcRenderer.send(IPC_CHANNELS.command.clearAnkiConnectHistory); }, onSecondarySub: (callback: (text: string) => void) => { ipcRenderer.on( IPC_CHANNELS.event.secondarySubtitleSet, (_event: IpcRendererEvent, text: string) => callback(text), ); }, onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => { ipcRenderer.on( IPC_CHANNELS.event.secondarySubtitleMode, (_event: IpcRendererEvent, mode: SecondarySubMode) => callback(mode), ); }, getSecondarySubMode: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getSecondarySubMode), getCurrentSecondarySub: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSecondarySub), focusMainWindow: () => ipcRenderer.invoke(IPC_CHANNELS.request.focusMainWindow) as Promise, getSubtitleStyle: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getSubtitleStyle), onSubsyncManualOpen: onSubsyncManualOpenEvent, runSubsyncManual: (request: SubsyncManualRunRequest): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.runSubsyncManual, request), onKikuFieldGroupingRequest: onKikuFieldGroupingRequestEvent, kikuBuildMergePreview: (request: KikuMergePreviewRequest): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.kikuBuildMergePreview, request), kikuFieldGroupingRespond: (choice: KikuFieldGroupingChoice) => { ipcRenderer.send(IPC_CHANNELS.command.kikuFieldGroupingRespond, choice); }, getRuntimeOptions: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.getRuntimeOptions), setRuntimeOptionValue: ( id: RuntimeOptionId, value: RuntimeOptionValue, ): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.setRuntimeOption, id, value), cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.cycleRuntimeOption, id, direction), onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => { ipcRenderer.on( IPC_CHANNELS.event.runtimeOptionsChanged, (_event: IpcRendererEvent, options: RuntimeOptionState[]) => { callback(options); }, ); }, onOpenRuntimeOptions: onOpenRuntimeOptionsEvent, onOpenJimaku: onOpenJimakuEvent, appendClipboardVideoToQueue: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue), notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => { ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal); }, notifyOverlayModalOpened: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => { ipcRenderer.send(IPC_CHANNELS.command.overlayModalOpened, modal); }, reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => { ipcRenderer.send(IPC_CHANNELS.command.reportOverlayContentBounds, measurement); }, onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => { ipcRenderer.on( IPC_CHANNELS.event.configHotReload, (_event: IpcRendererEvent, payload: ConfigHotReloadPayload) => { callback(payload); }, ); }, }; contextBridge.exposeInMainWorld('electronAPI', electronAPI);