/* * app.js * NovaPhonic Oberfläche. Ruft ausschließlich Tauri Rust Commands auf, die im * Hintergrund auto-editor, DeepFilterNet und FFmpeg als eigenständige Programme * (Sidecars) ausführen. Die Videodatei selbst wird nie in dieses Skript geladen, * es werden nur Dateipfade zwischen Oberfläche und Rust ausgetauscht. */ (function () { "use strict"; function $(id) { return document.getElementById(id); } const root = document.documentElement; function isTauri() { return typeof window.__TAURI__ !== 'undefined' && window.__TAURI__.core; } // ---------- Theme ---------- const THEME_KEY = 'novaphonic-theme'; const themeToggle = $('themeToggle'); const themeLabel = $('themeLabel'); function applyTheme(theme) { root.setAttribute('data-theme', theme); themeLabel.textContent = theme === 'dark' ? 'Dunkel' : 'Hell'; try { localStorage.setItem(THEME_KEY, theme); } catch (e) { /* ignore */ } } (function initTheme() { let saved = null; try { saved = localStorage.getItem(THEME_KEY); } catch (e) { /* ignore */ } applyTheme(saved === 'light' ? 'light' : 'dark'); })(); themeToggle.addEventListener('click', () => { const current = root.getAttribute('data-theme'); applyTheme(current === 'dark' ? 'light' : 'dark'); }); // ---------- Werkzeug Check ---------- async function checkTools() { if (!isTauri()) return; try { const missing = await window.__TAURI__.core.invoke('check_tools'); if (Array.isArray(missing) && missing.length > 0) { $('toolWarning').style.display = 'block'; $('toolWarningText').textContent = 'Nicht gefunden: ' + missing.join(', ') + '. Bitte in der BAUANLEITUNG.md nachsehen, wie diese Werkzeuge bereitgestellt werden.'; } } catch (e) { // check_tools selbst nicht verfügbar, z.B. während der Entwicklung im Browser } } // ---------- Dateiauswahl ---------- let selectedPath = null; function formatBytes(bytes) { if (!bytes || bytes <= 0) return ''; const units = ['B', 'KB', 'MB', 'GB']; let i = 0, val = bytes; while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; } return val.toFixed(val >= 10 || i === 0 ? 0 : 1) + ' ' + units[i]; } function setSelectedFile(path, sizeBytes) { selectedPath = path; const name = path.split(/[\\/]/).pop(); $('sfName').textContent = name; $('sfSize').textContent = sizeBytes ? formatBytes(sizeBytes) : ''; $('selectedFileBox').style.display = 'block'; $('startBtn').disabled = false; } function clearSelectedFile() { selectedPath = null; $('selectedFileBox').style.display = 'none'; $('startBtn').disabled = true; } async function pickFile() { if (!isTauri()) { alert('Dateiauswahl ist nur innerhalb der NovaPhonic Anwendung verfügbar, nicht im Browser.'); return; } try { const result = await window.__TAURI__.core.invoke('pick_video_file'); if (result && result.path) { setSelectedFile(result.path, result.sizeBytes); } } catch (e) { console.error(e); } } const dropzone = $('dropzone'); dropzone.addEventListener('click', pickFile); dropzone.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pickFile(); } }); $('sfRemove').addEventListener('click', (e) => { e.stopPropagation(); clearSelectedFile(); }); // Echtes Drag & Drop einer Datei aus dem Windows Explorer in das Fenster. // Liefert direkt einen Dateipfad, es wird nichts im Browser eingelesen. (function initDragDrop() { if (!isTauri() || !window.__TAURI__.webview) return; try { window.__TAURI__.webview.getCurrentWebview().onDragDropEvent((event) => { if (event.payload.type === 'over') { dropzone.classList.add('drag'); } else if (event.payload.type === 'drop') { dropzone.classList.remove('drag'); const paths = event.payload.paths || []; if (paths.length > 0) { const p = paths[0]; const ext = p.split('.').pop().toLowerCase(); if (['mp4', 'mov', 'mkv', 'avi', 'webm'].includes(ext)) { setSelectedFile(p, null); } else { alert('Bitte eine Videodatei ablegen (MP4, MOV, MKV, AVI oder WEBM).'); } } } else { dropzone.classList.remove('drag'); } }); } catch (e) { // Drag & Drop API in dieser Tauri Version evtl. anders benannt, kein Problem, // Klicken zum Auswählen funktioniert trotzdem. console.warn('Drag & Drop nicht verfügbar:', e); } })(); // ---------- Einstellungen ---------- const marginSlider = $('marginSlider'); const marginTag = $('marginTag'); function updateMarginTag() { const val = (parseInt(marginSlider.value, 10) / 10).toFixed(1); marginTag.textContent = val.replace('.', ',') + ' s'; } marginSlider.addEventListener('input', updateMarginTag); updateMarginTag(); const loudnormToggle = $('loudnormToggle'); const loudnormOptions = $('loudnormOptions'); function syncLoudnormOptions() { loudnormOptions.style.display = loudnormToggle.checked ? 'block' : 'none'; } loudnormToggle.addEventListener('change', syncLoudnormOptions); syncLoudnormOptions(); // ---------- Automatisches Speichern ---------- // Standard bleibt der "Speichern unter" Dialog nach jeder Verarbeitung (wie // bisher). Wer die Checkbox aktiviert, bekommt stattdessen jedes Ergebnis // automatisch in einem einmal gewählten Ordner abgelegt, ohne Dialog. Die // Einstellung wird über zwei kleine Rust Commands (load_settings/save_settings) // in einer settings Datei im App Konfigurationsordner gespeichert, damit sie // App-Neustarts übersteht. const autoSaveToggle = $('autoSaveToggle'); const autoSaveFolderRow = $('autoSaveFolderRow'); const autoSaveFolderPath = $('autoSaveFolderPath'); const chooseAutoSaveFolderBtn = $('chooseAutoSaveFolderBtn'); let appSettings = { autoSaveEnabled: false, autoSaveFolder: null }; function syncAutoSaveUi() { autoSaveFolderRow.style.display = autoSaveToggle.checked ? 'block' : 'none'; autoSaveFolderPath.textContent = appSettings.autoSaveFolder || 'Kein Ordner ausgewählt'; } async function persistSettings() { appSettings.autoSaveEnabled = autoSaveToggle.checked; if (!isTauri()) return; try { await window.__TAURI__.core.invoke('save_settings', { settings: appSettings }); } catch (e) { console.error(e); } } autoSaveToggle.addEventListener('change', () => { syncAutoSaveUi(); persistSettings(); }); chooseAutoSaveFolderBtn.addEventListener('click', async () => { if (!isTauri()) return; try { const folder = await window.__TAURI__.core.invoke('pick_save_folder'); if (folder) { appSettings.autoSaveFolder = folder; syncAutoSaveUi(); persistSettings(); } } catch (e) { console.error(e); } }); async function loadSettings() { if (!isTauri()) { syncAutoSaveUi(); return; } try { const loaded = await window.__TAURI__.core.invoke('load_settings'); if (loaded) appSettings = loaded; autoSaveToggle.checked = !!appSettings.autoSaveEnabled; } catch (e) { console.error(e); } syncAutoSaveUi(); } // ---------- Pipeline Ablauf ---------- const STEP_IDS = ['denoise', 'cut', 'loudnorm']; function resetSteps() { for (const id of STEP_IDS) { const el = $('step-' + id); el.classList.remove('running', 'done', 'error'); $('step-' + id + '-detail').textContent = ''; } $('logBox').textContent = ''; } function appendLog(line) { const box = $('logBox'); box.textContent += (box.textContent ? '\n' : '') + line; box.scrollTop = box.scrollHeight; } function setStepStatus(step, status, detail) { const el = $('step-' + step); if (!el) return; el.classList.remove('running', 'done', 'error'); if (status) el.classList.add(status); if (detail !== undefined) $('step-' + step + '-detail').textContent = detail || ''; } let unlistenProgress = null; async function initProgressListener() { if (!isTauri()) return; unlistenProgress = await window.__TAURI__.event.listen('pipeline-progress', (event) => { const p = event.payload || {}; if (p.step) setStepStatus(p.step, p.status, p.detail); if (p.log) appendLog(p.log); }); } const startBtn = $('startBtn'); startBtn.addEventListener('click', async () => { if (!selectedPath || !isTauri()) return; startBtn.disabled = true; dropzone.style.pointerEvents = 'none'; resetSteps(); $('resultArea').innerHTML = '