From 119f0da7a653168b549f7446398642b8f92e450d Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 10 Feb 2026 01:09:56 -0800 Subject: [PATCH] refactor: extract field grouping overlay runtime service --- package.json | 2 +- ...d-grouping-overlay-runtime-service.test.ts | 82 +++++++++++++++++++ .../field-grouping-overlay-runtime-service.ts | 77 +++++++++++++++++ src/main.ts | 55 ++++--------- 4 files changed, 176 insertions(+), 40 deletions(-) create mode 100644 src/core/services/field-grouping-overlay-runtime-service.test.ts create mode 100644 src/core/services/field-grouping-overlay-runtime-service.ts diff --git a/package.json b/package.json index 69c9f3b..22caa16 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/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-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 dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-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/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-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 dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-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/field-grouping-overlay-runtime-service.test.ts b/src/core/services/field-grouping-overlay-runtime-service.test.ts new file mode 100644 index 0000000..a6ff621 --- /dev/null +++ b/src/core/services/field-grouping-overlay-runtime-service.test.ts @@ -0,0 +1,82 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { KikuFieldGroupingChoice } from "../../types"; +import { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service"; + +test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets restore flag", () => { + const sent: unknown[][] = []; + let visible = false; + const restore = new Set<"runtime-options" | "subsync">(); + + const runtime = + createFieldGroupingOverlayRuntimeService<"runtime-options" | "subsync">({ + getMainWindow: () => ({ + isDestroyed: () => false, + webContents: { + send: (...args: unknown[]) => { + sent.push(args); + }, + }, + }), + getVisibleOverlayVisible: () => visible, + getInvisibleOverlayVisible: () => false, + setVisibleOverlayVisible: (next) => { + visible = next; + }, + setInvisibleOverlayVisible: () => {}, + getResolver: () => null, + setResolver: () => {}, + getRestoreVisibleOverlayOnModalClose: () => restore, + }); + + const ok = runtime.sendToVisibleOverlay("runtime-options:open", undefined, { + restoreOnModalClose: "runtime-options", + }); + + assert.equal(ok, true); + assert.equal(visible, true); + assert.equal(restore.has("runtime-options"), true); + assert.deepEqual(sent, [["runtime-options:open"]]); +}); + +test("createFieldGroupingOverlayRuntimeService callback cancels when send fails", async () => { + let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null; + const runtime = + createFieldGroupingOverlayRuntimeService<"runtime-options" | "subsync">({ + getMainWindow: () => null, + getVisibleOverlayVisible: () => false, + getInvisibleOverlayVisible: () => false, + setVisibleOverlayVisible: () => {}, + setInvisibleOverlayVisible: () => {}, + getResolver: () => resolver, + setResolver: (next) => { + resolver = next; + }, + getRestoreVisibleOverlayOnModalClose: () => + new Set<"runtime-options" | "subsync">(), + }); + + const callback = runtime.createFieldGroupingCallback(); + 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/field-grouping-overlay-runtime-service.ts b/src/core/services/field-grouping-overlay-runtime-service.ts new file mode 100644 index 0000000..21f4d6b --- /dev/null +++ b/src/core/services/field-grouping-overlay-runtime-service.ts @@ -0,0 +1,77 @@ +import { + KikuFieldGroupingChoice, + KikuFieldGroupingRequestData, +} from "../../types"; +import { + createFieldGroupingCallbackRuntimeService, + sendToVisibleOverlayRuntimeService, +} from "./overlay-bridge-runtime-service"; + +interface WindowLike { + isDestroyed: () => boolean; + webContents: { + send: (channel: string, payload?: unknown) => void; + }; +} + +export interface FieldGroupingOverlayRuntimeOptions { + getMainWindow: () => WindowLike | null; + getVisibleOverlayVisible: () => boolean; + getInvisibleOverlayVisible: () => boolean; + setVisibleOverlayVisible: (visible: boolean) => void; + setInvisibleOverlayVisible: (visible: boolean) => void; + getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; + setResolver: ( + resolver: ((choice: KikuFieldGroupingChoice) => void) | null, + ) => void; + getRestoreVisibleOverlayOnModalClose: () => Set; +} + +export function createFieldGroupingOverlayRuntimeService( + options: FieldGroupingOverlayRuntimeOptions, +): { + sendToVisibleOverlay: ( + channel: string, + payload?: unknown, + runtimeOptions?: { restoreOnModalClose?: T }, + ) => boolean; + createFieldGroupingCallback: () => ( + data: KikuFieldGroupingRequestData, + ) => Promise; +} { + const sendToVisibleOverlay = ( + channel: string, + payload?: unknown, + runtimeOptions?: { restoreOnModalClose?: T }, + ): boolean => { + return sendToVisibleOverlayRuntimeService({ + mainWindow: options.getMainWindow() as never, + visibleOverlayVisible: options.getVisibleOverlayVisible(), + setVisibleOverlayVisible: options.setVisibleOverlayVisible, + channel, + payload, + restoreOnModalClose: runtimeOptions?.restoreOnModalClose, + restoreVisibleOverlayOnModalClose: + options.getRestoreVisibleOverlayOnModalClose(), + }); + }; + + const createFieldGroupingCallback = (): (( + data: KikuFieldGroupingRequestData, + ) => Promise) => { + return createFieldGroupingCallbackRuntimeService({ + getVisibleOverlayVisible: options.getVisibleOverlayVisible, + getInvisibleOverlayVisible: options.getInvisibleOverlayVisible, + setVisibleOverlayVisible: options.setVisibleOverlayVisible, + setInvisibleOverlayVisible: options.setInvisibleOverlayVisible, + getResolver: options.getResolver, + setResolver: options.setResolver, + sendToVisibleOverlay, + }); + }; + + return { + sendToVisibleOverlay, + createFieldGroupingCallback, + }; +} diff --git a/src/main.ts b/src/main.ts index c8d68a4..e472e3e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -191,10 +191,6 @@ import { import { handleOverlayModalClosedService, } from "./core/services/overlay-modal-restore-service"; -import { - createFieldGroupingCallbackRuntimeService, - sendToVisibleOverlayRuntimeService, -} from "./core/services/overlay-bridge-runtime-service"; import { broadcastRuntimeOptionsChangedRuntimeService, broadcastToOverlayWindowsRuntimeService, @@ -208,6 +204,7 @@ import { createAppLifecycleDepsRuntimeService } from "./core/services/app-lifecy import { createCliCommandDepsRuntimeService } from "./core/services/cli-command-deps-runtime-service"; import { createIpcDepsRuntimeService } from "./core/services/ipc-deps-runtime-service"; import { createAnkiJimakuIpcDepsRuntimeService } from "./core/services/anki-jimaku-ipc-deps-runtime-service"; +import { createFieldGroupingOverlayRuntimeService } from "./core/services/field-grouping-overlay-runtime-service"; import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { @@ -320,6 +317,21 @@ let trackerNotReadyWarningShown = false; let overlayDebugVisualizationEnabled = false; type OverlayHostedModal = "runtime-options" | "subsync"; const restoreVisibleOverlayOnModalClose = new Set(); +const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService({ + getMainWindow: () => mainWindow, + getVisibleOverlayVisible: () => visibleOverlayVisible, + getInvisibleOverlayVisible: () => invisibleOverlayVisible, + setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), + setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible), + getResolver: () => fieldGroupingResolver, + setResolver: (resolver) => { + fieldGroupingResolver = resolver; + }, + getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose, +}); +const sendToVisibleOverlay = fieldGroupingOverlayRuntime.sendToVisibleOverlay; +const createFieldGroupingCallback = + fieldGroupingOverlayRuntime.createFieldGroupingCallback; const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions"); @@ -1289,41 +1301,6 @@ 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 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 sendToVisibleOverlayRuntimeService({ - mainWindow, - visibleOverlayVisible, - setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), - channel, - payload, - restoreOnModalClose: options?.restoreOnModalClose, - restoreVisibleOverlayOnModalClose, - }); -} - registerAnkiJimakuIpcRuntimeService( createAnkiJimakuIpcDepsRuntimeService({ patchAnkiConnectEnabled: (enabled) => {