Files
xplane-cockpit/desktop/ui/main.js
T
karim ebc33a78b7 Initial commit: X-Plane G1000 web cockpit + bridge + Tauri desktop app
- server/: Node bridge (datarefs/commands, navdata, CIFP procedures, flight plan)
- web/: React cockpit (PFD/MFD/Map, VFR six-pack, AFCS, FMS CDU), PWA, collapsible sidebar
- desktop/: Tauri 2 launcher (Bun sidecar, system tray, updater) + Linux build via Docker
- scripts/: prep-desktop, build-linux, Gitea release + latest.json

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:07:03 +02:00

195 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 102465535'; 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 — <a href="#" id="usePort">${alt} verwenden</a>`;
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();