From 8ab04c3fa609923065a9bb769c0eee2a7688cef3 Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 9 Feb 2026 22:43:10 -0800 Subject: [PATCH] refactor: extract mining and clipboard runtime service --- src/core/services/mining-runtime-service.ts | 179 ++++++++++++++++++++ src/main.ts | 155 +++++------------ 2 files changed, 225 insertions(+), 109 deletions(-) create mode 100644 src/core/services/mining-runtime-service.ts diff --git a/src/core/services/mining-runtime-service.ts b/src/core/services/mining-runtime-service.ts new file mode 100644 index 0000000..1f7c932 --- /dev/null +++ b/src/core/services/mining-runtime-service.ts @@ -0,0 +1,179 @@ +interface SubtitleTimingTrackerLike { + getRecentBlocks: (count: number) => string[]; + getCurrentSubtitle: () => string | null; + findTiming: (text: string) => { startTime: number; endTime: number } | null; +} + +interface AnkiIntegrationLike { + updateLastAddedFromClipboard: (clipboardText: string) => Promise; + triggerFieldGroupingForLastAddedCard: () => Promise; + markLastCardAsAudioCard: () => Promise; + createSentenceCard: ( + sentence: string, + startTime: number, + endTime: number, + secondarySub?: string, + ) => Promise; +} + +interface MpvClientLike { + connected: boolean; + currentSubText: string; + currentSubStart: number; + currentSubEnd: number; + currentSecondarySubText?: string; +} + +export function handleMultiCopyDigitService( + count: number, + deps: { + subtitleTimingTracker: SubtitleTimingTrackerLike | null; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + }, +): void { + if (!deps.subtitleTimingTracker) return; + + const availableCount = Math.min(count, 200); + const blocks = deps.subtitleTimingTracker.getRecentBlocks(availableCount); + if (blocks.length === 0) { + deps.showMpvOsd("No subtitle history available"); + return; + } + + const actualCount = blocks.length; + deps.writeClipboardText(blocks.join("\n\n")); + if (actualCount < count) { + deps.showMpvOsd(`Only ${actualCount} lines available, copied ${actualCount}`); + } else { + deps.showMpvOsd(`Copied ${actualCount} lines`); + } +} + +export function copyCurrentSubtitleService(deps: { + subtitleTimingTracker: SubtitleTimingTrackerLike | null; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; +}): void { + if (!deps.subtitleTimingTracker) { + deps.showMpvOsd("Subtitle tracker not available"); + return; + } + const currentSubtitle = deps.subtitleTimingTracker.getCurrentSubtitle(); + if (!currentSubtitle) { + deps.showMpvOsd("No current subtitle"); + return; + } + deps.writeClipboardText(currentSubtitle); + deps.showMpvOsd("Copied subtitle"); +} + +function requireAnkiIntegration( + ankiIntegration: AnkiIntegrationLike | null, + showMpvOsd: (text: string) => void, +): AnkiIntegrationLike | null { + if (!ankiIntegration) { + showMpvOsd("AnkiConnect integration not enabled"); + return null; + } + return ankiIntegration; +} + +export async function updateLastCardFromClipboardService(deps: { + ankiIntegration: AnkiIntegrationLike | null; + readClipboardText: () => string; + showMpvOsd: (text: string) => void; +}): Promise { + const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd); + if (!anki) return; + await anki.updateLastAddedFromClipboard(deps.readClipboardText()); +} + +export async function triggerFieldGroupingService(deps: { + ankiIntegration: AnkiIntegrationLike | null; + showMpvOsd: (text: string) => void; +}): Promise { + const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd); + if (!anki) return; + await anki.triggerFieldGroupingForLastAddedCard(); +} + +export async function markLastCardAsAudioCardService(deps: { + ankiIntegration: AnkiIntegrationLike | null; + showMpvOsd: (text: string) => void; +}): Promise { + const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd); + if (!anki) return; + await anki.markLastCardAsAudioCard(); +} + +export async function mineSentenceCardService(deps: { + ankiIntegration: AnkiIntegrationLike | null; + mpvClient: MpvClientLike | null; + showMpvOsd: (text: string) => void; +}): Promise { + const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd); + if (!anki) return; + + const mpvClient = deps.mpvClient; + if (!mpvClient || !mpvClient.connected) { + deps.showMpvOsd("MPV not connected"); + return; + } + if (!mpvClient.currentSubText) { + deps.showMpvOsd("No current subtitle"); + return; + } + + await anki.createSentenceCard( + mpvClient.currentSubText, + mpvClient.currentSubStart, + mpvClient.currentSubEnd, + mpvClient.currentSecondarySubText || undefined, + ); +} + +export function handleMineSentenceDigitService( + count: number, + deps: { + subtitleTimingTracker: SubtitleTimingTrackerLike | null; + ankiIntegration: AnkiIntegrationLike | null; + getCurrentSecondarySubText: () => string | undefined; + showMpvOsd: (text: string) => void; + logError: (message: string, err: unknown) => void; + }, +): void { + if (!deps.subtitleTimingTracker || !deps.ankiIntegration) return; + + const blocks = deps.subtitleTimingTracker.getRecentBlocks(count); + if (blocks.length === 0) { + deps.showMpvOsd("No subtitle history available"); + return; + } + + const timings: { startTime: number; endTime: number }[] = []; + for (const block of blocks) { + const timing = deps.subtitleTimingTracker.findTiming(block); + if (timing) timings.push(timing); + } + + if (timings.length === 0) { + deps.showMpvOsd("Subtitle timing not found"); + return; + } + + const rangeStart = Math.min(...timings.map((t) => t.startTime)); + const rangeEnd = Math.max(...timings.map((t) => t.endTime)); + const sentence = blocks.join(" "); + deps.ankiIntegration + .createSentenceCard( + sentence, + rangeStart, + rangeEnd, + deps.getCurrentSecondarySubText(), + ) + .catch((err) => { + deps.logError("mineSentenceMultiple failed:", err); + deps.showMpvOsd(`Mine sentence failed: ${(err as Error).message}`); + }); +} diff --git a/src/main.ts b/src/main.ts index 9fb8c24..dfb0c44 100644 --- a/src/main.ts +++ b/src/main.ts @@ -118,6 +118,15 @@ import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-sh import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service"; import { handleCliCommandService } from "./core/services/cli-command-service"; import { cycleSecondarySubModeService } from "./core/services/secondary-subtitle-service"; +import { + copyCurrentSubtitleService, + handleMineSentenceDigitService, + handleMultiCopyDigitService, + markLastCardAsAudioCardService, + mineSentenceCardService, + triggerFieldGroupingService, + updateLastCardFromClipboardService, +} from "./core/services/mining-runtime-service"; import { showDesktopNotification } from "./core/utils/notification"; import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service"; import { tokenizeSubtitleService } from "./core/services/tokenizer-service"; @@ -928,96 +937,49 @@ function startPendingMultiCopy(timeoutMs: number): void { } function handleMultiCopyDigit(count: number): void { - if (!subtitleTimingTracker) return; - - const availableCount = Math.min(count, 200); // Max history size - const blocks = subtitleTimingTracker.getRecentBlocks(availableCount); - - if (blocks.length === 0) { - showMpvOsd("No subtitle history available"); - return; - } - - const actualCount = blocks.length; - const clipboardText = blocks.join("\n\n"); - clipboard.writeText(clipboardText); - - if (actualCount < count) { - showMpvOsd(`Only ${actualCount} lines available, copied ${actualCount}`); - } else { - showMpvOsd(`Copied ${actualCount} lines`); - } + handleMultiCopyDigitService(count, { + subtitleTimingTracker, + writeClipboardText: (text) => clipboard.writeText(text), + showMpvOsd: (text) => showMpvOsd(text), + }); } function copyCurrentSubtitle(): void { - if (!subtitleTimingTracker) { - showMpvOsd("Subtitle tracker not available"); - return; - } - - const currentSubtitle = subtitleTimingTracker.getCurrentSubtitle(); - if (!currentSubtitle) { - showMpvOsd("No current subtitle"); - return; - } - - clipboard.writeText(currentSubtitle); - showMpvOsd("Copied subtitle"); + copyCurrentSubtitleService({ + subtitleTimingTracker, + writeClipboardText: (text) => clipboard.writeText(text), + showMpvOsd: (text) => showMpvOsd(text), + }); } async function updateLastCardFromClipboard(): Promise { - if (!ankiIntegration) { - showMpvOsd("AnkiConnect integration not enabled"); - return; - } - - const clipboardText = clipboard.readText(); - await ankiIntegration.updateLastAddedFromClipboard(clipboardText); + await updateLastCardFromClipboardService({ + ankiIntegration, + readClipboardText: () => clipboard.readText(), + showMpvOsd: (text) => showMpvOsd(text), + }); } async function triggerFieldGrouping(): Promise { - if (!ankiIntegration) { - showMpvOsd("AnkiConnect integration not enabled"); - return; - } - await ankiIntegration.triggerFieldGroupingForLastAddedCard(); + await triggerFieldGroupingService({ + ankiIntegration, + showMpvOsd: (text) => showMpvOsd(text), + }); } async function markLastCardAsAudioCard(): Promise { - if (!ankiIntegration) { - showMpvOsd("AnkiConnect integration not enabled"); - return; - } - await ankiIntegration.markLastCardAsAudioCard(); + await markLastCardAsAudioCardService({ + ankiIntegration, + showMpvOsd: (text) => showMpvOsd(text), + }); } async function mineSentenceCard(): Promise { - if (!ankiIntegration) { - showMpvOsd("AnkiConnect integration not enabled"); - return; - } - - if (!mpvClient || !mpvClient.connected) { - showMpvOsd("MPV not connected"); - return; - } - - const text = mpvClient.currentSubText; - if (!text) { - showMpvOsd("No current subtitle"); - return; - } - - const startTime = mpvClient.currentSubStart; - const endTime = mpvClient.currentSubEnd; - const secondarySub = mpvClient.currentSecondarySubText || undefined; - - await ankiIntegration.createSentenceCard( - text, - startTime, - endTime, - secondarySub, - ); + await mineSentenceCardService({ + ankiIntegration, + mpvClient, + showMpvOsd: (text) => showMpvOsd(text), + }); } function cancelPendingMineSentenceMultiple(): void { @@ -1037,40 +999,15 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void { } function handleMineSentenceDigit(count: number): void { - if (!subtitleTimingTracker || !ankiIntegration) - return; - - const blocks = subtitleTimingTracker.getRecentBlocks(count); - - if (blocks.length === 0) { - showMpvOsd("No subtitle history available"); - return; - } - - const timings: { startTime: number; endTime: number }[] = []; - for (const block of blocks) { - const timing = subtitleTimingTracker.findTiming(block); - if (timing) { - timings.push(timing); - } - } - - if (timings.length === 0) { - showMpvOsd("Subtitle timing not found"); - return; - } - - const rangeStart = Math.min(...timings.map((t) => t.startTime)); - const rangeEnd = Math.max(...timings.map((t) => t.endTime)); - const sentence = blocks.join(" "); - - const secondarySub = mpvClient?.currentSecondarySubText || undefined; - ankiIntegration - .createSentenceCard(sentence, rangeStart, rangeEnd, secondarySub) - .catch((err) => { - console.error("mineSentenceMultiple failed:", err); - showMpvOsd(`Mine sentence failed: ${(err as Error).message}`); - }); + handleMineSentenceDigitService(count, { + subtitleTimingTracker, + ankiIntegration, + getCurrentSecondarySubText: () => mpvClient?.currentSecondarySubText || undefined, + showMpvOsd: (text) => showMpvOsd(text), + logError: (message, err) => { + console.error(message, err); + }, + }); } function registerOverlayShortcuts(): void {