Improve startup dictionary sync UX and default playback keybindings

- Add default `f` fullscreen overlay binding and switch default AniSkip skip key to `Tab`
- Make character-dictionary auto-sync non-blocking at startup with tokenization gating for Yomitan mutations
- Add ordered startup OSD progress (checking/generating/updating/importing), refresh current subtitle on sync completion, and extend regression tests
This commit is contained in:
2026-03-09 00:50:32 -07:00
parent a0521aeeaf
commit e0f82d28f0
36 changed files with 2691 additions and 148 deletions

View File

@@ -0,0 +1,106 @@
export interface StartupOsdSequencerCharacterDictionaryEvent {
phase: 'checking' | 'generating' | 'syncing' | 'importing' | 'ready' | 'failed';
message: string;
}
export function createStartupOsdSequencer(deps: { showOsd: (message: string) => void }): {
reset: () => void;
markTokenizationReady: () => void;
showAnnotationLoading: (message: string) => void;
markAnnotationLoadingComplete: (message: string) => void;
notifyCharacterDictionaryStatus: (event: StartupOsdSequencerCharacterDictionaryEvent) => void;
} {
let tokenizationReady = false;
let annotationLoadingMessage: string | null = null;
let pendingDictionaryProgress: StartupOsdSequencerCharacterDictionaryEvent | null = null;
let pendingDictionaryFailure: StartupOsdSequencerCharacterDictionaryEvent | null = null;
let dictionaryProgressShown = false;
const canShowDictionaryStatus = (): boolean =>
tokenizationReady && annotationLoadingMessage === null;
const flushBufferedDictionaryStatus = (): boolean => {
if (!canShowDictionaryStatus()) {
return false;
}
if (pendingDictionaryProgress) {
deps.showOsd(pendingDictionaryProgress.message);
dictionaryProgressShown = true;
return true;
}
if (pendingDictionaryFailure) {
deps.showOsd(pendingDictionaryFailure.message);
pendingDictionaryFailure = null;
dictionaryProgressShown = false;
return true;
}
return false;
};
return {
reset: () => {
tokenizationReady = false;
annotationLoadingMessage = null;
pendingDictionaryProgress = null;
pendingDictionaryFailure = null;
dictionaryProgressShown = false;
},
markTokenizationReady: () => {
tokenizationReady = true;
if (annotationLoadingMessage !== null) {
deps.showOsd(annotationLoadingMessage);
return;
}
flushBufferedDictionaryStatus();
},
showAnnotationLoading: (message) => {
annotationLoadingMessage = message;
if (tokenizationReady) {
deps.showOsd(message);
}
},
markAnnotationLoadingComplete: (message) => {
annotationLoadingMessage = null;
if (!tokenizationReady) {
return;
}
if (flushBufferedDictionaryStatus()) {
return;
}
deps.showOsd(message);
},
notifyCharacterDictionaryStatus: (event) => {
if (
event.phase === 'checking' ||
event.phase === 'generating' ||
event.phase === 'syncing' ||
event.phase === 'importing'
) {
pendingDictionaryProgress = event;
pendingDictionaryFailure = null;
if (canShowDictionaryStatus()) {
deps.showOsd(event.message);
dictionaryProgressShown = true;
}
return;
}
pendingDictionaryProgress = null;
if (event.phase === 'failed') {
if (canShowDictionaryStatus()) {
deps.showOsd(event.message);
} else {
pendingDictionaryFailure = event;
}
dictionaryProgressShown = false;
return;
}
pendingDictionaryFailure = null;
if (canShowDictionaryStatus() && dictionaryProgressShown) {
deps.showOsd(event.message);
}
dictionaryProgressShown = false;
},
};
}