mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -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', () => {
|
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']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user