feat(notifications): add overlay notifications with position config (#110)

This commit is contained in:
2026-06-10 22:46:52 -07:00
committed by GitHub
parent c09d009a3e
commit 7be1843c41
177 changed files with 7524 additions and 440 deletions
@@ -271,3 +271,28 @@ test('manual clipboard subtitle update uses resolved mpv stream URLs for remote
assert.equal(updatedFields[0]?.Sentence, '一行目 二行目');
assert.match(updatedFields[0]?.Picture ?? '', /^<img src="image_\d+\.jpg">$/);
});
test('createSentenceCard relies on Anki progress notification without standalone status toast', async () => {
const statusMessages: string[] = [];
const progressMessages: string[] = [];
const { service } = createManualUpdateService({
showOsdNotification: (message) => {
statusMessages.push(message);
},
withUpdateProgress: async (message, action) => {
progressMessages.push(message);
return await action();
},
mediaGenerator: {
generateAudio: async () => null,
generateScreenshot: async () => null,
generateAnimatedImage: async () => null,
},
});
const created = await service.createSentenceCard('テスト', 0, 1);
assert.equal(created, true);
assert.deepEqual(progressMessages, ['Creating sentence card']);
assert.deepEqual(statusMessages, []);
});
-1
View File
@@ -511,7 +511,6 @@ export class CardCreationService {
endTime = startTime + maxMediaDuration;
}
this.deps.showOsdNotification('Creating sentence card...');
try {
return await this.deps.withUpdateProgress('Creating sentence card', async () => {
const videoPath = await resolveMediaGenerationInputPath(mpvClient, 'video');
+56 -1
View File
@@ -1,9 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
beginUpdateProgress,
createUiFeedbackState,
showProgressTick,
showStatusNotification,
showUpdateResult,
} from './ui-feedback';
@@ -65,3 +66,57 @@ test('showUpdateResult renders failed updates with an x marker', () => {
'x Sentence card failed: deck missing',
]);
});
test('showStatusNotification falls back to system when overlay delivery is unavailable', () => {
const calls: string[] = [];
showStatusNotification('Waiting for card update', {
getNotificationType: () => 'overlay',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showSystemNotification: (title, options) => {
calls.push(`system:${title}:${options.body}`);
},
});
assert.deepEqual(calls, ['system:SubMiner:Waiting for card update']);
});
test('showStatusNotification defaults to mpv osd when notification type is unset', () => {
const calls: string[] = [];
showStatusNotification('Card updated', {
getNotificationType: () => undefined,
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showOverlayNotification: (payload) => {
calls.push(`overlay:${payload.body}`);
},
showSystemNotification: (title, options) => {
calls.push(`system:${title}:${options.body}`);
},
});
assert.deepEqual(calls, ['osd:Card updated']);
});
test('showStatusNotification does not duplicate system notifications for both', () => {
const calls: string[] = [];
showStatusNotification('Card updated', {
getNotificationType: () => 'both',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showOverlayNotification: (payload) => {
calls.push(`overlay:${payload.body}`);
},
showSystemNotification: (title, options) => {
calls.push(`system:${title}:${options.body}`);
},
});
assert.deepEqual(calls, ['overlay:Card updated', 'system:SubMiner:Card updated']);
});
+23 -5
View File
@@ -1,4 +1,5 @@
import { NotificationOptions } from '../types/anki';
import type { NotificationOptions } from '../types/anki';
import type { NotificationType, OverlayNotificationPayload } from '../types/notification';
export interface UiFeedbackState {
progressDepth: number;
@@ -13,8 +14,9 @@ export interface UiFeedbackResult {
}
export interface UiFeedbackNotificationContext {
getNotificationType: () => string | undefined;
getNotificationType: () => NotificationType | undefined;
showOsd: (text: string) => void;
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
showSystemNotification: (title: string, options: NotificationOptions) => void;
}
@@ -36,13 +38,29 @@ export function showStatusNotification(
message: string,
context: UiFeedbackNotificationContext,
): void {
const type = context.getNotificationType() || 'osd';
const type = context.getNotificationType() ?? 'osd';
if (type === 'osd' || type === 'both') {
if (type === 'none') {
return;
}
if (type === 'overlay' || type === 'both') {
if (context.showOverlayNotification) {
context.showOverlayNotification({
title: 'SubMiner',
body: message,
variant: 'info',
});
} else if (type === 'overlay') {
context.showSystemNotification('SubMiner', { body: message });
}
}
if (type === 'osd' || type === 'osd-system') {
context.showOsd(message);
}
if (type === 'system' || type === 'both') {
if (type === 'system' || type === 'both' || type === 'osd-system') {
context.showSystemNotification('SubMiner', { body: message });
}
}