Auto-install Lua, smooth all panels, airspace overlay + launcher region picker
FlyWithLua auto-install: bridge drops fms-sync/ui-sync/terrain-probe into X-Plane's FlyWithLua Scripts dir on startup and self-updates (content-compare). Graceful when no X-Plane / no FlyWithLua. /api/lua/install + status in health. Desktop app bundles the scripts and passes LUA_SRC_DIR to the sidecar. Smoothing: shared useEased/useEasedAngle hook (api/ease.js) with render-bail on settle. VFR steam gauges now interpolate to 60fps instead of stepping at the ~10Hz value stream. MFD ownship no longer vibrates — position/heading eased in a single rAF loop, follow-pan without animated-panTo pile-up (pauses on range zoom). Airspace overlay: server/airspace.js loads per-region GeoJSON, classifies (B/C/D/TMA/CTR/MOA/Restricted/Prohibited/Danger), bbox query, and downloads regions on demand — FAA (US, key-free) and OpenAIP (Europe, user key). New AIRSPACE softkey draws chart-coloured boundaries (B blue, C magenta, D dashed), non-interactive so map-clicks still drop waypoints. Launcher gains a "Lufträume" section to pick/download regions via the running bridge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -119,13 +119,21 @@ async fn start_server(
|
||||
.resolve("web", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| format!("resource path: {e}"))?;
|
||||
|
||||
// The FlyWithLua companion scripts ship as a bundled resource; tell the
|
||||
// bridge where they live so it can auto-install them into X-Plane.
|
||||
let lua_src = app
|
||||
.path()
|
||||
.resolve("plugins", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| format!("resource path: {e}"))?;
|
||||
|
||||
let mut cmd = app
|
||||
.shell()
|
||||
.sidecar("xpbridge")
|
||||
.map_err(|e| format!("sidecar: {e}"))?
|
||||
.env("BRIDGE_PORT", port.to_string())
|
||||
.env("BRIDGE_HOST", "0.0.0.0")
|
||||
.env("WEB_DIST", web_dist.to_string_lossy().to_string());
|
||||
.env("WEB_DIST", web_dist.to_string_lossy().to_string())
|
||||
.env("LUA_SRC_DIR", lua_src.to_string_lossy().to_string());
|
||||
|
||||
if !xplane_path.is_empty() {
|
||||
cmd = cmd.env("XPLANE_ROOT", xplane_path);
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"binaries/xpbridge"
|
||||
],
|
||||
"resources": {
|
||||
"resources/web": "web"
|
||||
"resources/web": "web",
|
||||
"resources/plugins": "plugins"
|
||||
},
|
||||
"createUpdaterArtifacts": true,
|
||||
"macOS": {
|
||||
|
||||
@@ -65,6 +65,16 @@
|
||||
<div class="diag-row"><span>Navdata</span><b id="dNav">—</b></div>
|
||||
<div class="diag-row"><span>Datarefs</span><b id="dRefs">—</b></div>
|
||||
</div>
|
||||
|
||||
<details class="asp-wrap">
|
||||
<summary>Lufträume auf der Karte</summary>
|
||||
<div class="asp-body">
|
||||
<p class="asp-note">Wähle Regionen für die Luftraum-Anzeige (Class B/C/D, Restricted, MOA …). USA ist frei; Europa braucht einen kostenlosen <a href="#" id="aspKeyLink">OpenAIP-API-Key</a>.</p>
|
||||
<input id="aspKey" type="password" placeholder="OpenAIP API-Key (für Europa)" spellcheck="false" />
|
||||
<div id="aspRegions" class="asp-list"></div>
|
||||
<div id="aspHint" class="hint"></div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<details class="log-wrap">
|
||||
|
||||
+52
-1
@@ -8,7 +8,7 @@ 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;
|
||||
let running = false, healthTimer = null, serverPort = 0;
|
||||
|
||||
function setStatus(kind, text) {
|
||||
statusEl.className = 'status ' + kind;
|
||||
@@ -73,12 +73,14 @@ startBtn.addEventListener('click', async () => {
|
||||
demo: demoEl.checked,
|
||||
});
|
||||
running = true;
|
||||
serverPort = info.port;
|
||||
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);
|
||||
loadRegions();
|
||||
} catch (e) {
|
||||
appendLog('Fehler: ' + e);
|
||||
setStatus('off', 'Fehler');
|
||||
@@ -96,6 +98,8 @@ async function stop() {
|
||||
|
||||
function resetUi() {
|
||||
running = false;
|
||||
serverPort = 0;
|
||||
const ar = $('aspRegions'); if (ar) ar.innerHTML = '';
|
||||
liveCard.classList.add('hidden');
|
||||
startBtn.textContent = 'Server starten';
|
||||
startBtn.classList.remove('stop');
|
||||
@@ -125,6 +129,53 @@ function pollHealth(port) {
|
||||
healthTimer = setInterval(check, 3000);
|
||||
}
|
||||
|
||||
/* ---------------- airspace regions ---------------- */
|
||||
const aspBase = () => `http://127.0.0.1:${serverPort}/api/airspace`;
|
||||
|
||||
async function loadRegions() {
|
||||
const wrap = $('aspRegions');
|
||||
if (!wrap || !serverPort) return;
|
||||
try {
|
||||
const r = await fetch(`${aspBase()}/regions`, { cache: 'no-store' });
|
||||
const { regions } = await r.json();
|
||||
wrap.innerHTML = '';
|
||||
for (const reg of regions) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'asp-row';
|
||||
const installed = reg.installed > 0;
|
||||
row.innerHTML = `
|
||||
<span class="asp-name">${reg.label}${reg.needsKey ? ' <em>· Key</em>' : ''}</span>
|
||||
<span class="asp-count">${installed ? reg.installed + ' Zonen' : '—'}</span>
|
||||
<button class="btn ghost sm" data-region="${reg.id}" data-key="${reg.needsKey ? 1 : 0}">${installed ? 'Aktualisieren' : 'Laden'}</button>`;
|
||||
wrap.appendChild(row);
|
||||
}
|
||||
wrap.querySelectorAll('button[data-region]').forEach((btn) =>
|
||||
btn.addEventListener('click', () => installRegion(btn)));
|
||||
} catch (e) { appendLog('airspace: ' + e); }
|
||||
}
|
||||
|
||||
async function installRegion(btn) {
|
||||
const region = btn.dataset.region;
|
||||
const needsKey = btn.dataset.key === '1';
|
||||
const apiKey = $('aspKey').value.trim();
|
||||
const hint = $('aspHint');
|
||||
if (needsKey && !apiKey) { hint.textContent = '⚠ OpenAIP-API-Key oben eingeben'; hint.className = 'hint bad'; return; }
|
||||
btn.disabled = true; const was = btn.textContent; btn.textContent = 'Lädt…';
|
||||
hint.textContent = `Lade ${region.toUpperCase()} … (Fortschritt im Server-Log)`; hint.className = 'hint';
|
||||
try {
|
||||
const r = await fetch(`${aspBase()}/install`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ region, apiKey: needsKey ? apiKey : undefined }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.ok) { hint.textContent = `✓ ${region.toUpperCase()}: ${d.features} Zonen geladen`; hint.className = 'hint ok'; }
|
||||
else { hint.textContent = '⚠ ' + (d.error || 'Fehler'); hint.className = 'hint bad'; }
|
||||
} catch (e) { hint.textContent = '⚠ ' + e; hint.className = 'hint bad'; }
|
||||
finally { btn.disabled = false; btn.textContent = was; loadRegions(); }
|
||||
}
|
||||
|
||||
$('aspKeyLink')?.addEventListener('click', (e) => { e.preventDefault(); openUrl('https://www.openaip.net/'); });
|
||||
|
||||
$('copy').addEventListener('click', async () => {
|
||||
try { await navigator.clipboard.writeText(urlEl.textContent); $('copy').textContent = '✓'; setTimeout(() => ($('copy').textContent = '⧉'), 1200); } catch {}
|
||||
});
|
||||
|
||||
@@ -87,3 +87,20 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r
|
||||
.link:hover { text-decoration: underline; }
|
||||
.link:disabled { color: var(--mut); cursor: default; text-decoration: none; }
|
||||
.update-badge { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--green); margin-left: 6px; box-shadow: 0 0 6px var(--green); vertical-align: middle; }
|
||||
|
||||
/* airspace region picker (in the live card) */
|
||||
.asp-wrap { margin-top: 12px; border-top: 1px solid var(--line-soft); padding-top: 10px; }
|
||||
.asp-wrap > summary { cursor: pointer; color: var(--txt2); font-size: 13px; font-weight: 600; list-style: none; }
|
||||
.asp-wrap > summary::-webkit-details-marker { display: none; }
|
||||
.asp-wrap > summary::before { content: '▸ '; color: var(--mut); }
|
||||
.asp-wrap[open] > summary::before { content: '▾ '; }
|
||||
.asp-body { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.asp-note { color: var(--mut); font-size: 12px; line-height: 1.4; margin: 0; }
|
||||
.asp-note a { color: var(--green); }
|
||||
#aspKey { width: 100%; }
|
||||
.asp-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.asp-row { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 10px;
|
||||
background: var(--bg2); border: 1px solid var(--line-soft); border-radius: 8px; padding: 7px 10px; }
|
||||
.asp-name { color: var(--txt2); font-size: 13px; }
|
||||
.asp-name em { color: var(--mut); font-style: normal; font-size: 11px; }
|
||||
.asp-count { color: var(--mut); font-size: 12px; min-width: 56px; text-align: right; }
|
||||
|
||||
Reference in New Issue
Block a user