mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-07 03:22:17 -08:00
Overlay 2.0 (#12)
This commit is contained in:
@@ -2,6 +2,7 @@ import type { BrowserWindow, Extension } from 'electron';
|
||||
|
||||
interface LoggerLike {
|
||||
error: (message: string, ...args: unknown[]) => void;
|
||||
info?: (message: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
interface YomitanParserRuntimeDeps {
|
||||
@@ -14,6 +15,395 @@ interface YomitanParserRuntimeDeps {
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
}
|
||||
|
||||
export interface YomitanTermFrequency {
|
||||
term: string;
|
||||
reading: string | null;
|
||||
dictionary: string;
|
||||
dictionaryPriority: number;
|
||||
frequency: number;
|
||||
displayValue: string | null;
|
||||
displayValueParsed: boolean;
|
||||
}
|
||||
|
||||
export interface YomitanTermReadingPair {
|
||||
term: string;
|
||||
reading: string | null;
|
||||
}
|
||||
|
||||
interface YomitanProfileMetadata {
|
||||
profileIndex: number;
|
||||
scanLength: number;
|
||||
dictionaries: string[];
|
||||
dictionaryPriorityByName: Record<string, number>;
|
||||
}
|
||||
|
||||
const DEFAULT_YOMITAN_SCAN_LENGTH = 40;
|
||||
const yomitanProfileMetadataByWindow = new WeakMap<BrowserWindow, YomitanProfileMetadata>();
|
||||
const yomitanFrequencyCacheByWindow = new WeakMap<BrowserWindow, Map<string, YomitanTermFrequency[]>>();
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === 'object');
|
||||
}
|
||||
|
||||
function makeTermReadingCacheKey(term: string, reading: string | null): string {
|
||||
return `${term}\u0000${reading ?? ''}`;
|
||||
}
|
||||
|
||||
function getWindowFrequencyCache(window: BrowserWindow): Map<string, YomitanTermFrequency[]> {
|
||||
let cache = yomitanFrequencyCacheByWindow.get(window);
|
||||
if (!cache) {
|
||||
cache = new Map<string, YomitanTermFrequency[]>();
|
||||
yomitanFrequencyCacheByWindow.set(window, cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
function clearWindowCaches(window: BrowserWindow): void {
|
||||
yomitanProfileMetadataByWindow.delete(window);
|
||||
yomitanFrequencyCacheByWindow.delete(window);
|
||||
}
|
||||
export function clearYomitanParserCachesForWindow(window: BrowserWindow): void {
|
||||
clearWindowCaches(window);
|
||||
}
|
||||
|
||||
function asPositiveInteger(value: unknown): number | null {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(1, Math.floor(value));
|
||||
}
|
||||
|
||||
function parsePositiveFrequencyString(value: string): number | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numericPrefix = trimmed.match(/^\d[\d,]*/)?.[0];
|
||||
if (!numericPrefix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunks = numericPrefix.split(',');
|
||||
const normalizedNumber =
|
||||
chunks.length <= 1
|
||||
? chunks[0] ?? ''
|
||||
: chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk))
|
||||
? chunks.join('')
|
||||
: (chunks[0] ?? '');
|
||||
const parsed = Number.parseInt(normalizedNumber, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parsePositiveFrequencyValue(value: unknown): number | null {
|
||||
const numeric = asPositiveInteger(value);
|
||||
if (numeric !== null) {
|
||||
return numeric;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return parsePositiveFrequencyString(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const parsed = parsePositiveFrequencyValue(item);
|
||||
if (parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function toYomitanTermFrequency(value: unknown): YomitanTermFrequency | null {
|
||||
if (!isObject(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const term = typeof value.term === 'string' ? value.term.trim() : '';
|
||||
const dictionary = typeof value.dictionary === 'string' ? value.dictionary.trim() : '';
|
||||
const rawFrequency = parsePositiveFrequencyValue(value.frequency);
|
||||
const displayValueRaw = value.displayValue;
|
||||
const parsedDisplayFrequency =
|
||||
displayValueRaw !== null && displayValueRaw !== undefined
|
||||
? parsePositiveFrequencyValue(displayValueRaw)
|
||||
: null;
|
||||
const frequency = parsedDisplayFrequency ?? rawFrequency;
|
||||
if (!term || !dictionary || frequency === null) {
|
||||
return null;
|
||||
}
|
||||
const dictionaryPriorityRaw = (value as { dictionaryPriority?: unknown }).dictionaryPriority;
|
||||
const dictionaryPriority =
|
||||
typeof dictionaryPriorityRaw === 'number' && Number.isFinite(dictionaryPriorityRaw)
|
||||
? Math.max(0, Math.floor(dictionaryPriorityRaw))
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const reading =
|
||||
value.reading === null
|
||||
? null
|
||||
: typeof value.reading === 'string'
|
||||
? value.reading
|
||||
: null;
|
||||
const displayValue = typeof displayValueRaw === 'string' ? displayValueRaw : null;
|
||||
const displayValueParsed = value.displayValueParsed === true;
|
||||
|
||||
return {
|
||||
term,
|
||||
reading,
|
||||
dictionary,
|
||||
dictionaryPriority,
|
||||
frequency,
|
||||
displayValue,
|
||||
displayValueParsed,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTermReadingList(termReadingList: YomitanTermReadingPair[]): YomitanTermReadingPair[] {
|
||||
const normalized: YomitanTermReadingPair[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const pair of termReadingList) {
|
||||
const term = typeof pair.term === 'string' ? pair.term.trim() : '';
|
||||
if (!term) {
|
||||
continue;
|
||||
}
|
||||
const reading =
|
||||
typeof pair.reading === 'string' && pair.reading.trim().length > 0 ? pair.reading.trim() : null;
|
||||
const key = `${term}\u0000${reading ?? ''}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
normalized.push({ term, reading });
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function toYomitanProfileMetadata(value: unknown): YomitanProfileMetadata | null {
|
||||
if (!isObject(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileIndexRaw = value.profileIndex ?? value.profileCurrent;
|
||||
const profileIndex =
|
||||
typeof profileIndexRaw === 'number' && Number.isFinite(profileIndexRaw)
|
||||
? Math.max(0, Math.floor(profileIndexRaw))
|
||||
: 0;
|
||||
const scanLengthRaw =
|
||||
value.scanLength ??
|
||||
(Array.isArray(value.profiles) && isObject(value.profiles[profileIndex])
|
||||
? (value.profiles[profileIndex] as { options?: { scanning?: { length?: unknown } } }).options
|
||||
?.scanning?.length
|
||||
: undefined);
|
||||
const scanLength =
|
||||
typeof scanLengthRaw === 'number' && Number.isFinite(scanLengthRaw)
|
||||
? Math.max(1, Math.floor(scanLengthRaw))
|
||||
: DEFAULT_YOMITAN_SCAN_LENGTH;
|
||||
const dictionariesRaw =
|
||||
value.dictionaries ??
|
||||
(Array.isArray(value.profiles) && isObject(value.profiles[profileIndex])
|
||||
? (value.profiles[profileIndex] as { options?: { dictionaries?: unknown[] } }).options
|
||||
?.dictionaries
|
||||
: undefined);
|
||||
const dictionaries = Array.isArray(dictionariesRaw)
|
||||
? dictionariesRaw
|
||||
.map((entry, index) => {
|
||||
if (typeof entry === 'string') {
|
||||
return { name: entry.trim(), priority: index };
|
||||
}
|
||||
if (!isObject(entry) || entry.enabled === false || typeof entry.name !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const normalizedName = entry.name.trim();
|
||||
if (!normalizedName) {
|
||||
return null;
|
||||
}
|
||||
const priorityRaw = (entry as { id?: unknown }).id;
|
||||
const priority =
|
||||
typeof priorityRaw === 'number' && Number.isFinite(priorityRaw)
|
||||
? Math.max(0, Math.floor(priorityRaw))
|
||||
: index;
|
||||
return { name: normalizedName, priority };
|
||||
})
|
||||
.filter((entry): entry is { name: string; priority: number } => entry !== null)
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map((entry) => entry.name)
|
||||
.filter((entry) => entry.length > 0)
|
||||
: [];
|
||||
const dictionaryPriorityByNameRaw = value.dictionaryPriorityByName;
|
||||
const dictionaryPriorityByName: Record<string, number> = {};
|
||||
if (isObject(dictionaryPriorityByNameRaw)) {
|
||||
for (const [name, priorityRaw] of Object.entries(dictionaryPriorityByNameRaw)) {
|
||||
if (typeof priorityRaw !== 'number' || !Number.isFinite(priorityRaw)) {
|
||||
continue;
|
||||
}
|
||||
const normalizedName = name.trim();
|
||||
if (!normalizedName) {
|
||||
continue;
|
||||
}
|
||||
dictionaryPriorityByName[normalizedName] = Math.max(0, Math.floor(priorityRaw));
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < dictionaries.length; index += 1) {
|
||||
const dictionary = dictionaries[index];
|
||||
if (!dictionary) {
|
||||
continue;
|
||||
}
|
||||
if (dictionaryPriorityByName[dictionary] === undefined) {
|
||||
dictionaryPriorityByName[dictionary] = index;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
profileIndex,
|
||||
scanLength,
|
||||
dictionaries,
|
||||
dictionaryPriorityByName,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFrequencyEntriesWithPriority(
|
||||
rawResult: unknown[],
|
||||
dictionaryPriorityByName: Record<string, number>,
|
||||
): YomitanTermFrequency[] {
|
||||
const normalized: YomitanTermFrequency[] = [];
|
||||
for (const entry of rawResult) {
|
||||
const frequency = toYomitanTermFrequency(entry);
|
||||
if (!frequency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dictionaryPriority = dictionaryPriorityByName[frequency.dictionary];
|
||||
normalized.push({
|
||||
...frequency,
|
||||
dictionaryPriority:
|
||||
dictionaryPriority !== undefined ? dictionaryPriority : frequency.dictionaryPriority,
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function groupFrequencyEntriesByPair(
|
||||
entries: YomitanTermFrequency[],
|
||||
): Map<string, YomitanTermFrequency[]> {
|
||||
const grouped = new Map<string, YomitanTermFrequency[]>();
|
||||
for (const entry of entries) {
|
||||
const reading =
|
||||
typeof entry.reading === 'string' && entry.reading.trim().length > 0 ? entry.reading.trim() : null;
|
||||
const key = makeTermReadingCacheKey(entry.term.trim(), reading);
|
||||
const existing = grouped.get(key);
|
||||
if (existing) {
|
||||
existing.push(entry);
|
||||
continue;
|
||||
}
|
||||
grouped.set(key, [entry]);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function groupFrequencyEntriesByTerm(
|
||||
entries: YomitanTermFrequency[],
|
||||
): Map<string, YomitanTermFrequency[]> {
|
||||
const grouped = new Map<string, YomitanTermFrequency[]>();
|
||||
for (const entry of entries) {
|
||||
const term = entry.term.trim();
|
||||
if (!term) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = grouped.get(term);
|
||||
if (existing) {
|
||||
existing.push(entry);
|
||||
continue;
|
||||
}
|
||||
grouped.set(term, [entry]);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
async function requestYomitanProfileMetadata(
|
||||
parserWindow: BrowserWindow,
|
||||
logger: LoggerLike,
|
||||
): Promise<YomitanProfileMetadata | null> {
|
||||
const cached = yomitanProfileMetadataByWindow.get(parserWindow);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const script = `
|
||||
(async () => {
|
||||
const invoke = (action, params) =>
|
||||
new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action, params }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (!response || typeof response !== "object") {
|
||||
reject(new Error("Invalid response from Yomitan backend"));
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message || "Yomitan backend error"));
|
||||
return;
|
||||
}
|
||||
resolve(response.result);
|
||||
});
|
||||
});
|
||||
|
||||
const optionsFull = await invoke("optionsGetFull", undefined);
|
||||
const profileIndex =
|
||||
typeof optionsFull.profileCurrent === "number" && Number.isFinite(optionsFull.profileCurrent)
|
||||
? Math.max(0, Math.floor(optionsFull.profileCurrent))
|
||||
: 0;
|
||||
const scanLengthRaw = optionsFull.profiles?.[profileIndex]?.options?.scanning?.length;
|
||||
const scanLength =
|
||||
typeof scanLengthRaw === "number" && Number.isFinite(scanLengthRaw)
|
||||
? Math.max(1, Math.floor(scanLengthRaw))
|
||||
: ${DEFAULT_YOMITAN_SCAN_LENGTH};
|
||||
const dictionariesRaw = optionsFull.profiles?.[profileIndex]?.options?.dictionaries ?? [];
|
||||
const dictionaryEntries = Array.isArray(dictionariesRaw)
|
||||
? dictionariesRaw
|
||||
.filter((entry) => entry && typeof entry === "object" && entry.enabled === true && typeof entry.name === "string")
|
||||
.map((entry, index) => ({
|
||||
name: entry.name,
|
||||
id: typeof entry.id === "number" && Number.isFinite(entry.id) ? Math.max(0, Math.floor(entry.id)) : index
|
||||
}))
|
||||
.sort((a, b) => a.id - b.id)
|
||||
: [];
|
||||
const dictionaries = dictionaryEntries.map((entry) => entry.name);
|
||||
const dictionaryPriorityByName = dictionaryEntries.reduce((acc, entry, index) => {
|
||||
acc[entry.name] = index;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return { profileIndex, scanLength, dictionaries, dictionaryPriorityByName };
|
||||
})();
|
||||
`;
|
||||
|
||||
try {
|
||||
const rawMetadata = await parserWindow.webContents.executeJavaScript(script, true);
|
||||
const metadata = toYomitanProfileMetadata(rawMetadata);
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
yomitanProfileMetadataByWindow.set(parserWindow, metadata);
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
logger.error('Yomitan parser metadata request failed:', (err as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureYomitanParserWindow(
|
||||
deps: YomitanParserRuntimeDeps,
|
||||
logger: LoggerLike,
|
||||
@@ -58,6 +448,7 @@ async function ensureYomitanParserWindow(
|
||||
);
|
||||
|
||||
parserWindow.on('closed', () => {
|
||||
clearWindowCaches(parserWindow);
|
||||
if (deps.getYomitanParserWindow() === parserWindow) {
|
||||
deps.setYomitanParserWindow(null);
|
||||
deps.setYomitanParserReadyPromise(null);
|
||||
@@ -77,6 +468,7 @@ async function ensureYomitanParserWindow(
|
||||
if (!parserWindow.isDestroyed()) {
|
||||
parserWindow.destroy();
|
||||
}
|
||||
clearWindowCaches(parserWindow);
|
||||
if (deps.getYomitanParserWindow() === parserWindow) {
|
||||
deps.setYomitanParserWindow(null);
|
||||
deps.setYomitanParserReadyPromise(null);
|
||||
@@ -108,7 +500,40 @@ export async function requestYomitanParseResults(
|
||||
return null;
|
||||
}
|
||||
|
||||
const script = `
|
||||
const metadata = await requestYomitanProfileMetadata(parserWindow, logger);
|
||||
const script =
|
||||
metadata !== null
|
||||
? `
|
||||
(async () => {
|
||||
const invoke = (action, params) =>
|
||||
new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action, params }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (!response || typeof response !== "object") {
|
||||
reject(new Error("Invalid response from Yomitan backend"));
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message || "Yomitan backend error"));
|
||||
return;
|
||||
}
|
||||
resolve(response.result);
|
||||
});
|
||||
});
|
||||
|
||||
return await invoke("parseText", {
|
||||
text: ${JSON.stringify(text)},
|
||||
optionsContext: { index: ${metadata.profileIndex} },
|
||||
scanLength: ${metadata.scanLength},
|
||||
useInternalParser: true,
|
||||
useMecabParser: true
|
||||
});
|
||||
})();
|
||||
`
|
||||
: `
|
||||
(async () => {
|
||||
const invoke = (action, params) =>
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -132,7 +557,7 @@ export async function requestYomitanParseResults(
|
||||
const optionsFull = await invoke("optionsGetFull", undefined);
|
||||
const profileIndex = optionsFull.profileCurrent;
|
||||
const scanLength =
|
||||
optionsFull.profiles?.[profileIndex]?.options?.scanning?.length ?? 40;
|
||||
optionsFull.profiles?.[profileIndex]?.options?.scanning?.length ?? ${DEFAULT_YOMITAN_SCAN_LENGTH};
|
||||
|
||||
return await invoke("parseText", {
|
||||
text: ${JSON.stringify(text)},
|
||||
@@ -152,3 +577,278 @@ export async function requestYomitanParseResults(
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestYomitanTermFrequencies(
|
||||
termReadingList: YomitanTermReadingPair[],
|
||||
deps: YomitanParserRuntimeDeps,
|
||||
logger: LoggerLike,
|
||||
): Promise<YomitanTermFrequency[]> {
|
||||
const normalizedTermReadingList = normalizeTermReadingList(termReadingList);
|
||||
const yomitanExt = deps.getYomitanExt();
|
||||
if (normalizedTermReadingList.length === 0 || !yomitanExt) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isReady = await ensureYomitanParserWindow(deps, logger);
|
||||
const parserWindow = deps.getYomitanParserWindow();
|
||||
if (!isReady || !parserWindow || parserWindow.isDestroyed()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metadata = await requestYomitanProfileMetadata(parserWindow, logger);
|
||||
const frequencyCache = getWindowFrequencyCache(parserWindow);
|
||||
const missingTermReadingList: YomitanTermReadingPair[] = [];
|
||||
|
||||
const buildCachedResult = (): YomitanTermFrequency[] => {
|
||||
const result: YomitanTermFrequency[] = [];
|
||||
for (const pair of normalizedTermReadingList) {
|
||||
const key = makeTermReadingCacheKey(pair.term, pair.reading);
|
||||
const cached = frequencyCache.get(key);
|
||||
if (cached && cached.length > 0) {
|
||||
result.push(...cached);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
for (const pair of normalizedTermReadingList) {
|
||||
const key = makeTermReadingCacheKey(pair.term, pair.reading);
|
||||
if (!frequencyCache.has(key)) {
|
||||
missingTermReadingList.push(pair);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingTermReadingList.length === 0) {
|
||||
return buildCachedResult();
|
||||
}
|
||||
|
||||
if (metadata && metadata.dictionaries.length > 0) {
|
||||
const script = `
|
||||
(async () => {
|
||||
const invoke = (action, params) =>
|
||||
new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action, params }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (!response || typeof response !== "object") {
|
||||
reject(new Error("Invalid response from Yomitan backend"));
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message || "Yomitan backend error"));
|
||||
return;
|
||||
}
|
||||
resolve(response.result);
|
||||
});
|
||||
});
|
||||
|
||||
return await invoke("getTermFrequencies", {
|
||||
termReadingList: ${JSON.stringify(missingTermReadingList)},
|
||||
dictionaries: ${JSON.stringify(metadata.dictionaries)}
|
||||
});
|
||||
})();
|
||||
`;
|
||||
|
||||
try {
|
||||
const rawResult = await parserWindow.webContents.executeJavaScript(script, true);
|
||||
const fetchedEntries = Array.isArray(rawResult)
|
||||
? normalizeFrequencyEntriesWithPriority(rawResult, metadata.dictionaryPriorityByName)
|
||||
: [];
|
||||
const groupedByPair = groupFrequencyEntriesByPair(fetchedEntries);
|
||||
const groupedByTerm = groupFrequencyEntriesByTerm(fetchedEntries);
|
||||
const missingTerms = new Set(missingTermReadingList.map((pair) => pair.term));
|
||||
|
||||
for (const pair of missingTermReadingList) {
|
||||
const key = makeTermReadingCacheKey(pair.term, pair.reading);
|
||||
const exactEntries = groupedByPair.get(key);
|
||||
const termEntries = groupedByTerm.get(pair.term) ?? [];
|
||||
frequencyCache.set(key, exactEntries ?? termEntries);
|
||||
}
|
||||
|
||||
const cachedResult = buildCachedResult();
|
||||
const unmatchedEntries = fetchedEntries.filter((entry) => !missingTerms.has(entry.term.trim()));
|
||||
return [...cachedResult, ...unmatchedEntries];
|
||||
} catch (err) {
|
||||
logger.error('Yomitan term frequency request failed:', (err as Error).message);
|
||||
}
|
||||
|
||||
return buildCachedResult();
|
||||
}
|
||||
|
||||
const script = `
|
||||
(async () => {
|
||||
const invoke = (action, params) =>
|
||||
new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action, params }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (!response || typeof response !== "object") {
|
||||
reject(new Error("Invalid response from Yomitan backend"));
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message || "Yomitan backend error"));
|
||||
return;
|
||||
}
|
||||
resolve(response.result);
|
||||
});
|
||||
});
|
||||
|
||||
const optionsFull = await invoke("optionsGetFull", undefined);
|
||||
const profileIndex = optionsFull.profileCurrent;
|
||||
const dictionariesRaw = optionsFull.profiles?.[profileIndex]?.options?.dictionaries ?? [];
|
||||
const dictionaryEntries = Array.isArray(dictionariesRaw)
|
||||
? dictionariesRaw
|
||||
.filter((entry) => entry && typeof entry === "object" && entry.enabled === true && typeof entry.name === "string")
|
||||
.map((entry, index) => ({
|
||||
name: entry.name,
|
||||
id: typeof entry.id === "number" && Number.isFinite(entry.id) ? Math.floor(entry.id) : index
|
||||
}))
|
||||
.sort((a, b) => a.id - b.id)
|
||||
: [];
|
||||
const dictionaries = dictionaryEntries.map((entry) => entry.name);
|
||||
const dictionaryPriorityByName = dictionaryEntries.reduce((acc, entry, index) => {
|
||||
acc[entry.name] = index;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dictionaries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rawFrequencies = await invoke("getTermFrequencies", {
|
||||
termReadingList: ${JSON.stringify(missingTermReadingList)},
|
||||
dictionaries
|
||||
});
|
||||
|
||||
if (!Array.isArray(rawFrequencies)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rawFrequencies
|
||||
.filter((entry) => entry && typeof entry === "object")
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
dictionaryPriority:
|
||||
typeof entry.dictionary === "string" && dictionaryPriorityByName[entry.dictionary] !== undefined
|
||||
? dictionaryPriorityByName[entry.dictionary]
|
||||
: Number.MAX_SAFE_INTEGER
|
||||
}));
|
||||
})();
|
||||
`;
|
||||
|
||||
try {
|
||||
const rawResult = await parserWindow.webContents.executeJavaScript(script, true);
|
||||
const fetchedEntries = Array.isArray(rawResult)
|
||||
? rawResult
|
||||
.map((entry) => toYomitanTermFrequency(entry))
|
||||
.filter((entry): entry is YomitanTermFrequency => entry !== null)
|
||||
: [];
|
||||
const groupedByPair = groupFrequencyEntriesByPair(fetchedEntries);
|
||||
const groupedByTerm = groupFrequencyEntriesByTerm(fetchedEntries);
|
||||
const missingTerms = new Set(missingTermReadingList.map((pair) => pair.term));
|
||||
for (const pair of missingTermReadingList) {
|
||||
const key = makeTermReadingCacheKey(pair.term, pair.reading);
|
||||
const exactEntries = groupedByPair.get(key);
|
||||
const termEntries = groupedByTerm.get(pair.term) ?? [];
|
||||
frequencyCache.set(key, exactEntries ?? termEntries);
|
||||
}
|
||||
const cachedResult = buildCachedResult();
|
||||
const unmatchedEntries = fetchedEntries.filter((entry) => !missingTerms.has(entry.term.trim()));
|
||||
return [...cachedResult, ...unmatchedEntries];
|
||||
} catch (err) {
|
||||
logger.error('Yomitan term frequency request failed:', (err as Error).message);
|
||||
return buildCachedResult();
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncYomitanDefaultAnkiServer(
|
||||
serverUrl: string,
|
||||
deps: YomitanParserRuntimeDeps,
|
||||
logger: LoggerLike,
|
||||
): Promise<boolean> {
|
||||
const normalizedTargetServer = serverUrl.trim();
|
||||
if (!normalizedTargetServer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isReady = await ensureYomitanParserWindow(deps, logger);
|
||||
const parserWindow = deps.getYomitanParserWindow();
|
||||
if (!isReady || !parserWindow || parserWindow.isDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const script = `
|
||||
(async () => {
|
||||
const invoke = (action, params) =>
|
||||
new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action, params }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (!response || typeof response !== "object") {
|
||||
reject(new Error("Invalid response from Yomitan backend"));
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message || "Yomitan backend error"));
|
||||
return;
|
||||
}
|
||||
resolve(response.result);
|
||||
});
|
||||
});
|
||||
|
||||
const targetServer = ${JSON.stringify(normalizedTargetServer)};
|
||||
const optionsFull = await invoke("optionsGetFull", undefined);
|
||||
const profiles = Array.isArray(optionsFull.profiles) ? optionsFull.profiles : [];
|
||||
if (profiles.length === 0) {
|
||||
return { updated: false, reason: "no-profiles" };
|
||||
}
|
||||
|
||||
const defaultProfile = profiles[0];
|
||||
if (!defaultProfile || typeof defaultProfile !== "object") {
|
||||
return { updated: false, reason: "invalid-default-profile" };
|
||||
}
|
||||
|
||||
defaultProfile.options = defaultProfile.options && typeof defaultProfile.options === "object"
|
||||
? defaultProfile.options
|
||||
: {};
|
||||
defaultProfile.options.anki = defaultProfile.options.anki && typeof defaultProfile.options.anki === "object"
|
||||
? defaultProfile.options.anki
|
||||
: {};
|
||||
|
||||
const currentServerRaw = defaultProfile.options.anki.server;
|
||||
const currentServer = typeof currentServerRaw === "string" ? currentServerRaw.trim() : "";
|
||||
const canReplaceDefault =
|
||||
currentServer.length === 0 || currentServer === "http://127.0.0.1:8765";
|
||||
if (!canReplaceDefault || currentServer === targetServer) {
|
||||
return { updated: false, reason: "no-change", currentServer, targetServer };
|
||||
}
|
||||
|
||||
defaultProfile.options.anki.server = targetServer;
|
||||
await invoke("setAllSettings", { value: optionsFull, source: "subminer" });
|
||||
return { updated: true, currentServer, targetServer };
|
||||
})();
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await parserWindow.webContents.executeJavaScript(script, true);
|
||||
const updated =
|
||||
typeof result === 'object' &&
|
||||
result !== null &&
|
||||
(result as { updated?: unknown }).updated === true;
|
||||
if (updated) {
|
||||
logger.info?.(`Updated Yomitan default profile Anki server to ${normalizedTargetServer}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
logger.error('Failed to sync Yomitan default profile Anki server:', (err as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user