import React, { useState, useEffect } from 'react'; import { useXplane } from './api/useXplane.js'; import PFD from './components/PFD.jsx'; import AutopilotPanel from './components/AutopilotPanel.jsx'; import MFD from './components/MFD.jsx'; import MapView from './components/MapView.jsx'; import CDU from './components/CDU.jsx'; import VFR from './components/VFR.jsx'; import Bezel from './components/Bezel.jsx'; import DirectTo from './components/DirectTo.jsx'; import Proc from './components/Proc.jsx'; import FplPage from './components/FplPage.jsx'; import AudioPanel from './components/AudioPanel.jsx'; // Compact line icons for the nav rail (stroke = currentColor). const ICONS = { pfd: 'M11 3a8 8 0 100 16 8 8 0 000-16zM3.5 11h15M7 8l1.5 1M15 8l-1.5 1', mfd: 'M3 6l5-2 6 2 5-2v12l-5 2-6-2-5 2zM8 4v12M14 6v12', map: 'M11 2c-3.3 0-6 2.6-6 5.9 0 4.4 6 11.1 6 11.1s6-6.7 6-11.1C17 4.6 14.3 2 11 2z', fms: 'M4 6h14M4 11h14M4 16h9', ap: 'M11 4a7 7 0 100 14 7 7 0 000-14zM11 4v3M11 15v3M4 11h3M15 11h3', vfr: 'M11 4a7 7 0 100 14 7 7 0 000-14zM11 11l4.5-3', audio: 'M11 4a6 6 0 00-6 6v5M17 15v-5a6 6 0 00-6-6M4 14h2.5v4.5H4zM15.5 14H18v4.5h-2.5z', }; function Icon({ name }) { return ( {name === 'map' && } ); } const TABS = [ { id: 'pfd', label: 'PFD' }, { id: 'mfd', label: 'MFD' }, { id: 'map', label: 'Map' }, { id: 'fms', label: 'FMS' }, { id: 'vfr', label: 'VFR' }, { id: 'ap', label: 'Autopilot' }, { id: 'audio', label: 'Audio' }, ]; export default function App() { const xp = useXplane(); const [tab, setTab] = useState(() => location.hash.replace('#', '') || 'pfd'); // Collapsible nav rail: narrow (icons) ↔ wide (icons + labels), remembered. const [navWide, setNavWide] = useState(() => localStorage.getItem('navWide') === '1'); const go = (id) => { setTab(id); history.replaceState(null, '', `#${id}`); }; const toggleNav = () => setNavWide((w) => { localStorage.setItem('navWide', w ? '0' : '1'); return !w; }); // Knob interaction: 'arrows' (visible ˄‹›˅, touch-friendly) or 'zones' (click // the knob face). Settable in the settings panel, remembered. const [knobMode, setKnobMode] = useState(() => localStorage.getItem('knobMode') || 'arrows'); const [settings, setSettings] = useState(false); const setKnob = (m) => { localStorage.setItem('knobMode', m); setKnobMode(m); }; // Synthetic-terrain (3D) vs. classic blue/brown attitude — toggled by the // PFD → SYN TERR softkey, exactly like the real XPLANE 1000. const [svt3d, setSvt3d] = useState(false); // The PFD INSET map (bottom-left) is off by default and toggled by its softkey. const [inset, setInset] = useState(false); // INSET map options (base layer + declutter), set from the INSET submenu. const [insetMode, setInsetMode] = useState({ base: 'topo', dcltr: 0 }); // Like the real G1000, only ONE window is open at a time. A single string // holds the open one (nrst / tmr / dme / alerts / fpl / dto / proc); toggling // the same softkey closes it, opening another replaces it. const [win, setWin] = useState(null); const toggleWin = (id) => setWin((w) => (w === id ? null : id)); const nrst = win === 'nrst', tmr = win === 'tmr', dme = win === 'dme', alerts = win === 'alerts'; const fpl = win === 'fpl', dto = win === 'dto', proc = win === 'proc', menu = win === 'menu'; // MFD map mode (base layer + overlays), switched via the Map-Opt softkeys. const [mapMode, setMapMode] = useState({ base: 'topo' }); // VNAV profile control (FPL VNAV keys + Direct-To descent): enabled gates the // profile/chevrons, fpa is the descent angle (°), offsetNm levels off that far // before the waypoint. See FplPage CURRENT VNV PROFILE + PFD chevrons. const [vnavCfg, setVnavCfg] = useState({ enabled: true, fpa: 3, offsetNm: 0 }); // Synthetic-vision display options (PFD submenu): PATHWAY draws the flight-plan // route on the 3D terrain; APTSIGNS shows runway/airport labels. const [svtOpts, setSvtOpts] = useState({ pathway: true, aptSigns: true, hrznHdg: false }); // Altimeter barometric units (false = inHg, true = hectopascal) — PFD ALT UNIT softkey. const [baroHpa, setBaroHpa] = useState(false); // Barometric minimums (set in TMR/REF) — shown on the PFD altimeter as BARO MIN. const [minimums, setMinimums] = useState({ on: false, ft: 500 }); // OBS (omni-bearing select) mode — suspends GPS sequencing, course set by CRS knob. const [obs, setObs] = useState(false); // MFD page group (MAP / FPL / NRST) — selected by the FMS knob, like the real G1000. const MFD_PAGES = ['map', 'fpl', 'nrst']; const [mfdPage, setMfdPage] = useState('map'); const cycleMfd = (dir = 1) => setMfdPage((p) => MFD_PAGES[(MFD_PAGES.indexOf(p) + dir + MFD_PAGES.length) % MFD_PAGES.length]); // G1000 UI-state sync (Sim → App): follow the in-sim G1000 when the FlyWithLua // companion publishes its state. No-ops until then, so local control still works. const uiInset = xp.values.uiInset, uiPage = xp.values.uiMfdPage; useEffect(() => { if (uiInset === 0 || uiInset === 1) setInset(!!uiInset); }, [uiInset]); useEffect(() => { if (typeof uiPage === 'number' && MFD_PAGES[uiPage]) setMfdPage(MFD_PAGES[uiPage]); }, [uiPage]); const connKind = xp.xpConnected ? 'ok' : xp.connected ? 'warn' : 'bad'; const connText = xp.xpConnected ? 'X-PLANE' : xp.connected ? 'NO SIM' : 'OFFLINE'; // G1000 side-window dialogs — rendered inside the bezel display so they sit in // the display's lower-right (like the real unit), not over the whole app. const dialogs = ( <> {dto && setWin(null)} vnav={vnavCfg} onVnav={setVnavCfg} />} {proc && setWin(null)} />} {menu && (() => { const wps = xp.flightPlan?.waypoints || []; const act = (fn) => { fn(); setWin(null); }; const item = (label, on, dis) => ; return (
setWin(null)}>
e.stopPropagation()}>
PAGE MENU
{item('INVERT FLIGHT PLAN', () => xp.fp.set({ name: 'ACTIVE', waypoints: wps.slice().reverse(), activeLeg: 1 }), wps.length < 2)} {item('STORE FLIGHT PLAN', () => xp.fp.export('WEBFPL'), wps.length < 2)} {item('DELETE FLIGHT PLAN', () => xp.fp.clear(), wps.length < 1)} {item('CANCEL', () => {})}
); })()} {fpl && (
setWin(null)}>
e.stopPropagation()}> setWin(null)} vnav={vnavCfg} onVnav={setVnavCfg} />
)} ); return (
{tab === 'pfd' && ( setSvt3d((v) => !v)} svtOpts={svtOpts} onSvtOpt={setSvtOpts} inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode} nrst={nrst} onToggleNrst={() => toggleWin('nrst')} onDirect={() => toggleWin('dto')} tmr={tmr} onToggleTmr={() => toggleWin('tmr')} dme={dme} onToggleDme={() => toggleWin('dme')} alerts={alerts} onToggleAlerts={() => toggleWin('alerts')} onProc={() => toggleWin('proc')} onFpl={() => toggleWin('fpl')} onMenu={() => toggleWin('menu')} onClr={() => setWin(null)} altHpa={baroHpa} onAltUnit={setBaroHpa} obs={obs} onObs={() => setObs((v) => !v)}> setWin(null)} tmr={tmr} onCloseTmr={() => setWin(null)} dme={dme} onCloseDme={() => setWin(null)} alerts={alerts} onCloseAlerts={() => setWin(null)} baroHpa={baroHpa} obs={obs} minimums={minimums} onMinimums={setMinimums} flightPlan={xp.flightPlan} fp={xp.fp} vnav={vnavCfg} svtOpts={svtOpts} /> {dialogs} )} {tab === 'mfd' && ( toggleWin('dto')} onProc={() => toggleWin('proc')} onFms={cycleMfd} onFpl={() => setMfdPage('fpl')} onMenu={() => toggleWin('menu')} onClr={() => setWin(null)}> {dialogs} )} {tab === 'map' && } {tab === 'fms' && } {tab === 'vfr' && } {tab === 'ap' && } {tab === 'audio' && }
{settings && (
setSettings(false)}>
e.stopPropagation()} style={{ minWidth: 360 }}>
EINSTELLUNGEN
Knopf-Bedienung
Pfeiltasten sind touch-freundlich. Klickzonen: oben/unten = grob, links/rechts = fein, Mitte = PUSH.
)}
); }