refactor: extract overlay bridge runtime helpers

This commit is contained in:
2026-02-09 23:35:39 -08:00
parent 53c2e6353a
commit 7715c1ddd2
4 changed files with 171 additions and 6 deletions

View File

@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
import { KikuFieldGroupingChoice } from "../../types";
import {
createFieldGroupingCallbackRuntimeService,
sendToVisibleOverlayRuntimeService,
} from "./overlay-bridge-runtime-service";
test("sendToVisibleOverlayRuntimeService restores visibility flag when opening hidden overlay modal", () => {
const sent: unknown[][] = [];
const restoreSet = new Set<"runtime-options" | "subsync">();
let visibleOverlayVisible = false;
const ok = sendToVisibleOverlayRuntimeService({
mainWindow: {
isDestroyed: () => false,
webContents: {
send: (...args: unknown[]) => {
sent.push(args);
},
},
} as unknown as Electron.BrowserWindow,
visibleOverlayVisible,
setVisibleOverlayVisible: (visible) => {
visibleOverlayVisible = visible;
},
channel: "runtime-options:open",
restoreOnModalClose: "runtime-options",
restoreVisibleOverlayOnModalClose: restoreSet,
});
assert.equal(ok, true);
assert.equal(visibleOverlayVisible, true);
assert.equal(restoreSet.has("runtime-options"), true);
assert.deepEqual(sent, [["runtime-options:open"]]);
});
test("createFieldGroupingCallbackRuntimeService cancels when overlay request cannot be sent", async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const callback = createFieldGroupingCallbackRuntimeService<
"runtime-options" | "subsync"
>({
getVisibleOverlayVisible: () => false,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver,
setResolver: (next) => {
resolver = next;
},
sendToVisibleOverlay: () => false,
});
const result = await callback({
original: {
noteId: 1,
expression: "a",
sentencePreview: "a",
hasAudio: false,
hasImage: false,
isOriginal: true,
},
duplicate: {
noteId: 2,
expression: "b",
sentencePreview: "b",
hasAudio: false,
hasImage: false,
isOriginal: false,
},
});
assert.equal(result.cancelled, true);
assert.equal(result.keepNoteId, 0);
assert.equal(result.deleteNoteId, 0);
});

View File

@@ -0,0 +1,61 @@
import {
KikuFieldGroupingChoice,
KikuFieldGroupingRequestData,
} from "../../types";
import { addOverlayModalRestoreFlagService } from "./overlay-modal-restore-service";
import { sendToVisibleOverlayService } from "./overlay-send-service";
import { createFieldGroupingCallbackService } from "./field-grouping-service";
import { BrowserWindow } from "electron";
export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
mainWindow: BrowserWindow | null;
visibleOverlayVisible: boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
channel: string;
payload?: unknown;
restoreOnModalClose?: T;
restoreVisibleOverlayOnModalClose: Set<T>;
}): boolean {
return sendToVisibleOverlayService({
mainWindow: options.mainWindow,
visibleOverlayVisible: options.visibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
channel: options.channel,
payload: options.payload,
restoreOnModalClose: options.restoreOnModalClose,
addRestoreFlag: (modal) =>
addOverlayModalRestoreFlagService(
options.restoreVisibleOverlayOnModalClose,
modal as T,
),
});
}
export function createFieldGroupingCallbackRuntimeService<T extends string>(
options: {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
) => void;
sendToVisibleOverlay: (
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: T },
) => boolean;
},
): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> {
return createFieldGroupingCallbackService({
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
setInvisibleOverlayVisible: options.setInvisibleOverlayVisible,
getResolver: options.getResolver,
setResolver: options.setResolver,
sendRequestToVisibleOverlay: (data) =>
options.sendToVisibleOverlay("kiku:field-grouping-request", data),
});
}

View File

@@ -170,7 +170,6 @@ import {
ensureOverlayWindowLevelService,
updateOverlayBoundsService,
} from "./core/services/overlay-window-service";
import { createFieldGroupingCallbackService } from "./core/services/field-grouping-service";
import { initializeOverlayRuntimeService } from "./core/services/overlay-runtime-init-service";
import {
setInvisibleOverlayVisibleService,
@@ -185,11 +184,13 @@ import { applyMpvSubtitleRenderMetricsPatchService } from "./core/services/mpv-r
import {
handleMpvCommandFromIpcService,
} from "./core/services/ipc-command-service";
import { sendToVisibleOverlayService } from "./core/services/overlay-send-service";
import {
addOverlayModalRestoreFlagService,
handleOverlayModalClosedService,
} from "./core/services/overlay-modal-restore-service";
import {
createFieldGroupingCallbackRuntimeService,
sendToVisibleOverlayRuntimeService,
} from "./core/services/overlay-bridge-runtime-service";
import {
runSubsyncManualFromIpcRuntimeService,
triggerSubsyncFromConfigRuntimeService,
@@ -1235,9 +1236,36 @@ registerIpcHandlersService({
* Create and show a desktop notification with robust icon handling.
* Supports both file paths (preferred on Linux/Wayland) and data URLs (fallback).
*/
function createFieldGroupingCallback() { return createFieldGroupingCallbackService({ getVisibleOverlayVisible: () => visibleOverlayVisible, getInvisibleOverlayVisible: () => invisibleOverlayVisible, setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible), getResolver: () => fieldGroupingResolver, setResolver: (resolver) => { fieldGroupingResolver = resolver; }, sendRequestToVisibleOverlay: (data) => sendToVisibleOverlay("kiku:field-grouping-request", data) }); }
function createFieldGroupingCallback() {
return createFieldGroupingCallbackRuntimeService({
getVisibleOverlayVisible: () => visibleOverlayVisible,
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
getResolver: () => fieldGroupingResolver,
setResolver: (resolver) => {
fieldGroupingResolver = resolver;
},
sendToVisibleOverlay: (
channel,
payload,
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
) =>
sendToVisibleOverlay(channel, payload, runtimeOptions),
});
}
function sendToVisibleOverlay(channel: string, payload?: unknown, options?: { restoreOnModalClose?: OverlayHostedModal }): boolean { return sendToVisibleOverlayService({ mainWindow, visibleOverlayVisible, setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), channel, payload, restoreOnModalClose: options?.restoreOnModalClose, addRestoreFlag: (modal) => addOverlayModalRestoreFlagService(restoreVisibleOverlayOnModalClose, modal as OverlayHostedModal) }); }
function sendToVisibleOverlay(channel: string, payload?: unknown, options?: { restoreOnModalClose?: OverlayHostedModal }): boolean {
return sendToVisibleOverlayRuntimeService({
mainWindow,
visibleOverlayVisible,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
channel,
payload,
restoreOnModalClose: options?.restoreOnModalClose,
restoreVisibleOverlayOnModalClose,
});
}
registerAnkiJimakuIpcRuntimeService({
patchAnkiConnectEnabled: (enabled) => { configService.patchRawConfig({ ankiConnect: { enabled } }); },