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'; import KAP140 from './components/KAP140.jsx'; import CitPFD from './components/citation/CitPFD.jsx'; import CitMFD from './components/citation/CitMFD.jsx'; import CitDuo from './components/citation/CitDuo.jsx'; import CitEICAS from './components/citation/CitEICAS.jsx'; import CitAP from './components/citation/CitAP.jsx'; import CitRMU from './components/citation/CitRMU.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', eicas: 'M5 4v14M9 4v14M5 11h4M13 7h5M13 11h5M13 15h5', rmu: 'M4 5h14v12H4zM7 8h8M7 11h8M7 14h4', duo: 'M3 5h7v12H3zM12 5h7v12h-7z', }; function Icon({ name }) { return ( {name === 'map' && } ); } // Three selectable cockpit profiles. Each maps the app to a different aircraft's // avionics suite, sharing the same bridge/datarefs underneath. const PROFILES = { g1000: { label: 'Garmin G1000', short: 'G1000', 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' }, ], }, citation: { label: 'Cessna Citation X', short: 'CITATION X', tabs: [ { id: 'duo', label: 'PFD+MFD' }, { id: 'pfd', label: 'PFD' }, { id: 'mfd', label: 'MFD' }, { id: 'eicas', label: 'EICAS' }, { id: 'fms', label: 'CDU/FMS' }, { id: 'ap', label: 'Autopilot' }, { id: 'rmu', label: 'Radios' }, { id: 'map', label: 'Map' }, ], }, ga: { label: 'GA Steam (Bendix/King)', short: 'GA PANEL', tabs: [ { id: 'vfr', label: 'Panel' }, { id: 'ap', label: 'KAP 140' }, { id: 'map', label: 'Map' }, { id: 'audio', label: 'Audio' }, ], }, }; export default function App() { const xp = useXplane(); // Active cockpit profile — persisted; switches the whole avionics suite. const [profile, setProfile] = useState(() => localStorage.getItem('cockpitProfile') || 'g1000'); // Citation Nav Source Selector bearing-pointer sources (p24): pointer 1 = cyan // circle, pointer 2 = white diamond. Each OFF/VORn/ADFn/FMSn. Shared PFD↔RMU. const [navSrc, setNavSrc] = useState(() => { try { return JSON.parse(localStorage.getItem('citNavSrc')) || { brg1: 'VOR1', brg2: 'VOR2' }; } catch { return { brg1: 'VOR1', brg2: 'VOR2' }; } }); useEffect(() => { localStorage.setItem('citNavSrc', JSON.stringify(navSrc)); }, [navSrc]); const [profMenu, setProfMenu] = useState(false); const PROF = PROFILES[profile] || PROFILES.g1000; const TABS = PROF.tabs; const pickProfile = (p) => { localStorage.setItem('cockpitProfile', p); setProfile(p); setProfMenu(false); const first = PROFILES[p].tabs[0].id; setTab(first); history.replaceState(null, '', `#${first}`); }; 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]); // Keep the active tab valid for the current profile (e.g. after a hash deep-link // into a tab the profile doesn't have). useEffect(() => { if (!TABS.some((t) => t.id === tab)) { const f = TABS[0].id; setTab(f); history.replaceState(null, '', `#${f}`); } }, [profile]); // eslint-disable-line react-hooks/exhaustive-deps 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 (
{/* ---- Garmin G1000 suite ---- */} {profile === 'g1000' && 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} )} {profile === 'g1000' && tab === 'mfd' && ( toggleWin('dto')} onProc={() => toggleWin('proc')} onFms={cycleMfd} onFpl={() => setMfdPage('fpl')} onMenu={() => toggleWin('menu')} onClr={() => setWin(null)}> {dialogs} )} {/* ---- Cessna Citation X suite (Honeywell Primus 2000) ---- */} {profile === 'citation' && tab === 'duo' && } {profile === 'citation' && tab === 'pfd' && } {profile === 'citation' && tab === 'mfd' && } {profile === 'citation' && tab === 'eicas' && } {profile === 'citation' && tab === 'ap' && } {profile === 'citation' && tab === 'rmu' && } {/* ---- shared tabs ---- */} {tab === 'map' && } {tab === 'fms' && } {tab === 'vfr' && } {tab === 'audio' && } {tab === 'ap' && profile === 'g1000' && } {tab === 'ap' && profile === 'ga' && }
{settings && (
setSettings(false)}>
e.stopPropagation()} style={{ minWidth: 360 }}>
EINSTELLUNGEN
Knopf-Bedienung
Pfeiltasten sind touch-freundlich. Klickzonen: oben/unten = grob, links/rechts = fein, Mitte = PUSH.
)}
); }