Improve startup dictionary progress and fix overlay/plugin input handlin

- show a dedicated startup OSD "building" phase for character dictionary sync
- forward bare `Tab` from visible overlay to mpv so AniSkip works while focused
- fix Windows plugin env override resolution for `SUBMINER_BINARY_PATH`
This commit is contained in:
2026-03-09 02:35:03 -07:00
parent e0f82d28f0
commit e59192bbe1
28 changed files with 577 additions and 104 deletions

View File

@@ -331,7 +331,7 @@ test('auto sync invokes completion callback after successful sync', async () =>
test('auto sync emits progress events for start import and completion', async () => {
const userDataPath = makeTempDir();
const events: Array<{
phase: 'checking' | 'generating' | 'syncing' | 'importing' | 'ready' | 'failed';
phase: 'checking' | 'generating' | 'syncing' | 'building' | 'importing' | 'ready' | 'failed';
mediaId?: number;
mediaTitle?: string;
message: string;
@@ -406,6 +406,12 @@ test('auto sync emits progress events for start import and completion', async ()
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
message: 'Updating character dictionary for Rascal Does Not Dream of Bunny Girl Senpai...',
},
{
phase: 'building',
mediaId: 101291,
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
message: 'Building character dictionary for Rascal Does Not Dream of Bunny Girl Senpai...',
},
{
phase: 'importing',
mediaId: 101291,
@@ -425,7 +431,7 @@ test('auto sync emits progress events for start import and completion', async ()
test('auto sync emits checking before snapshot resolves and skips generating on cache hit', async () => {
const userDataPath = makeTempDir();
const events: Array<{
phase: 'checking' | 'generating' | 'syncing' | 'importing' | 'ready' | 'failed';
phase: 'checking' | 'generating' | 'syncing' | 'building' | 'importing' | 'ready' | 'failed';
mediaId?: number;
mediaTitle?: string;
message: string;
@@ -503,6 +509,77 @@ test('auto sync emits checking before snapshot resolves and skips generating on
);
});
test('auto sync emits building while merged dictionary generation is in flight', async () => {
const userDataPath = makeTempDir();
const events: Array<{
phase: 'checking' | 'generating' | 'building' | 'syncing' | 'importing' | 'ready' | 'failed';
mediaId?: number;
mediaTitle?: string;
message: string;
changed?: boolean;
}> = [];
const buildDeferred = createDeferred<{
zipPath: string;
revision: string;
dictionaryTitle: string;
entryCount: number;
}>();
let importedRevision: string | null = null;
const runtime = createCharacterDictionaryAutoSyncRuntimeService({
userDataPath,
getConfig: () => ({
enabled: true,
maxLoaded: 3,
profileScope: 'all',
}),
getOrCreateCurrentSnapshot: async (_targetPath, progress) => {
progress?.onChecking?.({
mediaId: 101291,
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
});
return {
mediaId: 101291,
mediaTitle: 'Rascal Does Not Dream of Bunny Girl Senpai',
entryCount: 2560,
fromCache: true,
updatedAt: 1000,
};
},
buildMergedDictionary: async () => await buildDeferred.promise,
getYomitanDictionaryInfo: async () =>
importedRevision
? [{ title: 'SubMiner Character Dictionary', revision: importedRevision }]
: [],
importYomitanDictionary: async () => {
importedRevision = 'rev-101291';
return true;
},
deleteYomitanDictionary: async () => true,
upsertYomitanDictionarySettings: async () => true,
now: () => 1000,
onSyncStatus: (event) => {
events.push(event);
},
});
const syncPromise = runtime.runSyncNow();
await Promise.resolve();
assert.equal(
events.some((event) => event.phase === 'building'),
true,
);
buildDeferred.resolve({
zipPath: '/tmp/merged.zip',
revision: 'rev-101291',
dictionaryTitle: 'SubMiner Character Dictionary',
entryCount: 2560,
});
await syncPromise;
});
test('auto sync waits for tokenization-ready gate before Yomitan mutations', async () => {
const userDataPath = makeTempDir();
const gate = (() => {