mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-30 12:15:26 -07:00
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:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: anki
|
||||||
|
|
||||||
|
- Dismissed active overlay card-update progress when notification settings switch to OSD before an update finishes.
|
||||||
@@ -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`.
|
|
||||||
@@ -1262,6 +1262,41 @@ test('AnkiIntegration dismisses persistent overlay update progress when no termi
|
|||||||
assert.deepEqual(dismissedIds, ['anki-update-progress']);
|
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 () => {
|
test('AnkiIntegration keeps overlay notification image when temp icon write fails', async () => {
|
||||||
const desktopNotifications: Array<{ title: string; body?: string; icon?: string }> = [];
|
const desktopNotifications: Array<{ title: string; body?: string; icon?: string }> = [];
|
||||||
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
const overlayNotifications: TestOverlayNotificationPayload[] = [];
|
||||||
|
|||||||
@@ -1237,11 +1237,11 @@ export class AnkiIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private endUpdateProgress(): void {
|
private endUpdateProgress(): void {
|
||||||
|
if (this.overlayUpdateProgressActive) {
|
||||||
|
this.overlayUpdateProgressActive = false;
|
||||||
|
this.overlayNotificationDismissCallback?.('anki-update-progress');
|
||||||
|
}
|
||||||
if (!this.shouldUseOsdNotifications()) {
|
if (!this.shouldUseOsdNotifications()) {
|
||||||
if (this.overlayUpdateProgressActive) {
|
|
||||||
this.overlayUpdateProgressActive = false;
|
|
||||||
this.overlayNotificationDismissCallback?.('anki-update-progress');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
endUpdateProgress(this.uiFeedbackState, (timer) => {
|
endUpdateProgress(this.uiFeedbackState, (timer) => {
|
||||||
|
|||||||
@@ -670,6 +670,61 @@ test('registerIpcHandlers accepts the keep-without-media timing decision', async
|
|||||||
assert.deepEqual(requests, [{ reviewId: 'review-1', decision: { action: 'skip-media' } }]);
|
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', () => {
|
test('registerIpcHandlers forwards yomitan lookup tracking commands to immersion tracker', () => {
|
||||||
const { registrar, handlers } = createFakeIpcRegistrar();
|
const { registrar, handlers } = createFakeIpcRegistrar();
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|||||||
@@ -288,7 +288,9 @@ function parseMediaTimingReviewResolveRequest(
|
|||||||
typeof decisionRecord.startTime === 'number' &&
|
typeof decisionRecord.startTime === 'number' &&
|
||||||
Number.isFinite(decisionRecord.startTime) &&
|
Number.isFinite(decisionRecord.startTime) &&
|
||||||
typeof decisionRecord.endTime === 'number' &&
|
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 {
|
return {
|
||||||
reviewId: record.reviewId,
|
reviewId: record.reviewId,
|
||||||
@@ -296,6 +298,7 @@ function parseMediaTimingReviewResolveRequest(
|
|||||||
action: 'confirm',
|
action: 'confirm',
|
||||||
startTime: decisionRecord.startTime,
|
startTime: decisionRecord.startTime,
|
||||||
endTime: decisionRecord.endTime,
|
endTime: decisionRecord.endTime,
|
||||||
|
...(decisionRecord.text === undefined ? {} : { text: decisionRecord.text }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ function createDefaultSocketPath(): string {
|
|||||||
const suffix = `${process.pid}-${randomUUID()}`;
|
const suffix = `${process.pid}-${randomUUID()}`;
|
||||||
return process.platform === 'win32'
|
return process.platform === 'win32'
|
||||||
? `\\\\.\\pipe\\subminer-timing-preview-${suffix}`
|
? `\\\\.\\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 {
|
function removePosixSocketFile(socketPath: string): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user