refactor: extract shortcut ui runtime deps

This commit is contained in:
2026-02-10 01:36:27 -08:00
parent a17c2296d5
commit cb93601e16
6 changed files with 246 additions and 28 deletions

View File

@@ -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",

View File

@@ -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();

View File

@@ -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);
});

View File

@@ -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(),
);
}

View File

@@ -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) {

View File

@@ -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 {