diff --git a/package.json b/package.json index e8bdad3..9c23b35 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "test:config": "pnpm run build && node --test dist/config/config.test.js", - "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js", + "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js", "test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js", "generate:config-example": "pnpm run build && node dist/generate-config-example.js", "start": "pnpm run build && electron . --start", diff --git a/src/core/services/overlay-bridge-runtime-service.test.ts b/src/core/services/overlay-bridge-runtime-service.test.ts new file mode 100644 index 0000000..a9284bd --- /dev/null +++ b/src/core/services/overlay-bridge-runtime-service.test.ts @@ -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); +}); diff --git a/src/core/services/overlay-bridge-runtime-service.ts b/src/core/services/overlay-bridge-runtime-service.ts new file mode 100644 index 0000000..9e2d8eb --- /dev/null +++ b/src/core/services/overlay-bridge-runtime-service.ts @@ -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(options: { + mainWindow: BrowserWindow | null; + visibleOverlayVisible: boolean; + setVisibleOverlayVisible: (visible: boolean) => void; + channel: string; + payload?: unknown; + restoreOnModalClose?: T; + restoreVisibleOverlayOnModalClose: Set; +}): 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( + 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 { + 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), + }); +} diff --git a/src/main.ts b/src/main.ts index f676bc1..c23de5b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 } }); },