mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-26 00:15:27 -07:00
fix(anki): keep overlay progress visible through card updates (#218)
This commit is contained in:
@@ -11,10 +11,12 @@ import type { MediaInput } from './media-input';
|
||||
import { AnkiConnectConfig } from './types';
|
||||
|
||||
type TestOverlayNotificationPayload = {
|
||||
id?: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
image?: string;
|
||||
variant?: string;
|
||||
persistent?: boolean;
|
||||
actions?: Array<{ id: string; label: string; noteId?: number }>;
|
||||
};
|
||||
|
||||
@@ -1182,6 +1184,82 @@ test('AnkiIntegration embeds generated notification image on overlay mined-card
|
||||
assert.deepEqual(cleanupPaths, [notificationIconPath]);
|
||||
});
|
||||
|
||||
test('AnkiIntegration keeps overlay card-update progress visible until the terminal notification', async () => {
|
||||
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||
const integration = new AnkiIntegration(
|
||||
{
|
||||
behavior: {
|
||||
notificationType: 'overlay',
|
||||
},
|
||||
},
|
||||
{} as never,
|
||||
{} as never,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
undefined,
|
||||
(payload) => {
|
||||
overlayNotifications.push(payload);
|
||||
},
|
||||
);
|
||||
const updateNotifications = integration as unknown as {
|
||||
beginUpdateProgress: (message: string) => void;
|
||||
showNotification: (noteId: number, label: string | number) => Promise<void>;
|
||||
};
|
||||
|
||||
updateNotifications.beginUpdateProgress('Updating card');
|
||||
await updateNotifications.showNotification(42, '食べる');
|
||||
|
||||
assert.deepEqual(
|
||||
overlayNotifications.map(({ id, variant, persistent }) => ({ id, variant, persistent })),
|
||||
[
|
||||
{ id: 'anki-update-progress', variant: 'progress', persistent: true },
|
||||
{ id: 'anki-update-progress', variant: 'success', persistent: false },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('AnkiIntegration dismisses persistent overlay update progress when no terminal notification replaces it', () => {
|
||||
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||
const dismissedIds: string[] = [];
|
||||
const integration = new AnkiIntegration(
|
||||
{
|
||||
behavior: {
|
||||
notificationType: 'overlay',
|
||||
},
|
||||
},
|
||||
{} as never,
|
||||
{} as never,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
undefined,
|
||||
(payload) => {
|
||||
overlayNotifications.push(payload);
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
(id) => {
|
||||
dismissedIds.push(id);
|
||||
},
|
||||
);
|
||||
const updateNotifications = integration as unknown as {
|
||||
beginUpdateProgress: (message: string) => void;
|
||||
endUpdateProgress: () => void;
|
||||
};
|
||||
|
||||
updateNotifications.beginUpdateProgress('Updating card');
|
||||
updateNotifications.endUpdateProgress();
|
||||
|
||||
assert.equal(overlayNotifications[0]?.persistent, true);
|
||||
assert.deepEqual(dismissedIds, ['anki-update-progress']);
|
||||
});
|
||||
|
||||
test('AnkiIntegration keeps overlay notification image when temp icon write fails', async () => {
|
||||
const desktopNotifications: Array<{ title: string; body?: string; icon?: string }> = [];
|
||||
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||
|
||||
+14
-2
@@ -218,6 +218,8 @@ export class AnkiIntegration {
|
||||
null;
|
||||
private overlayNotificationCallback: ((payload: OverlayNotificationPayload) => void) | null =
|
||||
null;
|
||||
private overlayNotificationDismissCallback: ((id: string) => void) | null = null;
|
||||
private overlayUpdateProgressActive = false;
|
||||
private updateInProgress = false;
|
||||
private uiFeedbackState: UiFeedbackState = createUiFeedbackState();
|
||||
private parseWarningKeys = new Set<string>();
|
||||
@@ -265,6 +267,7 @@ export class AnkiIntegration {
|
||||
getCachedMediaPath?: MediaGenerationInputResolverOptions['getCachedMediaPath'],
|
||||
shouldRequireRemoteMediaCache?: () => boolean,
|
||||
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined,
|
||||
overlayNotificationDismissCallback?: (id: string) => void,
|
||||
) {
|
||||
this.config = normalizeAnkiIntegrationConfig(config);
|
||||
this.aiConfig = { ...aiConfig };
|
||||
@@ -280,6 +283,7 @@ export class AnkiIntegration {
|
||||
this.getCachedMediaPath = getCachedMediaPath ?? null;
|
||||
this.shouldRequireRemoteMediaCache = shouldRequireRemoteMediaCache ?? null;
|
||||
this.getYoutubeMediaSourceUrl = getYoutubeMediaSourceUrl ?? null;
|
||||
this.overlayNotificationDismissCallback = overlayNotificationDismissCallback ?? null;
|
||||
this.pendingYoutubeMediaQueue = this.createPendingYoutubeMediaQueue();
|
||||
this.knownWordCache = this.createKnownWordCache(knownWordCacheStatePath);
|
||||
this.pollingRunner = this.createPollingRunner();
|
||||
@@ -1203,12 +1207,13 @@ export class AnkiIntegration {
|
||||
private beginUpdateProgress(initialMessage: string): void {
|
||||
if (!this.shouldUseOsdNotifications()) {
|
||||
if (this.shouldUseOverlayNotifications()) {
|
||||
this.overlayUpdateProgressActive = true;
|
||||
this.overlayNotificationCallback?.({
|
||||
id: 'anki-update-progress',
|
||||
title: 'Anki update',
|
||||
body: initialMessage,
|
||||
variant: 'progress',
|
||||
persistent: false,
|
||||
persistent: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -1220,6 +1225,10 @@ export class AnkiIntegration {
|
||||
|
||||
private endUpdateProgress(): void {
|
||||
if (!this.shouldUseOsdNotifications()) {
|
||||
if (this.overlayUpdateProgressActive) {
|
||||
this.overlayUpdateProgressActive = false;
|
||||
this.overlayNotificationDismissCallback?.('anki-update-progress');
|
||||
}
|
||||
return;
|
||||
}
|
||||
endUpdateProgress(this.uiFeedbackState, (timer) => {
|
||||
@@ -1243,18 +1252,20 @@ export class AnkiIntegration {
|
||||
if (!this.shouldUseOsdNotifications()) {
|
||||
this.updateInProgress = true;
|
||||
if (this.shouldUseOverlayNotifications()) {
|
||||
this.overlayUpdateProgressActive = true;
|
||||
this.overlayNotificationCallback?.({
|
||||
id: 'anki-update-progress',
|
||||
title: 'Anki update',
|
||||
body: initialMessage,
|
||||
variant: 'progress',
|
||||
persistent: false,
|
||||
persistent: true,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
this.updateInProgress = false;
|
||||
this.endUpdateProgress();
|
||||
}
|
||||
}
|
||||
return withUpdateProgress(
|
||||
@@ -1353,6 +1364,7 @@ export class AnkiIntegration {
|
||||
: undefined;
|
||||
|
||||
if (shouldShowOverlayNotification && this.overlayNotificationCallback) {
|
||||
this.overlayUpdateProgressActive = false;
|
||||
this.overlayNotificationCallback({
|
||||
id: 'anki-update-progress',
|
||||
title: 'Anki Card Updated',
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
@@ -166,6 +167,7 @@ export function registerAnkiJimakuIpcRuntime(
|
||||
options.getCachedMediaPath,
|
||||
options.shouldRequireRemoteMediaCache,
|
||||
options.getYoutubeMediaSourceUrl,
|
||||
options.dismissOverlayNotification,
|
||||
);
|
||||
integration.start();
|
||||
options.setAnkiIntegration(integration);
|
||||
|
||||
@@ -21,6 +21,7 @@ type CreateAnkiIntegrationArgs = {
|
||||
mpvClient: { send?: (payload: { command: string[] }) => void };
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
@@ -74,6 +75,7 @@ function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiInte
|
||||
args.getCachedMediaPath,
|
||||
args.shouldRequireRemoteMediaCache,
|
||||
args.getYoutubeMediaSourceUrl,
|
||||
args.dismissOverlayNotification,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,6 +139,7 @@ export function initializeOverlayRuntime(
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
@@ -177,6 +180,7 @@ export function initializeOverlayAnkiIntegration(options: {
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
@@ -219,6 +223,7 @@ export function initializeOverlayAnkiIntegration(options: {
|
||||
mpvClient,
|
||||
showDesktopNotification: options.showDesktopNotification,
|
||||
showOverlayNotification: options.showOverlayNotification,
|
||||
dismissOverlayNotification: options.dismissOverlayNotification,
|
||||
createFieldGroupingCallback: options.createFieldGroupingCallback,
|
||||
knownWordCacheStatePath: options.getKnownWordCacheStatePath(),
|
||||
...(options.getCachedMediaPath ? { getCachedMediaPath: options.getCachedMediaPath } : {}),
|
||||
|
||||
@@ -5925,6 +5925,8 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
showDesktopNotification,
|
||||
showOverlayNotification: (payload) =>
|
||||
overlayNotificationsRuntime.showOverlayNotification(payload),
|
||||
dismissOverlayNotification: (id) =>
|
||||
overlayNotificationsRuntime.dismissOverlayNotification(id),
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
broadcastRuntimeOptionsChanged: () =>
|
||||
overlayVisibilityComposer.broadcastRuntimeOptionsChanged(),
|
||||
@@ -6415,6 +6417,8 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
||||
showDesktopNotification,
|
||||
showOverlayNotification: (payload) =>
|
||||
overlayNotificationsRuntime.showOverlayNotification(payload),
|
||||
dismissOverlayNotification: (id) =>
|
||||
overlayNotificationsRuntime.dismissOverlayNotification(id),
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||
getCachedMediaPath: (currentVideoPath, kind) =>
|
||||
|
||||
@@ -132,6 +132,7 @@ export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
||||
getYoutubeMediaSourceUrl?: AnkiJimakuIpcRuntimeOptions['getYoutubeMediaSourceUrl'];
|
||||
showDesktopNotification: AnkiJimakuIpcRuntimeOptions['showDesktopNotification'];
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: AnkiJimakuIpcRuntimeOptions['createFieldGroupingCallback'];
|
||||
broadcastRuntimeOptionsChanged: AnkiJimakuIpcRuntimeOptions['broadcastRuntimeOptionsChanged'];
|
||||
getFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['getFieldGroupingResolver'];
|
||||
@@ -334,6 +335,7 @@ export function createAnkiJimakuIpcRuntimeServiceDeps(
|
||||
: {}),
|
||||
showDesktopNotification: params.showDesktopNotification,
|
||||
showOverlayNotification: params.showOverlayNotification,
|
||||
dismissOverlayNotification: params.dismissOverlayNotification,
|
||||
createFieldGroupingCallback: params.createFieldGroupingCallback,
|
||||
broadcastRuntimeOptionsChanged: params.broadcastRuntimeOptionsChanged,
|
||||
getFieldGroupingResolver: params.getFieldGroupingResolver,
|
||||
|
||||
@@ -26,6 +26,7 @@ type InitializeOverlayRuntimeCore = (options: {
|
||||
} | null;
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
|
||||
@@ -33,6 +33,8 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
getOverlayWindows: () => [],
|
||||
getResolvedConfig: () => ({}),
|
||||
showDesktopNotification: () => calls.push('notify'),
|
||||
showOverlayNotification: () => calls.push('show-overlay'),
|
||||
dismissOverlayNotification: () => calls.push('dismiss-overlay'),
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
@@ -57,6 +59,8 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
deps.refreshCurrentSubtitle?.();
|
||||
deps.syncOverlayShortcuts();
|
||||
deps.showDesktopNotification('title', {});
|
||||
deps.showOverlayNotification?.({ title: 'title' });
|
||||
deps.dismissOverlayNotification?.('notification-id');
|
||||
|
||||
const tracker = {
|
||||
close: () => {},
|
||||
@@ -73,6 +77,8 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
'refresh-subtitle',
|
||||
'sync-shortcuts',
|
||||
'notify',
|
||||
'show-overlay',
|
||||
'dismiss-overlay',
|
||||
]);
|
||||
assert.equal(appState.windowTracker, tracker);
|
||||
assert.deepEqual(appState.ankiIntegration, { id: 'anki' });
|
||||
|
||||
@@ -39,6 +39,7 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
getCachedMediaPath?: OverlayRuntimeOptionsMainDeps['getCachedMediaPath'];
|
||||
@@ -78,6 +79,7 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
},
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
showOverlayNotification: deps.showOverlayNotification,
|
||||
dismissOverlayNotification: deps.dismissOverlayNotification,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
||||
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
||||
|
||||
@@ -22,6 +22,8 @@ test('build initialize overlay runtime options maps dependencies', () => {
|
||||
getRuntimeOptionsManager: () => null,
|
||||
setAnkiIntegration: () => calls.push('set-anki'),
|
||||
showDesktopNotification: () => calls.push('notify'),
|
||||
showOverlayNotification: () => calls.push('show-overlay'),
|
||||
dismissOverlayNotification: () => calls.push('dismiss-overlay'),
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
@@ -47,6 +49,8 @@ test('build initialize overlay runtime options maps dependencies', () => {
|
||||
options.setWindowTracker(null);
|
||||
options.setAnkiIntegration(null);
|
||||
options.showDesktopNotification('title', {});
|
||||
options.showOverlayNotification?.({ title: 'title' });
|
||||
options.dismissOverlayNotification?.('notification-id');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'create-main',
|
||||
@@ -58,5 +62,7 @@ test('build initialize overlay runtime options maps dependencies', () => {
|
||||
'set-tracker',
|
||||
'set-anki',
|
||||
'notify',
|
||||
'show-overlay',
|
||||
'dismiss-overlay',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ type OverlayRuntimeOptions = {
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
@@ -73,6 +74,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
@@ -107,6 +109,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
setAnkiIntegration: deps.setAnkiIntegration,
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
showOverlayNotification: deps.showOverlayNotification,
|
||||
dismissOverlayNotification: deps.dismissOverlayNotification,
|
||||
createFieldGroupingCallback: deps.createFieldGroupingCallback,
|
||||
getKnownWordCacheStatePath: deps.getKnownWordCacheStatePath,
|
||||
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
||||
|
||||
Reference in New Issue
Block a user