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']);
|
||||
});
|
||||
|
||||
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[] = [];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user