mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-12 17:16:20 -07:00
fix(anki): close the timing review modal once main has dropped the review
Main ends a review on its own after the decision watchdog, on overlay teardown, or when the modal window opened twice, but the renderer only closed on a successful resolve. Every button then answered 'no longer active' forever and the overlay stayed interactive over mpv. Stale results now carry a typed flag and the modal closes on it.
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
## 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`.
|
||||||
@@ -549,14 +549,14 @@ test('media timing review rejects stale and out-of-range actions before allowing
|
|||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await runtime.previewRange({ reviewId: 'stale-review', startTime: 10, endTime: 12 }),
|
await runtime.previewRange({ reviewId: 'stale-review', startTime: 10, endTime: 12 }),
|
||||||
{ ok: false, message: 'This timing review is no longer active.' },
|
{ ok: false, stale: true, message: 'This timing review is no longer active.' },
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
runtime.resolveReview({
|
runtime.resolveReview({
|
||||||
reviewId: 'stale-review',
|
reviewId: 'stale-review',
|
||||||
decision: { action: 'confirm', startTime: 10, endTime: 12 },
|
decision: { action: 'confirm', startTime: 10, endTime: 12 },
|
||||||
}),
|
}),
|
||||||
{ ok: false, message: 'This timing review is no longer active.' },
|
{ ok: false, stale: true, message: 'This timing review is no longer active.' },
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
runtime.resolveReview({
|
runtime.resolveReview({
|
||||||
|
|||||||
@@ -159,6 +159,15 @@ export function collectMediaTimingContextLines(options: {
|
|||||||
return { previous, next };
|
return { previous, next };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result for requests that name a review main has already resolved or disposed (decision
|
||||||
|
* watchdog, overlay teardown, duplicate modal). The renderer closes on it instead of
|
||||||
|
* leaving the user with controls that can never succeed.
|
||||||
|
*/
|
||||||
|
function staleReviewResult(): MediaTimingReviewActionResult {
|
||||||
|
return { ok: false, stale: true, message: 'This timing review is no longer active.' };
|
||||||
|
}
|
||||||
|
|
||||||
function isValidMediaTimingRange(
|
function isValidMediaTimingRange(
|
||||||
payload: MediaTimingReviewOpenPayload,
|
payload: MediaTimingReviewOpenPayload,
|
||||||
startTime: number,
|
startTime: number,
|
||||||
@@ -443,7 +452,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
): Promise<MediaTimingReviewActionResult> {
|
): Promise<MediaTimingReviewActionResult> {
|
||||||
const current = active;
|
const current = active;
|
||||||
if (!current || request.reviewId !== current.payload.reviewId) {
|
if (!current || request.reviewId !== current.payload.reviewId) {
|
||||||
return { ok: false, message: 'This timing review is no longer active.' };
|
return staleReviewResult();
|
||||||
}
|
}
|
||||||
if (!isValidMediaTimingRange(current.payload, request.startTime, request.endTime)) {
|
if (!isValidMediaTimingRange(current.payload, request.startTime, request.endTime)) {
|
||||||
return { ok: false, message: 'The selected preview range is invalid.' };
|
return { ok: false, message: 'The selected preview range is invalid.' };
|
||||||
@@ -451,7 +460,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
try {
|
try {
|
||||||
const previewSession = await previewFor(current, request);
|
const previewSession = await previewFor(current, request);
|
||||||
if (active !== current) {
|
if (active !== current) {
|
||||||
return { ok: false, message: 'This timing review is no longer active.' };
|
return staleReviewResult();
|
||||||
}
|
}
|
||||||
await previewSession.play(request.startTime, request.endTime);
|
await previewSession.play(request.startTime, request.endTime);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -468,7 +477,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
): Promise<MediaTimingReviewWaveformResult> {
|
): Promise<MediaTimingReviewWaveformResult> {
|
||||||
const current = active;
|
const current = active;
|
||||||
if (!current || request.reviewId !== current.payload.reviewId) {
|
if (!current || request.reviewId !== current.payload.reviewId) {
|
||||||
return { ok: false, message: 'This timing review is no longer active.' };
|
return staleReviewResult();
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!Number.isFinite(request.startTime) ||
|
!Number.isFinite(request.startTime) ||
|
||||||
@@ -484,7 +493,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
try {
|
try {
|
||||||
const window = await ensureWindow(current, request);
|
const window = await ensureWindow(current, request);
|
||||||
if (active !== current) {
|
if (active !== current) {
|
||||||
return { ok: false, message: 'This timing review is no longer active.' };
|
return staleReviewResult();
|
||||||
}
|
}
|
||||||
const peaks = await deps.generateWaveform({
|
const peaks = await deps.generateWaveform({
|
||||||
mediaPath: window?.media ?? current.waveformMedia,
|
mediaPath: window?.media ?? current.waveformMedia,
|
||||||
@@ -506,7 +515,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
async function stopPreview(reviewId: string): Promise<MediaTimingReviewActionResult> {
|
async function stopPreview(reviewId: string): Promise<MediaTimingReviewActionResult> {
|
||||||
const current = active;
|
const current = active;
|
||||||
if (!current || reviewId !== current.payload.reviewId) {
|
if (!current || reviewId !== current.payload.reviewId) {
|
||||||
return { ok: false, message: 'This timing review is no longer active.' };
|
return staleReviewResult();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const previewSession = current.preview ? await current.preview.session : null;
|
const previewSession = current.preview ? await current.preview.session : null;
|
||||||
@@ -523,7 +532,7 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
|||||||
function resolveReview(request: MediaTimingReviewResolveRequest): MediaTimingReviewActionResult {
|
function resolveReview(request: MediaTimingReviewResolveRequest): MediaTimingReviewActionResult {
|
||||||
const current = active;
|
const current = active;
|
||||||
if (!current || request.reviewId !== current.payload.reviewId) {
|
if (!current || request.reviewId !== current.payload.reviewId) {
|
||||||
return { ok: false, message: 'This timing review is no longer active.' };
|
return staleReviewResult();
|
||||||
}
|
}
|
||||||
if (request.decision.action === 'confirm') {
|
if (request.decision.action === 'confirm') {
|
||||||
const { startTime, endTime, text } = request.decision;
|
const { startTime, endTime, text } = request.decision;
|
||||||
|
|||||||
@@ -387,6 +387,10 @@ export function createMediaTimingReviewModal(
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!result.ok && result.stale) {
|
||||||
|
closeResolvedReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const path = result.ok ? buildMediaTimingWaveformPath(result.peaks ?? []) : '';
|
const path = result.ok ? buildMediaTimingWaveformPath(result.peaks ?? []) : '';
|
||||||
if (!path) {
|
if (!path) {
|
||||||
setWaveformState('unavailable');
|
setWaveformState('unavailable');
|
||||||
@@ -707,11 +711,13 @@ export function createMediaTimingReviewModal(
|
|||||||
reviewId: payload.reviewId,
|
reviewId: payload.reviewId,
|
||||||
decision,
|
decision,
|
||||||
});
|
});
|
||||||
if (!result.ok) {
|
if (!result.ok && !result.stale) {
|
||||||
setStatus(result.message ?? 'The timing review could not be resolved.', true);
|
setStatus(result.message ?? 'The timing review could not be resolved.', true);
|
||||||
showEditor();
|
showEditor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A stale review was already settled by main; keeping the modal up would leave
|
||||||
|
// controls that can never succeed over a live mpv window.
|
||||||
closeResolvedReview();
|
closeResolvedReview();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error instanceof Error ? error.message : String(error), true);
|
setStatus(error instanceof Error ? error.message : String(error), true);
|
||||||
@@ -759,6 +765,10 @@ export function createMediaTimingReviewModal(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!result.ok && result.stale) {
|
||||||
|
closeResolvedReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
setStatus(result.message ?? 'Audio preview is unavailable.', true);
|
setStatus(result.message ?? 'Audio preview is unavailable.', true);
|
||||||
setPreviewPlaying(false);
|
setPreviewPlaying(false);
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export interface MediaTimingReviewResolveRequest {
|
|||||||
export interface MediaTimingReviewActionResult {
|
export interface MediaTimingReviewActionResult {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
/** The review this request targeted has already ended; the renderer should close. */
|
||||||
|
stale?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationOptions {
|
export interface NotificationOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user