feat: show mining frame toast on OSD and speed up stats session deletes

- Flash mined-frame screenshot as in-overlay image toast for OSD card notifications
- Batch cover art from stored DB blobs via POST /api/stats/covers (no extra AniList fetches)
- Show delete progress toast on stats home and sessions pages
- Refresh only affected rollups on session delete instead of full rebuild
- Rebuild lifetime summaries with aggregate SQL CTEs instead of per-session loop
This commit is contained in:
2026-06-04 22:00:51 -07:00
parent ea79e331fa
commit b343f1cf5c
45 changed files with 1199 additions and 209 deletions
+44 -25
View File
@@ -148,6 +148,7 @@ export class AnkiIntegration {
private runtime: AnkiIntegrationRuntime;
private aiConfig: AiConfig;
private recordCardsMinedCallback: ((count: number, noteIds?: number[]) => void) | null = null;
private miningImageOverlayCallback: ((image: string) => void) | null = null;
private knownWordCacheUpdatedCallback: (() => void) | null = null;
private consumeSubtitleMiningContextCallback: (() => SubtitleMiningContext | null) | null = null;
private noteIdRedirects = new Map<number, number>();
@@ -1018,40 +1019,54 @@ export class AnkiIntegration {
: `Updated card: ${label}`;
const type = this.config.behavior?.notificationType || 'osd';
const wantsOsd = type === 'osd' || type === 'both';
const wantsSystem =
(type === 'system' || type === 'both') && this.notificationCallback !== null;
const wantsOverlayImage = wantsOsd && this.miningImageOverlayCallback !== null;
if (type === 'osd' || type === 'both') {
if (wantsOsd) {
this.showUpdateResult(message, errorSuffix === undefined);
} else {
this.clearUpdateProgress();
}
if ((type === 'system' || type === 'both') && this.notificationCallback) {
let notificationIconPath: string | undefined;
if (!wantsSystem && !wantsOverlayImage) {
return;
}
if (this.mpvClient && this.mpvClient.currentVideoPath) {
try {
const timestamp = this.mpvClient.currentTimePos || 0;
const notificationIconSource = await resolveMediaGenerationInputPath(
this.mpvClient,
'video',
);
if (!notificationIconSource) {
throw new Error('No media source available for notification icon');
}
const iconBuffer = await this.mediaGenerator.generateNotificationIcon(
notificationIconSource,
timestamp,
);
if (iconBuffer && iconBuffer.length > 0) {
notificationIconPath = this.mediaGenerator.writeNotificationIconToFile(
iconBuffer,
noteId,
);
}
} catch (err) {
log.warn('Failed to generate notification icon:', (err as Error).message);
// Generate the frame screenshot once and reuse it for the system notification
// icon and the in-overlay image toast.
let iconBuffer: Buffer | undefined;
if (this.mpvClient && this.mpvClient.currentVideoPath) {
try {
const timestamp = this.mpvClient.currentTimePos || 0;
const notificationIconSource = await resolveMediaGenerationInputPath(
this.mpvClient,
'video',
);
if (!notificationIconSource) {
throw new Error('No media source available for notification icon');
}
const buffer = await this.mediaGenerator.generateNotificationIcon(
notificationIconSource,
timestamp,
);
if (buffer && buffer.length > 0) {
iconBuffer = buffer;
}
} catch (err) {
log.warn('Failed to generate notification icon:', (err as Error).message);
}
}
if (wantsOverlayImage && iconBuffer && this.miningImageOverlayCallback) {
this.miningImageOverlayCallback(`data:image/png;base64,${iconBuffer.toString('base64')}`);
}
if (wantsSystem && this.notificationCallback) {
const notificationIconPath = iconBuffer
? this.mediaGenerator.writeNotificationIconToFile(iconBuffer, noteId)
: undefined;
this.notificationCallback('Anki Card Updated', {
body: message,
@@ -1364,6 +1379,10 @@ export class AnkiIntegration {
this.knownWordCacheUpdatedCallback = callback;
}
setMiningImageOverlayCallback(callback: ((image: string) => void) | null): void {
this.miningImageOverlayCallback = callback;
}
setSubtitleMiningContextConsumer(callback: (() => SubtitleMiningContext | null) | null): void {
this.consumeSubtitleMiningContextCallback = callback;
}