/*
* 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);