mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-25 12:55:18 -07:00
fix: suppress overlay subtitle immediately when character dictionary modal opens (#84)
This commit is contained in:
@@ -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.
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
test('showMpvOsdRuntime sends show-text when connected', () => {
|
||||
const commands: (string | number)[][] = [];
|
||||
showMpvOsdRuntime(
|
||||
const shown = showMpvOsdRuntime(
|
||||
{
|
||||
connected: true,
|
||||
send: ({ command }) => {
|
||||
@@ -19,12 +19,13 @@ test('showMpvOsdRuntime sends show-text when connected', () => {
|
||||
},
|
||||
'hello',
|
||||
);
|
||||
assert.equal(shown, true);
|
||||
assert.deepEqual(commands, [['show-text', 'hello', '3000']]);
|
||||
});
|
||||
|
||||
test('showMpvOsdRuntime enables property expansion for placeholder-based messages', () => {
|
||||
const commands: (string | number)[][] = [];
|
||||
showMpvOsdRuntime(
|
||||
const shown = showMpvOsdRuntime(
|
||||
{
|
||||
connected: true,
|
||||
send: ({ command }) => {
|
||||
@@ -33,6 +34,7 @@ test('showMpvOsdRuntime enables property expansion for placeholder-based message
|
||||
},
|
||||
'Subtitle delay: ${sub-delay}',
|
||||
);
|
||||
assert.equal(shown, true);
|
||||
assert.deepEqual(commands, [
|
||||
['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', () => {
|
||||
const logs: string[] = [];
|
||||
showMpvOsdRuntime(
|
||||
const shown = showMpvOsdRuntime(
|
||||
{
|
||||
connected: false,
|
||||
send: () => {},
|
||||
@@ -50,6 +52,7 @@ test('showMpvOsdRuntime logs fallback when disconnected', () => {
|
||||
logs.push(line);
|
||||
},
|
||||
);
|
||||
assert.equal(shown, false);
|
||||
assert.deepEqual(logs, ['OSD (MPV not connected): hello']);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,15 +51,16 @@ export function showMpvOsdRuntime(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (text: string) => void = (line) => logger.info(line),
|
||||
): void {
|
||||
): boolean {
|
||||
if (mpvClient && mpvClient.connected) {
|
||||
const command = text.includes('${')
|
||||
? ['expand-properties', 'show-text', text, '3000']
|
||||
: ['show-text', text, '3000'];
|
||||
mpvClient.send({ command });
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function replayCurrentSubtitleRuntime(mpvClient: MpvRuntimeClientLike | null): void {
|
||||
|
||||
+3
-1
@@ -5347,7 +5347,9 @@ function getUpdateService() {
|
||||
{ notificationType: getResolvedConfig().updates.notificationType, version },
|
||||
{
|
||||
showSystemNotification: (title, body) => showDesktopNotification(title, { body }),
|
||||
showOsdNotification: (message) => showMpvOsd(message),
|
||||
showOsdNotification: (message) => {
|
||||
showMpvOsd(message);
|
||||
},
|
||||
log: (message) => logger.warn(message),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -22,31 +22,41 @@ test('auto sync notifications send osd updates for progress phases', () => {
|
||||
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('checking', 'checking'), {
|
||||
getNotificationType: () => 'osd',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating'), {
|
||||
getNotificationType: () => 'osd',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
|
||||
getNotificationType: () => 'osd',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
|
||||
getNotificationType: () => 'osd',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
|
||||
getNotificationType: () => 'osd',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
@@ -65,28 +75,85 @@ test('auto sync notifications never send desktop notifications', () => {
|
||||
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
|
||||
getNotificationType: () => 'both',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
|
||||
getNotificationType: () => 'both',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
|
||||
getNotificationType: () => 'both',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
notifyCharacterDictionaryAutoSyncStatus(makeEvent('failed', 'failed'), {
|
||||
getNotificationType: () => 'both',
|
||||
showOsd: (message) => calls.push(`osd:${message}`),
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
|
||||
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 {
|
||||
getNotificationType: () => 'osd' | 'system' | 'both' | 'none' | undefined;
|
||||
showOsd: (message: string) => void;
|
||||
showOsd: (message: string) => boolean | void;
|
||||
showDesktopNotification: (title: string, options: { body?: string }) => void;
|
||||
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';
|
||||
}
|
||||
|
||||
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(
|
||||
event: CharacterDictionaryAutoSyncNotificationEvent,
|
||||
deps: CharacterDictionaryAutoSyncNotificationDeps,
|
||||
@@ -23,12 +35,18 @@ export function notifyCharacterDictionaryAutoSyncStatus(
|
||||
const type = deps.getNotificationType();
|
||||
if (shouldShowOsd(type)) {
|
||||
if (deps.startupOsdSequencer) {
|
||||
deps.startupOsdSequencer.notifyCharacterDictionaryStatus({
|
||||
const shown = deps.startupOsdSequencer.notifyCharacterDictionaryStatus({
|
||||
phase: event.phase,
|
||||
message: event.message,
|
||||
});
|
||||
if (!shown && shouldFallbackToDesktop(type, event.phase)) {
|
||||
deps.showDesktopNotification('SubMiner', { body: event.message });
|
||||
}
|
||||
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) => {
|
||||
calls.push(`show:${text}`);
|
||||
fallbackLog('fallback');
|
||||
return false;
|
||||
},
|
||||
getMpvClient: () => client,
|
||||
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);
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -99,12 +99,14 @@ test('show mpv osd logs marker and forwards fallback logging', () => {
|
||||
showMpvOsdRuntime: (_client, text, fallbackLog) => {
|
||||
calls.push(`show:${text}`);
|
||||
fallbackLog('fallback-line');
|
||||
return false;
|
||||
},
|
||||
getMpvClient: () => client,
|
||||
logInfo: (line) => calls.push(`info:${line}`),
|
||||
});
|
||||
|
||||
showMpvOsd('subtitle copied');
|
||||
const shown = showMpvOsd('subtitle copied');
|
||||
assert.equal(shown, false);
|
||||
assert.deepEqual(calls, [
|
||||
'append:[OSD] subtitle copied',
|
||||
'show:subtitle copied',
|
||||
|
||||
@@ -57,13 +57,13 @@ export function createShowMpvOsdHandler(deps: {
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (line: string) => void,
|
||||
) => void;
|
||||
) => boolean;
|
||||
getMpvClient: () => MpvRuntimeClientLike | null;
|
||||
logInfo: (line: string) => void;
|
||||
}) {
|
||||
return (text: string): void => {
|
||||
return (text: string): boolean => {
|
||||
deps.appendToMpvLog(`[OSD] ${text}`);
|
||||
deps.showMpvOsdRuntime(deps.getMpvClient(), text, (line) => {
|
||||
return deps.showMpvOsdRuntime(deps.getMpvClient(), text, (line) => {
|
||||
deps.logInfo(line);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,15 +19,17 @@ test('mpv osd runtime handlers compose append and osd logging flow', async () =>
|
||||
showMpvOsdRuntime: (_client, text, fallbackLog) => {
|
||||
calls.push(`show:${text}`);
|
||||
fallbackLog('fallback');
|
||||
return false;
|
||||
},
|
||||
getMpvClient: () => null,
|
||||
logInfo: (line) => calls.push(`info:${line}`),
|
||||
}),
|
||||
});
|
||||
|
||||
runtime.showMpvOsd('hello');
|
||||
const shown = runtime.showMpvOsd('hello');
|
||||
await runtime.flushMpvLog();
|
||||
|
||||
assert.equal(shown, false);
|
||||
assert.deepEqual(calls, [
|
||||
'show:hello',
|
||||
'info:fallback',
|
||||
|
||||
@@ -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 sequencer = createStartupOsdSequencer({
|
||||
showOsd: (message) => {
|
||||
@@ -117,7 +117,10 @@ test('startup OSD skips buffered dictionary ready messages when progress complet
|
||||
sequencer.markTokenizationReady();
|
||||
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', () => {
|
||||
@@ -184,3 +187,38 @@ test('startup OSD shows later dictionary progress immediately once tokenization
|
||||
'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',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3,22 +3,24 @@ export interface StartupOsdSequencerCharacterDictionaryEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function createStartupOsdSequencer(deps: { showOsd: (message: string) => void }): {
|
||||
export function createStartupOsdSequencer(deps: { showOsd: (message: string) => boolean | void }): {
|
||||
reset: () => void;
|
||||
markTokenizationReady: () => void;
|
||||
showAnnotationLoading: (message: string) => void;
|
||||
markAnnotationLoadingComplete: (message: string) => void;
|
||||
notifyCharacterDictionaryStatus: (event: StartupOsdSequencerCharacterDictionaryEvent) => void;
|
||||
notifyCharacterDictionaryStatus: (event: StartupOsdSequencerCharacterDictionaryEvent) => boolean;
|
||||
} {
|
||||
let tokenizationReady = false;
|
||||
let tokenizationWarmupCompleted = false;
|
||||
let annotationLoadingMessage: string | null = null;
|
||||
let pendingDictionaryProgress: StartupOsdSequencerCharacterDictionaryEvent | null = null;
|
||||
let pendingDictionaryFailure: StartupOsdSequencerCharacterDictionaryEvent | null = null;
|
||||
let pendingDictionaryReady: StartupOsdSequencerCharacterDictionaryEvent | null = null;
|
||||
let dictionaryProgressShown = false;
|
||||
|
||||
const canShowDictionaryStatus = (): boolean =>
|
||||
tokenizationReady && annotationLoadingMessage === null;
|
||||
const showOsd = (message: string): boolean => deps.showOsd(message) !== false;
|
||||
|
||||
const flushBufferedDictionaryStatus = (): boolean => {
|
||||
if (!canShowDictionaryStatus()) {
|
||||
@@ -28,15 +30,24 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
if (dictionaryProgressShown) {
|
||||
return true;
|
||||
}
|
||||
deps.showOsd(pendingDictionaryProgress.message);
|
||||
dictionaryProgressShown = true;
|
||||
return true;
|
||||
dictionaryProgressShown = showOsd(pendingDictionaryProgress.message);
|
||||
return dictionaryProgressShown;
|
||||
}
|
||||
if (pendingDictionaryReady) {
|
||||
const shown = showOsd(pendingDictionaryReady.message);
|
||||
if (shown) {
|
||||
pendingDictionaryReady = null;
|
||||
dictionaryProgressShown = false;
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
if (pendingDictionaryFailure) {
|
||||
deps.showOsd(pendingDictionaryFailure.message);
|
||||
const shown = showOsd(pendingDictionaryFailure.message);
|
||||
if (shown) {
|
||||
pendingDictionaryFailure = null;
|
||||
dictionaryProgressShown = false;
|
||||
return true;
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -47,13 +58,14 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
annotationLoadingMessage = null;
|
||||
pendingDictionaryProgress = null;
|
||||
pendingDictionaryFailure = null;
|
||||
pendingDictionaryReady = null;
|
||||
dictionaryProgressShown = false;
|
||||
},
|
||||
markTokenizationReady: () => {
|
||||
tokenizationWarmupCompleted = true;
|
||||
tokenizationReady = true;
|
||||
if (annotationLoadingMessage !== null) {
|
||||
deps.showOsd(annotationLoadingMessage);
|
||||
showOsd(annotationLoadingMessage);
|
||||
return;
|
||||
}
|
||||
flushBufferedDictionaryStatus();
|
||||
@@ -61,7 +73,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
showAnnotationLoading: (message) => {
|
||||
annotationLoadingMessage = message;
|
||||
if (tokenizationReady) {
|
||||
deps.showOsd(message);
|
||||
showOsd(message);
|
||||
}
|
||||
},
|
||||
markAnnotationLoadingComplete: (message) => {
|
||||
@@ -72,7 +84,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
if (flushBufferedDictionaryStatus()) {
|
||||
return;
|
||||
}
|
||||
deps.showOsd(message);
|
||||
showOsd(message);
|
||||
},
|
||||
notifyCharacterDictionaryStatus: (event) => {
|
||||
if (
|
||||
@@ -84,32 +96,47 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
) {
|
||||
pendingDictionaryProgress = event;
|
||||
pendingDictionaryFailure = null;
|
||||
pendingDictionaryReady = null;
|
||||
if (canShowDictionaryStatus()) {
|
||||
deps.showOsd(event.message);
|
||||
dictionaryProgressShown = true;
|
||||
dictionaryProgressShown = showOsd(event.message);
|
||||
} else if (tokenizationReady) {
|
||||
deps.showOsd(event.message);
|
||||
dictionaryProgressShown = true;
|
||||
dictionaryProgressShown = showOsd(event.message);
|
||||
}
|
||||
return;
|
||||
return dictionaryProgressShown;
|
||||
}
|
||||
|
||||
pendingDictionaryProgress = null;
|
||||
if (event.phase === 'failed') {
|
||||
pendingDictionaryReady = null;
|
||||
if (canShowDictionaryStatus()) {
|
||||
deps.showOsd(event.message);
|
||||
if (!showOsd(event.message)) {
|
||||
pendingDictionaryFailure = event;
|
||||
return false;
|
||||
}
|
||||
dictionaryProgressShown = false;
|
||||
return true;
|
||||
} else {
|
||||
pendingDictionaryFailure = event;
|
||||
}
|
||||
dictionaryProgressShown = false;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingDictionaryFailure = null;
|
||||
if (canShowDictionaryStatus() && dictionaryProgressShown) {
|
||||
deps.showOsd(event.message);
|
||||
if (canShowDictionaryStatus()) {
|
||||
if (!showOsd(event.message)) {
|
||||
pendingDictionaryReady = event;
|
||||
dictionaryProgressShown = false;
|
||||
return false;
|
||||
}
|
||||
pendingDictionaryReady = null;
|
||||
dictionaryProgressShown = false;
|
||||
return true;
|
||||
} else {
|
||||
pendingDictionaryReady = event;
|
||||
}
|
||||
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 () => {
|
||||
const previousWindow = globalThis.window;
|
||||
const previousDocument = globalThis.document;
|
||||
@@ -95,11 +180,13 @@ test('character dictionary modal loads candidates and applies selected override'
|
||||
};
|
||||
},
|
||||
notifyOverlayModalClosed: () => {},
|
||||
notifyOverlayModalOpened: () => {},
|
||||
} satisfies Pick<
|
||||
ElectronAPI,
|
||||
| 'getCharacterDictionarySelection'
|
||||
| 'setCharacterDictionarySelection'
|
||||
| 'notifyOverlayModalClosed'
|
||||
| 'notifyOverlayModalOpened'
|
||||
>,
|
||||
},
|
||||
});
|
||||
@@ -175,11 +262,13 @@ test('character dictionary modal shows refresh errors without rejecting open', a
|
||||
staleMediaIds: [],
|
||||
}),
|
||||
notifyOverlayModalClosed: () => {},
|
||||
notifyOverlayModalOpened: () => {},
|
||||
} satisfies Pick<
|
||||
ElectronAPI,
|
||||
| 'getCharacterDictionarySelection'
|
||||
| 'setCharacterDictionarySelection'
|
||||
| 'notifyOverlayModalClosed'
|
||||
| 'notifyOverlayModalOpened'
|
||||
>,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -153,6 +153,7 @@ export function createCharacterDictionaryModal(
|
||||
ctx.dom.overlay.classList.add('interactive');
|
||||
ctx.dom.characterDictionaryModal.classList.remove('hidden');
|
||||
ctx.dom.characterDictionaryModal.setAttribute('aria-hidden', 'false');
|
||||
window.electronAPI.notifyOverlayModalOpened('character-dictionary');
|
||||
setStatus('Loading AniList candidates...');
|
||||
}
|
||||
|
||||
@@ -160,6 +161,7 @@ export function createCharacterDictionaryModal(
|
||||
if (!ctx.state.characterDictionaryModalOpen) {
|
||||
showShell();
|
||||
} else {
|
||||
window.electronAPI.notifyOverlayModalOpened('character-dictionary');
|
||||
setStatus('Refreshing AniList candidates...');
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -463,7 +463,6 @@ function registerModalOpenHandlers(): void {
|
||||
window.electronAPI.onOpenCharacterDictionary(() => {
|
||||
runGuardedAsync('character-dictionary:open', async () => {
|
||||
await characterDictionaryModal.openCharacterDictionaryModal();
|
||||
window.electronAPI.notifyOverlayModalOpened('character-dictionary');
|
||||
});
|
||||
});
|
||||
window.electronAPI.onOpenSessionHelp(() => {
|
||||
|
||||
Reference in New Issue
Block a user