diff --git a/package.json b/package.json index d5a2e60..b005c03 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/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", "generate:config-example": "pnpm run build && node dist/generate-config-example.js", "start": "pnpm run build && electron . --start", diff --git a/src/core/services/mining-runtime-deps-service.test.ts b/src/core/services/mining-runtime-deps-service.test.ts new file mode 100644 index 0000000..5b49637 --- /dev/null +++ b/src/core/services/mining-runtime-deps-service.test.ts @@ -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); +}); diff --git a/src/core/services/mining-runtime-deps-service.ts b/src/core/services/mining-runtime-deps-service.ts new file mode 100644 index 0000000..d33c3ec --- /dev/null +++ b/src/core/services/mining-runtime-deps-service.ts @@ -0,0 +1,107 @@ +import { + copyCurrentSubtitleService, + handleMineSentenceDigitService, + handleMultiCopyDigitService, + markLastCardAsAudioCardService, + mineSentenceCardService, + triggerFieldGroupingService, + updateLastCardFromClipboardService, +} from "./mining-runtime-service"; + +export function createHandleMultiCopyDigitDepsRuntimeService( + options: { + subtitleTimingTracker: Parameters[1]["subtitleTimingTracker"]; + writeClipboardText: Parameters[1]["writeClipboardText"]; + showMpvOsd: Parameters[1]["showMpvOsd"]; + }, +): Parameters[1] { + return { + subtitleTimingTracker: options.subtitleTimingTracker, + writeClipboardText: options.writeClipboardText, + showMpvOsd: options.showMpvOsd, + }; +} + +export function createCopyCurrentSubtitleDepsRuntimeService( + options: { + subtitleTimingTracker: Parameters[0]["subtitleTimingTracker"]; + writeClipboardText: Parameters[0]["writeClipboardText"]; + showMpvOsd: Parameters[0]["showMpvOsd"]; + }, +): Parameters[0] { + return { + subtitleTimingTracker: options.subtitleTimingTracker, + writeClipboardText: options.writeClipboardText, + showMpvOsd: options.showMpvOsd, + }; +} + +export function createUpdateLastCardFromClipboardDepsRuntimeService( + options: { + ankiIntegration: Parameters[0]["ankiIntegration"]; + readClipboardText: Parameters[0]["readClipboardText"]; + showMpvOsd: Parameters[0]["showMpvOsd"]; + }, +): Parameters[0] { + return { + ankiIntegration: options.ankiIntegration, + readClipboardText: options.readClipboardText, + showMpvOsd: options.showMpvOsd, + }; +} + +export function createTriggerFieldGroupingDepsRuntimeService( + options: { + ankiIntegration: Parameters[0]["ankiIntegration"]; + showMpvOsd: Parameters[0]["showMpvOsd"]; + }, +): Parameters[0] { + return { + ankiIntegration: options.ankiIntegration, + showMpvOsd: options.showMpvOsd, + }; +} + +export function createMarkLastCardAsAudioCardDepsRuntimeService( + options: { + ankiIntegration: Parameters[0]["ankiIntegration"]; + showMpvOsd: Parameters[0]["showMpvOsd"]; + }, +): Parameters[0] { + return { + ankiIntegration: options.ankiIntegration, + showMpvOsd: options.showMpvOsd, + }; +} + +export function createMineSentenceCardDepsRuntimeService( + options: { + ankiIntegration: Parameters[0]["ankiIntegration"]; + mpvClient: Parameters[0]["mpvClient"]; + showMpvOsd: Parameters[0]["showMpvOsd"]; + }, +): Parameters[0] { + return { + ankiIntegration: options.ankiIntegration, + mpvClient: options.mpvClient, + showMpvOsd: options.showMpvOsd, + }; +} + +export function createHandleMineSentenceDigitDepsRuntimeService( + options: { + subtitleTimingTracker: Parameters[1]["subtitleTimingTracker"]; + ankiIntegration: Parameters[1]["ankiIntegration"]; + getCurrentSecondarySubText: Parameters[1]["getCurrentSecondarySubText"]; + showMpvOsd: Parameters[1]["showMpvOsd"]; + logError: Parameters[1]["logError"]; + }, +): Parameters[1] { + return { + subtitleTimingTracker: options.subtitleTimingTracker, + ankiIntegration: options.ankiIntegration, + getCurrentSecondarySubText: options.getCurrentSecondarySubText, + showMpvOsd: options.showMpvOsd, + logError: options.logError, + }; +} diff --git a/src/core/services/overlay-shortcut-runtime-deps-service.test.ts b/src/core/services/overlay-shortcut-runtime-deps-service.test.ts new file mode 100644 index 0000000..f3be2f1 --- /dev/null +++ b/src/core/services/overlay-shortcut-runtime-deps-service.test.ts @@ -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()); +}); diff --git a/src/core/services/overlay-shortcut-runtime-deps-service.ts b/src/core/services/overlay-shortcut-runtime-deps-service.ts new file mode 100644 index 0000000..40699df --- /dev/null +++ b/src/core/services/overlay-shortcut-runtime-deps-service.ts @@ -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; + copySubtitleMultiple: (timeoutMs: number) => void; + copySubtitle: () => void; + toggleSecondarySub: () => void; + updateLastCardFromClipboard: () => Promise; + triggerFieldGrouping: () => Promise; + triggerSubsync: () => Promise; + mineSentence: () => Promise; + 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, + }; +} diff --git a/src/main.ts b/src/main.ts index 0f5eaaf..b5b6da4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -207,6 +207,19 @@ import { createOverlayWindowRuntimeDepsService, createVisibleOverlayVisibilityDepsRuntimeService, } 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 { createStartupAppReadyDepsRuntimeService, createStartupAppShutdownDepsRuntimeService, @@ -905,7 +918,8 @@ function registerGlobalShortcuts(): void { registerGlobalShortcutsService({ shor function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); } function getOverlayShortcutRuntimeHandlers() { - return createOverlayShortcutRuntimeHandlers({ + return createOverlayShortcutRuntimeHandlers( + createOverlayShortcutRuntimeDepsService({ showMpvOsd: (text) => showMpvOsd(text), openRuntimeOptions: () => { openRuntimeOptionsPalette(); @@ -928,7 +942,8 @@ function getOverlayShortcutRuntimeHandlers() { mineSentenceMultiple: (timeoutMs) => { startPendingMineSentenceMultiple(timeoutMs); }, - }); + }), + ); } function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean { @@ -1034,49 +1049,62 @@ function startPendingMultiCopy(timeoutMs: number): void { } function handleMultiCopyDigit(count: number): void { - handleMultiCopyDigitService(count, { - subtitleTimingTracker, - writeClipboardText: (text) => clipboard.writeText(text), - showMpvOsd: (text) => showMpvOsd(text), - }); + handleMultiCopyDigitService( + count, + createHandleMultiCopyDigitDepsRuntimeService({ + subtitleTimingTracker, + writeClipboardText: (text) => clipboard.writeText(text), + showMpvOsd: (text) => showMpvOsd(text), + }), + ); } function copyCurrentSubtitle(): void { - copyCurrentSubtitleService({ - subtitleTimingTracker, - writeClipboardText: (text) => clipboard.writeText(text), - showMpvOsd: (text) => showMpvOsd(text), - }); + copyCurrentSubtitleService( + createCopyCurrentSubtitleDepsRuntimeService({ + subtitleTimingTracker, + writeClipboardText: (text) => clipboard.writeText(text), + showMpvOsd: (text) => showMpvOsd(text), + }), + ); } async function updateLastCardFromClipboard(): Promise { - await updateLastCardFromClipboardService({ - ankiIntegration, - readClipboardText: () => clipboard.readText(), - showMpvOsd: (text) => showMpvOsd(text), - }); + await updateLastCardFromClipboardService( + createUpdateLastCardFromClipboardDepsRuntimeService({ + ankiIntegration, + readClipboardText: () => clipboard.readText(), + showMpvOsd: (text) => showMpvOsd(text), + }), + ); } async function triggerFieldGrouping(): Promise { - await triggerFieldGroupingService({ - ankiIntegration, - showMpvOsd: (text) => showMpvOsd(text), - }); + await triggerFieldGroupingService( + createTriggerFieldGroupingDepsRuntimeService({ + ankiIntegration, + showMpvOsd: (text) => showMpvOsd(text), + }), + ); } async function markLastCardAsAudioCard(): Promise { - await markLastCardAsAudioCardService({ - ankiIntegration, - showMpvOsd: (text) => showMpvOsd(text), - }); + await markLastCardAsAudioCardService( + createMarkLastCardAsAudioCardDepsRuntimeService({ + ankiIntegration, + showMpvOsd: (text) => showMpvOsd(text), + }), + ); } async function mineSentenceCard(): Promise { - await mineSentenceCardService({ - ankiIntegration, - mpvClient, - showMpvOsd: (text) => showMpvOsd(text), - }); + await mineSentenceCardService( + createMineSentenceCardDepsRuntimeService({ + ankiIntegration, + mpvClient, + showMpvOsd: (text) => showMpvOsd(text), + }), + ); } function cancelPendingMineSentenceMultiple(): void { @@ -1096,15 +1124,19 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void { } function handleMineSentenceDigit(count: number): void { - handleMineSentenceDigitService(count, { - subtitleTimingTracker, - ankiIntegration, - getCurrentSecondarySubText: () => mpvClient?.currentSecondarySubText || undefined, - showMpvOsd: (text) => showMpvOsd(text), - logError: (message, err) => { - console.error(message, err); - }, - }); + handleMineSentenceDigitService( + count, + createHandleMineSentenceDigitDepsRuntimeService({ + subtitleTimingTracker, + ankiIntegration, + getCurrentSecondarySubText: () => + mpvClient?.currentSecondarySubText || undefined, + showMpvOsd: (text) => showMpvOsd(text), + logError: (message, err) => { + console.error(message, err); + }, + }), + ); } function registerOverlayShortcuts(): void { @@ -1115,12 +1147,12 @@ function registerOverlayShortcuts(): void { } function getOverlayShortcutLifecycleDeps() { - return { + return createOverlayShortcutLifecycleDepsRuntimeService({ getConfiguredShortcuts: () => getConfiguredShortcuts(), getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers, cancelPendingMultiCopy: () => cancelPendingMultiCopy(), cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(), - }; + }); } function unregisterOverlayShortcuts(): void {