mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 12:55:20 -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 existingValue || newValue;
|
||||||
}
|
}
|
||||||
return this.sortEntriesByGroupIdDescending([...keepEntries, ...sourceEntries])
|
return this.sortEntriesByGroupIdDescending([...keepEntries, ...sourceEntries])
|
||||||
.map((entry) => this.ensureImageGroupId(entry.tag, entry.groupId))
|
.map((entry) => entry.tag)
|
||||||
.join('');
|
.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 () => {
|
test('registerIpcHandlers returns empty stats overview shape without a tracker', async () => {
|
||||||
const { registrar, handlers } = createFakeIpcRegistrar();
|
const { registrar, handlers } = createFakeIpcRegistrar();
|
||||||
registerIpcHandlers(createRegisterIpcDeps(), registrar);
|
registerIpcHandlers(createRegisterIpcDeps(), registrar);
|
||||||
|
|||||||
@@ -465,7 +465,14 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipc.on(IPC_CHANNELS.command.recordYomitanLookup, (_event: unknown, payload: unknown) => {
|
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();
|
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', () => {
|
test('handleMultiCopyDigit copies available history and reports truncation', () => {
|
||||||
const osd: string[] = [];
|
const osd: string[] = [];
|
||||||
const copied: string[] = [];
|
const copied: string[] = [];
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface MpvClientLike {
|
|||||||
currentSubStart: number;
|
currentSubStart: number;
|
||||||
currentSubEnd: number;
|
currentSubEnd: number;
|
||||||
currentSecondarySubText?: string;
|
currentSecondarySubText?: string;
|
||||||
|
requestProperty?: (name: string) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleMultiCopyDigit(
|
export function handleMultiCopyDigit(
|
||||||
@@ -95,6 +96,35 @@ function getSecondarySubTextForMinedBlocks(
|
|||||||
return getCurrentSecondarySubText();
|
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: {
|
export async function updateLastCardFromClipboard(deps: {
|
||||||
ankiIntegration: AnkiIntegrationLike | null;
|
ankiIntegration: AnkiIntegrationLike | null;
|
||||||
readClipboardText: () => string;
|
readClipboardText: () => string;
|
||||||
@@ -141,11 +171,12 @@ export async function mineSentenceCard(deps: {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const secondarySubText = await getCurrentSecondarySubTextForSentenceCard(mpvClient);
|
||||||
return await anki.createSentenceCard(
|
return await anki.createSentenceCard(
|
||||||
mpvClient.currentSubText,
|
mpvClient.currentSubText,
|
||||||
mpvClient.currentSubStart,
|
mpvClient.currentSubStart,
|
||||||
mpvClient.currentSubEnd,
|
mpvClient.currentSubEnd,
|
||||||
mpvClient.currentSecondarySubText || undefined,
|
secondarySubText,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user