// Control-panel logic. Uses the global Tauri API (withGlobalTauri). const T = window.__TAURI__ || {}; const invoke = T.core.invoke; const listen = T.event.listen; const $ = (id) => document.getElementById(id); const xpPath = $('xpPath'), portEl = $('port'), demoEl = $('demo'); const startBtn = $('startBtn'), liveCard = $('liveCard'), urlEl = $('url'); const statusEl = $('status'), statusText = $('statusText'), logEl = $('log'); let running = false, healthTimer = null; function setStatus(kind, text) { statusEl.className = 'status ' + kind; statusText.textContent = text; } async function validatePath() { const p = xpPath.value.trim(); const hint = $('xpHint'); if (!p) { hint.textContent = ''; hint.className = 'hint'; return false; } const ok = await invoke('valid_xplane_path', { path: p }); hint.textContent = ok ? '✓ X-Plane erkannt' : '⚠ kein „Resources/default data“ — Demo-Modus nutzen oder Pfad prüfen'; hint.className = 'hint ' + (ok ? 'ok' : 'bad'); return ok; } // Is the chosen port free? If not, offer the next free one. async function validatePort() { const hint = $('portHint'); const port = parseInt(portEl.value, 10) || 0; if (port < 1024 || port > 65535) { hint.textContent = '⚠ Port 1024–65535'; hint.className = 'hint bad'; return false; } const free = await invoke('port_free', { port }); if (free) { hint.textContent = '✓ Port frei'; hint.className = 'hint ok'; return true; } const alt = await invoke('suggest_port', { start: port + 1 }); hint.innerHTML = `⚠ Port ${port} belegt — ${alt} verwenden`; hint.className = 'hint bad'; const a = $('usePort'); if (a) a.onclick = (e) => { e.preventDefault(); portEl.value = alt; validatePort(); }; return false; } async function init() { try { $('ver').textContent = 'v' + (await T.app.getVersion()); } catch {} try { const def = await invoke('default_xplane_path'); if (def) { xpPath.value = def; validatePath(); } } catch {} validatePort(); checkUpdate(true); // silent on launch } xpPath.addEventListener('change', validatePath); xpPath.addEventListener('blur', validatePath); portEl.addEventListener('change', validatePort); portEl.addEventListener('blur', validatePort); $('browse').addEventListener('click', async () => { try { const dir = await T.dialog.open({ directory: true, multiple: false, title: 'X-Plane 12 Ordner wählen' }); if (dir) { xpPath.value = dir; validatePath(); } } catch (e) { appendLog('dialog: ' + e); } }); startBtn.addEventListener('click', async () => { if (running) return stop(); if (!(await validatePort())) return; // refuse a busy port up front startBtn.disabled = true; try { const info = await invoke('start_server', { xplanePath: xpPath.value.trim(), port: parseInt(portEl.value, 10) || 8080, demo: demoEl.checked, }); running = true; urlEl.textContent = info.url; liveCard.classList.remove('hidden'); startBtn.textContent = 'Server stoppen'; startBtn.classList.add('stop'); setStatus('warn', 'Server läuft · warte auf Sim'); pollHealth(info.port); } catch (e) { appendLog('Fehler: ' + e); setStatus('off', 'Fehler'); } finally { startBtn.disabled = false; } }); async function stop() { startBtn.disabled = true; try { await invoke('stop_server'); } catch (e) { appendLog('stop: ' + e); } resetUi(); startBtn.disabled = false; } function resetUi() { running = false; liveCard.classList.add('hidden'); startBtn.textContent = 'Server starten'; startBtn.classList.remove('stop'); setStatus('off', 'Gestoppt'); if (healthTimer) { clearInterval(healthTimer); healthTimer = null; } } function pollHealth(port) { if (healthTimer) clearInterval(healthTimer); const dXp = $('dXp'), dClients = $('dClients'), dNav = $('dNav'), dRefs = $('dRefs'); const check = async () => { try { const r = await fetch(`http://127.0.0.1:${port}/api/health`, { cache: 'no-store' }); const d = await r.json(); const sim = d.xpConnected; if (sim) setStatus('run', demoEl.checked ? 'Demo läuft' : 'X-Plane verbunden'); else setStatus('warn', 'Server läuft · kein Sim'); dXp.textContent = sim ? (demoEl.checked ? 'Demo' : 'verbunden') : 'kein Sim'; dXp.className = sim ? 'ok' : 'warn'; dClients.textContent = d.clients ?? 0; const n = d.nav || {}; dNav.textContent = n.loaded ? `${n.airports ?? 0} APT · ${n.navaids ?? 0} Navaids` : 'lädt…'; dRefs.textContent = d.datarefs ?? 0; } catch { setStatus('warn', 'Server läuft'); } }; check(); healthTimer = setInterval(check, 3000); } $('copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(urlEl.textContent); $('copy').textContent = '✓'; setTimeout(() => ($('copy').textContent = '⧉'), 1200); } catch {} }); const openUrl = (u) => { try { T.opener.openUrl(u); } catch (e) { appendLog('open: ' + e); } }; $('openBtn').addEventListener('click', () => openUrl(urlEl.textContent)); document.querySelectorAll('.quick .btn').forEach((b) => b.addEventListener('click', () => openUrl(urlEl.textContent + '/#' + b.dataset.page))); function appendLog(line) { logEl.textContent += line; if (logEl.textContent.length > 8000) logEl.textContent = logEl.textContent.slice(-6000); logEl.scrollTop = logEl.scrollHeight; } listen('server-log', (e) => appendLog(e.payload)); listen('server-exited', () => { appendLog('\n[Server beendet]\n'); resetUi(); }); // Tray actions routed to the panel (which holds the current URL + start logic). listen('tray-open', () => { if (urlEl.textContent && urlEl.textContent !== '—') openUrl(urlEl.textContent); }); listen('tray-toggle', () => startBtn.click()); /* ---------------- updates ---------------- */ let pendingUpdate = null; async function checkUpdate(silent) { const btn = $('updateBtn'); if (!silent) { btn.disabled = true; btn.textContent = 'Suche…'; } try { const update = await T.updater.check(); if (update) { pendingUpdate = update; $('ubTitle').textContent = `Update ${update.version} verfügbar`; $('ubNotes').textContent = update.body || ''; $('updateBanner').classList.remove('hidden'); if (!document.querySelector('.update-badge')) { const dot = document.createElement('span'); dot.className = 'update-badge'; btn.after(dot); } if (!silent) { btn.textContent = 'Nach Updates suchen'; btn.disabled = false; } } else if (!silent) { btn.textContent = 'Aktuell ✓'; setTimeout(() => { btn.textContent = 'Nach Updates suchen'; btn.disabled = false; }, 2500); } } catch (e) { if (!silent) { appendLog('update: ' + e); btn.textContent = 'Update fehlgeschlagen'; setTimeout(() => { btn.textContent = 'Nach Updates suchen'; btn.disabled = false; }, 2500); } } } async function installUpdate() { if (!pendingUpdate) return; $('ubInstall').disabled = true; $('ubInstall').textContent = 'Lädt…'; try { await pendingUpdate.downloadAndInstall(); await T.process.relaunch(); } catch (e) { appendLog('install: ' + e); $('ubInstall').disabled = false; $('ubInstall').textContent = 'Installieren'; } } $('updateBtn').addEventListener('click', () => checkUpdate(false)); $('ubInstall').addEventListener('click', installUpdate); $('ubDismiss').addEventListener('click', () => $('updateBanner').classList.add('hidden')); init();