mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-09 15:13:32 -07:00
feat(overlay): add loading OSD spinner and queue notifications until ren
- Show mpv OSD spinner from start-file until subminer-overlay-loading-ready; force-shown for visible-overlay startup regardless of osd_messages setting - Gate non-macOS overlay visibility on content-ready so first subtitle line is immediately hoverable and clickable - Queue startup notifications in main process until overlay window finishes loading; upsert progress cards by id to avoid cold-start floods - Defer background warmups until after overlay runtime init so queued notifications can deliver promptly - Preserve character dictionary checking/building/importing/ready phases as distinct history entries; route building and importing to system notifications when notificationType is both
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
getPlaybackFeedbackNotificationOptions,
|
||||
notifyConfiguredStatus,
|
||||
} from './configured-status-notification';
|
||||
import { createOverlayNotificationDelivery } from './overlay-notification-delivery';
|
||||
|
||||
test('notifyConfiguredStatus routes both to overlay and system without osd', () => {
|
||||
const calls: string[] = [];
|
||||
@@ -27,7 +28,7 @@ test('notifyConfiguredStatus routes both to overlay and system without osd', ()
|
||||
]);
|
||||
});
|
||||
|
||||
test('notifyConfiguredStatus routes pre-overlay both status to osd and desktop', () => {
|
||||
test('notifyConfiguredStatus queues pre-overlay both status through overlay sender and desktop', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
notifyConfiguredStatus('Overlay loading...', {
|
||||
@@ -42,7 +43,25 @@ test('notifyConfiguredStatus routes pre-overlay both status to osd and desktop',
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, ['osd:Overlay loading...', 'desktop:SubMiner:Overlay loading...']);
|
||||
assert.deepEqual(calls, ['overlay::Overlay loading...', 'desktop:SubMiner:Overlay loading...']);
|
||||
});
|
||||
|
||||
test('notifyConfiguredStatus queues pre-overlay overlay-only status without osd fallback', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
notifyConfiguredStatus('Overlay loading...', {
|
||||
getNotificationType: () => 'overlay',
|
||||
isOverlayReady: () => false,
|
||||
showOsd: (message) => {
|
||||
calls.push(`osd:${message}`);
|
||||
},
|
||||
showOverlayNotification: (payload) =>
|
||||
calls.push(`overlay:${payload.id ?? ''}:${payload.body ?? ''}`),
|
||||
showDesktopNotification: (title, options) =>
|
||||
calls.push(`desktop:${title}:${options.body ?? ''}`),
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, ['overlay::Overlay loading...']);
|
||||
});
|
||||
|
||||
test('notifyConfiguredStatus routes pre-overlay system status to desktop only', () => {
|
||||
@@ -190,3 +209,231 @@ test('notifyConfiguredStatus falls back to desktop if overlay is unavailable', (
|
||||
|
||||
assert.deepEqual(calls, ['desktop:SubMiner:Overlay unavailable.']);
|
||||
});
|
||||
|
||||
test('overlay notification delivery queues until an overlay window is ready', () => {
|
||||
const sent: string[] = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) => sent.push(`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}`),
|
||||
});
|
||||
|
||||
delivery.send({ id: 'startup-tokenization', title: 'Subtitle tokenization', body: 'Loading' });
|
||||
delivery.send({ id: 'character-dictionary-auto-sync', title: 'Dictionary', body: 'Building' });
|
||||
|
||||
assert.equal(delivery.getQueuedCount(), 2);
|
||||
assert.deepEqual(sent, []);
|
||||
|
||||
ready = true;
|
||||
delivery.flush();
|
||||
|
||||
assert.equal(delivery.getQueuedCount(), 0);
|
||||
assert.deepEqual(sent, [
|
||||
'startup-tokenization:Loading',
|
||||
'character-dictionary-auto-sync:Building',
|
||||
]);
|
||||
});
|
||||
|
||||
test('overlay notification delivery upserts queued progress by notification id', () => {
|
||||
const sent: string[] = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) => sent.push(`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}`),
|
||||
});
|
||||
|
||||
delivery.send({ id: 'startup-subtitle-annotations', title: 'Subtitle annotations', body: '|' });
|
||||
delivery.send({ id: 'startup-subtitle-annotations', title: 'Subtitle annotations', body: '/' });
|
||||
delivery.send({ id: 'startup-tokenization', title: 'Subtitle tokenization', body: 'Ready' });
|
||||
|
||||
ready = true;
|
||||
delivery.flush();
|
||||
|
||||
assert.deepEqual(sent, ['startup-subtitle-annotations:/', 'startup-tokenization:Ready']);
|
||||
});
|
||||
|
||||
test('overlay notification delivery preserves queued events with distinct history ids', () => {
|
||||
const sent: string[] = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) =>
|
||||
sent.push(
|
||||
`${payload.id ?? ''}:${'historyId' in payload ? payload.historyId : ''}:${'body' in payload ? payload.body : ''}`,
|
||||
),
|
||||
});
|
||||
|
||||
delivery.send({
|
||||
id: 'character-dictionary-auto-sync',
|
||||
historyId: 'character-dictionary-auto-sync-checking',
|
||||
title: 'Character dictionary',
|
||||
body: 'Checking character dictionary...',
|
||||
persistent: true,
|
||||
});
|
||||
delivery.send({
|
||||
id: 'character-dictionary-auto-sync',
|
||||
historyId: 'character-dictionary-auto-sync-building',
|
||||
title: 'Character dictionary',
|
||||
body: 'Building character dictionary...',
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
ready = true;
|
||||
delivery.flush();
|
||||
|
||||
assert.deepEqual(sent, [
|
||||
'character-dictionary-auto-sync:character-dictionary-auto-sync-checking:Checking character dictionary...',
|
||||
'character-dictionary-auto-sync:character-dictionary-auto-sync-building:Building character dictionary...',
|
||||
]);
|
||||
});
|
||||
|
||||
test('overlay notification delivery preserves queued startup progress before terminal update', () => {
|
||||
const sent: string[] = [];
|
||||
const scheduled: Array<() => void> = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) =>
|
||||
sent.push(
|
||||
`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}:${'persistent' in payload && payload.persistent ? 'pin' : 'auto'}`,
|
||||
),
|
||||
scheduleFlushRetry: (callback) => {
|
||||
scheduled.push(callback);
|
||||
},
|
||||
});
|
||||
|
||||
delivery.send({
|
||||
id: 'startup-tokenization',
|
||||
title: 'Subtitle tokenization',
|
||||
body: 'Loading subtitle tokenization...',
|
||||
variant: 'progress',
|
||||
persistent: true,
|
||||
});
|
||||
delivery.send({
|
||||
id: 'startup-tokenization',
|
||||
title: 'Subtitle tokenization',
|
||||
body: 'Subtitle tokenization ready',
|
||||
variant: 'success',
|
||||
persistent: false,
|
||||
});
|
||||
|
||||
ready = true;
|
||||
delivery.flush();
|
||||
scheduled.shift()?.();
|
||||
|
||||
assert.deepEqual(sent, [
|
||||
'startup-tokenization:Loading subtitle tokenization...:pin',
|
||||
'startup-tokenization:Subtitle tokenization ready:auto',
|
||||
]);
|
||||
});
|
||||
|
||||
test('overlay notification delivery defers terminal update after first queued progress paint', () => {
|
||||
const sent: string[] = [];
|
||||
const scheduled: Array<() => void> = [];
|
||||
const delays: number[] = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) =>
|
||||
sent.push(
|
||||
`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}:${'persistent' in payload && payload.persistent ? 'pin' : 'auto'}`,
|
||||
),
|
||||
scheduleFlushRetry: (callback, delayMs) => {
|
||||
scheduled.push(callback);
|
||||
delays.push(delayMs);
|
||||
},
|
||||
terminalUpdateDelayMs: 750,
|
||||
});
|
||||
|
||||
delivery.send({
|
||||
id: 'startup-subtitle-annotations',
|
||||
title: 'Subtitle annotations',
|
||||
body: 'Loading subtitle annotations |',
|
||||
variant: 'progress',
|
||||
persistent: true,
|
||||
});
|
||||
delivery.send({
|
||||
id: 'startup-subtitle-annotations',
|
||||
title: 'Subtitle annotations',
|
||||
body: 'Subtitle annotations loaded',
|
||||
variant: 'success',
|
||||
persistent: false,
|
||||
});
|
||||
|
||||
ready = true;
|
||||
delivery.flush();
|
||||
|
||||
assert.deepEqual(sent, ['startup-subtitle-annotations:Loading subtitle annotations |:pin']);
|
||||
assert.equal(delivery.getQueuedCount(), 1);
|
||||
assert.deepEqual(delays, [750]);
|
||||
|
||||
scheduled.shift()?.();
|
||||
|
||||
assert.equal(delivery.getQueuedCount(), 0);
|
||||
assert.deepEqual(sent, [
|
||||
'startup-subtitle-annotations:Loading subtitle annotations |:pin',
|
||||
'startup-subtitle-annotations:Subtitle annotations loaded:auto',
|
||||
]);
|
||||
});
|
||||
|
||||
test('overlay notification delivery retries flush when lifecycle fires before window readiness settles', () => {
|
||||
const sent: string[] = [];
|
||||
const scheduled: Array<() => void> = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) => sent.push(`${payload.id ?? ''}:${'body' in payload ? payload.body : ''}`),
|
||||
scheduleFlushRetry: (callback) => {
|
||||
scheduled.push(callback);
|
||||
},
|
||||
});
|
||||
|
||||
delivery.send({ id: 'startup-tokenization', title: 'Subtitle tokenization', body: 'Loading' });
|
||||
delivery.flush();
|
||||
|
||||
assert.equal(delivery.getQueuedCount(), 1);
|
||||
assert.equal(scheduled.length, 1);
|
||||
assert.deepEqual(sent, []);
|
||||
|
||||
ready = true;
|
||||
scheduled.shift()?.();
|
||||
|
||||
assert.equal(delivery.getQueuedCount(), 0);
|
||||
assert.deepEqual(sent, ['startup-tokenization:Loading']);
|
||||
});
|
||||
|
||||
test('overlay notification delivery drops queued notification when dismissed before flush', () => {
|
||||
const sent: string[] = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) =>
|
||||
sent.push('dismiss' in payload ? `dismiss:${payload.id}` : `show:${payload.id ?? ''}`),
|
||||
});
|
||||
|
||||
delivery.send({ id: 'overlay-loading-status', title: 'SubMiner', body: 'Overlay loading' });
|
||||
delivery.send({ id: 'overlay-loading-status', dismiss: true });
|
||||
|
||||
ready = true;
|
||||
delivery.flush();
|
||||
|
||||
assert.deepEqual(sent, []);
|
||||
});
|
||||
|
||||
test('overlay notification delivery removes queued notification when dismissed at readiness', () => {
|
||||
const sent: string[] = [];
|
||||
let ready = false;
|
||||
const delivery = createOverlayNotificationDelivery({
|
||||
hasReadyOverlayWindow: () => ready,
|
||||
send: (payload) =>
|
||||
sent.push('dismiss' in payload ? `dismiss:${payload.id}` : `show:${payload.id ?? ''}`),
|
||||
});
|
||||
|
||||
delivery.send({ id: 'overlay-loading-status', title: 'SubMiner', body: 'Overlay loading' });
|
||||
|
||||
ready = true;
|
||||
delivery.send({ id: 'overlay-loading-status', dismiss: true });
|
||||
delivery.flush();
|
||||
|
||||
assert.deepEqual(sent, ['dismiss:overlay-loading-status']);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user