feat(core): add Electron runtime, services, and app composition

This commit is contained in:
2026-02-22 21:43:43 -08:00
parent 448ce03fd4
commit d3fd47f0ec
562 changed files with 69719 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
import type { SubsyncManualPayload } from '../../types';
import type { ModalStateReader, RendererContext } from '../context';
export function createSubsyncModal(
ctx: RendererContext,
options: {
modalStateReader: Pick<ModalStateReader, 'isAnyModalOpen'>;
syncSettingsModalSubtitleSuppression: () => void;
},
) {
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 {
if (ctx.platform.isInvisibleLayer) return;
ctx.state.subsyncSubmitting = false;
ctx.dom.subsyncRunButton.disabled = false;
ctx.state.subsyncSourceTracks = payload.sourceTracks;
const hasSources = ctx.state.subsyncSourceTracks.length > 0;
ctx.dom.subsyncEngineAlass.checked = hasSources;
ctx.dom.subsyncEngineFfsubsync.checked = !hasSources;
renderSubsyncSourceTracks();
updateSubsyncSourceVisibility();
setSubsyncStatus(
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');
}
async function runSubsyncManualFromModal(): Promise<void> {
if (ctx.state.subsyncSubmitting) return;
const engine = ctx.dom.subsyncEngineAlass.checked ? '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;
}
ctx.state.subsyncSubmitting = true;
ctx.dom.subsyncRunButton.disabled = true;
closeSubsyncModal();
try {
await window.electronAPI.runSubsyncManual({
engine,
sourceTrackId,
});
} 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,
};
}