mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-01 12:15:27 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da71af5769
|
||
|
|
8f9287806b
|
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: mining
|
||||
|
||||
- Multi-line copy and mining now select backward from the current subtitle in timeline order after seeking, instead of copying lines in playback encounter order.
|
||||
@@ -35,7 +35,7 @@ These work when the overlay window has focus.
|
||||
| `Ctrl/Cmd+G` | Trigger field grouping (Kiku merge check) | `shortcuts.triggerFieldGrouping` |
|
||||
| `Ctrl/Cmd+Shift+A` | Mark last card as audio card | `shortcuts.markAudioCard` |
|
||||
|
||||
The multi-line shortcuts open a digit selector with a 3-second timeout (`shortcuts.multiCopyTimeoutMs`). Press `1`–`9` to select how many recent subtitle lines to combine. When the shortcut starts from mpv, SubMiner focuses the visible overlay for that selector instead of reserving the number keys in the mpv plugin.
|
||||
The multi-line shortcuts open a digit selector with a 3-second timeout (`shortcuts.multiCopyTimeoutMs`). Press `1`–`9` to select the total number of subtitle lines to combine, ending at the current line and moving backward through the subtitle timeline. The current line counts toward the selected total. When the shortcut starts from mpv, SubMiner focuses the visible overlay for that selector instead of reserving the number keys in the mpv plugin.
|
||||
|
||||
## Overlay Controls
|
||||
|
||||
|
||||
@@ -244,6 +244,35 @@ test('handleMultiCopyDigit copies available history and reports truncation', ()
|
||||
assert.equal(osd.at(-1), 'Only 2 lines available, copied 2');
|
||||
});
|
||||
|
||||
test('handleMultiCopyDigit copies backward from the current subtitle after a backward seek', () => {
|
||||
const copied: string[] = [];
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
|
||||
try {
|
||||
tracker.recordSubtitle('A', 1, 2);
|
||||
tracker.recordSubtitle('B', 3, 4);
|
||||
tracker.recordSubtitle('C', 5, 6);
|
||||
tracker.recordSubtitle('B', 3, 4);
|
||||
|
||||
const deps = {
|
||||
subtitleTimingTracker: tracker,
|
||||
writeClipboardText: (text: string) => copied.push(text),
|
||||
showMpvOsd: () => {},
|
||||
};
|
||||
|
||||
handleMultiCopyDigit(1, deps);
|
||||
handleMultiCopyDigit(2, deps);
|
||||
|
||||
assert.deepEqual(copied, ['B', 'A\n\nB']);
|
||||
assert.deepEqual(tracker.getRecentEntries(2), [
|
||||
{ displayText: 'A', startTime: 1, endTime: 2, secondaryText: undefined },
|
||||
{ displayText: 'B', startTime: 3, endTime: 4, secondaryText: undefined },
|
||||
]);
|
||||
} finally {
|
||||
tracker.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('handleMineSentenceDigit reports async create failures', async () => {
|
||||
const osd: string[] = [];
|
||||
const logs: Array<{ message: string; err: unknown }> = [];
|
||||
@@ -344,6 +373,22 @@ test('handleMineSentenceDigit keeps per-entry timings when subtitle text repeats
|
||||
}
|
||||
});
|
||||
|
||||
test('subtitle timing history preserves adjacent repeated text with distinct timings', () => {
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
|
||||
try {
|
||||
tracker.recordSubtitle('same', 1, 2);
|
||||
tracker.recordSubtitle('same', 3, 4);
|
||||
|
||||
assert.deepEqual(tracker.getRecentEntries(2), [
|
||||
{ displayText: 'same', startTime: 1, endTime: 2, secondaryText: undefined },
|
||||
{ displayText: 'same', startTime: 3, endTime: 4, secondaryText: undefined },
|
||||
]);
|
||||
} finally {
|
||||
tracker.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('handleMineSentenceDigit joins per-entry secondary subtitles when available', async () => {
|
||||
const created: Array<{ sentence: string; secondarySub?: string }> = [];
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
|
||||
@@ -67,8 +67,13 @@ export class SubtitleTimingTracker {
|
||||
|
||||
// Check for duplicate of most recent entry (deduplicate adjacent repeats)
|
||||
const lastEntry = this.history[this.history.length - 1];
|
||||
if (lastEntry && lastEntry.timingKey === timingKey) {
|
||||
// Update timing to most recent occurrence
|
||||
if (
|
||||
lastEntry &&
|
||||
lastEntry.timingKey === timingKey &&
|
||||
lastEntry.startTime === startTime &&
|
||||
lastEntry.endTime === endTime
|
||||
) {
|
||||
// Refresh metadata for repeated notifications of the same subtitle event.
|
||||
lastEntry.startTime = startTime;
|
||||
lastEntry.endTime = endTime;
|
||||
lastEntry.secondaryText = displaySecondaryText;
|
||||
@@ -107,28 +112,20 @@ export class SubtitleTimingTracker {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent subtitle blocks in chronological order.
|
||||
* Returns the last `count` subtitle events (oldest → newest).
|
||||
* Get recent subtitle blocks in timeline order.
|
||||
* Returns up to `count` known subtitle events ending at the current event.
|
||||
* Blocks preserve internal line breaks and are joined with blank lines.
|
||||
*/
|
||||
getRecentBlocks(count: number): string[] {
|
||||
if (count <= 0) return [];
|
||||
if (count > this.history.length) {
|
||||
count = this.history.length;
|
||||
}
|
||||
return this.history.slice(-count).map((entry) => entry.displayText);
|
||||
return this.getRecentTimelineEntries(count).map((entry) => entry.displayText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent subtitle blocks with their original event timings.
|
||||
* Returns the last `count` subtitle events (oldest → newest).
|
||||
* Get recent subtitle blocks with their original event timings in timeline order.
|
||||
* Returns up to `count` known subtitle events ending at the current event.
|
||||
*/
|
||||
getRecentEntries(count: number): SubtitleTimingBlock[] {
|
||||
if (count <= 0) return [];
|
||||
if (count > this.history.length) {
|
||||
count = this.history.length;
|
||||
}
|
||||
return this.history.slice(-count).map((entry) => ({
|
||||
return this.getRecentTimelineEntries(count).map((entry) => ({
|
||||
displayText: entry.displayText,
|
||||
startTime: entry.startTime,
|
||||
endTime: entry.endTime,
|
||||
@@ -144,6 +141,43 @@ export class SubtitleTimingTracker {
|
||||
return lastEntry ? lastEntry.displayText : null;
|
||||
}
|
||||
|
||||
private getRecentTimelineEntries(count: number): HistoryEntry[] {
|
||||
if (count <= 0) return [];
|
||||
|
||||
const currentEntry = this.history[this.history.length - 1];
|
||||
if (!currentEntry) return [];
|
||||
|
||||
const timelineEntries: HistoryEntry[] = [];
|
||||
for (const entry of this.history) {
|
||||
const existingIndex = timelineEntries.findIndex((candidate) =>
|
||||
this.isSameSubtitleEvent(candidate, entry),
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
timelineEntries.push(entry);
|
||||
} else {
|
||||
timelineEntries[existingIndex] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
timelineEntries.sort(
|
||||
(left, right) => left.startTime - right.startTime || left.endTime - right.endTime,
|
||||
);
|
||||
const currentIndex = timelineEntries.findIndex((entry) =>
|
||||
this.isSameSubtitleEvent(entry, currentEntry),
|
||||
);
|
||||
if (currentIndex === -1) return [];
|
||||
|
||||
return timelineEntries.slice(Math.max(0, currentIndex - count + 1), currentIndex + 1);
|
||||
}
|
||||
|
||||
private isSameSubtitleEvent(left: HistoryEntry, right: HistoryEntry): boolean {
|
||||
return (
|
||||
left.timingKey === right.timingKey &&
|
||||
left.startTime === right.startTime &&
|
||||
left.endTime === right.endTime
|
||||
);
|
||||
}
|
||||
|
||||
private findFuzzyMatch(text: string): { startTime: number; endTime: number } | null {
|
||||
let bestMatch: TimingEntry | null = null;
|
||||
let bestScore = 0;
|
||||
|
||||
Reference in New Issue
Block a user