refactor: extract shortcut and mining runtime deps

This commit is contained in:
2026-02-10 01:32:03 -08:00
parent b177be0831
commit a17c2296d5
6 changed files with 357 additions and 42 deletions

View File

@@ -16,7 +16,7 @@
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "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: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/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/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", "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", "generate:config-example": "pnpm run build && node dist/generate-config-example.js",
"start": "pnpm run build && electron . --start", "start": "pnpm run build && electron . --start",

View File

@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createCopyCurrentSubtitleDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMineSentenceCardDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
} from "./mining-runtime-deps-service";
test("mining runtime deps builders preserve references", () => {
const showMpvOsd = (_text: string) => {};
const writeClipboardText = (_text: string) => {};
const readClipboardText = () => "x";
const logError = (_message: string, _err: unknown) => {};
const subtitleTimingTracker = null;
const ankiIntegration = null;
const mpvClient = null;
const multiCopy = createHandleMultiCopyDigitDepsRuntimeService({
subtitleTimingTracker,
writeClipboardText,
showMpvOsd,
});
const copyCurrent = createCopyCurrentSubtitleDepsRuntimeService({
subtitleTimingTracker,
writeClipboardText,
showMpvOsd,
});
const updateLast = createUpdateLastCardFromClipboardDepsRuntimeService({
ankiIntegration,
readClipboardText,
showMpvOsd,
});
const fieldGrouping = createTriggerFieldGroupingDepsRuntimeService({
ankiIntegration,
showMpvOsd,
});
const markAudio = createMarkLastCardAsAudioCardDepsRuntimeService({
ankiIntegration,
showMpvOsd,
});
const mineCard = createMineSentenceCardDepsRuntimeService({
ankiIntegration,
mpvClient,
showMpvOsd,
});
const mineDigit = createHandleMineSentenceDigitDepsRuntimeService({
subtitleTimingTracker,
ankiIntegration,
getCurrentSecondarySubText: () => undefined,
showMpvOsd,
logError,
});
assert.equal(multiCopy.writeClipboardText, writeClipboardText);
assert.equal(copyCurrent.showMpvOsd, showMpvOsd);
assert.equal(updateLast.readClipboardText, readClipboardText);
assert.equal(fieldGrouping.ankiIntegration, ankiIntegration);
assert.equal(markAudio.showMpvOsd, showMpvOsd);
assert.equal(mineCard.mpvClient, mpvClient);
assert.equal(mineDigit.logError, logError);
});

View File

@@ -0,0 +1,107 @@
import {
copyCurrentSubtitleService,
handleMineSentenceDigitService,
handleMultiCopyDigitService,
markLastCardAsAudioCardService,
mineSentenceCardService,
triggerFieldGroupingService,
updateLastCardFromClipboardService,
} from "./mining-runtime-service";
export function createHandleMultiCopyDigitDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof handleMultiCopyDigitService>[1]["subtitleTimingTracker"];
writeClipboardText: Parameters<typeof handleMultiCopyDigitService>[1]["writeClipboardText"];
showMpvOsd: Parameters<typeof handleMultiCopyDigitService>[1]["showMpvOsd"];
},
): Parameters<typeof handleMultiCopyDigitService>[1] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
writeClipboardText: options.writeClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createCopyCurrentSubtitleDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof copyCurrentSubtitleService>[0]["subtitleTimingTracker"];
writeClipboardText: Parameters<typeof copyCurrentSubtitleService>[0]["writeClipboardText"];
showMpvOsd: Parameters<typeof copyCurrentSubtitleService>[0]["showMpvOsd"];
},
): Parameters<typeof copyCurrentSubtitleService>[0] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
writeClipboardText: options.writeClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createUpdateLastCardFromClipboardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof updateLastCardFromClipboardService>[0]["ankiIntegration"];
readClipboardText: Parameters<typeof updateLastCardFromClipboardService>[0]["readClipboardText"];
showMpvOsd: Parameters<typeof updateLastCardFromClipboardService>[0]["showMpvOsd"];
},
): Parameters<typeof updateLastCardFromClipboardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
readClipboardText: options.readClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createTriggerFieldGroupingDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof triggerFieldGroupingService>[0]["ankiIntegration"];
showMpvOsd: Parameters<typeof triggerFieldGroupingService>[0]["showMpvOsd"];
},
): Parameters<typeof triggerFieldGroupingService>[0] {
return {
ankiIntegration: options.ankiIntegration,
showMpvOsd: options.showMpvOsd,
};
}
export function createMarkLastCardAsAudioCardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof markLastCardAsAudioCardService>[0]["ankiIntegration"];
showMpvOsd: Parameters<typeof markLastCardAsAudioCardService>[0]["showMpvOsd"];
},
): Parameters<typeof markLastCardAsAudioCardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
showMpvOsd: options.showMpvOsd,
};
}
export function createMineSentenceCardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof mineSentenceCardService>[0]["ankiIntegration"];
mpvClient: Parameters<typeof mineSentenceCardService>[0]["mpvClient"];
showMpvOsd: Parameters<typeof mineSentenceCardService>[0]["showMpvOsd"];
},
): Parameters<typeof mineSentenceCardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
mpvClient: options.mpvClient,
showMpvOsd: options.showMpvOsd,
};
}
export function createHandleMineSentenceDigitDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof handleMineSentenceDigitService>[1]["subtitleTimingTracker"];
ankiIntegration: Parameters<typeof handleMineSentenceDigitService>[1]["ankiIntegration"];
getCurrentSecondarySubText: Parameters<typeof handleMineSentenceDigitService>[1]["getCurrentSecondarySubText"];
showMpvOsd: Parameters<typeof handleMineSentenceDigitService>[1]["showMpvOsd"];
logError: Parameters<typeof handleMineSentenceDigitService>[1]["logError"];
},
): Parameters<typeof handleMineSentenceDigitService>[1] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
ankiIntegration: options.ankiIntegration,
getCurrentSecondarySubText: options.getCurrentSecondarySubText,
showMpvOsd: options.showMpvOsd,
logError: options.logError,
};
}

View File

@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
} from "./overlay-shortcut-runtime-deps-service";
test("createOverlayShortcutRuntimeDepsService returns callable runtime deps", async () => {
const calls: string[] = [];
const deps = createOverlayShortcutRuntimeDepsService({
showMpvOsd: () => calls.push("showMpvOsd"),
openRuntimeOptions: () => calls.push("openRuntimeOptions"),
openJimaku: () => calls.push("openJimaku"),
markAudioCard: async () => {
calls.push("markAudioCard");
},
copySubtitleMultiple: () => calls.push("copySubtitleMultiple"),
copySubtitle: () => calls.push("copySubtitle"),
toggleSecondarySub: () => calls.push("toggleSecondarySub"),
updateLastCardFromClipboard: async () => {
calls.push("updateLastCardFromClipboard");
},
triggerFieldGrouping: async () => {
calls.push("triggerFieldGrouping");
},
triggerSubsync: async () => {
calls.push("triggerSubsync");
},
mineSentence: async () => {
calls.push("mineSentence");
},
mineSentenceMultiple: () => calls.push("mineSentenceMultiple"),
});
deps.copySubtitle();
await deps.mineSentence();
deps.mineSentenceMultiple(2);
assert.deepEqual(calls, ["copySubtitle", "mineSentence", "mineSentenceMultiple"]);
});
test("createOverlayShortcutLifecycleDepsRuntimeService returns lifecycle passthrough", () => {
const deps = createOverlayShortcutLifecycleDepsRuntimeService({
getConfiguredShortcuts: () => ({ actions: [] } as never),
getOverlayHandlers: () => ({} as never),
cancelPendingMultiCopy: () => {},
cancelPendingMineSentenceMultiple: () => {},
});
assert.ok(deps.getConfiguredShortcuts());
assert.ok(deps.getOverlayHandlers());
});

View File

@@ -0,0 +1,59 @@
import {
OverlayShortcutLifecycleDeps,
} from "./overlay-shortcut-lifecycle-service";
import {
OverlayShortcutRuntimeDeps,
} from "./overlay-shortcut-runtime-service";
export interface OverlayShortcutRuntimeDepsOptions {
showMpvOsd: (text: string) => void;
openRuntimeOptions: () => void;
openJimaku: () => void;
markAudioCard: () => Promise<void>;
copySubtitleMultiple: (timeoutMs: number) => void;
copySubtitle: () => void;
toggleSecondarySub: () => void;
updateLastCardFromClipboard: () => Promise<void>;
triggerFieldGrouping: () => Promise<void>;
triggerSubsync: () => Promise<void>;
mineSentence: () => Promise<void>;
mineSentenceMultiple: (timeoutMs: number) => void;
}
export interface OverlayShortcutLifecycleDepsOptions {
getConfiguredShortcuts: OverlayShortcutLifecycleDeps["getConfiguredShortcuts"];
getOverlayHandlers: OverlayShortcutLifecycleDeps["getOverlayHandlers"];
cancelPendingMultiCopy: () => void;
cancelPendingMineSentenceMultiple: () => void;
}
export function createOverlayShortcutRuntimeDepsService(
options: OverlayShortcutRuntimeDepsOptions,
): OverlayShortcutRuntimeDeps {
return {
showMpvOsd: options.showMpvOsd,
openRuntimeOptions: options.openRuntimeOptions,
openJimaku: options.openJimaku,
markAudioCard: options.markAudioCard,
copySubtitleMultiple: options.copySubtitleMultiple,
copySubtitle: options.copySubtitle,
toggleSecondarySub: options.toggleSecondarySub,
updateLastCardFromClipboard: options.updateLastCardFromClipboard,
triggerFieldGrouping: options.triggerFieldGrouping,
triggerSubsync: options.triggerSubsync,
mineSentence: options.mineSentence,
mineSentenceMultiple: options.mineSentenceMultiple,
};
}
export function createOverlayShortcutLifecycleDepsRuntimeService(
options: OverlayShortcutLifecycleDepsOptions,
): OverlayShortcutLifecycleDeps {
return {
getConfiguredShortcuts: options.getConfiguredShortcuts,
getOverlayHandlers: options.getOverlayHandlers,
cancelPendingMultiCopy: options.cancelPendingMultiCopy,
cancelPendingMineSentenceMultiple:
options.cancelPendingMineSentenceMultiple,
};
}

View File

@@ -207,6 +207,19 @@ import {
createOverlayWindowRuntimeDepsService, createOverlayWindowRuntimeDepsService,
createVisibleOverlayVisibilityDepsRuntimeService, createVisibleOverlayVisibilityDepsRuntimeService,
} from "./core/services/overlay-runtime-deps-service"; } from "./core/services/overlay-runtime-deps-service";
import {
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
} from "./core/services/overlay-shortcut-runtime-deps-service";
import {
createCopyCurrentSubtitleDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMineSentenceCardDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
} from "./core/services/mining-runtime-deps-service";
import { import {
createStartupAppReadyDepsRuntimeService, createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService, createStartupAppShutdownDepsRuntimeService,
@@ -905,7 +918,8 @@ function registerGlobalShortcuts(): void { registerGlobalShortcutsService({ shor
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); } function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
function getOverlayShortcutRuntimeHandlers() { function getOverlayShortcutRuntimeHandlers() {
return createOverlayShortcutRuntimeHandlers({ return createOverlayShortcutRuntimeHandlers(
createOverlayShortcutRuntimeDepsService({
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
openRuntimeOptions: () => { openRuntimeOptions: () => {
openRuntimeOptionsPalette(); openRuntimeOptionsPalette();
@@ -928,7 +942,8 @@ function getOverlayShortcutRuntimeHandlers() {
mineSentenceMultiple: (timeoutMs) => { mineSentenceMultiple: (timeoutMs) => {
startPendingMineSentenceMultiple(timeoutMs); startPendingMineSentenceMultiple(timeoutMs);
}, },
}); }),
);
} }
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean { function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
@@ -1034,49 +1049,62 @@ function startPendingMultiCopy(timeoutMs: number): void {
} }
function handleMultiCopyDigit(count: number): void { function handleMultiCopyDigit(count: number): void {
handleMultiCopyDigitService(count, { handleMultiCopyDigitService(
subtitleTimingTracker, count,
writeClipboardText: (text) => clipboard.writeText(text), createHandleMultiCopyDigitDepsRuntimeService({
showMpvOsd: (text) => showMpvOsd(text), subtitleTimingTracker,
}); writeClipboardText: (text) => clipboard.writeText(text),
showMpvOsd: (text) => showMpvOsd(text),
}),
);
} }
function copyCurrentSubtitle(): void { function copyCurrentSubtitle(): void {
copyCurrentSubtitleService({ copyCurrentSubtitleService(
subtitleTimingTracker, createCopyCurrentSubtitleDepsRuntimeService({
writeClipboardText: (text) => clipboard.writeText(text), subtitleTimingTracker,
showMpvOsd: (text) => showMpvOsd(text), writeClipboardText: (text) => clipboard.writeText(text),
}); showMpvOsd: (text) => showMpvOsd(text),
}),
);
} }
async function updateLastCardFromClipboard(): Promise<void> { async function updateLastCardFromClipboard(): Promise<void> {
await updateLastCardFromClipboardService({ await updateLastCardFromClipboardService(
ankiIntegration, createUpdateLastCardFromClipboardDepsRuntimeService({
readClipboardText: () => clipboard.readText(), ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), readClipboardText: () => clipboard.readText(),
}); showMpvOsd: (text) => showMpvOsd(text),
}),
);
} }
async function triggerFieldGrouping(): Promise<void> { async function triggerFieldGrouping(): Promise<void> {
await triggerFieldGroupingService({ await triggerFieldGroupingService(
ankiIntegration, createTriggerFieldGroupingDepsRuntimeService({
showMpvOsd: (text) => showMpvOsd(text), ankiIntegration,
}); showMpvOsd: (text) => showMpvOsd(text),
}),
);
} }
async function markLastCardAsAudioCard(): Promise<void> { async function markLastCardAsAudioCard(): Promise<void> {
await markLastCardAsAudioCardService({ await markLastCardAsAudioCardService(
ankiIntegration, createMarkLastCardAsAudioCardDepsRuntimeService({
showMpvOsd: (text) => showMpvOsd(text), ankiIntegration,
}); showMpvOsd: (text) => showMpvOsd(text),
}),
);
} }
async function mineSentenceCard(): Promise<void> { async function mineSentenceCard(): Promise<void> {
await mineSentenceCardService({ await mineSentenceCardService(
ankiIntegration, createMineSentenceCardDepsRuntimeService({
mpvClient, ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), mpvClient,
}); showMpvOsd: (text) => showMpvOsd(text),
}),
);
} }
function cancelPendingMineSentenceMultiple(): void { function cancelPendingMineSentenceMultiple(): void {
@@ -1096,15 +1124,19 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
} }
function handleMineSentenceDigit(count: number): void { function handleMineSentenceDigit(count: number): void {
handleMineSentenceDigitService(count, { handleMineSentenceDigitService(
subtitleTimingTracker, count,
ankiIntegration, createHandleMineSentenceDigitDepsRuntimeService({
getCurrentSecondarySubText: () => mpvClient?.currentSecondarySubText || undefined, subtitleTimingTracker,
showMpvOsd: (text) => showMpvOsd(text), ankiIntegration,
logError: (message, err) => { getCurrentSecondarySubText: () =>
console.error(message, err); mpvClient?.currentSecondarySubText || undefined,
}, showMpvOsd: (text) => showMpvOsd(text),
}); logError: (message, err) => {
console.error(message, err);
},
}),
);
} }
function registerOverlayShortcuts(): void { function registerOverlayShortcuts(): void {
@@ -1115,12 +1147,12 @@ function registerOverlayShortcuts(): void {
} }
function getOverlayShortcutLifecycleDeps() { function getOverlayShortcutLifecycleDeps() {
return { return createOverlayShortcutLifecycleDepsRuntimeService({
getConfiguredShortcuts: () => getConfiguredShortcuts(), getConfiguredShortcuts: () => getConfiguredShortcuts(),
getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers, getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers,
cancelPendingMultiCopy: () => cancelPendingMultiCopy(), cancelPendingMultiCopy: () => cancelPendingMultiCopy(),
cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(), cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(),
}; });
} }
function unregisterOverlayShortcuts(): void { function unregisterOverlayShortcuts(): void {