diff --git a/src/main.ts b/src/main.ts index 2953cd63..88d552ba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -609,6 +609,7 @@ import { notifyConfiguredStatus, type ConfiguredStatusNotificationOptions, } from './main/runtime/configured-status-notification'; +import { resolveOverlayReadinessNotificationType } from './main/runtime/notification-routing'; import { createUpdateDialogPresenter } from './main/runtime/update/update-dialogs'; import { runUpdateCliCommand, @@ -3337,10 +3338,7 @@ function isVisibleOverlayContentReady(): boolean { function getConfiguredStatusNotificationType(): NotificationType { const configuredType = getResolvedConfig().ankiConnect.behavior.notificationType; - if (configuredType === 'none' || isVisibleOverlayContentReady()) { - return configuredType; - } - return 'osd'; + return resolveOverlayReadinessNotificationType(configuredType, isVisibleOverlayContentReady()); } function sendOverlayNotificationEvent(payload: OverlayNotificationEventPayload): void { @@ -3414,7 +3412,13 @@ function showYoutubeFlowStatusNotification(message: string): void { } function showOverlayLoadingStatusNotification(message: string): void { - showMpvOsd(message); + showConfiguredStatusNotification(message, { + id: 'overlay-loading-status', + title: 'SubMiner', + variant: 'progress', + persistent: true, + desktop: false, + }); } const buildBroadcastRuntimeOptionsChangedMainDepsHandler = diff --git a/src/main/runtime/character-dictionary-auto-sync-notifications.ts b/src/main/runtime/character-dictionary-auto-sync-notifications.ts index 03a306b6..47b80a91 100644 --- a/src/main/runtime/character-dictionary-auto-sync-notifications.ts +++ b/src/main/runtime/character-dictionary-auto-sync-notifications.ts @@ -1,6 +1,7 @@ import type { CharacterDictionaryAutoSyncStatusEvent } from './character-dictionary-auto-sync'; import type { StartupOsdSequencerCharacterDictionaryEvent } from './startup-osd-sequencer'; import type { NotificationType, OverlayNotificationPayload } from '../../types/notification'; +import { shouldShowDesktop, shouldShowOverlay, shouldShowOsd } from './notification-routing'; export type CharacterDictionaryAutoSyncNotificationEvent = CharacterDictionaryAutoSyncStatusEvent; @@ -16,18 +17,6 @@ export interface CharacterDictionaryAutoSyncNotificationDeps { }; } -function shouldShowOsd(type: NotificationType): boolean { - return type === 'osd' || type === 'osd-system'; -} - -function shouldShowOverlay(type: NotificationType): boolean { - return type === 'overlay' || type === 'both'; -} - -function shouldShowDesktop(type: NotificationType): boolean { - return type === 'system' || type === 'both' || type === 'osd-system'; -} - function isTerminalPhase(phase: CharacterDictionaryAutoSyncNotificationEvent['phase']): boolean { return phase === 'ready' || phase === 'failed'; } diff --git a/src/main/runtime/configured-status-notification.test.ts b/src/main/runtime/configured-status-notification.test.ts index 6441a04e..837ac5e0 100644 --- a/src/main/runtime/configured-status-notification.test.ts +++ b/src/main/runtime/configured-status-notification.test.ts @@ -27,7 +27,7 @@ test('notifyConfiguredStatus routes both to overlay and system without osd', () ]); }); -test('notifyConfiguredStatus routes pre-overlay status to osd only', () => { +test('notifyConfiguredStatus routes pre-overlay both status to osd and desktop', () => { const calls: string[] = []; notifyConfiguredStatus('Overlay loading...', { @@ -42,7 +42,25 @@ test('notifyConfiguredStatus routes pre-overlay status to osd only', () => { calls.push(`desktop:${title}:${options.body ?? ''}`), }); - assert.deepEqual(calls, ['osd:Overlay loading...']); + assert.deepEqual(calls, ['osd:Overlay loading...', 'desktop:SubMiner:Overlay loading...']); +}); + +test('notifyConfiguredStatus routes pre-overlay system status to desktop only', () => { + const calls: string[] = []; + + notifyConfiguredStatus('Overlay loading...', { + getNotificationType: () => 'system', + 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, ['desktop:SubMiner:Overlay loading...']); }); test('notifyConfiguredStatus keeps osd-system on legacy surfaces', () => { diff --git a/src/main/runtime/configured-status-notification.ts b/src/main/runtime/configured-status-notification.ts index 58c86d38..12124c9c 100644 --- a/src/main/runtime/configured-status-notification.ts +++ b/src/main/runtime/configured-status-notification.ts @@ -1,4 +1,5 @@ import type { NotificationType, OverlayNotificationPayload } from '../../types/notification'; +import { shouldShowDesktop, shouldShowOverlay, shouldShowOsd } from './notification-routing'; export interface ConfiguredStatusNotificationDeps { getNotificationType: () => NotificationType | undefined; @@ -17,18 +18,6 @@ export interface ConfiguredStatusNotificationOptions { delivery?: 'notification' | 'feedback'; } -function shouldShowOverlay(type: NotificationType): boolean { - return type === 'overlay' || type === 'both'; -} - -function shouldShowOsd(type: NotificationType): boolean { - return type === 'osd' || type === 'osd-system'; -} - -function shouldShowDesktop(type: NotificationType): boolean { - return type === 'system' || type === 'both' || type === 'osd-system'; -} - export function getPlaybackFeedbackNotificationOptions( message: string, ): ConfiguredStatusNotificationOptions { @@ -60,12 +49,9 @@ export function notifyConfiguredStatus( return; } - if (deps.isOverlayReady?.() === false) { - deps.showOsd(message); - return; - } + const overlayReady = deps.isOverlayReady?.() !== false; - if (showOverlay) { + if (showOverlay && overlayReady) { if (deps.showOverlayNotification) { deps.showOverlayNotification({ id: options.id, @@ -77,6 +63,8 @@ export function notifyConfiguredStatus( } else if (desktopEnabled && !shouldShowDesktop(type)) { deps.showDesktopNotification(options.title ?? 'SubMiner', { body: message }); } + } else if (showOverlay && !showOsd) { + deps.showOsd(message); } if (showOsd) { diff --git a/src/main/runtime/notification-routing.test.ts b/src/main/runtime/notification-routing.test.ts new file mode 100644 index 00000000..1d244b13 --- /dev/null +++ b/src/main/runtime/notification-routing.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + resolveOverlayReadinessNotificationType, + shouldShowDesktop, + shouldShowOverlay, + shouldShowOsd, +} from './notification-routing'; + +test('notification routing preserves system notification while overlay is not ready', () => { + assert.equal(resolveOverlayReadinessNotificationType('system', false), 'system'); +}); + +test('notification routing preserves both as osd plus system while overlay is not ready', () => { + assert.equal(resolveOverlayReadinessNotificationType('both', false), 'osd-system'); +}); + +test('notification routing falls back overlay-only notification to osd while overlay is not ready', () => { + assert.equal(resolveOverlayReadinessNotificationType('overlay', false), 'osd'); +}); + +test('notification routing predicates classify delivery channels', () => { + assert.equal(shouldShowOverlay('both'), true); + assert.equal(shouldShowOverlay('system'), false); + assert.equal(shouldShowOsd('osd-system'), true); + assert.equal(shouldShowOsd('both'), false); + assert.equal(shouldShowDesktop('osd-system'), true); + assert.equal(shouldShowDesktop('overlay'), false); +}); diff --git a/src/main/runtime/notification-routing.ts b/src/main/runtime/notification-routing.ts new file mode 100644 index 00000000..ab5d046d --- /dev/null +++ b/src/main/runtime/notification-routing.ts @@ -0,0 +1,29 @@ +import type { NotificationType } from '../../types/notification'; + +export function shouldShowOsd(type: NotificationType): boolean { + return type === 'osd' || type === 'osd-system'; +} + +export function shouldShowOverlay(type: NotificationType): boolean { + return type === 'overlay' || type === 'both'; +} + +export function shouldShowDesktop(type: NotificationType): boolean { + return type === 'system' || type === 'both' || type === 'osd-system'; +} + +export function resolveOverlayReadinessNotificationType( + type: NotificationType, + overlayReady: boolean, +): NotificationType { + if (overlayReady) { + return type; + } + if (type === 'overlay') { + return 'osd'; + } + if (type === 'both') { + return 'osd-system'; + } + return type; +} diff --git a/src/main/runtime/startup-osd-sequencer.ts b/src/main/runtime/startup-osd-sequencer.ts index bca9a263..13ff62c9 100644 --- a/src/main/runtime/startup-osd-sequencer.ts +++ b/src/main/runtime/startup-osd-sequencer.ts @@ -1,4 +1,5 @@ import type { NotificationType, OverlayNotificationPayload } from '../../types/notification'; +import { shouldShowDesktop, shouldShowOverlay, shouldShowOsd } from './notification-routing'; export interface StartupOsdSequencerCharacterDictionaryEvent { phase: 'checking' | 'generating' | 'syncing' | 'building' | 'importing' | 'ready' | 'failed'; @@ -21,18 +22,6 @@ interface StartupStatusNotificationOptions { desktop?: boolean; } -function shouldShowOsd(type: NotificationType): boolean { - return type === 'osd' || type === 'osd-system'; -} - -function shouldShowOverlay(type: NotificationType): boolean { - return type === 'overlay' || type === 'both'; -} - -function shouldShowDesktop(type: NotificationType): boolean { - return type === 'system' || type === 'both' || type === 'osd-system'; -} - export function createStartupOsdSequencer(deps: StartupOsdSequencerDeps): { reset: () => void; showTokenizationLoading: (message: string) => void;