mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-05 07:21:34 -07:00
feat(subsync): add reference and target subtitle track picker (#181)
This commit is contained in:
@@ -359,9 +359,13 @@
|
||||
ffsubsync
|
||||
</label>
|
||||
</div>
|
||||
<label id="subsyncSourceLabel" class="subsync-field">
|
||||
<span>Source Subtitle (for alass)</span>
|
||||
<select id="subsyncSourceSelect"></select>
|
||||
<label id="subsyncReferenceLabel" class="subsync-field">
|
||||
<span>Reference (correct timing, for alass)</span>
|
||||
<select id="subsyncReferenceSelect"></select>
|
||||
</label>
|
||||
<label id="subsyncTargetLabel" class="subsync-field">
|
||||
<span>Out-of-sync Subtitle (gets retimed)</span>
|
||||
<select id="subsyncTargetSelect"></select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="subsyncStatus" class="runtime-options-status"></div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { createSubsyncModal } from './subsync.js';
|
||||
import type { SubsyncManualPayload, SubsyncManualRunRequest } from '../../types';
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
@@ -52,18 +53,54 @@ function createDeferred<T>() {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; message: string }>) {
|
||||
function createSelectStub() {
|
||||
const options: Array<{ value: string; textContent: string }> = [];
|
||||
const events = createEventTarget();
|
||||
let innerHTML = '';
|
||||
let value = '';
|
||||
|
||||
return {
|
||||
options,
|
||||
disabled: false,
|
||||
addEventListener: events.addEventListener,
|
||||
dispatch: events.dispatch,
|
||||
get innerHTML(): string {
|
||||
return innerHTML;
|
||||
},
|
||||
set innerHTML(next: string) {
|
||||
innerHTML = next;
|
||||
if (next === '') {
|
||||
options.length = 0;
|
||||
value = '';
|
||||
}
|
||||
},
|
||||
get value(): string {
|
||||
return value;
|
||||
},
|
||||
set value(next: string) {
|
||||
value = next;
|
||||
},
|
||||
appendChild(option: { value: string; textContent: string }) {
|
||||
options.push(option);
|
||||
if (!value) value = option.value;
|
||||
return option;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTestHarness(
|
||||
runSubsyncManual: (request: SubsyncManualRunRequest) => Promise<{ ok: boolean; message: string }>,
|
||||
) {
|
||||
const overlayClassList = createClassList();
|
||||
const modalClassList = createClassList();
|
||||
const statusClassList = createClassList();
|
||||
const sourceLabelClassList = createClassList();
|
||||
const referenceLabelClassList = createClassList();
|
||||
const targetLabelClassList = createClassList();
|
||||
const runButtonEvents = createEventTarget();
|
||||
const closeButtonEvents = createEventTarget();
|
||||
const engineAlassEvents = createEventTarget();
|
||||
const engineFfsubsyncEvents = createEventTarget();
|
||||
|
||||
const sourceOptions: Array<{ value: string; textContent: string }> = [];
|
||||
|
||||
const runButton = {
|
||||
disabled: false,
|
||||
addEventListener: runButtonEvents.addEventListener,
|
||||
@@ -77,6 +114,7 @@ function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; messag
|
||||
|
||||
const subsyncEngineAlass = {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
addEventListener: engineAlassEvents.addEventListener,
|
||||
dispatch: engineAlassEvents.dispatch,
|
||||
};
|
||||
@@ -88,18 +126,8 @@ function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; messag
|
||||
dispatch: engineFfsubsyncEvents.dispatch,
|
||||
};
|
||||
|
||||
const sourceSelect = {
|
||||
innerHTML: '',
|
||||
value: '',
|
||||
disabled: false,
|
||||
appendChild: (option: { value: string; textContent: string }) => {
|
||||
sourceOptions.push(option);
|
||||
if (!sourceSelect.value) {
|
||||
sourceSelect.value = option.value;
|
||||
}
|
||||
return option;
|
||||
},
|
||||
};
|
||||
const referenceSelect = createSelectStub();
|
||||
const targetSelect = createSelectStub();
|
||||
|
||||
let notifyClosedCalls = 0;
|
||||
let notifyOpenedCalls = 0;
|
||||
@@ -139,8 +167,10 @@ function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; messag
|
||||
subsyncCloseButton: closeButton,
|
||||
subsyncEngineAlass,
|
||||
subsyncEngineFfsubsync,
|
||||
subsyncSourceLabel: { classList: sourceLabelClassList },
|
||||
subsyncSourceSelect: sourceSelect,
|
||||
subsyncReferenceLabel: { classList: referenceLabelClassList },
|
||||
subsyncReferenceSelect: referenceSelect,
|
||||
subsyncTargetLabel: { classList: targetLabelClassList },
|
||||
subsyncTargetSelect: targetSelect,
|
||||
subsyncRunButton: runButton,
|
||||
subsyncStatus: {
|
||||
textContent: '',
|
||||
@@ -149,7 +179,7 @@ function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; messag
|
||||
},
|
||||
state: {
|
||||
subsyncModalOpen: false,
|
||||
subsyncSourceTracks: [],
|
||||
subsyncSubtitleTracks: [],
|
||||
subsyncSubmitting: false,
|
||||
isOverSubtitle: false,
|
||||
},
|
||||
@@ -166,6 +196,9 @@ function createTestHarness(runSubsyncManual: () => Promise<{ ok: boolean; messag
|
||||
ctx,
|
||||
modal,
|
||||
runButton,
|
||||
referenceSelect,
|
||||
targetSelect,
|
||||
referenceLabelClassList,
|
||||
statusClassList,
|
||||
getNotifyClosedCalls: () => notifyClosedCalls,
|
||||
getNotifyOpenedCalls: () => notifyOpenedCalls,
|
||||
@@ -187,16 +220,28 @@ async function flushMicrotasks(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
const BASE_PAYLOAD: SubsyncManualPayload = {
|
||||
subtitleTracks: [
|
||||
{ id: 1, label: 'External #1 - jpn (active)' },
|
||||
{ id: 2, label: 'External #2 - eng' },
|
||||
],
|
||||
defaultReferenceTrackId: 2,
|
||||
defaultTargetTrackId: 1,
|
||||
videoReferenceAvailable: true,
|
||||
ffsubsyncAvailable: true,
|
||||
};
|
||||
|
||||
function payloadWith(overrides: Partial<SubsyncManualPayload>): SubsyncManualPayload {
|
||||
return { ...BASE_PAYLOAD, ...overrides };
|
||||
}
|
||||
|
||||
test('manual subsync failure closes during run, then reopens modal with error', async () => {
|
||||
const deferred = createDeferred<{ ok: boolean; message: string }>();
|
||||
const harness = createTestHarness(async () => deferred.promise);
|
||||
|
||||
try {
|
||||
harness.modal.wireDomEvents();
|
||||
harness.modal.openSubsyncModal({
|
||||
sourceTracks: [{ id: 2, label: 'External #2 - eng' }],
|
||||
ffsubsyncAvailable: true,
|
||||
});
|
||||
harness.modal.openSubsyncModal(BASE_PAYLOAD);
|
||||
|
||||
harness.runButton.dispatch('click');
|
||||
await Promise.resolve();
|
||||
@@ -219,7 +264,8 @@ test('manual subsync failure closes during run, then reopens modal with error',
|
||||
assert.equal(harness.statusClassList.contains('error'), true);
|
||||
assert.equal(harness.ctx.dom.subsyncRunButton.disabled, false);
|
||||
assert.equal(harness.ctx.dom.subsyncEngineAlass.checked, true);
|
||||
assert.equal(harness.ctx.dom.subsyncSourceSelect.value, '2');
|
||||
assert.equal(harness.referenceSelect.value, '2');
|
||||
assert.equal(harness.targetSelect.value, '1');
|
||||
assert.equal(harness.getNotifyClosedCalls(), 1);
|
||||
assert.equal(harness.getNotifyOpenedCalls(), 1);
|
||||
} finally {
|
||||
@@ -231,15 +277,17 @@ test('subsync modal disables ffsubsync when payload marks it unavailable', () =>
|
||||
const harness = createTestHarness(async () => ({ ok: true, message: 'ok' }));
|
||||
|
||||
try {
|
||||
harness.modal.openSubsyncModal({
|
||||
sourceTracks: [{ id: 2, label: 'External #2 - eng' }],
|
||||
ffsubsyncAvailable: false,
|
||||
});
|
||||
harness.modal.openSubsyncModal(
|
||||
payloadWith({ ffsubsyncAvailable: false, videoReferenceAvailable: false }),
|
||||
);
|
||||
|
||||
assert.equal(harness.ctx.dom.subsyncEngineAlass.checked, true);
|
||||
assert.equal(harness.ctx.dom.subsyncEngineFfsubsync.checked, false);
|
||||
assert.equal(harness.ctx.dom.subsyncEngineFfsubsync.disabled, true);
|
||||
assert.equal(harness.ctx.dom.subsyncStatus.textContent, 'Choose alass source, then run.');
|
||||
assert.equal(
|
||||
harness.ctx.dom.subsyncStatus.textContent,
|
||||
'Choose the alass reference and out-of-sync subtitle, then run.',
|
||||
);
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
@@ -253,10 +301,14 @@ test('subsync modal ignores enter submission when no sync engine is available',
|
||||
});
|
||||
|
||||
try {
|
||||
harness.modal.openSubsyncModal({
|
||||
sourceTracks: [],
|
||||
ffsubsyncAvailable: false,
|
||||
});
|
||||
harness.modal.openSubsyncModal(
|
||||
payloadWith({
|
||||
subtitleTracks: [{ id: 1, label: 'External #1 - jpn (active)' }],
|
||||
defaultReferenceTrackId: null,
|
||||
videoReferenceAvailable: false,
|
||||
ffsubsyncAvailable: false,
|
||||
}),
|
||||
);
|
||||
|
||||
harness.modal.handleSubsyncKeydown({
|
||||
key: 'Enter',
|
||||
@@ -270,3 +322,102 @@ test('subsync modal ignores enter submission when no sync engine is available',
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('subsync modal defaults reference to the secondary track and target to the primary', async () => {
|
||||
let request: SubsyncManualRunRequest | null = null;
|
||||
const harness = createTestHarness(async (nextRequest) => {
|
||||
request = nextRequest;
|
||||
return { ok: true, message: 'ok' };
|
||||
});
|
||||
|
||||
try {
|
||||
harness.modal.wireDomEvents();
|
||||
harness.modal.openSubsyncModal(BASE_PAYLOAD);
|
||||
|
||||
assert.equal(harness.referenceSelect.value, '2');
|
||||
assert.equal(harness.targetSelect.value, '1');
|
||||
|
||||
harness.runButton.dispatch('click');
|
||||
await flushMicrotasks();
|
||||
|
||||
assert.deepEqual(request, {
|
||||
engine: 'alass',
|
||||
targetTrackId: 1,
|
||||
referenceMode: 'track',
|
||||
referenceTrackId: 2,
|
||||
});
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('subsync modal offers the video file as an alass reference and excludes the target track', () => {
|
||||
const harness = createTestHarness(async () => ({ ok: true, message: 'ok' }));
|
||||
|
||||
try {
|
||||
harness.modal.wireDomEvents();
|
||||
harness.modal.openSubsyncModal(BASE_PAYLOAD);
|
||||
|
||||
assert.deepEqual(
|
||||
harness.referenceSelect.options.map((option) => option.value),
|
||||
['2', 'video'],
|
||||
);
|
||||
|
||||
harness.targetSelect.value = '2';
|
||||
harness.targetSelect.dispatch('change');
|
||||
|
||||
assert.deepEqual(
|
||||
harness.referenceSelect.options.map((option) => option.value),
|
||||
['1', 'video'],
|
||||
);
|
||||
assert.equal(harness.referenceSelect.value, '1');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('subsync modal sends the video reference mode when the video file is selected', async () => {
|
||||
let request: SubsyncManualRunRequest | null = null;
|
||||
const harness = createTestHarness(async (nextRequest) => {
|
||||
request = nextRequest;
|
||||
return { ok: true, message: 'ok' };
|
||||
});
|
||||
|
||||
try {
|
||||
harness.modal.wireDomEvents();
|
||||
harness.modal.openSubsyncModal(BASE_PAYLOAD);
|
||||
|
||||
harness.referenceSelect.value = 'video';
|
||||
harness.runButton.dispatch('click');
|
||||
await flushMicrotasks();
|
||||
|
||||
assert.deepEqual(request, {
|
||||
engine: 'alass',
|
||||
targetTrackId: 1,
|
||||
referenceMode: 'video',
|
||||
referenceTrackId: null,
|
||||
});
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
test('subsync modal hides the reference picker for ffsubsync but keeps the target picker', () => {
|
||||
const harness = createTestHarness(async () => ({ ok: true, message: 'ok' }));
|
||||
|
||||
try {
|
||||
harness.modal.wireDomEvents();
|
||||
harness.modal.openSubsyncModal(BASE_PAYLOAD);
|
||||
|
||||
assert.equal(harness.referenceLabelClassList.contains('hidden'), false);
|
||||
|
||||
harness.ctx.dom.subsyncEngineAlass.checked = false;
|
||||
harness.ctx.dom.subsyncEngineFfsubsync.checked = true;
|
||||
harness.ctx.dom.subsyncEngineFfsubsync.dispatch('change');
|
||||
|
||||
assert.equal(harness.referenceLabelClassList.contains('hidden'), true);
|
||||
assert.equal(harness.targetSelect.value, '1');
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
+151
-67
@@ -1,6 +1,14 @@
|
||||
import type { SubsyncManualPayload } from '../../types';
|
||||
import type { SubsyncManualPayload, SubsyncManualRunRequest } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
|
||||
const VIDEO_REFERENCE_VALUE = 'video';
|
||||
|
||||
interface SubsyncSelection {
|
||||
engine: 'alass' | 'ffsubsync';
|
||||
referenceValue: string;
|
||||
targetTrackId: number | null;
|
||||
}
|
||||
|
||||
export function createSubsyncModal(
|
||||
ctx: RendererContext,
|
||||
options: {
|
||||
@@ -8,27 +16,103 @@ export function createSubsyncModal(
|
||||
syncSettingsModalSubtitleSuppression: () => void;
|
||||
},
|
||||
) {
|
||||
let ffsubsyncAvailable = true;
|
||||
let currentPayload: SubsyncManualPayload | null = null;
|
||||
|
||||
function setSubsyncStatus(message: string, isError = false): void {
|
||||
ctx.dom.subsyncStatus.textContent = message;
|
||||
ctx.dom.subsyncStatus.classList.toggle('error', isError);
|
||||
}
|
||||
|
||||
function updateSubsyncSourceVisibility(): void {
|
||||
const useAlass = ctx.dom.subsyncEngineAlass.checked;
|
||||
ctx.dom.subsyncSourceLabel.classList.toggle('hidden', !useAlass);
|
||||
function hasAlassReference(): boolean {
|
||||
if (!currentPayload) return false;
|
||||
return currentPayload.videoReferenceAvailable || currentPayload.subtitleTracks.length > 1;
|
||||
}
|
||||
|
||||
function renderSubsyncSourceTracks(): void {
|
||||
ctx.dom.subsyncSourceSelect.innerHTML = '';
|
||||
for (const track of ctx.state.subsyncSourceTracks) {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(track.id);
|
||||
option.textContent = track.label;
|
||||
ctx.dom.subsyncSourceSelect.appendChild(option);
|
||||
function updateSubsyncFieldVisibility(): void {
|
||||
const useAlass = ctx.dom.subsyncEngineAlass.checked;
|
||||
ctx.dom.subsyncReferenceLabel.classList.toggle('hidden', !useAlass);
|
||||
ctx.dom.subsyncTargetLabel.classList.toggle(
|
||||
'hidden',
|
||||
ctx.state.subsyncSubtitleTracks.length === 0,
|
||||
);
|
||||
}
|
||||
|
||||
function appendOption(select: HTMLSelectElement, value: string, label: string): void {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = label;
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
function getSelectedTargetTrackId(): number | null {
|
||||
const raw = Number.parseInt(ctx.dom.subsyncTargetSelect.value, 10);
|
||||
return Number.isFinite(raw) ? raw : null;
|
||||
}
|
||||
|
||||
function renderTargetTracks(preferredTrackId: number | null): void {
|
||||
const select = ctx.dom.subsyncTargetSelect;
|
||||
select.innerHTML = '';
|
||||
select.value = '';
|
||||
for (const track of ctx.state.subsyncSubtitleTracks) {
|
||||
appendOption(select, String(track.id), track.label);
|
||||
}
|
||||
ctx.dom.subsyncSourceSelect.disabled = ctx.state.subsyncSourceTracks.length === 0;
|
||||
select.disabled = ctx.state.subsyncSubtitleTracks.length === 0;
|
||||
|
||||
const preferred = ctx.state.subsyncSubtitleTracks.find(
|
||||
(track) => track.id === preferredTrackId,
|
||||
);
|
||||
const fallback = ctx.state.subsyncSubtitleTracks[0];
|
||||
const selected = preferred ?? fallback;
|
||||
if (selected) {
|
||||
select.value = String(selected.id);
|
||||
}
|
||||
}
|
||||
|
||||
function renderReferenceTracks(preferredValue: string | null): void {
|
||||
const select = ctx.dom.subsyncReferenceSelect;
|
||||
const targetTrackId = getSelectedTargetTrackId();
|
||||
const values: string[] = [];
|
||||
|
||||
select.innerHTML = '';
|
||||
select.value = '';
|
||||
for (const track of ctx.state.subsyncSubtitleTracks) {
|
||||
if (track.id === targetTrackId) continue;
|
||||
appendOption(select, String(track.id), track.label);
|
||||
values.push(String(track.id));
|
||||
}
|
||||
if (currentPayload?.videoReferenceAvailable) {
|
||||
appendOption(select, VIDEO_REFERENCE_VALUE, 'Video file (audio reference)');
|
||||
values.push(VIDEO_REFERENCE_VALUE);
|
||||
}
|
||||
select.disabled = values.length === 0;
|
||||
|
||||
const preferred = preferredValue && values.includes(preferredValue) ? preferredValue : null;
|
||||
const defaultTrackValue =
|
||||
currentPayload?.defaultReferenceTrackId !== null &&
|
||||
currentPayload?.defaultReferenceTrackId !== undefined
|
||||
? String(currentPayload.defaultReferenceTrackId)
|
||||
: null;
|
||||
const fallback =
|
||||
defaultTrackValue && values.includes(defaultTrackValue) ? defaultTrackValue : values[0];
|
||||
const selected = preferred ?? fallback;
|
||||
if (selected) {
|
||||
select.value = selected;
|
||||
}
|
||||
}
|
||||
|
||||
function describeSubsyncState(): string {
|
||||
if (!currentPayload) return '';
|
||||
const alassReady = hasAlassReference();
|
||||
if (alassReady && currentPayload.ffsubsyncAvailable) {
|
||||
return 'Choose engine, reference and out-of-sync subtitle, then run.';
|
||||
}
|
||||
if (alassReady) {
|
||||
return 'Choose the alass reference and out-of-sync subtitle, then run.';
|
||||
}
|
||||
if (currentPayload.ffsubsyncAvailable) {
|
||||
return 'No reference available for alass. Use ffsubsync.';
|
||||
}
|
||||
return 'No sync engine available for current media.';
|
||||
}
|
||||
|
||||
function closeSubsyncModal(): void {
|
||||
@@ -46,30 +130,27 @@ export function createSubsyncModal(
|
||||
}
|
||||
}
|
||||
|
||||
function openSubsyncModal(payload: SubsyncManualPayload): void {
|
||||
function openSubsyncModal(payload: SubsyncManualPayload, selection?: SubsyncSelection): void {
|
||||
ctx.state.subsyncSubmitting = false;
|
||||
ctx.state.subsyncSourceTracks = payload.sourceTracks;
|
||||
ffsubsyncAvailable = payload.ffsubsyncAvailable;
|
||||
ctx.state.subsyncSubtitleTracks = payload.subtitleTracks;
|
||||
currentPayload = payload;
|
||||
|
||||
const hasSources = ctx.state.subsyncSourceTracks.length > 0;
|
||||
ctx.dom.subsyncEngineAlass.checked = hasSources;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = !hasSources && ffsubsyncAvailable;
|
||||
ctx.dom.subsyncEngineFfsubsync.disabled = !ffsubsyncAvailable;
|
||||
ctx.dom.subsyncRunButton.disabled = !hasSources && !ffsubsyncAvailable;
|
||||
const alassReady = hasAlassReference();
|
||||
const useAlass = selection ? selection.engine === 'alass' && alassReady : alassReady;
|
||||
ctx.dom.subsyncEngineAlass.checked = useAlass;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = !useAlass && payload.ffsubsyncAvailable;
|
||||
ctx.dom.subsyncEngineAlass.disabled = !alassReady;
|
||||
ctx.dom.subsyncEngineFfsubsync.disabled = !payload.ffsubsyncAvailable;
|
||||
ctx.dom.subsyncRunButton.disabled = !alassReady && !payload.ffsubsyncAvailable;
|
||||
|
||||
renderSubsyncSourceTracks();
|
||||
updateSubsyncSourceVisibility();
|
||||
|
||||
setSubsyncStatus(
|
||||
!ffsubsyncAvailable && hasSources
|
||||
? 'Choose alass source, then run.'
|
||||
: !ffsubsyncAvailable
|
||||
? 'No source subtitles available for alass.'
|
||||
: hasSources
|
||||
? 'Choose engine and source, then run.'
|
||||
: 'No source subtitles available for alass. Use ffsubsync.',
|
||||
false,
|
||||
renderTargetTracks(
|
||||
selection
|
||||
? (selection.targetTrackId ?? payload.defaultTargetTrackId)
|
||||
: payload.defaultTargetTrackId,
|
||||
);
|
||||
renderReferenceTracks(selection?.referenceValue ?? null);
|
||||
updateSubsyncFieldVisibility();
|
||||
setSubsyncStatus(describeSubsyncState(), false);
|
||||
|
||||
ctx.state.subsyncModalOpen = true;
|
||||
options.syncSettingsModalSubtitleSuppression();
|
||||
@@ -80,25 +161,11 @@ export function createSubsyncModal(
|
||||
}
|
||||
|
||||
function reopenSubsyncModalWithError(
|
||||
sourceTracks: SubsyncManualPayload['sourceTracks'],
|
||||
engine: 'alass' | 'ffsubsync',
|
||||
sourceTrackId: number | null,
|
||||
payload: SubsyncManualPayload,
|
||||
selection: SubsyncSelection,
|
||||
message: string,
|
||||
): void {
|
||||
openSubsyncModal({ sourceTracks, ffsubsyncAvailable });
|
||||
|
||||
if (engine === 'alass' && sourceTracks.length > 0) {
|
||||
ctx.dom.subsyncEngineAlass.checked = true;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = false;
|
||||
if (Number.isFinite(sourceTrackId)) {
|
||||
ctx.dom.subsyncSourceSelect.value = String(sourceTrackId);
|
||||
}
|
||||
} else if (ffsubsyncAvailable) {
|
||||
ctx.dom.subsyncEngineAlass.checked = false;
|
||||
ctx.dom.subsyncEngineFfsubsync.checked = true;
|
||||
}
|
||||
|
||||
updateSubsyncSourceVisibility();
|
||||
openSubsyncModal(payload, selection);
|
||||
setSubsyncStatus(message, true);
|
||||
window.electronAPI.notifyOverlayModalOpened('subsync');
|
||||
}
|
||||
@@ -106,6 +173,7 @@ export function createSubsyncModal(
|
||||
async function runSubsyncManualFromModal(): Promise<void> {
|
||||
if (ctx.state.subsyncSubmitting) return;
|
||||
if (ctx.dom.subsyncRunButton.disabled) return;
|
||||
if (!currentPayload) return;
|
||||
|
||||
const useAlass = ctx.dom.subsyncEngineAlass.checked;
|
||||
const useFfsubsync = ctx.dom.subsyncEngineFfsubsync.checked;
|
||||
@@ -115,33 +183,46 @@ export function createSubsyncModal(
|
||||
}
|
||||
|
||||
const engine = useAlass ? 'alass' : 'ffsubsync';
|
||||
const sourceTrackId =
|
||||
engine === 'alass' && ctx.dom.subsyncSourceSelect.value
|
||||
? Number.parseInt(ctx.dom.subsyncSourceSelect.value, 10)
|
||||
: null;
|
||||
const referenceValue = ctx.dom.subsyncReferenceSelect.value;
|
||||
const targetTrackId = getSelectedTargetTrackId();
|
||||
|
||||
if (engine === 'alass' && !Number.isFinite(sourceTrackId)) {
|
||||
setSubsyncStatus('Select a source subtitle track for alass.', true);
|
||||
if (targetTrackId === null) {
|
||||
setSubsyncStatus('Select the out-of-sync subtitle track to retime.', true);
|
||||
return;
|
||||
}
|
||||
if (engine === 'alass' && !referenceValue) {
|
||||
setSubsyncStatus('Select a reference for alass.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceTracksSnapshot = ctx.state.subsyncSourceTracks.map((track) => ({ ...track }));
|
||||
const useVideoReference = referenceValue === VIDEO_REFERENCE_VALUE;
|
||||
const request: SubsyncManualRunRequest = {
|
||||
engine,
|
||||
targetTrackId,
|
||||
};
|
||||
if (engine === 'alass') {
|
||||
request.referenceMode = useVideoReference ? 'video' : 'track';
|
||||
request.referenceTrackId = useVideoReference ? null : Number.parseInt(referenceValue, 10);
|
||||
}
|
||||
|
||||
const payloadSnapshot: SubsyncManualPayload = {
|
||||
...currentPayload,
|
||||
subtitleTracks: currentPayload.subtitleTracks.map((track) => ({ ...track })),
|
||||
};
|
||||
const selection: SubsyncSelection = { engine, referenceValue, targetTrackId };
|
||||
|
||||
ctx.state.subsyncSubmitting = true;
|
||||
ctx.dom.subsyncRunButton.disabled = true;
|
||||
closeSubsyncModal();
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.runSubsyncManual({
|
||||
engine,
|
||||
sourceTrackId,
|
||||
});
|
||||
const result = await window.electronAPI.runSubsyncManual(request);
|
||||
if (result.ok) return;
|
||||
reopenSubsyncModalWithError(sourceTracksSnapshot, engine, sourceTrackId, result.message);
|
||||
reopenSubsyncModalWithError(payloadSnapshot, selection, result.message);
|
||||
} catch (error) {
|
||||
reopenSubsyncModalWithError(
|
||||
sourceTracksSnapshot,
|
||||
engine,
|
||||
sourceTrackId,
|
||||
payloadSnapshot,
|
||||
selection,
|
||||
`Subsync failed: ${(error as Error).message}`,
|
||||
);
|
||||
} finally {
|
||||
@@ -171,10 +252,13 @@ export function createSubsyncModal(
|
||||
closeSubsyncModal();
|
||||
});
|
||||
ctx.dom.subsyncEngineAlass.addEventListener('change', () => {
|
||||
updateSubsyncSourceVisibility();
|
||||
updateSubsyncFieldVisibility();
|
||||
});
|
||||
ctx.dom.subsyncEngineFfsubsync.addEventListener('change', () => {
|
||||
updateSubsyncSourceVisibility();
|
||||
updateSubsyncFieldVisibility();
|
||||
});
|
||||
ctx.dom.subsyncTargetSelect.addEventListener('change', () => {
|
||||
renderReferenceTracks(ctx.dom.subsyncReferenceSelect.value || null);
|
||||
});
|
||||
ctx.dom.subsyncRunButton.addEventListener('click', () => {
|
||||
void runSubsyncManualFromModal();
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
SubtitlePosition,
|
||||
SubtitleSidebarSnapshotConfig,
|
||||
SubtitleCue,
|
||||
SubsyncSourceTrack,
|
||||
SubsyncSubtitleTrack,
|
||||
YoutubePickerOpenPayload,
|
||||
} from '../types';
|
||||
|
||||
@@ -85,7 +85,7 @@ export type RendererState = {
|
||||
characterDictionaryStatus: string;
|
||||
|
||||
subsyncModalOpen: boolean;
|
||||
subsyncSourceTracks: SubsyncSourceTrack[];
|
||||
subsyncSubtitleTracks: SubsyncSubtitleTrack[];
|
||||
subsyncSubmitting: boolean;
|
||||
|
||||
controllerSelectModalOpen: boolean;
|
||||
@@ -213,7 +213,7 @@ export function createRendererState(): RendererState {
|
||||
characterDictionaryStatus: '',
|
||||
|
||||
subsyncModalOpen: false,
|
||||
subsyncSourceTracks: [],
|
||||
subsyncSubtitleTracks: [],
|
||||
subsyncSubmitting: false,
|
||||
|
||||
controllerSelectModalOpen: false,
|
||||
|
||||
@@ -90,8 +90,10 @@ export type RendererDom = {
|
||||
subsyncCloseButton: HTMLButtonElement;
|
||||
subsyncEngineAlass: HTMLInputElement;
|
||||
subsyncEngineFfsubsync: HTMLInputElement;
|
||||
subsyncSourceLabel: HTMLLabelElement;
|
||||
subsyncSourceSelect: HTMLSelectElement;
|
||||
subsyncReferenceLabel: HTMLLabelElement;
|
||||
subsyncReferenceSelect: HTMLSelectElement;
|
||||
subsyncTargetLabel: HTMLLabelElement;
|
||||
subsyncTargetSelect: HTMLSelectElement;
|
||||
subsyncRunButton: HTMLButtonElement;
|
||||
subsyncStatus: HTMLDivElement;
|
||||
|
||||
@@ -255,8 +257,10 @@ export function resolveRendererDom(): RendererDom {
|
||||
subsyncCloseButton: getRequiredElement<HTMLButtonElement>('subsyncClose'),
|
||||
subsyncEngineAlass: getRequiredElement<HTMLInputElement>('subsyncEngineAlass'),
|
||||
subsyncEngineFfsubsync: getRequiredElement<HTMLInputElement>('subsyncEngineFfsubsync'),
|
||||
subsyncSourceLabel: getRequiredElement<HTMLLabelElement>('subsyncSourceLabel'),
|
||||
subsyncSourceSelect: getRequiredElement<HTMLSelectElement>('subsyncSourceSelect'),
|
||||
subsyncReferenceLabel: getRequiredElement<HTMLLabelElement>('subsyncReferenceLabel'),
|
||||
subsyncReferenceSelect: getRequiredElement<HTMLSelectElement>('subsyncReferenceSelect'),
|
||||
subsyncTargetLabel: getRequiredElement<HTMLLabelElement>('subsyncTargetLabel'),
|
||||
subsyncTargetSelect: getRequiredElement<HTMLSelectElement>('subsyncTargetSelect'),
|
||||
subsyncRunButton: getRequiredElement<HTMLButtonElement>('subsyncRun'),
|
||||
subsyncStatus: getRequiredElement<HTMLDivElement>('subsyncStatus'),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user