feat: streamline Kiku duplicate grouping and popup flow (#38)

This commit is contained in:
2026-04-01 00:04:03 -07:00
committed by GitHub
parent 3502cdc607
commit d6c72806bb
31 changed files with 1227 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ import * as path from 'path';
import test from 'node:test';
import * as vm from 'node:vm';
import {
addYomitanNoteViaSearch,
getYomitanDictionaryInfo,
importYomitanDictionaryFromZip,
deleteYomitanDictionaryByTitle,
@@ -1373,3 +1374,48 @@ test('deleteYomitanDictionaryByTitle uses settings automation bridge instead of
false,
);
});
test('addYomitanNoteViaSearch returns note and duplicate ids from the bridge payload', async () => {
const deps = createDeps(async (_script) => ({
noteId: 42,
duplicateNoteIds: [18, 7, 18],
}));
const result = await addYomitanNoteViaSearch('食べる', deps, {
error: () => undefined,
});
assert.deepEqual(result, {
noteId: 42,
duplicateNoteIds: [18, 7, 18],
});
});
test('addYomitanNoteViaSearch rejects invalid numeric note ids from the bridge shortcut', async () => {
const deps = createDeps(async () => NaN);
const result = await addYomitanNoteViaSearch('食べる', deps, {
error: () => undefined,
});
assert.deepEqual(result, {
noteId: null,
duplicateNoteIds: [],
});
});
test('addYomitanNoteViaSearch sanitizes invalid payload note ids while keeping valid duplicate ids', async () => {
const deps = createDeps(async (_script) => ({
noteId: -1,
duplicateNoteIds: [18, 0, 7.5, 7],
}));
const result = await addYomitanNoteViaSearch('食べる', deps, {
error: () => undefined,
});
assert.deepEqual(result, {
noteId: null,
duplicateNoteIds: [18, 7],
});
});

View File

@@ -63,6 +63,11 @@ interface YomitanProfileMetadata {
dictionaryFrequencyModeByName: Partial<Record<string, YomitanFrequencyMode>>;
}
export interface YomitanAddNoteResult {
noteId: number | null;
duplicateNoteIds: number[];
}
const DEFAULT_YOMITAN_SCAN_LENGTH = 40;
const yomitanProfileMetadataByWindow = new WeakMap<BrowserWindow, YomitanProfileMetadata>();
const yomitanFrequencyCacheByWindow = new WeakMap<
@@ -1984,11 +1989,11 @@ export async function addYomitanNoteViaSearch(
word: string,
deps: YomitanParserRuntimeDeps,
logger: LoggerLike,
): Promise<number | null> {
): Promise<YomitanAddNoteResult> {
const isReady = await ensureYomitanParserWindow(deps, logger);
const parserWindow = deps.getYomitanParserWindow();
if (!isReady || !parserWindow || parserWindow.isDestroyed()) {
return null;
return { noteId: null, duplicateNoteIds: [] };
}
const escapedWord = JSON.stringify(word);
@@ -2003,10 +2008,35 @@ export async function addYomitanNoteViaSearch(
`;
try {
const noteId = await parserWindow.webContents.executeJavaScript(script, true);
return typeof noteId === 'number' ? noteId : null;
const result = await parserWindow.webContents.executeJavaScript(script, true);
if (typeof result === 'number') {
return {
noteId: Number.isInteger(result) && result > 0 ? result : null,
duplicateNoteIds: [],
};
}
if (result && typeof result === 'object' && !Array.isArray(result)) {
const envelope = result as {
noteId?: unknown;
duplicateNoteIds?: unknown;
};
return {
noteId:
typeof envelope.noteId === 'number' &&
Number.isInteger(envelope.noteId) &&
envelope.noteId > 0
? envelope.noteId
: null,
duplicateNoteIds: Array.isArray(envelope.duplicateNoteIds)
? envelope.duplicateNoteIds.filter(
(entry): entry is number => typeof entry === 'number' && Number.isInteger(entry) && entry > 0,
)
: [],
};
}
return { noteId: null, duplicateNoteIds: [] };
} catch (err) {
logger.error('Yomitan addNoteFromWord failed:', (err as Error).message);
return null;
return { noteId: null, duplicateNoteIds: [] };
}
}