diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ef8fd94..ff78b3f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -97,6 +97,19 @@ fn server_running(state: State) -> bool { state.child.lock().unwrap().is_some() } +// Is FlyWithLua NG+ installed in this X-Plane? It's the prerequisite for the +// FMS/terrain sync — the bridge auto-installs OUR scripts into its Scripts +// folder, but only if the plugin itself is present. Checked by the setup wizard. +#[tauri::command] +fn flywithlua_present(path: String) -> bool { + !path.is_empty() + && PathBuf::from(&path) + .join("Resources") + .join("plugins") + .join("FlyWithLua") + .is_dir() +} + #[tauri::command] async fn start_server( app: tauri::AppHandle, @@ -203,6 +216,7 @@ pub fn run() { suggest_port, default_xplane_path, valid_xplane_path, + flywithlua_present, server_running, start_server, stop_server diff --git a/desktop/ui/index.html b/desktop/ui/index.html index 39acf7c..1a5aa12 100644 --- a/desktop/ui/index.html +++ b/desktop/ui/index.html @@ -10,6 +10,7 @@
G1000·web
+
Gestoppt
@@ -64,6 +65,7 @@
Verbundene Geräte
Navdata
Datarefs
+
FlyWithLua-Sync
@@ -88,6 +90,66 @@ + + + + diff --git a/desktop/ui/main.js b/desktop/ui/main.js index 3b1ee62..e4094a8 100644 --- a/desktop/ui/main.js +++ b/desktop/ui/main.js @@ -107,9 +107,19 @@ function resetUi() { if (healthTimer) { clearInterval(healthTimer); healthTimer = null; } } +// Human-readable FlyWithLua-install status from /api/health.lua. +function luaText(lua) { + if (!lua) return { t: '—', cls: '' }; + if (lua.reason === 'no-xplane') return { t: 'kein X-Plane', cls: 'warn' }; + if (lua.reason === 'no-flywithlua') return { t: 'FlyWithLua fehlt', cls: 'bad' }; + if (lua.reason === 'no-source') return { t: 'Skripte fehlen', cls: 'bad' }; + const n = (lua.installed?.length || 0) + (lua.updated?.length || 0) + (lua.unchanged?.length || 0); + return { t: `${n} Skripte aktiv`, cls: 'ok' }; +} + function pollHealth(port) { if (healthTimer) clearInterval(healthTimer); - const dXp = $('dXp'), dClients = $('dClients'), dNav = $('dNav'), dRefs = $('dRefs'); + const dXp = $('dXp'), dClients = $('dClients'), dNav = $('dNav'), dRefs = $('dRefs'), dLua = $('dLua'); const check = async () => { try { const r = await fetch(`http://127.0.0.1:${port}/api/health`, { cache: 'no-store' }); @@ -123,6 +133,7 @@ function pollHealth(port) { const n = d.nav || {}; dNav.textContent = n.loaded ? `${n.airports ?? 0} APT · ${n.navaids ?? 0} Navaids` : 'lädt…'; dRefs.textContent = d.datarefs ?? 0; + if (dLua) { const l = luaText(d.lua); dLua.textContent = l.t; dLua.className = l.cls; } } catch { setStatus('warn', 'Server läuft'); } }; check(); @@ -242,4 +253,66 @@ $('updateBtn').addEventListener('click', () => checkUpdate(false)); $('ubInstall').addEventListener('click', installUpdate); $('ubDismiss').addEventListener('click', () => $('updateBanner').classList.add('hidden')); +/* ---------------- first-run setup wizard ---------------- */ +const FWL_URL = 'https://github.com/X-Friese/FlyWithLua/releases'; +let wizStep = 1; +const wiz = $('wizard'); + +function showStep(n) { + wizStep = Math.max(1, Math.min(4, n)); + document.querySelectorAll('.wiz-step').forEach((s) => s.classList.toggle('hidden', +s.dataset.step !== wizStep)); + document.querySelectorAll('.wiz-steps span').forEach((s) => s.classList.toggle('on', +s.dataset.s <= wizStep)); + $('wizBack').disabled = wizStep === 1; + $('wizNext').textContent = wizStep === 4 ? (demoEl.checked || $('wizDemo').checked ? 'Demo starten' : 'Lua installieren & starten') : 'Weiter'; + if (wizStep === 2) checkFwl(); +} +function openWizard() { + wiz.classList.remove('hidden'); + $('wizPath').value = xpPath.value || ''; + $('wizDemo').checked = demoEl.checked; + validateWizPath(); + showStep(1); +} +function closeWizard() { wiz.classList.add('hidden'); localStorage.setItem('setupDone', '1'); } + +async function validateWizPath() { + const p = $('wizPath').value.trim(); + const h = $('wizPathHint'); + if (!p) { h.textContent = 'Leer = Demo-Modus möglich.'; h.className = 'hint'; return; } + const ok = await invoke('valid_xplane_path', { path: p }); + h.textContent = ok ? '✓ X-Plane erkannt' : '⚠ kein „Resources/default data" — Pfad prüfen'; + h.className = 'hint ' + (ok ? 'ok' : 'bad'); +} +async function checkFwl() { + const el = $('wizFwl'); const p = $('wizPath').value.trim(); + if (!p) { el.textContent = '— (kein X-Plane gewählt; im Demo-Modus nicht nötig)'; el.className = 'wiz-status'; return; } + const present = await invoke('flywithlua_present', { path: p }); + el.textContent = present ? '✓ FlyWithLua ist installiert' : '✗ FlyWithLua nicht gefunden'; + el.className = 'wiz-status ' + (present ? 'ok' : 'bad'); +} + +$('setupBtn').addEventListener('click', openWizard); +$('wizClose').addEventListener('click', closeWizard); +$('wizBrowse').addEventListener('click', async () => { + try { const dir = await T.dialog.open({ directory: true, multiple: false, title: 'X-Plane 12 Ordner wählen' }); + if (dir) { $('wizPath').value = dir; validateWizPath(); } } catch (e) { appendLog('dialog: ' + e); } +}); +$('wizPath').addEventListener('input', validateWizPath); +$('wizFwlCheck').addEventListener('click', checkFwl); +$('wizFwlLink').addEventListener('click', (e) => { e.preventDefault(); openUrl(FWL_URL); }); +$('wizDemo').addEventListener('change', () => { demoEl.checked = $('wizDemo').checked; showStep(wizStep); }); +$('wizBack').addEventListener('click', () => showStep(wizStep - 1)); +$('wizNext').addEventListener('click', async () => { + if (wizStep < 4) { showStep(wizStep + 1); return; } + // final step: carry the chosen path/demo to the main controls and start + xpPath.value = $('wizPath').value.trim(); + demoEl.checked = $('wizDemo').checked; + await validatePath(); + $('wizResult').textContent = 'Starte Server …'; $('wizResult').className = 'wiz-status'; + closeWizard(); + if (!running) startBtn.click(); +}); + init(); +// Offer the wizard automatically on the very first launch. +if (!localStorage.getItem('setupDone')) setTimeout(openWizard, 400); diff --git a/desktop/ui/styles.css b/desktop/ui/styles.css index 0389642..05a114c 100644 --- a/desktop/ui/styles.css +++ b/desktop/ui/styles.css @@ -76,7 +76,7 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r .diag { margin-top: 10px; border-top: 1px solid var(--line-soft); padding-top: 8px; display: flex; flex-direction: column; gap: 5px; } .diag-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--mut); } .diag-row b { color: var(--txt2); font-weight: 600; } -.diag-row b.ok { color: var(--green); } .diag-row b.warn { color: var(--amber); } +.diag-row b.ok { color: var(--green); } .diag-row b.warn { color: var(--amber); } .diag-row b.bad { color: #ff6b6b; } .log-wrap { background: var(--bg2); border: 1px solid var(--line-soft); border-radius: 12px; padding: 6px 12px; } .log-wrap summary { color: var(--mut); font-size: 12px; cursor: pointer; padding: 4px 0; } @@ -104,3 +104,30 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r .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; } + +/* header setup button sits between brand and status */ +.hd { display: flex; align-items: center; gap: 10px; } +.hd #setupBtn { margin-left: auto; } +.hd #status { margin-left: 8px; } + +/* first-run setup wizard */ +.wiz { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 50; } +.wiz.hidden { display: none; } +.wiz-box { width: min(440px, 92vw); background: var(--bg2); border: 1px solid var(--line); border-radius: 14px; padding: 18px; box-shadow: 0 20px 60px rgba(0,0,0,.5); } +.wiz-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } +.wiz-head b { font-size: 15px; } +.wiz-steps { display: flex; gap: 6px; margin-left: auto; } +.wiz-steps span { width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; background: var(--bg3); color: var(--mut); } +.wiz-steps span.on { background: var(--green); color: #042b10; font-weight: 700; } +.wiz-x { background: none; border: none; color: var(--mut); font-size: 16px; cursor: pointer; } +.wiz-step h3 { margin: 0 0 8px; font-size: 14px; color: var(--txt); } +.wiz-step p { margin: 0 0 10px; color: var(--txt2); font-size: 13px; line-height: 1.45; } +.wiz-step .wiz-sub { color: var(--mut); font-size: 12px; } +.wiz-step a { color: var(--green); } +.wiz-step code { background: var(--bg3); padding: 1px 5px; border-radius: 4px; font-size: 12px; } +.wiz-ol { margin: 0 0 10px; padding-left: 20px; color: var(--txt2); font-size: 13px; line-height: 1.6; } +.wiz-status { background: var(--bg3); border: 1px solid var(--line-soft); border-radius: 8px; padding: 8px 10px; font-size: 13px; margin: 8px 0; } +.wiz-status.ok { color: var(--green); border-color: #1d4a2c; } +.wiz-status.bad { color: #ff6b6b; border-color: #5a2424; } +.wiz-foot { display: flex; justify-content: space-between; gap: 10px; margin-top: 16px; } +.wiz-foot .btn { flex: 1; }