From cb93601e16dc22be2501f4f6e77c8a91eb6e298a Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 10 Feb 2026 01:36:27 -0800 Subject: [PATCH] refactor: extract shortcut ui runtime deps --- package.json | 2 +- src/core/services/shortcut-service.ts | 8 +- .../shortcut-ui-runtime-deps-service.test.ts | 97 +++++++++++++++++++ .../shortcut-ui-runtime-deps-service.ts | 83 ++++++++++++++++ src/core/services/yomitan-settings-service.ts | 8 +- src/main.ts | 76 ++++++++++----- 6 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 src/core/services/shortcut-ui-runtime-deps-service.test.ts create mode 100644 src/core/services/shortcut-ui-runtime-deps-service.ts diff --git a/package.json b/package.json index b005c03..504e3ae 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/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-service.test.js dist/core/services/startup-lifecycle-runtime-deps-service.test.js dist/core/services/overlay-shortcut-runtime-deps-service.test.js dist/core/services/mining-runtime-deps-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/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-service.test.js dist/core/services/startup-lifecycle-runtime-deps-service.test.js dist/core/services/overlay-shortcut-runtime-deps-service.test.js dist/core/services/mining-runtime-deps-service.test.js dist/core/services/shortcut-ui-runtime-deps-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/shortcut-service.ts b/src/core/services/shortcut-service.ts index 6e00ec0..c7f17a0 100644 --- a/src/core/services/shortcut-service.ts +++ b/src/core/services/shortcut-service.ts @@ -5,14 +5,18 @@ export interface GlobalShortcutConfig { toggleInvisibleOverlayGlobal: string | null | undefined; } -export function registerGlobalShortcutsService(options: { +export interface RegisterGlobalShortcutsServiceOptions { shortcuts: GlobalShortcutConfig; onToggleVisibleOverlay: () => void; onToggleInvisibleOverlay: () => void; onOpenYomitanSettings: () => void; isDev: boolean; getMainWindow: () => BrowserWindow | null; -}): void { +} + +export function registerGlobalShortcutsService( + options: RegisterGlobalShortcutsServiceOptions, +): void { const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal; const invisibleShortcut = options.shortcuts.toggleInvisibleOverlayGlobal; const normalizedVisible = visibleShortcut?.replace(/\s+/g, "").toLowerCase(); diff --git a/src/core/services/shortcut-ui-runtime-deps-service.test.ts b/src/core/services/shortcut-ui-runtime-deps-service.test.ts new file mode 100644 index 0000000..0f5e592 --- /dev/null +++ b/src/core/services/shortcut-ui-runtime-deps-service.test.ts @@ -0,0 +1,97 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + createGlobalShortcutRegistrationDepsRuntimeService, + createSecondarySubtitleCycleDepsRuntimeService, + createYomitanSettingsWindowDepsRuntimeService, + runOverlayShortcutLocalFallbackRuntimeService, +} from "./shortcut-ui-runtime-deps-service"; + +function makeOptions() { + return { + yomitanExt: null, + getYomitanSettingsWindow: () => null, + setYomitanSettingsWindow: () => {}, + + shortcuts: { + toggleVisibleOverlayGlobal: "Ctrl+Shift+O", + toggleInvisibleOverlayGlobal: "Ctrl+Alt+O", + }, + onToggleVisibleOverlay: () => {}, + onToggleInvisibleOverlay: () => {}, + onOpenYomitanSettings: () => {}, + isDev: false, + getMainWindow: () => null, + + getSecondarySubMode: () => "hover" as const, + setSecondarySubMode: () => {}, + getLastSecondarySubToggleAtMs: () => 0, + setLastSecondarySubToggleAtMs: () => {}, + broadcastSecondarySubMode: () => {}, + showMpvOsd: () => {}, + + getConfiguredShortcuts: () => ({ + toggleVisibleOverlayGlobal: null, + toggleInvisibleOverlayGlobal: null, + copySubtitle: null, + copySubtitleMultiple: null, + updateLastCardFromClipboard: null, + triggerFieldGrouping: null, + triggerSubsync: null, + mineSentence: null, + mineSentenceMultiple: null, + multiCopyTimeoutMs: 5000, + toggleSecondarySub: null, + markAudioCard: null, + openRuntimeOptions: "Ctrl+R", + openJimaku: null, + }), + getOverlayShortcutFallbackHandlers: () => ({ + openRuntimeOptions: () => {}, + openJimaku: () => {}, + markAudioCard: () => {}, + copySubtitleMultiple: () => {}, + copySubtitle: () => {}, + toggleSecondarySub: () => {}, + updateLastCardFromClipboard: () => {}, + triggerFieldGrouping: () => {}, + triggerSubsync: () => {}, + mineSentence: () => {}, + mineSentenceMultiple: () => {}, + }), + shortcutMatcher: () => false, + }; +} + +test("shortcut ui deps builders return expected adapters", () => { + const options = makeOptions(); + const yomitan = createYomitanSettingsWindowDepsRuntimeService(options); + const globalShortcuts = createGlobalShortcutRegistrationDepsRuntimeService(options); + const secondary = createSecondarySubtitleCycleDepsRuntimeService(options); + + assert.equal(yomitan.yomitanExt, null); + assert.equal(typeof globalShortcuts.onOpenYomitanSettings, "function"); + assert.equal(secondary.getSecondarySubMode(), "hover"); +}); + +test("runOverlayShortcutLocalFallbackRuntimeService delegates and returns boolean", () => { + const options = { + ...makeOptions(), + shortcutMatcher: () => true, + }; + + const handled = runOverlayShortcutLocalFallbackRuntimeService( + { + key: "r", + code: "KeyR", + alt: false, + control: true, + shift: false, + meta: false, + type: "keyDown", + } as Electron.Input, + options, + ); + + assert.equal(handled, true); +}); diff --git a/src/core/services/shortcut-ui-runtime-deps-service.ts b/src/core/services/shortcut-ui-runtime-deps-service.ts new file mode 100644 index 0000000..ae77c40 --- /dev/null +++ b/src/core/services/shortcut-ui-runtime-deps-service.ts @@ -0,0 +1,83 @@ +import { Extension } from "electron"; +import { SecondarySubMode } from "../../types"; +import { ConfiguredShortcuts } from "../utils/shortcut-config"; +import { CycleSecondarySubModeDeps } from "./secondary-subtitle-service"; +import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner"; +import { OpenYomitanSettingsWindowOptions } from "./yomitan-settings-service"; +import { RegisterGlobalShortcutsServiceOptions } from "./shortcut-service"; + +export interface ShortcutUiRuntimeDepsOptions { + yomitanExt: Extension | null; + getYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["getExistingWindow"]; + setYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["setWindow"]; + + shortcuts: RegisterGlobalShortcutsServiceOptions["shortcuts"]; + onToggleVisibleOverlay: () => void; + onToggleInvisibleOverlay: () => void; + onOpenYomitanSettings: () => void; + isDev: boolean; + getMainWindow: RegisterGlobalShortcutsServiceOptions["getMainWindow"]; + + getSecondarySubMode: () => SecondarySubMode; + setSecondarySubMode: (mode: SecondarySubMode) => void; + getLastSecondarySubToggleAtMs: () => number; + setLastSecondarySubToggleAtMs: (timestampMs: number) => void; + broadcastSecondarySubMode: (mode: SecondarySubMode) => void; + showMpvOsd: (text: string) => void; + + getConfiguredShortcuts: () => ConfiguredShortcuts; + getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers; + shortcutMatcher: ( + input: Electron.Input, + accelerator: string, + allowWhenRegistered?: boolean, + ) => boolean; +} + +export function createYomitanSettingsWindowDepsRuntimeService( + options: ShortcutUiRuntimeDepsOptions, +): OpenYomitanSettingsWindowOptions { + return { + yomitanExt: options.yomitanExt, + getExistingWindow: options.getYomitanSettingsWindow, + setWindow: options.setYomitanSettingsWindow, + }; +} + +export function createGlobalShortcutRegistrationDepsRuntimeService( + options: ShortcutUiRuntimeDepsOptions, +): RegisterGlobalShortcutsServiceOptions { + return { + shortcuts: options.shortcuts, + onToggleVisibleOverlay: options.onToggleVisibleOverlay, + onToggleInvisibleOverlay: options.onToggleInvisibleOverlay, + onOpenYomitanSettings: options.onOpenYomitanSettings, + isDev: options.isDev, + getMainWindow: options.getMainWindow, + }; +} + +export function createSecondarySubtitleCycleDepsRuntimeService( + options: ShortcutUiRuntimeDepsOptions, +): CycleSecondarySubModeDeps { + return { + getSecondarySubMode: options.getSecondarySubMode, + setSecondarySubMode: options.setSecondarySubMode, + getLastSecondarySubToggleAtMs: options.getLastSecondarySubToggleAtMs, + setLastSecondarySubToggleAtMs: options.setLastSecondarySubToggleAtMs, + broadcastSecondarySubMode: options.broadcastSecondarySubMode, + showMpvOsd: options.showMpvOsd, + }; +} + +export function runOverlayShortcutLocalFallbackRuntimeService( + input: Electron.Input, + options: ShortcutUiRuntimeDepsOptions, +): boolean { + return runOverlayShortcutLocalFallback( + input, + options.getConfiguredShortcuts(), + options.shortcutMatcher, + options.getOverlayShortcutFallbackHandlers(), + ); +} diff --git a/src/core/services/yomitan-settings-service.ts b/src/core/services/yomitan-settings-service.ts index 56bfdc0..ebe4950 100644 --- a/src/core/services/yomitan-settings-service.ts +++ b/src/core/services/yomitan-settings-service.ts @@ -1,10 +1,14 @@ import { BrowserWindow, Extension, session } from "electron"; -export function openYomitanSettingsWindow(options: { +export interface OpenYomitanSettingsWindowOptions { yomitanExt: Extension | null; getExistingWindow: () => BrowserWindow | null; setWindow: (window: BrowserWindow | null) => void; -}): void { +} + +export function openYomitanSettingsWindow( + options: OpenYomitanSettingsWindowOptions, +): void { console.log("openYomitanSettings called"); if (!options.yomitanExt) { diff --git a/src/main.ts b/src/main.ts index b5b6da4..edbd95b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -107,7 +107,6 @@ import { import { registerOverlayShortcutsService, } from "./core/services/overlay-shortcut-service"; -import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner"; import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-shortcut-runtime-service"; import { handleCliCommandService } from "./core/services/cli-command-service"; import { cycleSecondarySubModeService } from "./core/services/secondary-subtitle-service"; @@ -220,6 +219,12 @@ import { createTriggerFieldGroupingDepsRuntimeService, createUpdateLastCardFromClipboardDepsRuntimeService, } from "./core/services/mining-runtime-deps-service"; +import { + createGlobalShortcutRegistrationDepsRuntimeService, + createSecondarySubtitleCycleDepsRuntimeService, + createYomitanSettingsWindowDepsRuntimeService, + runOverlayShortcutLocalFallbackRuntimeService, +} from "./core/services/shortcut-ui-runtime-deps-service"; import { createStartupAppReadyDepsRuntimeService, createStartupAppShutdownDepsRuntimeService, @@ -912,8 +917,48 @@ function initializeOverlayRuntime(): void { overlayRuntimeInitialized = true; } -function openYomitanSettings(): void { openYomitanSettingsWindow({ yomitanExt, getExistingWindow: () => yomitanSettingsWindow, setWindow: (window) => (yomitanSettingsWindow = window) }); } -function registerGlobalShortcuts(): void { registerGlobalShortcutsService({ shortcuts: getConfiguredShortcuts(), onToggleVisibleOverlay: () => toggleVisibleOverlay(), onToggleInvisibleOverlay: () => toggleInvisibleOverlay(), onOpenYomitanSettings: () => openYomitanSettings(), isDev, getMainWindow: () => mainWindow }); } +function getShortcutUiRuntimeDeps() { + return { + yomitanExt, + getYomitanSettingsWindow: () => yomitanSettingsWindow, + setYomitanSettingsWindow: (window: BrowserWindow | null) => { + yomitanSettingsWindow = window; + }, + shortcuts: getConfiguredShortcuts(), + onToggleVisibleOverlay: () => toggleVisibleOverlay(), + onToggleInvisibleOverlay: () => toggleInvisibleOverlay(), + onOpenYomitanSettings: () => openYomitanSettings(), + isDev, + getMainWindow: () => mainWindow, + getSecondarySubMode: () => secondarySubMode, + setSecondarySubMode: (mode: SecondarySubMode) => { + secondarySubMode = mode; + }, + getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs, + setLastSecondarySubToggleAtMs: (timestampMs: number) => { + lastSecondarySubToggleAtMs = timestampMs; + }, + broadcastSecondarySubMode: (mode: SecondarySubMode) => { + broadcastToOverlayWindows("secondary-subtitle:mode", mode); + }, + showMpvOsd: (text: string) => showMpvOsd(text), + getConfiguredShortcuts: () => getConfiguredShortcuts(), + getOverlayShortcutFallbackHandlers: () => + getOverlayShortcutRuntimeHandlers().fallbackHandlers, + shortcutMatcher: shortcutMatchesInputForLocalFallback, + }; +} + +function openYomitanSettings(): void { + openYomitanSettingsWindow( + createYomitanSettingsWindowDepsRuntimeService(getShortcutUiRuntimeDeps()), + ); +} +function registerGlobalShortcuts(): void { + registerGlobalShortcutsService( + createGlobalShortcutRegistrationDepsRuntimeService(getShortcutUiRuntimeDeps()), + ); +} function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); } @@ -947,31 +992,16 @@ function getOverlayShortcutRuntimeHandlers() { } function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean { - const shortcuts = getConfiguredShortcuts(); - const handlers = getOverlayShortcutRuntimeHandlers(); - return runOverlayShortcutLocalFallback( + return runOverlayShortcutLocalFallbackRuntimeService( input, - shortcuts, - shortcutMatchesInputForLocalFallback, - handlers.fallbackHandlers, + getShortcutUiRuntimeDeps(), ); } function cycleSecondarySubMode(): void { - cycleSecondarySubModeService({ - getSecondarySubMode: () => secondarySubMode, - setSecondarySubMode: (mode) => { - secondarySubMode = mode; - }, - getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs, - setLastSecondarySubToggleAtMs: (timestampMs) => { - lastSecondarySubToggleAtMs = timestampMs; - }, - broadcastSecondarySubMode: (mode) => { - broadcastToOverlayWindows("secondary-subtitle:mode", mode); - }, - showMpvOsd: (text) => showMpvOsd(text), - }); + cycleSecondarySubModeService( + createSecondarySubtitleCycleDepsRuntimeService(getShortcutUiRuntimeDeps()), + ); } function showMpvOsd(text: string): void {