mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
fix: suppress overlay subtitle immediately when character dictionary modal opens (#84)
This commit is contained in:
@@ -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);
|
||||
pendingDictionaryFailure = null;
|
||||
dictionaryProgressShown = false;
|
||||
return true;
|
||||
const shown = showOsd(pendingDictionaryFailure.message);
|
||||
if (shown) {
|
||||
pendingDictionaryFailure = null;
|
||||
dictionaryProgressShown = false;
|
||||
}
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user