fix: suppress overlay subtitle immediately when character dictionary modal opens (#84)

This commit is contained in:
2026-05-25 02:30:33 -07:00
committed by GitHub
parent 9fe13601fb
commit 7e6f9672cf
15 changed files with 307 additions and 49 deletions
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Hid the visible subtitle overlay as soon as the character dictionary modal opens, including while AniList lookup is still loading or returns no results.
+6 -3
View File
@@ -10,7 +10,7 @@ import {
test('showMpvOsdRuntime sends show-text when connected', () => { test('showMpvOsdRuntime sends show-text when connected', () => {
const commands: (string | number)[][] = []; const commands: (string | number)[][] = [];
showMpvOsdRuntime( const shown = showMpvOsdRuntime(
{ {
connected: true, connected: true,
send: ({ command }) => { send: ({ command }) => {
@@ -19,12 +19,13 @@ test('showMpvOsdRuntime sends show-text when connected', () => {
}, },
'hello', 'hello',
); );
assert.equal(shown, true);
assert.deepEqual(commands, [['show-text', 'hello', '3000']]); assert.deepEqual(commands, [['show-text', 'hello', '3000']]);
}); });
test('showMpvOsdRuntime enables property expansion for placeholder-based messages', () => { test('showMpvOsdRuntime enables property expansion for placeholder-based messages', () => {
const commands: (string | number)[][] = []; const commands: (string | number)[][] = [];
showMpvOsdRuntime( const shown = showMpvOsdRuntime(
{ {
connected: true, connected: true,
send: ({ command }) => { send: ({ command }) => {
@@ -33,6 +34,7 @@ test('showMpvOsdRuntime enables property expansion for placeholder-based message
}, },
'Subtitle delay: ${sub-delay}', 'Subtitle delay: ${sub-delay}',
); );
assert.equal(shown, true);
assert.deepEqual(commands, [ assert.deepEqual(commands, [
['expand-properties', 'show-text', 'Subtitle delay: ${sub-delay}', '3000'], ['expand-properties', 'show-text', 'Subtitle delay: ${sub-delay}', '3000'],
]); ]);
@@ -40,7 +42,7 @@ test('showMpvOsdRuntime enables property expansion for placeholder-based message
test('showMpvOsdRuntime logs fallback when disconnected', () => { test('showMpvOsdRuntime logs fallback when disconnected', () => {
const logs: string[] = []; const logs: string[] = [];
showMpvOsdRuntime( const shown = showMpvOsdRuntime(
{ {
connected: false, connected: false,
send: () => {}, send: () => {},
@@ -50,6 +52,7 @@ test('showMpvOsdRuntime logs fallback when disconnected', () => {
logs.push(line); logs.push(line);
}, },
); );
assert.equal(shown, false);
assert.deepEqual(logs, ['OSD (MPV not connected): hello']); assert.deepEqual(logs, ['OSD (MPV not connected): hello']);
}); });
+3 -2
View File
@@ -51,15 +51,16 @@ export function showMpvOsdRuntime(
mpvClient: MpvRuntimeClientLike | null, mpvClient: MpvRuntimeClientLike | null,
text: string, text: string,
fallbackLog: (text: string) => void = (line) => logger.info(line), fallbackLog: (text: string) => void = (line) => logger.info(line),
): void { ): boolean {
if (mpvClient && mpvClient.connected) { if (mpvClient && mpvClient.connected) {
const command = text.includes('${') const command = text.includes('${')
? ['expand-properties', 'show-text', text, '3000'] ? ['expand-properties', 'show-text', text, '3000']
: ['show-text', text, '3000']; : ['show-text', text, '3000'];
mpvClient.send({ command }); mpvClient.send({ command });
return; return true;
} }
fallbackLog(`OSD (MPV not connected): ${text}`); fallbackLog(`OSD (MPV not connected): ${text}`);
return false;
} }
export function replayCurrentSubtitleRuntime(mpvClient: MpvRuntimeClientLike | null): void { export function replayCurrentSubtitleRuntime(mpvClient: MpvRuntimeClientLike | null): void {
+3 -1
View File
@@ -5347,7 +5347,9 @@ function getUpdateService() {
{ notificationType: getResolvedConfig().updates.notificationType, version }, { notificationType: getResolvedConfig().updates.notificationType, version },
{ {
showSystemNotification: (title, body) => showDesktopNotification(title, { body }), showSystemNotification: (title, body) => showDesktopNotification(title, { body }),
showOsdNotification: (message) => showMpvOsd(message), showOsdNotification: (message) => {
showMpvOsd(message);
},
log: (message) => logger.warn(message), log: (message) => logger.warn(message),
}, },
), ),
@@ -22,31 +22,41 @@ test('auto sync notifications send osd updates for progress phases', () => {
notifyCharacterDictionaryAutoSyncStatus(makeEvent('checking', 'checking'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('checking', 'checking'), {
getNotificationType: () => 'osd', getNotificationType: () => 'osd',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating'), {
getNotificationType: () => 'osd', getNotificationType: () => 'osd',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
getNotificationType: () => 'osd', getNotificationType: () => 'osd',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
getNotificationType: () => 'osd', getNotificationType: () => 'osd',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
getNotificationType: () => 'osd', getNotificationType: () => 'osd',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
@@ -65,28 +75,85 @@ test('auto sync notifications never send desktop notifications', () => {
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
getNotificationType: () => 'both', getNotificationType: () => 'both',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
getNotificationType: () => 'both', getNotificationType: () => 'both',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
getNotificationType: () => 'both', getNotificationType: () => 'both',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
notifyCharacterDictionaryAutoSyncStatus(makeEvent('failed', 'failed'), { notifyCharacterDictionaryAutoSyncStatus(makeEvent('failed', 'failed'), {
getNotificationType: () => 'both', getNotificationType: () => 'both',
showOsd: (message) => calls.push(`osd:${message}`), showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) => showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`), calls.push(`desktop:${title}:${options.body ?? ''}`),
}); });
assert.deepEqual(calls, ['osd:syncing', 'osd:importing', 'osd:ready', 'osd:failed']); assert.deepEqual(calls, ['osd:syncing', 'osd:importing', 'osd:ready', 'osd:failed']);
}); });
test('auto sync notifications fall back to desktop for long progress when osd is unavailable', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating'), {
getNotificationType: () => 'both',
showOsd: (message) => {
calls.push(`osd:${message}`);
return false;
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
getNotificationType: () => 'both',
showOsd: (message) => {
calls.push(`osd:${message}`);
return false;
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
assert.deepEqual(calls, ['osd:generating', 'desktop:SubMiner:generating', 'osd:ready']);
});
test('auto sync notifications fall back to desktop when startup sequencer cannot show osd', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
getNotificationType: () => 'both',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
startupOsdSequencer: {
notifyCharacterDictionaryStatus: (event) => {
calls.push(`sequencer:${event.phase}:${event.message}`);
return false;
},
},
});
assert.deepEqual(calls, [
'sequencer:importing:importing',
'desktop:SubMiner:importing',
]);
});
@@ -5,10 +5,12 @@ export type CharacterDictionaryAutoSyncNotificationEvent = CharacterDictionaryAu
export interface CharacterDictionaryAutoSyncNotificationDeps { export interface CharacterDictionaryAutoSyncNotificationDeps {
getNotificationType: () => 'osd' | 'system' | 'both' | 'none' | undefined; getNotificationType: () => 'osd' | 'system' | 'both' | 'none' | undefined;
showOsd: (message: string) => void; showOsd: (message: string) => boolean | void;
showDesktopNotification: (title: string, options: { body?: string }) => void; showDesktopNotification: (title: string, options: { body?: string }) => void;
startupOsdSequencer?: { startupOsdSequencer?: {
notifyCharacterDictionaryStatus: (event: StartupOsdSequencerCharacterDictionaryEvent) => void; notifyCharacterDictionaryStatus: (
event: StartupOsdSequencerCharacterDictionaryEvent,
) => boolean;
}; };
} }
@@ -16,6 +18,16 @@ function shouldShowOsd(type: 'osd' | 'system' | 'both' | 'none' | undefined): bo
return type !== 'none'; return type !== 'none';
} }
function shouldFallbackToDesktop(
type: 'osd' | 'system' | 'both' | 'none' | undefined,
phase: CharacterDictionaryAutoSyncNotificationEvent['phase'],
): boolean {
return (
(type === 'system' || type === 'both') &&
(phase === 'generating' || phase === 'building' || phase === 'importing')
);
}
export function notifyCharacterDictionaryAutoSyncStatus( export function notifyCharacterDictionaryAutoSyncStatus(
event: CharacterDictionaryAutoSyncNotificationEvent, event: CharacterDictionaryAutoSyncNotificationEvent,
deps: CharacterDictionaryAutoSyncNotificationDeps, deps: CharacterDictionaryAutoSyncNotificationDeps,
@@ -23,12 +35,18 @@ export function notifyCharacterDictionaryAutoSyncStatus(
const type = deps.getNotificationType(); const type = deps.getNotificationType();
if (shouldShowOsd(type)) { if (shouldShowOsd(type)) {
if (deps.startupOsdSequencer) { if (deps.startupOsdSequencer) {
deps.startupOsdSequencer.notifyCharacterDictionaryStatus({ const shown = deps.startupOsdSequencer.notifyCharacterDictionaryStatus({
phase: event.phase, phase: event.phase,
message: event.message, message: event.message,
}); });
if (!shown && shouldFallbackToDesktop(type, event.phase)) {
deps.showDesktopNotification('SubMiner', { body: event.message });
}
return; return;
} }
deps.showOsd(event.message); const shown = deps.showOsd(event.message) !== false;
if (!shown && shouldFallbackToDesktop(type, event.phase)) {
deps.showDesktopNotification('SubMiner', { body: event.message });
}
} }
} }
@@ -41,6 +41,7 @@ test('show mpv osd main deps map runtime delegates and logging callback', () =>
showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => { showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => {
calls.push(`show:${text}`); calls.push(`show:${text}`);
fallbackLog('fallback'); fallbackLog('fallback');
return false;
}, },
getMpvClient: () => client, getMpvClient: () => client,
logInfo: (line) => calls.push(`info:${line}`), logInfo: (line) => calls.push(`info:${line}`),
@@ -48,6 +49,9 @@ test('show mpv osd main deps map runtime delegates and logging callback', () =>
assert.deepEqual(deps.getMpvClient(), client); assert.deepEqual(deps.getMpvClient(), client);
deps.appendToMpvLog('hello'); deps.appendToMpvLog('hello');
deps.showMpvOsdRuntime(deps.getMpvClient(), 'subtitle', (line) => deps.logInfo(line)); const shown = deps.showMpvOsdRuntime(deps.getMpvClient(), 'subtitle', (line) =>
deps.logInfo(line),
);
assert.equal(shown, false);
assert.deepEqual(calls, ['append:hello', 'show:subtitle', 'info:fallback']); assert.deepEqual(calls, ['append:hello', 'show:subtitle', 'info:fallback']);
}); });
+3 -1
View File
@@ -99,12 +99,14 @@ test('show mpv osd logs marker and forwards fallback logging', () => {
showMpvOsdRuntime: (_client, text, fallbackLog) => { showMpvOsdRuntime: (_client, text, fallbackLog) => {
calls.push(`show:${text}`); calls.push(`show:${text}`);
fallbackLog('fallback-line'); fallbackLog('fallback-line');
return false;
}, },
getMpvClient: () => client, getMpvClient: () => client,
logInfo: (line) => calls.push(`info:${line}`), logInfo: (line) => calls.push(`info:${line}`),
}); });
showMpvOsd('subtitle copied'); const shown = showMpvOsd('subtitle copied');
assert.equal(shown, false);
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'append:[OSD] subtitle copied', 'append:[OSD] subtitle copied',
'show:subtitle copied', 'show:subtitle copied',
+3 -3
View File
@@ -57,13 +57,13 @@ export function createShowMpvOsdHandler(deps: {
mpvClient: MpvRuntimeClientLike | null, mpvClient: MpvRuntimeClientLike | null,
text: string, text: string,
fallbackLog: (line: string) => void, fallbackLog: (line: string) => void,
) => void; ) => boolean;
getMpvClient: () => MpvRuntimeClientLike | null; getMpvClient: () => MpvRuntimeClientLike | null;
logInfo: (line: string) => void; logInfo: (line: string) => void;
}) { }) {
return (text: string): void => { return (text: string): boolean => {
deps.appendToMpvLog(`[OSD] ${text}`); deps.appendToMpvLog(`[OSD] ${text}`);
deps.showMpvOsdRuntime(deps.getMpvClient(), text, (line) => { return deps.showMpvOsdRuntime(deps.getMpvClient(), text, (line) => {
deps.logInfo(line); deps.logInfo(line);
}); });
}; };
@@ -19,15 +19,17 @@ test('mpv osd runtime handlers compose append and osd logging flow', async () =>
showMpvOsdRuntime: (_client, text, fallbackLog) => { showMpvOsdRuntime: (_client, text, fallbackLog) => {
calls.push(`show:${text}`); calls.push(`show:${text}`);
fallbackLog('fallback'); fallbackLog('fallback');
return false;
}, },
getMpvClient: () => null, getMpvClient: () => null,
logInfo: (line) => calls.push(`info:${line}`), logInfo: (line) => calls.push(`info:${line}`),
}), }),
}); });
runtime.showMpvOsd('hello'); const shown = runtime.showMpvOsd('hello');
await runtime.flushMpvLog(); await runtime.flushMpvLog();
assert.equal(shown, false);
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'show:hello', 'show:hello',
'info:fallback', 'info:fallback',
+40 -2
View File
@@ -100,7 +100,7 @@ test('startup OSD replaces earlier dictionary progress with later building progr
]); ]);
}); });
test('startup OSD skips buffered dictionary ready messages when progress completed before it became visible', () => { test('startup OSD shows dictionary ready when progress completed before it became visible', () => {
const osdMessages: string[] = []; const osdMessages: string[] = [];
const sequencer = createStartupOsdSequencer({ const sequencer = createStartupOsdSequencer({
showOsd: (message) => { showOsd: (message) => {
@@ -117,7 +117,10 @@ test('startup OSD skips buffered dictionary ready messages when progress complet
sequencer.markTokenizationReady(); sequencer.markTokenizationReady();
sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded'); sequencer.markAnnotationLoadingComplete('Subtitle annotations loaded');
assert.deepEqual(osdMessages, ['Subtitle annotations loaded']); assert.deepEqual(osdMessages, [
'Character dictionary ready for Frieren',
'Subtitle annotations loaded',
]);
}); });
test('startup OSD shows dictionary failure after annotation loading completes', () => { test('startup OSD shows dictionary failure after annotation loading completes', () => {
@@ -184,3 +187,38 @@ test('startup OSD shows later dictionary progress immediately once tokenization
'Generating character dictionary for Frieren...', 'Generating character dictionary for Frieren...',
]); ]);
}); });
test('startup OSD keeps dictionary progress pending when mpv osd is unavailable', () => {
const osdMessages: string[] = [];
let osdAvailable = false;
const sequencer = createStartupOsdSequencer({
showOsd: (message) => {
osdMessages.push(message);
return osdAvailable;
},
});
sequencer.markTokenizationReady();
sequencer.notifyCharacterDictionaryStatus(
makeDictionaryEvent('generating', 'Generating character dictionary for Frieren...'),
);
sequencer.notifyCharacterDictionaryStatus(
makeDictionaryEvent('ready', 'Character dictionary ready for Frieren'),
);
assert.deepEqual(osdMessages, [
'Generating character dictionary for Frieren...',
'Character dictionary ready for Frieren',
]);
osdAvailable = true;
sequencer.notifyCharacterDictionaryStatus(
makeDictionaryEvent('ready', 'Character dictionary ready for Frieren'),
);
assert.deepEqual(osdMessages, [
'Generating character dictionary for Frieren...',
'Character dictionary ready for Frieren',
'Character dictionary ready for Frieren',
]);
});
+48 -21
View File
@@ -3,22 +3,24 @@ export interface StartupOsdSequencerCharacterDictionaryEvent {
message: string; message: string;
} }
export function createStartupOsdSequencer(deps: { showOsd: (message: string) => void }): { export function createStartupOsdSequencer(deps: { showOsd: (message: string) => boolean | void }): {
reset: () => void; reset: () => void;
markTokenizationReady: () => void; markTokenizationReady: () => void;
showAnnotationLoading: (message: string) => void; showAnnotationLoading: (message: string) => void;
markAnnotationLoadingComplete: (message: string) => void; markAnnotationLoadingComplete: (message: string) => void;
notifyCharacterDictionaryStatus: (event: StartupOsdSequencerCharacterDictionaryEvent) => void; notifyCharacterDictionaryStatus: (event: StartupOsdSequencerCharacterDictionaryEvent) => boolean;
} { } {
let tokenizationReady = false; let tokenizationReady = false;
let tokenizationWarmupCompleted = false; let tokenizationWarmupCompleted = false;
let annotationLoadingMessage: string | null = null; let annotationLoadingMessage: string | null = null;
let pendingDictionaryProgress: StartupOsdSequencerCharacterDictionaryEvent | null = null; let pendingDictionaryProgress: StartupOsdSequencerCharacterDictionaryEvent | null = null;
let pendingDictionaryFailure: StartupOsdSequencerCharacterDictionaryEvent | null = null; let pendingDictionaryFailure: StartupOsdSequencerCharacterDictionaryEvent | null = null;
let pendingDictionaryReady: StartupOsdSequencerCharacterDictionaryEvent | null = null;
let dictionaryProgressShown = false; let dictionaryProgressShown = false;
const canShowDictionaryStatus = (): boolean => const canShowDictionaryStatus = (): boolean =>
tokenizationReady && annotationLoadingMessage === null; tokenizationReady && annotationLoadingMessage === null;
const showOsd = (message: string): boolean => deps.showOsd(message) !== false;
const flushBufferedDictionaryStatus = (): boolean => { const flushBufferedDictionaryStatus = (): boolean => {
if (!canShowDictionaryStatus()) { if (!canShowDictionaryStatus()) {
@@ -28,15 +30,24 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
if (dictionaryProgressShown) { if (dictionaryProgressShown) {
return true; return true;
} }
deps.showOsd(pendingDictionaryProgress.message); dictionaryProgressShown = showOsd(pendingDictionaryProgress.message);
dictionaryProgressShown = true; return dictionaryProgressShown;
return true; }
if (pendingDictionaryReady) {
const shown = showOsd(pendingDictionaryReady.message);
if (shown) {
pendingDictionaryReady = null;
dictionaryProgressShown = false;
}
return shown;
} }
if (pendingDictionaryFailure) { if (pendingDictionaryFailure) {
deps.showOsd(pendingDictionaryFailure.message); const shown = showOsd(pendingDictionaryFailure.message);
pendingDictionaryFailure = null; if (shown) {
dictionaryProgressShown = false; pendingDictionaryFailure = null;
return true; dictionaryProgressShown = false;
}
return shown;
} }
return false; return false;
}; };
@@ -47,13 +58,14 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
annotationLoadingMessage = null; annotationLoadingMessage = null;
pendingDictionaryProgress = null; pendingDictionaryProgress = null;
pendingDictionaryFailure = null; pendingDictionaryFailure = null;
pendingDictionaryReady = null;
dictionaryProgressShown = false; dictionaryProgressShown = false;
}, },
markTokenizationReady: () => { markTokenizationReady: () => {
tokenizationWarmupCompleted = true; tokenizationWarmupCompleted = true;
tokenizationReady = true; tokenizationReady = true;
if (annotationLoadingMessage !== null) { if (annotationLoadingMessage !== null) {
deps.showOsd(annotationLoadingMessage); showOsd(annotationLoadingMessage);
return; return;
} }
flushBufferedDictionaryStatus(); flushBufferedDictionaryStatus();
@@ -61,7 +73,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
showAnnotationLoading: (message) => { showAnnotationLoading: (message) => {
annotationLoadingMessage = message; annotationLoadingMessage = message;
if (tokenizationReady) { if (tokenizationReady) {
deps.showOsd(message); showOsd(message);
} }
}, },
markAnnotationLoadingComplete: (message) => { markAnnotationLoadingComplete: (message) => {
@@ -72,7 +84,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
if (flushBufferedDictionaryStatus()) { if (flushBufferedDictionaryStatus()) {
return; return;
} }
deps.showOsd(message); showOsd(message);
}, },
notifyCharacterDictionaryStatus: (event) => { notifyCharacterDictionaryStatus: (event) => {
if ( if (
@@ -84,32 +96,47 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
) { ) {
pendingDictionaryProgress = event; pendingDictionaryProgress = event;
pendingDictionaryFailure = null; pendingDictionaryFailure = null;
pendingDictionaryReady = null;
if (canShowDictionaryStatus()) { if (canShowDictionaryStatus()) {
deps.showOsd(event.message); dictionaryProgressShown = showOsd(event.message);
dictionaryProgressShown = true;
} else if (tokenizationReady) { } else if (tokenizationReady) {
deps.showOsd(event.message); dictionaryProgressShown = showOsd(event.message);
dictionaryProgressShown = true;
} }
return; return dictionaryProgressShown;
} }
pendingDictionaryProgress = null; pendingDictionaryProgress = null;
if (event.phase === 'failed') { if (event.phase === 'failed') {
pendingDictionaryReady = null;
if (canShowDictionaryStatus()) { if (canShowDictionaryStatus()) {
deps.showOsd(event.message); if (!showOsd(event.message)) {
pendingDictionaryFailure = event;
return false;
}
dictionaryProgressShown = false;
return true;
} else { } else {
pendingDictionaryFailure = event; pendingDictionaryFailure = event;
} }
dictionaryProgressShown = false; dictionaryProgressShown = false;
return; return false;
} }
pendingDictionaryFailure = null; pendingDictionaryFailure = null;
if (canShowDictionaryStatus() && dictionaryProgressShown) { if (canShowDictionaryStatus()) {
deps.showOsd(event.message); if (!showOsd(event.message)) {
pendingDictionaryReady = event;
dictionaryProgressShown = false;
return false;
}
pendingDictionaryReady = null;
dictionaryProgressShown = false;
return true;
} else {
pendingDictionaryReady = event;
} }
dictionaryProgressShown = false; dictionaryProgressShown = false;
return false;
}, },
}; };
} }
@@ -62,6 +62,91 @@ function flushAsyncWork(): Promise<void> {
}); });
} }
test('character dictionary modal announces open before AniList refresh resolves', async () => {
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
let resolveSelection: (snapshot: CharacterDictionarySelectionSnapshot) => void = () => {};
const selectionPromise = new Promise<CharacterDictionarySelectionSnapshot>((resolve) => {
resolveSelection = resolve;
});
const events: string[] = [];
const overlay = createNodeStub();
const modalNode = createNodeStub(true);
const state = createRendererState();
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
electronAPI: {
getCharacterDictionarySelection: () => selectionPromise,
setCharacterDictionarySelection: async () => ({
ok: false,
seriesKey: 'test',
selected: { id: 0, title: '', episodes: null },
staleMediaIds: [],
}),
notifyOverlayModalClosed: () => {},
notifyOverlayModalOpened: (modal: string) => {
events.push(`notify:${modal}`);
},
} satisfies Pick<
ElectronAPI,
| 'getCharacterDictionarySelection'
| 'setCharacterDictionarySelection'
| 'notifyOverlayModalClosed'
| 'notifyOverlayModalOpened'
>,
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createElementStub(),
},
});
try {
const modal = createCharacterDictionaryModal(
{
state,
dom: {
overlay,
characterDictionaryModal: modalNode,
characterDictionaryClose: createNodeStub(),
characterDictionarySummary: createNodeStub(),
characterDictionaryCurrent: createNodeStub(),
characterDictionaryCandidates: createNodeStub(),
characterDictionaryStatus: createNodeStub(),
},
} as never,
{
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {
events.push('sync-subtitle-suppression');
},
},
);
const openPromise = modal.openCharacterDictionaryModal();
assert.equal(state.characterDictionaryModalOpen, true);
assert.equal(modalNode.classList.contains('hidden'), false);
assert.deepEqual(events, ['sync-subtitle-suppression', 'notify:character-dictionary']);
resolveSelection({
seriesKey: 'tower-of-god-2020',
guessTitle: 'Tower of God',
current: null,
override: null,
candidates: [{ id: 115230, title: 'Tower of God', episodes: 13 }],
});
await openPromise;
} finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
}
});
test('character dictionary modal loads candidates and applies selected override', async () => { test('character dictionary modal loads candidates and applies selected override', async () => {
const previousWindow = globalThis.window; const previousWindow = globalThis.window;
const previousDocument = globalThis.document; const previousDocument = globalThis.document;
@@ -95,11 +180,13 @@ test('character dictionary modal loads candidates and applies selected override'
}; };
}, },
notifyOverlayModalClosed: () => {}, notifyOverlayModalClosed: () => {},
notifyOverlayModalOpened: () => {},
} satisfies Pick< } satisfies Pick<
ElectronAPI, ElectronAPI,
| 'getCharacterDictionarySelection' | 'getCharacterDictionarySelection'
| 'setCharacterDictionarySelection' | 'setCharacterDictionarySelection'
| 'notifyOverlayModalClosed' | 'notifyOverlayModalClosed'
| 'notifyOverlayModalOpened'
>, >,
}, },
}); });
@@ -175,11 +262,13 @@ test('character dictionary modal shows refresh errors without rejecting open', a
staleMediaIds: [], staleMediaIds: [],
}), }),
notifyOverlayModalClosed: () => {}, notifyOverlayModalClosed: () => {},
notifyOverlayModalOpened: () => {},
} satisfies Pick< } satisfies Pick<
ElectronAPI, ElectronAPI,
| 'getCharacterDictionarySelection' | 'getCharacterDictionarySelection'
| 'setCharacterDictionarySelection' | 'setCharacterDictionarySelection'
| 'notifyOverlayModalClosed' | 'notifyOverlayModalClosed'
| 'notifyOverlayModalOpened'
>, >,
}, },
}); });
@@ -153,6 +153,7 @@ export function createCharacterDictionaryModal(
ctx.dom.overlay.classList.add('interactive'); ctx.dom.overlay.classList.add('interactive');
ctx.dom.characterDictionaryModal.classList.remove('hidden'); ctx.dom.characterDictionaryModal.classList.remove('hidden');
ctx.dom.characterDictionaryModal.setAttribute('aria-hidden', 'false'); ctx.dom.characterDictionaryModal.setAttribute('aria-hidden', 'false');
window.electronAPI.notifyOverlayModalOpened('character-dictionary');
setStatus('Loading AniList candidates...'); setStatus('Loading AniList candidates...');
} }
@@ -160,6 +161,7 @@ export function createCharacterDictionaryModal(
if (!ctx.state.characterDictionaryModalOpen) { if (!ctx.state.characterDictionaryModalOpen) {
showShell(); showShell();
} else { } else {
window.electronAPI.notifyOverlayModalOpened('character-dictionary');
setStatus('Refreshing AniList candidates...'); setStatus('Refreshing AniList candidates...');
} }
try { try {
-1
View File
@@ -463,7 +463,6 @@ function registerModalOpenHandlers(): void {
window.electronAPI.onOpenCharacterDictionary(() => { window.electronAPI.onOpenCharacterDictionary(() => {
runGuardedAsync('character-dictionary:open', async () => { runGuardedAsync('character-dictionary:open', async () => {
await characterDictionaryModal.openCharacterDictionaryModal(); await characterDictionaryModal.openCharacterDictionaryModal();
window.electronAPI.notifyOverlayModalOpened('character-dictionary');
}); });
}); });
window.electronAPI.onOpenSessionHelp(() => { window.electronAPI.onOpenSessionHelp(() => {