mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix: delegate multi-line digit selection to visible overlay (#78)
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
handleMultiCopyDigit,
|
||||
mineSentenceCard,
|
||||
} from './mining';
|
||||
import { SubtitleTimingTracker } from '../../subtitle-timing-tracker';
|
||||
|
||||
test('copyCurrentSubtitle reports tracker and subtitle guards', () => {
|
||||
const osd: string[] = [];
|
||||
@@ -207,3 +208,76 @@ test('handleMineSentenceDigit increments successful card count', async () => {
|
||||
|
||||
assert.equal(cardsMined, 1);
|
||||
});
|
||||
|
||||
test('handleMineSentenceDigit keeps per-entry timings when subtitle text repeats', async () => {
|
||||
const created: Array<{ sentence: string; startTime: number; endTime: number }> = [];
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
|
||||
try {
|
||||
tracker.recordSubtitle('same', 1, 2);
|
||||
tracker.recordSubtitle('other', 3, 4);
|
||||
tracker.recordSubtitle('same', 5, 6);
|
||||
|
||||
handleMineSentenceDigit(3, {
|
||||
subtitleTimingTracker: tracker,
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async (sentence, startTime, endTime) => {
|
||||
created.push({ sentence, startTime, endTime });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
getCurrentSecondarySubText: () => undefined,
|
||||
showMpvOsd: () => {},
|
||||
logError: () => {},
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(created, [{ sentence: 'same other same', startTime: 1, endTime: 6 }]);
|
||||
} finally {
|
||||
tracker.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('handleMineSentenceDigit joins per-entry secondary subtitles when available', async () => {
|
||||
const created: Array<{ sentence: string; secondarySub?: string }> = [];
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
const recordSubtitleWithSecondary = tracker.recordSubtitle as (
|
||||
text: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
secondaryText?: string,
|
||||
) => void;
|
||||
|
||||
try {
|
||||
recordSubtitleWithSecondary.call(tracker, 'one', 1, 2, 'translation one');
|
||||
recordSubtitleWithSecondary.call(tracker, 'two', 3, 4, 'translation two');
|
||||
|
||||
handleMineSentenceDigit(2, {
|
||||
subtitleTimingTracker: tracker,
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async (sentence, _startTime, _endTime, secondarySub) => {
|
||||
created.push({ sentence, secondarySub });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
getCurrentSecondarySubText: () => 'current translation only',
|
||||
showMpvOsd: () => {},
|
||||
logError: () => {},
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(created, [
|
||||
{ sentence: 'one two', secondarySub: 'translation one translation two' },
|
||||
]);
|
||||
} finally {
|
||||
tracker.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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;
|
||||
}
|
||||
@@ -79,6 +82,19 @@ function requireAnkiIntegration(
|
||||
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();
|
||||
}
|
||||
|
||||
export async function updateLastCardFromClipboard(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
readClipboardText: () => string;
|
||||
@@ -146,17 +162,20 @@ export function handleMineSentenceDigit(
|
||||
): void {
|
||||
if (!deps.subtitleTimingTracker || !deps.ankiIntegration) return;
|
||||
|
||||
const blocks = deps.subtitleTimingTracker.getRecentBlocks(count);
|
||||
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 }[] = [];
|
||||
for (const block of blocks) {
|
||||
const timing = deps.subtitleTimingTracker.findTiming(block);
|
||||
if (timing) timings.push(timing);
|
||||
}
|
||||
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');
|
||||
@@ -166,9 +185,13 @@ export function handleMineSentenceDigit(
|
||||
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, deps.getCurrentSecondarySubText())
|
||||
.createSentenceCard(sentence, rangeStart, rangeEnd, secondarySubText)
|
||||
.then((created) => {
|
||||
if (created) {
|
||||
deps.onCardsMined?.(cardsToMine);
|
||||
|
||||
@@ -843,7 +843,7 @@ export function createStatsApp(
|
||||
const client = new AnkiConnectClient(ankiConfig.url ?? 'http://127.0.0.1:8765');
|
||||
const mediaGen = new MediaGenerator();
|
||||
|
||||
const audioPadding = ankiConfig.media?.audioPadding ?? 0.5;
|
||||
const audioPadding = ankiConfig.media?.audioPadding ?? 0;
|
||||
const maxMediaDuration = ankiConfig.media?.maxMediaDuration ?? 30;
|
||||
|
||||
const startSec = startMs / 1000;
|
||||
|
||||
Reference in New Issue
Block a user