Files
SubMiner/src/renderer/modals/subsync.ts
T
sudacode 536d99251e fix(jellyfin): fix discovery loop, device identity, tray state, and Disc
- Derive device identity from OS hostname; remove legacy configurable client/device fields
- Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores
- Restart stale tray discovery sessions without re-login when server drops SubMiner cast target
- Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes
- Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads
- Fix picker library discovery when log level is above info
- Fix config.example.jsonc trailing commas and array formatting
2026-05-22 01:37:37 -07:00

191 lines
6.1 KiB
TypeScript

import type { SubsyncManualPayload } from '../../types';
import type { ModalStateReader, RendererContext } from '../context';
export function createSubsyncModal(
ctx: RendererContext,
options: {
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
syncSettingsModalSubtitleSuppression: () => void;
},
) {
let ffsubsyncAvailable = true;
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 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);
}
ctx.dom.subsyncSourceSelect.disabled = ctx.state.subsyncSourceTracks.length === 0;
}
function closeSubsyncModal(): void {
if (!ctx.state.subsyncModalOpen) return;
ctx.state.subsyncModalOpen = false;
options.syncSettingsModalSubtitleSuppression();
ctx.dom.subsyncModal.classList.add('hidden');
ctx.dom.subsyncModal.setAttribute('aria-hidden', 'true');
window.electronAPI.notifyOverlayModalClosed('subsync');
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
ctx.dom.overlay.classList.remove('interactive');
}
}
function openSubsyncModal(payload: SubsyncManualPayload): void {
ctx.state.subsyncSubmitting = false;
ctx.state.subsyncSourceTracks = payload.sourceTracks;
ffsubsyncAvailable = payload.ffsubsyncAvailable;
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;
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,
);
ctx.state.subsyncModalOpen = true;
options.syncSettingsModalSubtitleSuppression();
ctx.dom.overlay.classList.add('interactive');
ctx.dom.subsyncModal.classList.remove('hidden');
ctx.dom.subsyncModal.setAttribute('aria-hidden', 'false');
}
function reopenSubsyncModalWithError(
sourceTracks: SubsyncManualPayload['sourceTracks'],
engine: 'alass' | 'ffsubsync',
sourceTrackId: number | null,
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();
setSubsyncStatus(message, true);
window.electronAPI.notifyOverlayModalOpened('subsync');
}
async function runSubsyncManualFromModal(): Promise<void> {
if (ctx.state.subsyncSubmitting) return;
if (ctx.dom.subsyncRunButton.disabled) return;
const useAlass = ctx.dom.subsyncEngineAlass.checked;
const useFfsubsync = ctx.dom.subsyncEngineFfsubsync.checked;
if (!useAlass && !useFfsubsync) {
setSubsyncStatus('No sync engine available for current media.', true);
return;
}
const engine = useAlass ? 'alass' : 'ffsubsync';
const sourceTrackId =
engine === 'alass' && ctx.dom.subsyncSourceSelect.value
? Number.parseInt(ctx.dom.subsyncSourceSelect.value, 10)
: null;
if (engine === 'alass' && !Number.isFinite(sourceTrackId)) {
setSubsyncStatus('Select a source subtitle track for alass.', true);
return;
}
const sourceTracksSnapshot = ctx.state.subsyncSourceTracks.map((track) => ({ ...track }));
ctx.state.subsyncSubmitting = true;
ctx.dom.subsyncRunButton.disabled = true;
closeSubsyncModal();
try {
const result = await window.electronAPI.runSubsyncManual({
engine,
sourceTrackId,
});
if (result.ok) return;
reopenSubsyncModalWithError(sourceTracksSnapshot, engine, sourceTrackId, result.message);
} catch (error) {
reopenSubsyncModalWithError(
sourceTracksSnapshot,
engine,
sourceTrackId,
`Subsync failed: ${(error as Error).message}`,
);
} finally {
ctx.state.subsyncSubmitting = false;
ctx.dom.subsyncRunButton.disabled = false;
}
}
function handleSubsyncKeydown(e: KeyboardEvent): boolean {
if (e.key === 'Escape') {
e.preventDefault();
closeSubsyncModal();
return true;
}
if (e.key === 'Enter') {
e.preventDefault();
void runSubsyncManualFromModal();
return true;
}
return true;
}
function wireDomEvents(): void {
ctx.dom.subsyncCloseButton.addEventListener('click', () => {
closeSubsyncModal();
});
ctx.dom.subsyncEngineAlass.addEventListener('change', () => {
updateSubsyncSourceVisibility();
});
ctx.dom.subsyncEngineFfsubsync.addEventListener('change', () => {
updateSubsyncSourceVisibility();
});
ctx.dom.subsyncRunButton.addEventListener('click', () => {
void runSubsyncManualFromModal();
});
}
return {
closeSubsyncModal,
handleSubsyncKeydown,
openSubsyncModal,
wireDomEvents,
};
}