refactor: extract mining and clipboard runtime service

This commit is contained in:
2026-02-09 22:43:10 -08:00
parent 469091a2a8
commit 8ab04c3fa6
2 changed files with 225 additions and 109 deletions

View File

@@ -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<void>;
triggerFieldGroupingForLastAddedCard: () => Promise<void>;
markLastCardAsAudioCard: () => Promise<void>;
createSentenceCard: (
sentence: string,
startTime: number,
endTime: number,
secondarySub?: string,
) => Promise<void>;
}
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<void> {
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<void> {
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<void> {
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<void> {
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}`);
});
}

View File

@@ -118,6 +118,15 @@ import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-sh
import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service"; import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service";
import { handleCliCommandService } from "./core/services/cli-command-service"; import { handleCliCommandService } from "./core/services/cli-command-service";
import { cycleSecondarySubModeService } from "./core/services/secondary-subtitle-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 { showDesktopNotification } from "./core/utils/notification";
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service"; import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
import { tokenizeSubtitleService } from "./core/services/tokenizer-service"; import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
@@ -928,96 +937,49 @@ function startPendingMultiCopy(timeoutMs: number): void {
} }
function handleMultiCopyDigit(count: number): void { function handleMultiCopyDigit(count: number): void {
if (!subtitleTimingTracker) return; handleMultiCopyDigitService(count, {
subtitleTimingTracker,
const availableCount = Math.min(count, 200); // Max history size writeClipboardText: (text) => clipboard.writeText(text),
const blocks = subtitleTimingTracker.getRecentBlocks(availableCount); showMpvOsd: (text) => showMpvOsd(text),
});
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`);
}
} }
function copyCurrentSubtitle(): void { function copyCurrentSubtitle(): void {
if (!subtitleTimingTracker) { copyCurrentSubtitleService({
showMpvOsd("Subtitle tracker not available"); subtitleTimingTracker,
return; writeClipboardText: (text) => clipboard.writeText(text),
} showMpvOsd: (text) => showMpvOsd(text),
});
const currentSubtitle = subtitleTimingTracker.getCurrentSubtitle();
if (!currentSubtitle) {
showMpvOsd("No current subtitle");
return;
}
clipboard.writeText(currentSubtitle);
showMpvOsd("Copied subtitle");
} }
async function updateLastCardFromClipboard(): Promise<void> { async function updateLastCardFromClipboard(): Promise<void> {
if (!ankiIntegration) { await updateLastCardFromClipboardService({
showMpvOsd("AnkiConnect integration not enabled"); ankiIntegration,
return; readClipboardText: () => clipboard.readText(),
} showMpvOsd: (text) => showMpvOsd(text),
});
const clipboardText = clipboard.readText();
await ankiIntegration.updateLastAddedFromClipboard(clipboardText);
} }
async function triggerFieldGrouping(): Promise<void> { async function triggerFieldGrouping(): Promise<void> {
if (!ankiIntegration) { await triggerFieldGroupingService({
showMpvOsd("AnkiConnect integration not enabled"); ankiIntegration,
return; showMpvOsd: (text) => showMpvOsd(text),
} });
await ankiIntegration.triggerFieldGroupingForLastAddedCard();
} }
async function markLastCardAsAudioCard(): Promise<void> { async function markLastCardAsAudioCard(): Promise<void> {
if (!ankiIntegration) { await markLastCardAsAudioCardService({
showMpvOsd("AnkiConnect integration not enabled"); ankiIntegration,
return; showMpvOsd: (text) => showMpvOsd(text),
} });
await ankiIntegration.markLastCardAsAudioCard();
} }
async function mineSentenceCard(): Promise<void> { async function mineSentenceCard(): Promise<void> {
if (!ankiIntegration) { await mineSentenceCardService({
showMpvOsd("AnkiConnect integration not enabled"); ankiIntegration,
return; mpvClient,
} showMpvOsd: (text) => showMpvOsd(text),
});
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,
);
} }
function cancelPendingMineSentenceMultiple(): void { function cancelPendingMineSentenceMultiple(): void {
@@ -1037,40 +999,15 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
} }
function handleMineSentenceDigit(count: number): void { function handleMineSentenceDigit(count: number): void {
if (!subtitleTimingTracker || !ankiIntegration) handleMineSentenceDigitService(count, {
return; subtitleTimingTracker,
ankiIntegration,
const blocks = subtitleTimingTracker.getRecentBlocks(count); getCurrentSecondarySubText: () => mpvClient?.currentSecondarySubText || undefined,
showMpvOsd: (text) => showMpvOsd(text),
if (blocks.length === 0) { logError: (message, err) => {
showMpvOsd("No subtitle history available"); console.error(message, err);
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}`);
});
} }
function registerOverlayShortcuts(): void { function registerOverlayShortcuts(): void {