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