mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
fix(anki): refresh secondary subtitle before creating sentence card
- Fetch secondary-sub-text live via requestProperty so SelectionText uses current translation, not stale cache - Suppress secondary text when it equals primary (deduplication) - Make recordSubtitleMiningContext errors non-fatal; warn and continue to record lookup - Fix field-grouping merge to use raw tag instead of re-wrapping with ensureImageGroupId
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: anki
|
||||
|
||||
- Sentence cards now refresh the current secondary subtitle before saving, so SelectionText uses the loaded translation instead of repeating the primary subtitle.
|
||||
@@ -357,7 +357,7 @@ export class FieldGroupingMergeCollaborator {
|
||||
return existingValue || newValue;
|
||||
}
|
||||
return this.sortEntriesByGroupIdDescending([...keepEntries, ...sourceEntries])
|
||||
.map((entry) => this.ensureImageGroupId(entry.tag, entry.groupId))
|
||||
.map((entry) => entry.tag)
|
||||
.join('');
|
||||
}
|
||||
|
||||
|
||||
@@ -667,6 +667,46 @@ test('registerIpcHandlers forwards valid subtitle sidebar mining context', () =>
|
||||
]);
|
||||
});
|
||||
|
||||
test('registerIpcHandlers records yomitan lookup when subtitle context recording fails', () => {
|
||||
const { registrar, handlers } = createFakeIpcRegistrar();
|
||||
const calls: string[] = [];
|
||||
const warnings: unknown[][] = [];
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
warnings.push(args);
|
||||
};
|
||||
const deps = createRegisterIpcDeps({
|
||||
immersionTracker: createFakeImmersionTracker({
|
||||
recordYomitanLookup: () => {
|
||||
calls.push('lookup');
|
||||
},
|
||||
}),
|
||||
}) as IpcServiceDeps & {
|
||||
recordSubtitleMiningContext: (context: unknown | null) => void;
|
||||
};
|
||||
deps.recordSubtitleMiningContext = () => {
|
||||
throw new Error('context write failed');
|
||||
};
|
||||
|
||||
try {
|
||||
registerIpcHandlers(deps, registrar);
|
||||
|
||||
const handler = handlers.on.get(IPC_CHANNELS.command.recordYomitanLookup);
|
||||
assert.equal(typeof handler, 'function');
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
handler?.({}, { source: 'subtitle-sidebar', text: 'line', startTime: 1, endTime: 2 });
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, ['lookup']);
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.equal(warnings[0]?.[0], 'Failed to record subtitle mining context:');
|
||||
assert.equal(warnings[0]?.[1], 'context write failed');
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
});
|
||||
|
||||
test('registerIpcHandlers returns empty stats overview shape without a tracker', async () => {
|
||||
const { registrar, handlers } = createFakeIpcRegistrar();
|
||||
registerIpcHandlers(createRegisterIpcDeps(), registrar);
|
||||
|
||||
@@ -465,7 +465,14 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
||||
});
|
||||
|
||||
ipc.on(IPC_CHANNELS.command.recordYomitanLookup, (_event: unknown, payload: unknown) => {
|
||||
deps.recordSubtitleMiningContext?.(parseSubtitleMiningContext(payload));
|
||||
try {
|
||||
deps.recordSubtitleMiningContext?.(parseSubtitleMiningContext(payload));
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to record subtitle mining context:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
deps.immersionTracker?.recordYomitanLookup();
|
||||
});
|
||||
|
||||
|
||||
@@ -124,6 +124,38 @@ test('mineSentenceCard creates sentence card from mpv subtitle state', async ()
|
||||
]);
|
||||
});
|
||||
|
||||
test('mineSentenceCard refreshes secondary subtitle text before creating card', async () => {
|
||||
const created: Array<{ sentence: string; secondarySub?: string }> = [];
|
||||
const requestedProperties: string[] = [];
|
||||
|
||||
await mineSentenceCard({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async (sentence, _startTime, _endTime, secondarySub) => {
|
||||
created.push({ sentence, secondarySub });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
mpvClient: {
|
||||
connected: true,
|
||||
currentSubText: '日本語字幕',
|
||||
currentSubStart: 10,
|
||||
currentSubEnd: 12,
|
||||
currentSecondarySubText: '日本語字幕',
|
||||
requestProperty: async (name: string) => {
|
||||
requestedProperties.push(name);
|
||||
return name === 'secondary-sub-text' ? 'English subtitle' : null;
|
||||
},
|
||||
},
|
||||
showMpvOsd: () => {},
|
||||
});
|
||||
|
||||
assert.deepEqual(requestedProperties, ['secondary-sub-text']);
|
||||
assert.deepEqual(created, [{ sentence: '日本語字幕', secondarySub: 'English subtitle' }]);
|
||||
});
|
||||
|
||||
test('handleMultiCopyDigit copies available history and reports truncation', () => {
|
||||
const osd: string[] = [];
|
||||
const copied: string[] = [];
|
||||
|
||||
@@ -25,6 +25,7 @@ interface MpvClientLike {
|
||||
currentSubStart: number;
|
||||
currentSubEnd: number;
|
||||
currentSecondarySubText?: string;
|
||||
requestProperty?: (name: string) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export function handleMultiCopyDigit(
|
||||
@@ -95,6 +96,35 @@ function getSecondarySubTextForMinedBlocks(
|
||||
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');
|
||||
const normalizedLatest = normalizeSecondarySubText(latestSecondaryText, primaryText);
|
||||
if (normalizedLatest) {
|
||||
return normalizedLatest;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the cached secondary subtitle below.
|
||||
}
|
||||
}
|
||||
return normalizeSecondarySubText(mpvClient.currentSecondarySubText, primaryText);
|
||||
}
|
||||
|
||||
export async function updateLastCardFromClipboard(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
readClipboardText: () => string;
|
||||
@@ -141,11 +171,12 @@ export async function mineSentenceCard(deps: {
|
||||
return false;
|
||||
}
|
||||
|
||||
const secondarySubText = await getCurrentSecondarySubTextForSentenceCard(mpvClient);
|
||||
return await anki.createSentenceCard(
|
||||
mpvClient.currentSubText,
|
||||
mpvClient.currentSubStart,
|
||||
mpvClient.currentSubEnd,
|
||||
mpvClient.currentSecondarySubText || undefined,
|
||||
secondarySubText,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user