mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-06 19:21:31 -07:00
233 lines
7.1 KiB
TypeScript
233 lines
7.1 KiB
TypeScript
import type { SubtitleTimingBlock } from '../../subtitle-timing-tracker';
|
|
|
|
interface SubtitleTimingTrackerLike {
|
|
getRecentBlocks: (count: number) => string[];
|
|
getRecentEntries?: (count: number) => SubtitleTimingBlock[];
|
|
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<boolean>;
|
|
}
|
|
|
|
interface MpvClientLike {
|
|
connected: boolean;
|
|
currentSubText: string;
|
|
currentSubStart: number;
|
|
currentSubEnd: number;
|
|
currentSecondarySubText?: string;
|
|
requestProperty?: (name: string) => Promise<unknown>;
|
|
}
|
|
|
|
export function handleMultiCopyDigit(
|
|
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 copyCurrentSubtitle(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;
|
|
}
|
|
|
|
function getSecondarySubTextForMinedBlocks(
|
|
entries: SubtitleTimingBlock[] | undefined,
|
|
getCurrentSecondarySubText: () => string | undefined,
|
|
): string | undefined {
|
|
const secondaryBlocks = entries
|
|
?.map((entry) => entry.secondaryText?.trim())
|
|
.filter((text): text is string => Boolean(text));
|
|
if (secondaryBlocks && secondaryBlocks.length > 0) {
|
|
return secondaryBlocks.join(' ');
|
|
}
|
|
return getCurrentSecondarySubText();
|
|
}
|
|
|
|
function normalizeSecondarySubText(text: unknown, primaryText: string): string | undefined {
|
|
if (typeof text !== 'string') {
|
|
return undefined;
|
|
}
|
|
const trimmed = text.trim();
|
|
if (!trimmed || trimmed === primaryText.trim()) {
|
|
return undefined;
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
async function getCurrentSecondarySubTextForSentenceCard(
|
|
mpvClient: MpvClientLike,
|
|
): Promise<string | undefined> {
|
|
const primaryText = mpvClient.currentSubText;
|
|
if (mpvClient.requestProperty) {
|
|
try {
|
|
const latestSecondaryText = await mpvClient.requestProperty('secondary-sub-text');
|
|
return normalizeSecondarySubText(latestSecondaryText, primaryText);
|
|
} catch {
|
|
// Fall back to the cached secondary subtitle below.
|
|
}
|
|
}
|
|
return normalizeSecondarySubText(mpvClient.currentSecondarySubText, primaryText);
|
|
}
|
|
|
|
export async function updateLastCardFromClipboard(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 triggerFieldGrouping(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 markLastCardAsAudioCard(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 mineSentenceCard(deps: {
|
|
ankiIntegration: AnkiIntegrationLike | null;
|
|
mpvClient: MpvClientLike | null;
|
|
showMpvOsd: (text: string) => void;
|
|
}): Promise<boolean> {
|
|
const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd);
|
|
if (!anki) return false;
|
|
|
|
const mpvClient = deps.mpvClient;
|
|
if (!mpvClient || !mpvClient.connected) {
|
|
deps.showMpvOsd('MPV not connected');
|
|
return false;
|
|
}
|
|
if (!mpvClient.currentSubText) {
|
|
deps.showMpvOsd('No current subtitle');
|
|
return false;
|
|
}
|
|
|
|
const secondarySubText = await getCurrentSecondarySubTextForSentenceCard(mpvClient);
|
|
return await anki.createSentenceCard(
|
|
mpvClient.currentSubText,
|
|
mpvClient.currentSubStart,
|
|
mpvClient.currentSubEnd,
|
|
secondarySubText,
|
|
);
|
|
}
|
|
|
|
export function handleMineSentenceDigit(
|
|
count: number,
|
|
deps: {
|
|
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
|
ankiIntegration: AnkiIntegrationLike | null;
|
|
getCurrentSecondarySubText: () => string | undefined;
|
|
showMpvOsd: (text: string) => void;
|
|
logError: (message: string, err: unknown) => void;
|
|
onCardsMined?: (count: number) => void;
|
|
},
|
|
): void {
|
|
if (!deps.subtitleTimingTracker || !deps.ankiIntegration) return;
|
|
|
|
const entries = deps.subtitleTimingTracker.getRecentEntries?.(count);
|
|
const blocks =
|
|
entries?.map((entry) => entry.displayText) ?? deps.subtitleTimingTracker.getRecentBlocks(count);
|
|
if (blocks.length === 0) {
|
|
deps.showMpvOsd('No subtitle history available');
|
|
return;
|
|
}
|
|
|
|
const timings: { startTime: number; endTime: number }[] =
|
|
entries ??
|
|
blocks.flatMap((block) => {
|
|
const timing = deps.subtitleTimingTracker?.findTiming(block);
|
|
return timing ? [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(' ');
|
|
const secondarySubText = getSecondarySubTextForMinedBlocks(
|
|
entries,
|
|
deps.getCurrentSecondarySubText,
|
|
);
|
|
const cardsToMine = 1;
|
|
deps.ankiIntegration
|
|
.createSentenceCard(sentence, rangeStart, rangeEnd, secondarySubText)
|
|
.then((created) => {
|
|
if (created) {
|
|
deps.onCardsMined?.(cardsToMine);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
deps.logError('mineSentenceMultiple failed:', err);
|
|
deps.showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
|
});
|
|
}
|