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
This commit is contained in:
2026-08-30 02:49:37 -07:00
parent 90a5dcd2af
commit b58f391b42
7 changed files with 107 additions and 39 deletions
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Dismissed active overlay card-update progress when notification settings switch to OSD before an update finishes.
-33
View File
@@ -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`.
+35
View File
@@ -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<AnkiConnectConfig['behavior']> = {
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[] = [];
+4 -4
View File
@@ -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) => {
+55
View File
@@ -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[] = [];
+4 -1
View File
@@ -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 }),
},
};
}
+5 -1
View File
@@ -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 {