From b58f391b42db11e536e9510986bbeec78941207c Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 30 Aug 2026 02:49:37 -0700 Subject: [PATCH] fix: handle notification changes and timing review text safely - Dismiss stale overlay progress when notification mode changes - Validate and forward combined timing review text - Keep macOS timing preview socket paths within system limits --- changes/anki-update-notifications.md | 4 ++ release/release-notes.md | 33 -------------- src/anki-integration.test.ts | 35 +++++++++++++++ src/anki-integration.ts | 8 ++-- src/core/services/ipc.test.ts | 55 +++++++++++++++++++++++ src/core/services/ipc.ts | 5 ++- src/core/services/media-timing-preview.ts | 6 ++- 7 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 changes/anki-update-notifications.md delete mode 100644 release/release-notes.md diff --git a/changes/anki-update-notifications.md b/changes/anki-update-notifications.md new file mode 100644 index 00000000..9d3deff7 --- /dev/null +++ b/changes/anki-update-notifications.md @@ -0,0 +1,4 @@ +type: fixed +area: anki + +- Dismissed active overlay card-update progress when notification settings switch to OSD before an update finishes. diff --git a/release/release-notes.md b/release/release-notes.md deleted file mode 100644 index 476c69fc..00000000 --- a/release/release-notes.md +++ /dev/null @@ -1,33 +0,0 @@ -## Highlights -### Fixed - -- **Anki Card Update Progress**: The update spinner now stays visible until audio and image updates actually finish, so you won't mistake an in-progress update for a failure. -- **Word-Card Field Enrichment**: Word-card enrichment now reliably writes sentence text and audio into whichever AnkiConnect fields you've configured, while the dedicated Lapis/Kiku sentence-card and audio-card actions still use their expected field names. -- **Overlapping Subtitles**: - - Lines that start while another is still on screen now show together instead of staying hidden until you switch tracks or seek. - - Subtitles shown at the same time now stack by their authored screen position, with signs and song lyrics above dialogue. - - Half-size ASS furigana no longer shows up as if it were its own subtitle line. -- **YouTube Auto-Generated Captions**: - - Captions now follow their intended timing instead of drifting off sync. - - Long speech is paged across two rows instead of piling into a wall of text. - - Timed sound cues like `[音楽]` no longer linger over later dialogue. - -## What's Changed - -- fix(anki): keep overlay progress visible through card updates by @ksyasuda in #218 -- fix(youtube): keep auto captions on screen for their full span by @ksyasuda in #219 -- fix(subtitles): keep overlapping lines that join an already active cue by @ksyasuda in #221 -- fix(anki): respect configured fields for word-card enrichment by @ksyasuda in #223 - -## Installation - -See the README and docs/installation guide for full setup steps. - -## Assets - -- Linux: `SubMiner.AppImage` -- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip` -- Windows: `SubMiner-*.exe` and `SubMiner-*-win.zip` -- Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher - -Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`. diff --git a/src/anki-integration.test.ts b/src/anki-integration.test.ts index 2971fb37..cbdaee4d 100644 --- a/src/anki-integration.test.ts +++ b/src/anki-integration.test.ts @@ -1262,6 +1262,41 @@ test('AnkiIntegration dismisses persistent overlay update progress when no termi assert.deepEqual(dismissedIds, ['anki-update-progress']); }); +test('AnkiIntegration dismisses overlay update progress after notifications switch to OSD', () => { + const behavior: NonNullable = { + notificationType: 'overlay', + }; + const dismissedIds: string[] = []; + const integration = new AnkiIntegration( + { behavior }, + {} as never, + {} as never, + undefined, + undefined, + undefined, + undefined, + {}, + undefined, + () => {}, + undefined, + undefined, + undefined, + (id) => { + dismissedIds.push(id); + }, + ); + const updateNotifications = integration as unknown as { + beginUpdateProgress: (message: string) => void; + endUpdateProgress: () => void; + }; + + updateNotifications.beginUpdateProgress('Updating card'); + behavior.notificationType = 'osd'; + updateNotifications.endUpdateProgress(); + + 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[] = []; diff --git a/src/anki-integration.ts b/src/anki-integration.ts index 4792f75b..d8dba610 100644 --- a/src/anki-integration.ts +++ b/src/anki-integration.ts @@ -1237,11 +1237,11 @@ export class AnkiIntegration { } private endUpdateProgress(): void { + if (this.overlayUpdateProgressActive) { + this.overlayUpdateProgressActive = false; + this.overlayNotificationDismissCallback?.('anki-update-progress'); + } if (!this.shouldUseOsdNotifications()) { - if (this.overlayUpdateProgressActive) { - this.overlayUpdateProgressActive = false; - this.overlayNotificationDismissCallback?.('anki-update-progress'); - } return; } endUpdateProgress(this.uiFeedbackState, (timer) => { diff --git a/src/core/services/ipc.test.ts b/src/core/services/ipc.test.ts index 69aa24a1..18f98564 100644 --- a/src/core/services/ipc.test.ts +++ b/src/core/services/ipc.test.ts @@ -670,6 +670,61 @@ test('registerIpcHandlers accepts the keep-without-media timing decision', async assert.deepEqual(requests, [{ reviewId: 'review-1', decision: { action: 'skip-media' } }]); }); +test('registerIpcHandlers validates and forwards combined timing review text', async () => { + const { registrar, handlers } = createFakeIpcRegistrar(); + const requests: unknown[] = []; + registerIpcHandlers( + createRegisterIpcDeps({ + resolveMediaTimingReview: async (request) => { + requests.push(request); + return { ok: true }; + }, + }), + registrar, + ); + + const handler = handlers.handle.get(IPC_CHANNELS.request.mediaTimingReviewResolve); + assert.ok(handler); + assert.deepEqual( + await handler!( + {}, + { + reviewId: 'review-1', + decision: { + action: 'confirm', + startTime: 10, + endTime: 12, + text: '前の行 対象の行', + }, + }, + ), + { ok: true }, + ); + assert.deepEqual(requests, [ + { + reviewId: 'review-1', + decision: { + action: 'confirm', + startTime: 10, + endTime: 12, + text: '前の行 対象の行', + }, + }, + ]); + + assert.deepEqual( + await handler!( + {}, + { + reviewId: 'review-1', + decision: { action: 'confirm', startTime: 10, endTime: 12, text: ' ' }, + }, + ), + { ok: false, message: 'Timing review is unavailable.' }, + ); + assert.equal(requests.length, 1); +}); + test('registerIpcHandlers forwards yomitan lookup tracking commands to immersion tracker', () => { const { registrar, handlers } = createFakeIpcRegistrar(); const calls: string[] = []; diff --git a/src/core/services/ipc.ts b/src/core/services/ipc.ts index 7b90e99e..91bbaa45 100644 --- a/src/core/services/ipc.ts +++ b/src/core/services/ipc.ts @@ -288,7 +288,9 @@ function parseMediaTimingReviewResolveRequest( typeof decisionRecord.startTime === 'number' && Number.isFinite(decisionRecord.startTime) && typeof decisionRecord.endTime === 'number' && - Number.isFinite(decisionRecord.endTime) + Number.isFinite(decisionRecord.endTime) && + (decisionRecord.text === undefined || + (typeof decisionRecord.text === 'string' && decisionRecord.text.trim().length > 0)) ) { return { reviewId: record.reviewId, @@ -296,6 +298,7 @@ function parseMediaTimingReviewResolveRequest( action: 'confirm', startTime: decisionRecord.startTime, endTime: decisionRecord.endTime, + ...(decisionRecord.text === undefined ? {} : { text: decisionRecord.text }), }, }; } diff --git a/src/core/services/media-timing-preview.ts b/src/core/services/media-timing-preview.ts index f7c5ab9c..693721e3 100644 --- a/src/core/services/media-timing-preview.ts +++ b/src/core/services/media-timing-preview.ts @@ -59,7 +59,11 @@ function createDefaultSocketPath(): string { const suffix = `${process.pid}-${randomUUID()}`; return process.platform === 'win32' ? `\\\\.\\pipe\\subminer-timing-preview-${suffix}` - : path.join(os.tmpdir(), `subminer-timing-preview-${suffix}.sock`); + : path.join( + // macOS limits Unix socket paths to 104 bytes, while its temp directory can be long. + process.platform === 'darwin' ? '/tmp' : os.tmpdir(), + `subminer-timing-preview-${suffix}.sock`, + ); } function removePosixSocketFile(socketPath: string): void {