commit ebc33a78b78e131588b83aea35fdf560b7652f11 Author: karim Date: Mon Jun 1 15:07:03 2026 +0200 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a06dda4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# dependencies +node_modules/ +web/node_modules/ +desktop/node_modules/ + +# build output +web/dist/ +desktop/src-tauri/target/ +target-linux/ +desktop/src-tauri/gen/ +desktop/latest.json +fms-out/ + +# generated bundle inputs (recreated by scripts/prep-desktop.sh) +desktop/src-tauri/binaries/ +desktop/src-tauri/resources/web/ + +# SECRETS — never commit the updater signing private key / password +desktop/.tauri-signing.key +desktop/.tauri-signing.pw + +# local agent + editor + misc +.claude/ +screenshots/ +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..1189296 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# X-Plane Glass Cockpit (Web) + +Bring X-Plane 12 instruments — a G1000-style **PFD**, an **MFD**, and an +**autopilot** panel — to any iPad, tablet or laptop on your network. Pure web, +no app install on the tablets. Just open a browser. + +``` + X-Plane 12 Node bridge (this repo) your devices + ┌──────────┐ ws ┌────────────────────┐ ws/http ┌──────────┐ + │ Web API │◀──────▶│ resolves dataref │◀───────────▶│ iPad │ + │ :8086 │ REST │ IDs, streams values │ │ laptop │ + │ (local) │ │ serves the React UI │ │ phone │ + └──────────┘ └────────────────────┘ └──────────┘ + binds 0.0.0.0:8080 +``` + +## Why a bridge? +X-Plane's built-in web server (v12.1.1+) only listens on `localhost`, dataref +IDs change every session, and CORS blocks browsers. The bridge runs **on the +same PC as X-Plane**, talks to it locally, and re-broadcasts everything to your +LAN — to as many tablets as you like at once. + +## Requirements +- **X-Plane 12.1.1 or newer** (the web API ships built-in; nothing to enable). +- **Node.js 18+** on the PC running X-Plane (`node --version`). + +## Setup (run these on the X-Plane PC) +```bash +cd X-PLANE-MOD +npm install # also installs the web app's deps +npm run build # builds the React UI into web/dist +npm start # starts the bridge on http://0.0.0.0:8080 +``` + +## Open it +1. Make sure X-Plane is running and you're in a flight. +2. On a tablet/laptop on the **same Wi-Fi**, open: + `http://:8080` + - Find the IP: macOS `ipconfig getifaddr en0` · Windows `ipconfig` (IPv4). +3. The status pill top-right shows **X-PLANE** (green) when data is flowing, + **NO SIM** if the bridge is up but X-Plane isn't reachable, **OFFLINE** if + the tablet can't reach the bridge. +4. Tip: on iPad, "Add to Home Screen" → it opens full-screen like a real app. + +## Development (hot reload) +```bash +npm run dev:bridge # terminal 1 — the bridge on :8080 +npm run dev:web # terminal 2 — Vite dev server with HMR (proxies to bridge) +``` +Open the URL Vite prints. Edits to `web/src/**` reload instantly. + +## Configuration +All env vars are optional (defaults shown): +| Var | Default | Meaning | +|-----|---------|---------| +| `BRIDGE_PORT` | `8080` | Port the UI/LAN server listens on | +| `XPLANE_HOST` | `localhost` | Where X-Plane's web API is | +| `XPLANE_PORT` | `8086` | X-Plane web API port | + +## Adding instruments / datarefs +Everything is driven by [`server/config.js`](server/config.js): +- **`DATAREFS`** — values streamed to the UI (alias → `sim/...` name). +- **`WRITABLE_DATAREFS`** — values the UI may set (knobs/bugs). +- **`COMMANDS`** — buttons the UI may press (mode toggles). + +Add a `sim/...` name there, then read `values.` in any component. For +**G1000-specific** gauges, add that aircraft's `laminar/...` or +`sim/cockpit2/...` datarefs the same way. + +## Notes & limits +- Update rate is X-Plane's (~10–20 Hz) — fine for instruments, this isn't a + scenery stream. +- The autopilot buttons fire X-Plane's own commands, so the sim stays the + source of truth. Mode-highlight bits (`AP_BITS` in `AutopilotPanel.jsx`) are + best-effort and can differ per aircraft. +- LAN only by design. Don't expose port 8080 to the public internet. diff --git a/check.mjs b/check.mjs new file mode 100644 index 0000000..622c302 --- /dev/null +++ b/check.mjs @@ -0,0 +1,15 @@ +import { chromium } from 'playwright'; +const b = await chromium.launch(); +const p = await b.newPage({ viewport: { width: 1180, height: 820 } }); +const errs = []; +p.on('pageerror', e => errs.push('PAGEERR: ' + e.message)); +p.on('console', m => { if (m.type() === 'error') errs.push('CONSOLE: ' + m.text()); }); +await p.goto('http://localhost:8099', { waitUntil: 'networkidle' }); +await p.getByRole('button', { name: 'MFD', exact: true }).click(); +await p.waitForTimeout(1500); +const apKeys = await p.$$eval('.ap-key', els => els.map(e => e.textContent)); +const title = await p.$eval('.bezel-title', e => e.textContent).catch(() => null); +console.log('AP keys on MFD:', JSON.stringify(apKeys)); +console.log('Bezel title:', title); +console.log('Errors:', errs.length ? errs.join(' | ') : 'NONE'); +await b.close(); diff --git a/debug-svt.mjs b/debug-svt.mjs new file mode 100644 index 0000000..9c24548 --- /dev/null +++ b/debug-svt.mjs @@ -0,0 +1,21 @@ +import { chromium } from 'playwright'; +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1180, height: 820 } }); +page.on('console', (m) => console.log('[console]', m.type(), m.text())); +page.on('pageerror', (e) => console.log('[pageerror]', e.message)); +await page.goto('http://localhost:8099', { waitUntil: 'networkidle' }); +await page.getByRole('button', { name: 'PFD', exact: true }).click(); +await page.waitForTimeout(5000); +// report canvas presence + size +const info = await page.evaluate(() => { + const c = document.querySelector('.svt-canvas canvas'); + const f = document.querySelector('.svt-pos'); + return { + hasCanvas: !!c, + canvasSize: c ? [c.width, c.height] : null, + svtPos: f ? f.getBoundingClientRect() : null, + webgl: (() => { try { return !!document.createElement('canvas').getContext('webgl2'); } catch { return false; } })(), + }; +}); +console.log('[info]', JSON.stringify(info)); +await browser.close(); diff --git a/desktop/.tauri-signing.key.pub b/desktop/.tauri-signing.key.pub new file mode 100644 index 0000000..7b8cd91 --- /dev/null +++ b/desktop/.tauri-signing.key.pub @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU5MzFGQTUzOEUyOURFOTkKUldTWjNpbU9VL294V1ZWZllVMzc5MGR6OVFVcGRkSTVkcG1LUDJXODJzT2psbFZoY2JYT0E3dEIK \ No newline at end of file diff --git a/desktop/Dockerfile.linux b/desktop/Dockerfile.linux new file mode 100644 index 0000000..bf3e304 --- /dev/null +++ b/desktop/Dockerfile.linux @@ -0,0 +1,31 @@ +# Linux build image for the Tauri app (x86_64). Used to cross-build an AppImage +# + .deb from the macOS dev machine via Docker (linux/amd64). The Node bridge +# sidecar is compiled on the host by Bun, so this image only needs the Rust / +# Tauri / GTK / WebKit toolchain. +FROM rust:1-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libssl-dev \ + libxdo-dev \ + patchelf \ + file \ + wget \ + curl \ + xz-utils \ + ca-certificates \ + fuse \ + desktop-file-utils \ + xdg-utils \ + && rm -rf /var/lib/apt/lists/* + +# Node + a GLOBAL Tauri CLI (so we never touch the mounted node_modules, which +# holds the host/macOS CLI binary). +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs && rm -rf /var/lib/apt/lists/* \ + && npm install -g @tauri-apps/cli@2 + +WORKDIR /work/desktop diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 0000000..78b28cd --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,71 @@ +# X-Plane Cockpit — Desktop App + +A small launcher (Tauri) that runs the G1000 web cockpit's server on your PC and +shows the LAN address tablets/laptops open. The Node "bridge" server is bundled +as a Bun-compiled sidecar, so **nothing else needs to be installed**. + +## Using it (for a tester) + +1. Install & open **X-Plane Cockpit**. + - macOS: open the `.dmg`, drag the app to Applications. It's **ad-hoc signed** + (no Apple Developer ID), so on first launch use **right-click → Open** and + confirm, or run `xattr -dr com.apple.quarantine "/Applications/X-Plane Cockpit.app"`. + - Linux: make the `.AppImage` executable (`chmod +x`) and run it, or install the `.deb`. +2. Point it at your **X-Plane 12 folder** (it auto-detects common locations). + No X-Plane handy? Tick **Demo-Modus** to try the cockpit with synthetic data. +3. Make sure X-Plane's Web API is on: X-Plane → Settings → Network → + *Enable web server / API* (X-Plane 12.1.1+). +4. Click **Server starten**. Open the shown URL (e.g. `http://192.168.1.27:8080`) + on any tablet/laptop on the same Wi-Fi. The PFD/MFD/Map/FMS buttons open pages directly. + +Updates: **Nach Updates suchen** in the footer pulls the latest release from Gitea. + +> LAN only by design. Don't expose the port to the public internet. + +## Building (for the developer) + +From the repo root: + +```bash +# 1. prep: build the web cockpit + compile the Bun sidecars (mac + linux) +bash scripts/prep-desktop.sh + +# 2a. macOS app (native, ad-hoc signed) + updater artifacts +APPLE_SIGNING_IDENTITY="-" \ +TAURI_SIGNING_PRIVATE_KEY="$(cat desktop/.tauri-signing.key)" \ +TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat desktop/.tauri-signing.pw)" \ + npx --prefix desktop tauri build --target aarch64-apple-darwin + +# 2b. Linux AppImage + .deb (x86_64) via Docker +bash scripts/build-linux.sh + +# 3. publish to Gitea + refresh the updater's latest.json +GITEA_URL=https://git.kgva.ch GITEA_REPO=karim/xplane-cockpit \ +GITEA_TOKEN=$(cat /tmp/gitea_token) \ + node scripts/release-gitea.mjs +``` + +## Testing the auto-updater (two versions) + +The updater only fires when `latest.json` advertises a version **newer** than the +installed one. So to test it end-to-end: + +```bash +# 1. publish the baseline the tester installs +# (version 0.1.0 in tauri.conf.json) → release-gitea.mjs uploads assets + latest.json +# 2. install that 0.1.0 build on the test machine +# 3. bump the version, rebuild, re-release: +# edit desktop/src-tauri/tauri.conf.json "version": "0.1.1" +bash scripts/prep-desktop.sh +APPLE_SIGNING_IDENTITY="-" TAURI_SIGNING_PRIVATE_KEY="$(cat desktop/.tauri-signing.key)" \ +TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat desktop/.tauri-signing.pw)" \ + npx --prefix desktop tauri build --target aarch64-apple-darwin +bash scripts/build-linux.sh +GITEA_TOKEN=$(cat /tmp/gitea_token) node scripts/release-gitea.mjs +# 4. in the installed 0.1.0 app: launch → silent check shows the update banner, +# or click "Nach Updates suchen" → Installieren → app relaunches as 0.1.1. +``` + +The updater signing keypair lives in `desktop/.tauri-signing.key(.pub/.pw)` — +**keep the private key + password safe; they never go into the bundle or Gitea.** +The matching public key is embedded in `tauri.conf.json` (`plugins.updater.pubkey`). diff --git a/desktop/icon-src.png b/desktop/icon-src.png new file mode 100644 index 0000000..b974627 Binary files /dev/null and b/desktop/icon-src.png differ diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..7fe9ee1 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,247 @@ +{ + "name": "xplane-cockpit-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xplane-cockpit-desktop", + "version": "0.1.0", + "devDependencies": { + "@tauri-apps/cli": "^2" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", + "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.2", + "@tauri-apps/cli-darwin-x64": "2.11.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", + "@tauri-apps/cli-linux-arm64-musl": "2.11.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-musl": "2.11.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", + "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", + "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", + "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", + "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", + "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", + "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", + "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", + "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", + "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", + "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", + "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", + "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..6a91faf --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,12 @@ +{ + "name": "xplane-cockpit-desktop", + "version": "0.1.0", + "private": true, + "scripts": { + "tauri": "tauri", + "build": "tauri build" + }, + "devDependencies": { + "@tauri-apps/cli": "^2" + } +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..2ccd62e --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,6153 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "local-ip-address" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa08fb2b1ec3ea84575e94b489d06d4ce0cbf052d12acd515838f50e3c3d63e3" +dependencies = [ + "libc", + "neli", + "windows-sys 0.61.2", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "neli" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" +dependencies = [ + "bitflags 2.11.1", + "byteorder", + "derive_builder", + "getset", + "libc", + "log", + "neli-proc-macros", + "parking_lot", +] + +[[package]] +name = "neli-proc-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xplane-cockpit" +version = "0.1.3" +dependencies = [ + "local-ip-address", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-clipboard-manager", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-shell", + "tauri-plugin-updater", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..a7a2b9c --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "xplane-cockpit" +version = "0.1.3" +description = "Desktop launcher for the X-Plane G1000 web cockpit" +authors = ["karim"] +edition = "2021" +rust-version = "1.77" + +[lib] +name = "xplane_cockpit_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon", "image-png"] } +tauri-plugin-shell = "2" +tauri-plugin-dialog = "2" +tauri-plugin-opener = "2" +tauri-plugin-updater = "2" +tauri-plugin-process = "2" +tauri-plugin-clipboard-manager = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +local-ip-address = "0.6" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..0c90573 --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,22 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capabilities for the control panel window", + "windows": ["main"], + "permissions": [ + "core:default", + "core:event:default", + "core:window:allow-start-dragging", + { + "identifier": "shell:allow-execute", + "allow": [{ "name": "binaries/xpbridge", "sidecar": true, "args": true }] + }, + "shell:allow-spawn", + "shell:allow-kill", + "dialog:allow-open", + "opener:allow-open-url", + "updater:default", + "process:allow-restart", + "clipboard-manager:allow-write-text" + ] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000..9897107 Binary files /dev/null and b/desktop/src-tauri/icons/128x128.png differ diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..15321d0 Binary files /dev/null and b/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000..a817ff0 Binary files /dev/null and b/desktop/src-tauri/icons/32x32.png differ diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000..338d3e7 Binary files /dev/null and b/desktop/src-tauri/icons/64x64.png differ diff --git a/desktop/src-tauri/icons/Square107x107Logo.png b/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..1a01181 Binary files /dev/null and b/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/desktop/src-tauri/icons/Square142x142Logo.png b/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..523565e Binary files /dev/null and b/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/desktop/src-tauri/icons/Square150x150Logo.png b/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..734b8e7 Binary files /dev/null and b/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/desktop/src-tauri/icons/Square284x284Logo.png b/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..8b7c2d3 Binary files /dev/null and b/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/desktop/src-tauri/icons/Square30x30Logo.png b/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..672fd56 Binary files /dev/null and b/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/desktop/src-tauri/icons/Square310x310Logo.png b/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..a0a46dc Binary files /dev/null and b/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/desktop/src-tauri/icons/Square44x44Logo.png b/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..22d10e5 Binary files /dev/null and b/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/desktop/src-tauri/icons/Square71x71Logo.png b/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..0ce5660 Binary files /dev/null and b/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/desktop/src-tauri/icons/Square89x89Logo.png b/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..5ab5a65 Binary files /dev/null and b/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/desktop/src-tauri/icons/StoreLogo.png b/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..1aba318 Binary files /dev/null and b/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..d2a7f35 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f0040b2 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..ba60c2f Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..f749cf9 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b146c52 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..2d1fdc2 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..63afabc Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a801421 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..bd95448 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..ca9edf6 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..05d485c Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..54fff5e Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4a0e57d Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e2748ea Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b5e1229 Binary files /dev/null and b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/desktop/src-tauri/icons/android/values/ic_launcher_background.xml b/desktop/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/desktop/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000..4cb4669 Binary files /dev/null and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000..9b5e4ef Binary files /dev/null and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000..9cee423 Binary files /dev/null and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..1bf7028 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..2affed9 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..2affed9 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png b/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..86cf532 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..f81fd74 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..99f2140 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..99f2140 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..623d678 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..2affed9 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..c8f3eb0 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..c8f3eb0 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..88caacd Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/desktop/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..609b53b Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..88caacd Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..a69f169 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..36586f1 Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..10ce5dc Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..89e4a2a Binary files /dev/null and b/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..3b4240c --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,269 @@ +// X-Plane Cockpit desktop launcher. +// +// A small control panel that: (1) lets the user point at their X-Plane 12 +// install, (2) starts/stops the bundled Node "bridge" server (a Bun-compiled +// sidecar), and (3) shows the LAN URL tablets open to see the G1000 cockpit. +// The cockpit web files travel with the app as a resource (WEB_DIST). A system +// tray keeps the server running when the window is closed. + +use std::net::TcpListener; +use std::path::PathBuf; +use std::sync::Mutex; + +use serde::Serialize; +use tauri::menu::{MenuBuilder, MenuItem, PredefinedMenuItem}; +use tauri::tray::{TrayIconBuilder, TrayIconEvent}; +use tauri::{Emitter, Manager, State}; +use tauri_plugin_clipboard_manager::ClipboardExt; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; + +#[derive(Default)] +struct ServerState { + child: Mutex>, + port: Mutex, + url: Mutex, +} + +#[derive(Serialize, Clone)] +struct ServerInfo { + ip: String, + port: u16, + url: String, +} + +fn lan_ipv4() -> String { + local_ip_address::local_ip() + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "127.0.0.1".to_string()) +} + +#[tauri::command] +fn lan_ip() -> String { + lan_ipv4() +} + +// Can we bind this TCP port on the LAN interface? (i.e. is it free) +fn is_port_free(port: u16) -> bool { + TcpListener::bind(("0.0.0.0", port)).is_ok() +} + +#[tauri::command] +fn port_free(port: u16) -> bool { + is_port_free(port) +} + +// First free port at/after `start` (so the UI can offer an alternative). +#[tauri::command] +fn suggest_port(start: u16) -> u16 { + let mut p = start.max(1024); + for _ in 0..200 { + if is_port_free(p) { + return p; + } + p = p.saturating_add(1); + } + start +} + +#[tauri::command] +fn default_xplane_path() -> Option { + let home = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_default(); + let candidates = [ + format!("{home}/X-Plane 12"), + format!("{home}/Desktop/X-Plane 12"), + "/Applications/X-Plane 12".to_string(), + "C:/X-Plane 12".to_string(), + "D:/X-Plane 12".to_string(), + ]; + candidates + .into_iter() + .find(|c| PathBuf::from(c).join("Resources").join("default data").is_dir()) +} + +#[tauri::command] +fn valid_xplane_path(path: String) -> bool { + !path.is_empty() + && PathBuf::from(&path) + .join("Resources") + .join("default data") + .is_dir() +} + +#[tauri::command] +fn server_running(state: State) -> bool { + state.child.lock().unwrap().is_some() +} + +#[tauri::command] +async fn start_server( + app: tauri::AppHandle, + state: State<'_, ServerState>, + xplane_path: String, + port: u16, + demo: bool, +) -> Result { + if state.child.lock().unwrap().is_some() { + let p = *state.port.lock().unwrap(); + let ip = lan_ipv4(); + return Ok(ServerInfo { url: format!("http://{ip}:{p}"), ip, port: p }); + } + if !is_port_free(port) { + return Err(format!("Port {port} ist belegt — wähle einen anderen.")); + } + + let web_dist = app + .path() + .resolve("web", 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()); + + if !xplane_path.is_empty() { + cmd = cmd.env("XPLANE_ROOT", xplane_path); + } + if demo { + cmd = cmd.env("DEMO", "1"); + } + + let (mut rx, child) = cmd.spawn().map_err(|e| format!("spawn: {e}"))?; + + let app2 = app.clone(); + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + let line = match event { + CommandEvent::Stdout(b) | CommandEvent::Stderr(b) => { + String::from_utf8_lossy(&b).to_string() + } + CommandEvent::Terminated(_) => { + let _ = app2.emit("server-exited", ()); + break; + } + _ => continue, + }; + let _ = app2.emit("server-log", line); + } + }); + + let ip = lan_ipv4(); + let url = format!("http://{ip}:{port}"); + *state.child.lock().unwrap() = Some(child); + *state.port.lock().unwrap() = port; + *state.url.lock().unwrap() = url.clone(); + + Ok(ServerInfo { url, ip, port }) +} + +#[tauri::command] +fn stop_server(state: State) -> Result<(), String> { + if let Some(child) = state.child.lock().unwrap().take() { + child.kill().map_err(|e| format!("kill: {e}"))?; + } + *state.url.lock().unwrap() = String::new(); + Ok(()) +} + +fn kill_sidecar(app: &tauri::AppHandle) { + if let Some(state) = app.try_state::() { + if let Some(child) = state.child.lock().unwrap().take() { + let _ = child.kill(); + } + } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) + .manage(ServerState::default()) + .invoke_handler(tauri::generate_handler![ + lan_ip, + port_free, + suggest_port, + default_xplane_path, + valid_xplane_path, + server_running, + start_server, + stop_server + ]) + .setup(|app| { + build_tray(app.handle())?; + Ok(()) + }) + // Closing the window hides it instead of quitting, so the server keeps + // serving tablets in the background. Quit from the tray. + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window.hide(); + } + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +fn build_tray(app: &tauri::AppHandle) -> tauri::Result<()> { + let show = MenuItem::with_id(app, "show", "Panel anzeigen", true, None::<&str>)?; + let open = MenuItem::with_id(app, "open", "Cockpit öffnen", true, None::<&str>)?; + let copy = MenuItem::with_id(app, "copy", "URL kopieren", true, None::<&str>)?; + let toggle = MenuItem::with_id(app, "toggle", "Server starten / stoppen", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?; + let sep = PredefinedMenuItem::separator(app)?; + let menu = MenuBuilder::new(app) + .item(&show) + .item(&open) + .item(©) + .item(&sep) + .item(&toggle) + .item(&sep) + .item(&quit) + .build()?; + + TrayIconBuilder::with_id("main") + .icon(app.default_window_icon().unwrap().clone()) + .tooltip("X-Plane Cockpit") + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id().as_ref() { + "show" => show_main(app), + "open" => { let _ = app.emit("tray-open", ()); } + "copy" => { + if let Some(state) = app.try_state::() { + let url = state.url.lock().unwrap().clone(); + if !url.is_empty() { + let _ = app.clipboard().write_text(url); + } + } + } + "toggle" => { let _ = app.emit("tray-toggle", ()); } + "quit" => { kill_sidecar(app); app.exit(0); } + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { .. } = event { + show_main(tray.app_handle()); + } + }) + .build(app)?; + Ok(()) +} + +fn show_main(app: &tauri::AppHandle) { + if let Some(win) = app.get_webview_window("main") { + let _ = win.show(); + let _ = win.set_focus(); + } +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..9afb3a1 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an extra console window on Windows in release. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + xplane_cockpit_lib::run() +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..f363e17 --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "X-Plane Cockpit", + "version": "0.1.3", + "identifier": "ch.kgva.xplanecockpit", + "build": { + "frontendDist": "../ui" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "X-Plane Cockpit", + "width": 480, + "height": 720, + "minWidth": 420, + "minHeight": 560, + "resizable": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": [ + "app", + "dmg", + "appimage", + "deb" + ], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": [ + "binaries/xpbridge" + ], + "resources": { + "resources/web": "web" + }, + "createUpdaterArtifacts": true, + "macOS": { + "minimumSystemVersion": "10.15" + }, + "linux": { + "appimage": { + "bundleMediaFramework": false + } + } + }, + "plugins": { + "updater": { + "endpoints": [ + "https://git.kgva.ch/karim/xplane-cockpit/releases/download/updater/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU5MzFGQTUzOEUyOURFOTkKUldTWjNpbU9VL294V1ZWZllVMzc5MGR6OVFVcGRkSTVkcG1LUDJXODJzT2psbFZoY2JYT0E3dEIK" + } + } +} diff --git a/desktop/ui/index.html b/desktop/ui/index.html new file mode 100644 index 0000000..052d8c6 --- /dev/null +++ b/desktop/ui/index.html @@ -0,0 +1,83 @@ + + + + + + X-Plane Cockpit + + + +
+
+
G1000·web
+
Gestoppt
+
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+
+
+ + + + + +
+ Server-Log +

+      
+
+ +
+ v— + +
+
+ + + diff --git a/desktop/ui/main.js b/desktop/ui/main.js new file mode 100644 index 0000000..d3b5e3a --- /dev/null +++ b/desktop/ui/main.js @@ -0,0 +1,194 @@ +// 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 1024–65535'; 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 — ${alt} verwenden`; + 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(); diff --git a/desktop/ui/styles.css b/desktop/ui/styles.css new file mode 100644 index 0000000..b81d13c --- /dev/null +++ b/desktop/ui/styles.css @@ -0,0 +1,89 @@ +/* macOS-style dark theme: neutral graphite surfaces, SF system font, subtle + separators, a single green accent for the running/start state. No blue. */ +:root { + --bg: #1c1c1e; /* system background (dark) */ + --bg2: #2c2c2e; /* elevated surface */ + --bg3: #3a3a3c; /* control fill */ + --line: #48484a; /* separators / borders */ + --line-soft: #38383a; + --txt: #ffffff; + --txt2: #ebebf5; + --mut: #8e8e93; /* secondary label */ + --green: #30d158; /* system green */ + --green-d: #248a3d; + --amber: #ffd60a; + --red: #ff453a; +} +* { box-sizing: border-box; } +html, body { margin: 0; height: 100%; } +body { + background: var(--bg); + color: var(--txt); + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif; + font-size: 13px; user-select: none; -webkit-font-smoothing: antialiased; +} +.panel { display: flex; flex-direction: column; height: 100vh; padding: 16px; gap: 14px; } +.hd { display: flex; align-items: center; justify-content: space-between; } +.brand { font-weight: 700; letter-spacing: .2px; font-size: 17px; } +.brand span { color: var(--mut); font-weight: 500; } +.status { display: flex; align-items: center; gap: 7px; font-size: 12px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--line-soft); background: var(--bg2); color: var(--txt2); } +.status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--mut); transition: background .2s; } +.status.run .dot { background: var(--green); box-shadow: 0 0 8px var(--green); } +.status.warn .dot { background: var(--amber); box-shadow: 0 0 8px var(--amber); } + +main { flex: 1; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; } +.card { background: var(--bg2); border: 1px solid var(--line-soft); border-radius: 12px; padding: 14px; display: flex; flex-direction: column; gap: 9px; } +.lbl { color: var(--mut); font-size: 11px; font-weight: 600; } +.row { display: flex; gap: 8px; align-items: center; } +.row.gap { gap: 16px; margin-top: 2px; } +.field { display: flex; flex-direction: column; gap: 6px; } +.field input { width: 96px; } +input[type="text"], input[type="number"] { + flex: 1; background: var(--bg); border: 1px solid var(--line); color: var(--txt); + border-radius: 7px; padding: 8px 10px; font-size: 13px; font-family: inherit; +} +input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px rgba(48,209,88,.2); } +.toggle { display: flex; align-items: center; gap: 8px; color: var(--txt2); cursor: pointer; align-self: flex-end; padding-bottom: 8px; } +.toggle input { width: 15px; height: 15px; accent-color: var(--green); } +.hint { font-size: 12px; min-height: 16px; color: var(--mut); } +.hint.ok { color: var(--green); } .hint.bad { color: var(--amber); } +.hint a { color: var(--green); } + +.btn { border: 1px solid var(--line); background: var(--bg3); color: var(--txt); border-radius: 8px; padding: 8px 14px; font-size: 13px; font-family: inherit; cursor: pointer; transition: filter .12s, background .12s; } +.btn:hover { filter: brightness(1.18); } +.btn:active { transform: translateY(1px); } +.btn.ghost { background: transparent; color: var(--txt2); border-color: var(--line); } +.btn.sm { padding: 5px 10px; font-size: 12px; } +.btn.big { padding: 13px; font-size: 15px; font-weight: 600; } +.btn.primary { background: var(--green); color: #042b10; border-color: transparent; font-weight: 600; } +.btn.big.stop { background: var(--red); color: #2a0603; } +.btn.ok { background: var(--green); color: #042b10; border-color: transparent; font-weight: 600; } +.btn:disabled { opacity: .45; cursor: default; } + +.update-banner { display: flex; gap: 10px; align-items: center; justify-content: space-between; background: rgba(48,209,88,.10); border: 1px solid var(--green-d); border-radius: 12px; padding: 10px 12px; } +.update-banner.hidden { display: none; } +.ub-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.ub-text b { color: var(--green); font-size: 13px; } +.ub-text span { color: var(--mut); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 230px; } +.ub-actions { display: flex; gap: 6px; flex: 0 0 auto; } + +.live.hidden { display: none; } +.url-row { display: flex; gap: 8px; align-items: center; } +.url-row code { flex: 1; background: var(--bg); border: 1px solid var(--line); color: var(--green); border-radius: 7px; padding: 10px 12px; font-size: 16px; font-weight: 600; letter-spacing: .3px; user-select: text; font-family: ui-monospace, "SF Mono", Menlo, monospace; } +.quick { display: flex; gap: 6px; } +.quick .btn { flex: 1; } + +.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); } + +.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; } +#log { margin: 6px 0 2px; max-height: 140px; overflow-y: auto; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; color: var(--mut); white-space: pre-wrap; } + +.ft { display: flex; align-items: center; justify-content: space-between; color: var(--mut); font-size: 12px; } +.link { background: none; border: none; color: var(--green); cursor: pointer; font-size: 12px; font-family: inherit; } +.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; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6e725a6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,900 @@ +{ + "name": "xplane-glass-cockpit", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xplane-glass-cockpit", + "version": "0.1.0", + "hasInstallScript": true, + "dependencies": { + "express": "^4.21.2", + "ws": "^8.18.0" + }, + "devDependencies": { + "playwright": "^1.60.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce27569 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "xplane-glass-cockpit", + "version": "0.1.0", + "description": "Bring X-Plane 12 instruments (PFD/MFD, G1000-style, autopilot) to any tablet or laptop on your LAN via React.", + "type": "module", + "scripts": { + "postinstall": "cd web && npm install", + "build": "cd web && npm run build", + "start": "node server/bridge.js", + "dev:bridge": "node --watch server/bridge.js", + "dev:web": "cd web && npm run dev" + }, + "dependencies": { + "express": "^4.21.2", + "ws": "^8.18.0" + }, + "devDependencies": { + "playwright": "^1.60.0" + } +} diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100644 index 0000000..6df42bf --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Build the Linux AppImage + .deb (x86_64) in Docker. Run from the repo root, +# AFTER scripts/prep-desktop.sh has produced the linux sidecar + web resources. +# On Apple Silicon this runs under qemu (linux/amd64) and is slow but works. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +IMG=xpcockpit-linux-builder +echo "==> building docker image ($IMG)" +docker build --platform linux/amd64 -f desktop/Dockerfile.linux -t "$IMG" desktop >/dev/null + +# The updater signing key is a secret and is NOT in the repo. If it's present +# locally we sign + emit updater artifacts; otherwise we build plain installable +# bundles (appimage + deb) with updater artifacts disabled via a config override. +SIGN_ENV=(); BUNDLES="appimage,deb,updater"; CFG="" +if [[ -f desktop/.tauri-signing.key && -f desktop/.tauri-signing.pw ]]; then + SIGN_ENV=(-e "TAURI_SIGNING_PRIVATE_KEY=$(cat desktop/.tauri-signing.key)" + -e "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$(cat desktop/.tauri-signing.pw)") + echo "==> signing key found — emitting signed updater artifacts" +else + BUNDLES="appimage,deb"; CFG='--config {"bundle":{"createUpdaterArtifacts":false}}' + echo "==> no signing key — building plain appimage + deb (no updater artifacts)" +fi + +echo "==> tauri build (x86_64-unknown-linux-gnu) in container — this takes a while under emulation" +docker run --rm --platform linux/amd64 \ + -v "$ROOT":/work \ + -e CARGO_TARGET_DIR=/work/target-linux \ + -e APPIMAGE_EXTRACT_AND_RUN=1 \ + -e ARCH=x86_64 \ + "${SIGN_ENV[@]}" \ + -w /work/desktop \ + "$IMG" \ + bash -c "export PATH=/usr/local/cargo/bin:\$PATH; tauri build --target x86_64-unknown-linux-gnu --bundles $BUNDLES $CFG" + +echo "==> artifacts:" +find target-linux/x86_64-unknown-linux-gnu/release/bundle -maxdepth 2 -type f \ + \( -name '*.AppImage' -o -name '*.deb' -o -name '*.AppImage.sig' -o -name '*.tar.gz' -o -name '*.sig' \) 2>/dev/null diff --git a/scripts/prep-desktop.sh b/scripts/prep-desktop.sh new file mode 100755 index 0000000..86337c0 --- /dev/null +++ b/scripts/prep-desktop.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Prepare the Tauri bundle inputs: build the web cockpit, copy it in as a +# resource, and compile the Node bridge into Bun single-file sidecars named with +# the Tauri target triples. Run from the repo root. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +export PATH="$HOME/.bun/bin:$PATH" + +echo "==> building web cockpit" +( cd web && npm run build >/dev/null ) + +echo "==> copying cockpit into desktop resources" +rm -rf desktop/src-tauri/resources/web +mkdir -p desktop/src-tauri/resources/web +cp -R web/dist/. desktop/src-tauri/resources/web/ + +echo "==> compiling bridge sidecars (Bun)" +mkdir -p desktop/src-tauri/binaries +bun build --compile --target=bun-darwin-arm64 server/bridge.js \ + --outfile desktop/src-tauri/binaries/xpbridge-aarch64-apple-darwin +bun build --compile --target=bun-linux-x64-baseline server/bridge.js \ + --outfile desktop/src-tauri/binaries/xpbridge-x86_64-unknown-linux-gnu +chmod +x desktop/src-tauri/binaries/xpbridge-* + +echo "==> done" +ls -lh desktop/src-tauri/binaries/ diff --git a/scripts/release-gitea.mjs b/scripts/release-gitea.mjs new file mode 100644 index 0000000..16b63aa --- /dev/null +++ b/scripts/release-gitea.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// Publish a release to Gitea and (re)write the updater's latest.json. +// +// Reads the built artifacts (macOS .app.tar.gz + .sig, Linux .AppImage + .sig, +// plus .dmg / .deb for manual install), creates a versioned release with those +// assets, then builds latest.json (Tauri v2 updater format) pointing at the +// release download URLs and uploads it to a fixed-tag "updater" release so the +// app's updater endpoint URL stays constant. +// +// Env: GITEA_URL (e.g. https://git.kgva.ch), GITEA_REPO (owner/name), +// GITEA_TOKEN (or a token file at /tmp/gitea_token). +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +const GITEA_URL = (process.env.GITEA_URL || 'https://git.kgva.ch').replace(/\/$/, ''); +const REPO = process.env.GITEA_REPO || 'karim/xplane-cockpit'; +const TOKEN = process.env.GITEA_TOKEN + || (fs.existsSync('/tmp/gitea_token') ? fs.readFileSync('/tmp/gitea_token', 'utf8').trim() : ''); +const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'desktop/src-tauri/tauri.conf.json'), 'utf8')).version; +const UPDATER_TAG = 'updater'; + +if (!TOKEN) { console.error('No GITEA_TOKEN (env or /tmp/gitea_token).'); process.exit(1); } + +const api = (p, opts = {}) => fetch(`${GITEA_URL}/api/v1${p}`, { + ...opts, + headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json', ...(opts.headers || {}) }, +}); + +// Collect built artifacts that exist (mac and/or linux builds may have run). +function findArtifacts() { + const macBundle = path.join(ROOT, 'desktop/src-tauri/target/aarch64-apple-darwin/release/bundle'); + const linBundle = path.join(ROOT, 'target-linux/x86_64-unknown-linux-gnu/release/bundle'); + const out = { assets: [], updater: {} }; + const add = (file, platformKey) => { + if (!fs.existsSync(file)) return; + out.assets.push(file); + if (platformKey) { + const sig = file + '.sig'; + if (fs.existsSync(sig)) out.updater[platformKey] = { file, sig: fs.readFileSync(sig, 'utf8').trim() }; + } + }; + const glob1 = (dir, re) => fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => re.test(f)).map((f) => path.join(dir, f)) : []; + // macOS + glob1(path.join(macBundle, 'macos'), /\.app\.tar\.gz$/).forEach((f) => add(f, 'darwin-aarch64')); + glob1(path.join(macBundle, 'dmg'), /\.dmg$/).forEach((f) => out.assets.push(f)); + // Linux + glob1(path.join(linBundle, 'appimage'), /\.AppImage$/).forEach((f) => add(f, 'linux-x86_64')); + glob1(path.join(linBundle, 'deb'), /\.deb$/).forEach((f) => out.assets.push(f)); + return out; +} + +async function getReleaseByTag(tag) { + const r = await api(`/repos/${REPO}/releases/tags/${encodeURIComponent(tag)}`); + return r.ok ? r.json() : null; +} + +async function ensureRelease(tag, name, body) { + let rel = await getReleaseByTag(tag); + if (rel) return rel; + const r = await api(`/repos/${REPO}/releases`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tag_name: tag, name, body, draft: false, prerelease: false }), + }); + if (!r.ok) throw new Error(`create release ${tag}: ${r.status} ${await r.text()}`); + return r.json(); +} + +async function uploadAsset(rel, file, asName) { + const name = asName || path.basename(file); + // delete an existing asset with the same name first (Gitea won't overwrite) + for (const a of rel.assets || []) { + if (a.name === name) await api(`/repos/${REPO}/releases/${rel.id}/assets/${a.id}`, { method: 'DELETE' }); + } + const blob = new Blob([fs.readFileSync(file)]); + const fd = new FormData(); + fd.append('attachment', blob, name); + const r = await api(`/repos/${REPO}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, { method: 'POST', body: fd }); + if (!r.ok) throw new Error(`upload ${name}: ${r.status} ${await r.text()}`); + console.log(' ↑', name); + return r.json(); +} + +const dlUrl = (tag, name) => `${GITEA_URL}/${REPO}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`; + +async function main() { + const { assets, updater } = findArtifacts(); + if (!assets.length) { console.error('No build artifacts found — run the builds first.'); process.exit(1); } + console.log(`Release v${VERSION} → ${GITEA_URL}/${REPO}`); + console.log('Artifacts:', assets.map((a) => path.basename(a)).join(', ')); + + const verTag = `v${VERSION}`; + const rel = await ensureRelease(verTag, `X-Plane Cockpit ${verTag}`, `Automated release ${verTag}.`); + for (const f of assets) await uploadAsset(rel, f); + const relFresh = await getReleaseByTag(verTag); // refresh asset list + + // Build latest.json referencing this release's updater artifacts. + const platforms = {}; + for (const [key, { file, sig }] of Object.entries(updater)) { + platforms[key] = { signature: sig, url: dlUrl(verTag, path.basename(file)) }; + } + const latest = { + version: VERSION, + notes: `X-Plane Cockpit ${verTag}`, + pub_date: new Date().toISOString(), + platforms, + }; + const latestPath = path.join(ROOT, 'desktop/latest.json'); + fs.writeFileSync(latestPath, JSON.stringify(latest, null, 2)); + console.log('latest.json platforms:', Object.keys(platforms).join(', ') || '(none)'); + + // Upload latest.json to the fixed "updater" release (constant endpoint URL). + const upd = await ensureRelease(UPDATER_TAG, 'Updater channel', 'Rolling pointer used by the in-app updater.'); + await uploadAsset(upd, latestPath, 'latest.json'); + console.log('Updater endpoint:', dlUrl(UPDATER_TAG, 'latest.json')); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/server/bridge.js b/server/bridge.js new file mode 100644 index 0000000..d9f718d --- /dev/null +++ b/server/bridge.js @@ -0,0 +1,305 @@ +// X-Plane Glass Cockpit — Bridge +// ------------------------------------------------------------------------- +// Connects to X-Plane 12's built-in web API (localhost only), resolves +// dataref/command names to per-session IDs, subscribes to live values, and +// fans them out over a LAN-facing WebSocket to any number of tablets/laptops. +// Also serves the built React UI. + +import express from 'express'; +import { WebSocketServer, WebSocket as WsClient } from 'ws'; +import http from 'node:http'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { CONFIG, DATAREFS, WRITABLE_DATAREFS, COMMANDS } from './config.js'; +import { loadNavData, search as navSearch, navStatus, nearest as navNearest, bbox as navBbox, runwaysNear as navRunways } from './navdata.js'; +import { parseProcedures, procedureLegs as procLegs } from './procedures.js'; +import * as fp from './flightplan.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// WEB_DIST can be overridden (e.g. the desktop app points it at the cockpit +// files it bundles as a resource); otherwise default to ../web/dist. +const WEB_DIST = process.env.WEB_DIST || path.join(__dirname, '..', 'web', 'dist'); +const REST = `http://${CONFIG.xplaneHost}:${CONFIG.xplanePort}${CONFIG.xplaneApiBase}`; +const WS_URL = `ws://${CONFIG.xplaneHost}:${CONFIG.xplanePort}${CONFIG.xplaneApiBase}`; + +const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a); + +// ---- shared state --------------------------------------------------------- +const state = { + xpConnected: false, + values: {}, // alias -> latest value + drefIdToAlias: new Map(), // X-Plane dataref id -> our alias + drefNameToId: new Map(), // sim/... -> id + cmdNameToId: new Map(), // sim/... -> id + xpSocket: null, + reqId: 1, +}; + +const clients = new Set(); // connected browser sockets + +// ---- helpers -------------------------------------------------------------- +function broadcast(obj) { + const msg = JSON.stringify(obj); + for (const c of clients) { + if (c.readyState === WsClient.OPEN) c.send(msg); + } +} + +function broadcastPlan() { + broadcast({ type: 'flightplan', data: fp.getPlan() }); +} + +async function fetchAllByName(resource, names) { + // X-Plane's list endpoints can be filtered by name. We query each name so we + // don't pull the full ~15k dataref catalogue. + const map = new Map(); + await Promise.all( + [...new Set(names)].map(async (name) => { + try { + const url = `${REST}/${resource}?filter[name]=${encodeURIComponent(name)}`; + const res = await fetch(url, { headers: { Accept: 'application/json' } }); + if (!res.ok) return; + const body = await res.json(); + const item = (body.data || []).find((d) => d.name === name); + if (item) map.set(name, item.id); + else log(`! ${resource} not found: ${name}`); + } catch (e) { + log(`! lookup failed for ${name}: ${e.message}`); + } + }) + ); + return map; +} + +// ---- X-Plane connection --------------------------------------------------- +async function resolveIds() { + const drefNames = Object.values(DATAREFS); + const cmdNames = Object.values(COMMANDS); + state.drefNameToId = await fetchAllByName('datarefs', [ + ...drefNames, + ...Object.values(WRITABLE_DATAREFS), + ]); + state.cmdNameToId = await fetchAllByName('commands', cmdNames); + + // build reverse map id -> alias for incoming updates + state.drefIdToAlias.clear(); + for (const [alias, name] of Object.entries(DATAREFS)) { + const id = state.drefNameToId.get(name); + if (id != null) state.drefIdToAlias.set(id, alias); + } + log(`resolved ${state.drefNameToId.size} datarefs, ${state.cmdNameToId.size} commands`); +} + +function subscribeValues() { + const datarefs = []; + for (const id of state.drefIdToAlias.keys()) datarefs.push({ id }); + if (!datarefs.length) return; + state.xpSocket.send( + JSON.stringify({ + req_id: state.reqId++, + type: 'dataref_subscribe_values', + params: { datarefs }, + }) + ); + log(`subscribed to ${datarefs.length} datarefs`); +} + +function connectXPlane() { + log(`connecting to X-Plane @ ${WS_URL} ...`); + let sock; + try { + sock = new WsClient(WS_URL); + } catch (e) { + log('X-Plane connect threw, retrying in 3s:', e.message); + return setTimeout(connectXPlane, 3000); + } + state.xpSocket = sock; + + sock.on('open', async () => { + try { + await resolveIds(); + subscribeValues(); + state.xpConnected = true; + broadcast({ type: 'status', xpConnected: true }); + log('X-Plane connected ✓'); + } catch (e) { + log('setup after connect failed:', e.message); + } + }); + + sock.on('message', (raw) => { + let msg; + try { msg = JSON.parse(raw); } catch { return; } + if (msg.type === 'dataref_update_values' && msg.data) { + const patch = {}; + for (const [id, value] of Object.entries(msg.data)) { + const alias = state.drefIdToAlias.get(Number(id)); + if (alias) { state.values[alias] = value; patch[alias] = value; } + } + if (Object.keys(patch).length) broadcast({ type: 'values', data: patch }); + } + }); + + const onDown = (why) => { + if (state.xpConnected) log(`X-Plane disconnected (${why})`); + state.xpConnected = false; + broadcast({ type: 'status', xpConnected: false }); + if (state.xpSocket === sock) state.xpSocket = null; + setTimeout(connectXPlane, 3000); + }; + sock.on('close', () => onDown('close')); + sock.on('error', (e) => onDown(e.message)); +} + +// ---- commands coming FROM the browser ------------------------------------ +function handleClientMessage(msg) { + // --- flight plan (works even without a sim connection) --- + if (msg.type === 'fp_set') { fp.setPlan(msg.plan); return broadcastPlan(); } + if (msg.type === 'fp_add') { + const r = fp.addWaypoint(msg.ident); + if (!r.ok) return; // silently ignore unknown idents + return broadcastPlan(); + } + if (msg.type === 'fp_remove') { fp.removeWaypoint(msg.index); return broadcastPlan(); } + if (msg.type === 'fp_active') { fp.setActiveLeg(msg.index); return broadcastPlan(); } + if (msg.type === 'fp_clear') { fp.setPlan({ waypoints: [] }); return broadcastPlan(); } + if (msg.type === 'fp_export') { + const r = fp.exportFms(msg.name || 'WEBFPL'); + broadcast({ type: 'fp_export_result', ...r }); + return; + } + + // --- everything below talks to X-Plane; needs a live sim socket --- + if (!state.xpSocket || state.xpSocket.readyState !== WsClient.OPEN) return; + + if (msg.type === 'command') { + const name = COMMANDS[msg.name]; + const id = name && state.cmdNameToId.get(name); + if (id == null) return log(`! unknown command alias: ${msg.name}`); + state.xpSocket.send( + JSON.stringify({ + req_id: state.reqId++, + type: 'command_set_is_active', + params: { commands: [{ id, is_active: true, duration: msg.duration ?? 0 }] }, + }) + ); + } else if (msg.type === 'setDataref') { + const name = WRITABLE_DATAREFS[msg.name]; + const id = name && state.drefNameToId.get(name); + if (id == null) return log(`! unknown writable dataref alias: ${msg.name}`); + state.xpSocket.send( + JSON.stringify({ + req_id: state.reqId++, + type: 'dataref_set_values', + params: { datarefs: [{ id, value: Number(msg.value) }] }, + }) + ); + } +} + +// ---- HTTP + LAN WebSocket server ----------------------------------------- +const app = express(); +// Allow the desktop launcher (a different origin) to read the JSON API. LAN-only +// by design, so a wildcard here is harmless and keeps tablets/the app simple. +app.use('/api', (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); next(); }); +app.get('/api/health', (_req, res) => + res.json({ xpConnected: state.xpConnected, datarefs: state.drefIdToAlias.size, clients: clients.size, nav: navStatus() }) +); +// Waypoint / navaid / airport search from X-Plane's own nav database. +app.get('/api/nav/search', (req, res) => res.json(navSearch(req.query.q || '', 25))); +// NEAREST airports/navaids to a point (NRST page). +app.get('/api/nav/nearest', (req, res) => + res.json(navNearest(+req.query.lat, +req.query.lon, { count: +req.query.count || 15, type: req.query.type || 'apt' })) +); +// Features inside a map window (airports/navaids/fixes) for the moving map. +app.get('/api/nav/bbox', (req, res) => + res.json(navBbox(+req.query.s, +req.query.w, +req.query.n, +req.query.e, + (req.query.types || 'apt,vor,ndb').split(','), +req.query.limit || 800)) +); +// Runways near a point — drawn in the PFD synthetic-vision view. +app.get('/api/nav/runways', (req, res) => + res.json(navRunways(+req.query.lat, +req.query.lon, +req.query.radius || 12)) +); +// PROC: an airport's procedures (SIDs/STARs/approaches) and the resolved leg +// fixes for a chosen procedure+transition (from X-Plane's CIFP data). +app.get('/api/nav/procs', (req, res) => { + const p = parseProcedures(String(req.query.icao || '')); + if (!p) return res.status(404).json({ error: 'no procedures for ' + req.query.icao }); + res.json({ icao: p.icao, runways: p.runways, sids: p.sids, stars: p.stars, approaches: p.approaches }); +}); +app.get('/api/nav/proc', (req, res) => + res.json(procLegs(String(req.query.icao || ''), req.query.type, req.query.name, req.query.trans)) +); +app.use(express.static(WEB_DIST)); +// SPA fallback so client-side routes work. +app.get('*', (_req, res) => res.sendFile(path.join(WEB_DIST, 'index.html'))); + +const server = http.createServer(app); +const wss = new WebSocketServer({ server, path: '/ws' }); + +wss.on('connection', (ws) => { + clients.add(ws); + log(`browser connected (${clients.size} total)`); + // send current snapshot immediately so the UI isn't blank + ws.send(JSON.stringify({ type: 'status', xpConnected: state.xpConnected })); + ws.send(JSON.stringify({ type: 'values', data: state.values })); + ws.send(JSON.stringify({ type: 'flightplan', data: fp.getPlan() })); + + ws.on('message', (raw) => { + try { handleClientMessage(JSON.parse(raw)); } catch { /* ignore */ } + }); + ws.on('close', () => { clients.delete(ws); log(`browser left (${clients.size} total)`); }); + ws.on('error', () => clients.delete(ws)); +}); + +// ---- demo mode: synthetic values when there's no X-Plane (for previews) --- +function startDemo() { + log('DEMO mode — emitting synthetic values, not connecting to X-Plane'); + state.xpConnected = true; + Object.assign(state.values, { + airspeed: 124, altitude: 5500, vspeed: 320, pitch: 4.5, roll: -12, + heading: 87, slip: 0.3, gForce: 1.04, oat: 9, + apState: (1 << 0) | (1 << 1) | (1 << 14), // FD + HDG + ALT + apEngaged: 1, apHdgBug: 90, apAltBug: 6000, apVsBug: 500, apSpdBug: 120, + lat: 47.45, lon: -122.31, track: 90, groundspeed: 64, gpsDistNm: 18.4, gpsBearing: 92, + // radios (XP freq units: nav/com in 10 kHz, e.g. 11030 = 110.30) + nav1: 11030, nav1Sb: 11150, nav2: 11380, nav2Sb: 10890, + com1: 12190, com1Sb: 13000, com2: 12475, com2Sb: 12180, + // HSI / data fields + obsCrs: 175, hsiDef: -0.6, hsiToFrom: 1, navBearing: 168, gsDef: 0.7, + baro: 29.92, tas: 131, windSpd: 14, windDir: 240, + xpdrCode: 1200, xpdrMode: 2, fdPitch: 5, fdRoll: -10, + // engine strip (arrays, like the sim) + engRpm: [2410], fuelFlow: [0.0072], oilTemp: [88], oilPress: [52], egt: [720], + fuelQty: [60, 58], volts: [28.0], amps: [12], + }); + // a sample plan so the map/FMS show something in demo mode + fp.setPlan({ name: 'DEMO', waypoints: [ + { id: 'KSEA', lat: 47.449, lon: -122.309, type: 'APT' }, + { id: 'SEA', lat: 47.435, lon: -122.310, type: 'VOR', alt: 4000 }, + { id: 'KPDX', lat: 45.589, lon: -122.597, type: 'APT', alt: 1200 }, + ]}); + let t = 0; + setInterval(() => { + t += 0.1; + state.values.roll = -12 + Math.sin(t) * 4; + state.values.pitch = 4.5 + Math.cos(t * 0.7) * 1.5; + state.values.heading = (87 + Math.sin(t * 0.3) * 3 + 360) % 360; + state.values.track = state.values.heading; + state.values.altitude = 5500 + Math.sin(t * 0.5) * 40; + state.values.airspeed = 124 + Math.sin(t * 0.4) * 3; + // creep south-east so the aircraft visibly moves on the map + state.values.lat -= 0.0006; + state.values.lon -= 0.0009; + broadcast({ type: 'status', xpConnected: true }); + broadcast({ type: 'values', data: state.values }); + }, 100); +} + +server.listen(CONFIG.bridgePort, CONFIG.bridgeHost, () => { + log(`Bridge UI: http://${CONFIG.bridgeHost}:${CONFIG.bridgePort}`); + log(`On tablets: http://:${CONFIG.bridgePort}`); + loadNavData(); // async; FMS resolves idents once ready + if (process.env.DEMO) startDemo(); + else connectXPlane(); +}); diff --git a/server/config.js b/server/config.js new file mode 100644 index 0000000..46f14cb --- /dev/null +++ b/server/config.js @@ -0,0 +1,141 @@ +// Central configuration: which X-Plane datarefs/commands the cockpit needs. +// +// These are *universal* datarefs that work on virtually every aircraft. +// To add a G1000- or aircraft-specific instrument, just add its dataref name +// here under DATAREFS (read) and/or WRITABLE_DATAREFS / COMMANDS (interact). + +export const CONFIG = { + // Where X-Plane's built-in web server listens (on the same PC). X-Plane 12.1.1+. + xplaneHost: process.env.XPLANE_HOST || 'localhost', + xplanePort: Number(process.env.XPLANE_PORT || 8086), + xplaneApiBase: '/api/v3', + + // Where THIS bridge serves the UI + relays data. 0.0.0.0 => reachable from the LAN. + bridgeHost: process.env.BRIDGE_HOST || '0.0.0.0', + bridgePort: Number(process.env.BRIDGE_PORT || 8080), + + // How often X-Plane pushes value updates (it caps near 10–20 Hz anyway). + updateHz: Number(process.env.UPDATE_HZ || 20), +}; + +// Datarefs we SUBSCRIBE to and stream to every client. Keyed by a short alias +// the frontend uses, so the long sim/... names live in exactly one place. +export const DATAREFS = { + // --- primary flight data --- + airspeed: 'sim/cockpit2/gauges/indicators/airspeed_kts_pilot', + altitude: 'sim/cockpit2/gauges/indicators/altitude_ft_pilot', + vspeed: 'sim/cockpit2/gauges/indicators/vvi_fpm_pilot', + pitch: 'sim/cockpit2/gauges/indicators/pitch_AHARS_deg_pilot', + roll: 'sim/cockpit2/gauges/indicators/roll_AHARS_deg_pilot', + heading: 'sim/cockpit2/gauges/indicators/heading_AHARS_deg_mag_pilot', + slip: 'sim/cockpit2/gauges/indicators/slip_deg', + gForce: 'sim/flightmodel/forces/g_nrml', + + // --- position / navigation (for the moving map) --- + lat: 'sim/flightmodel/position/latitude', + lon: 'sim/flightmodel/position/longitude', + groundspeed: 'sim/flightmodel/position/groundspeed', // m/s + track: 'sim/cockpit2/gauges/indicators/ground_track_mag_pilot', // deg + gpsDistNm: 'sim/cockpit2/radios/indicators/gps_dme_distance_nm', + gpsBearing: 'sim/cockpit2/radios/indicators/gps_bearing_deg_mag', + + // --- engine / misc (handy on an MFD) --- + fuelTotal: 'sim/cockpit2/fuel/fuel_quantity', // array + oat: 'sim/cockpit2/temperature/outside_air_temp_degc', + + // --- G1000 PFD: radios (NAV/COM active + standby) --- + nav1: 'sim/cockpit2/radios/actuators/nav1_frequency_hz', + nav1Sb: 'sim/cockpit2/radios/actuators/nav1_standby_frequency_hz', + nav2: 'sim/cockpit2/radios/actuators/nav2_frequency_hz', + nav2Sb: 'sim/cockpit2/radios/actuators/nav2_standby_frequency_hz', + com1: 'sim/cockpit2/radios/actuators/com1_frequency_hz', + com1Sb: 'sim/cockpit2/radios/actuators/com1_standby_frequency_hz', + com2: 'sim/cockpit2/radios/actuators/com2_frequency_hz', + com2Sb: 'sim/cockpit2/radios/actuators/com2_standby_frequency_hz', + + // --- G1000 PFD: HSI / CDI --- + obsCrs: 'sim/cockpit2/radios/actuators/nav1_obs_deg_mag_pilot', + gsDef: 'sim/cockpit/radios/nav1_vdef', // glideslope vertical deflection (dots) + hsiDef: 'sim/cockpit2/radios/indicators/hsi_hdef_dots_pilot', + hsiToFrom: 'sim/cockpit2/radios/indicators/hsi_flag_from_to_pilot', + navBearing: 'sim/cockpit2/radios/indicators/hsi_bearing_deg_mag_pilot', + + // --- G1000 PFD: data fields --- + baro: 'sim/cockpit2/gauges/actuators/barometer_setting_in_hg_pilot', + tas: 'sim/cockpit2/gauges/indicators/true_airspeed_kts_pilot', + windSpd: 'sim/cockpit2/gauges/indicators/wind_speed_kts', + windDir: 'sim/cockpit2/gauges/indicators/wind_heading_deg_mag', + xpdrCode: 'sim/cockpit2/radios/actuators/transponder_code', + xpdrMode: 'sim/cockpit2/radios/actuators/transponder_mode', + fdPitch: 'sim/cockpit2/autopilot/flight_director_pitch_deg', + fdRoll: 'sim/cockpit2/autopilot/flight_director_roll_deg', + + // --- G1000 MFD: engine strip (arrays — UI reads index 0/1) --- + engRpm: 'sim/cockpit2/engine/indicators/engine_speed_rpm', + fuelFlow: 'sim/cockpit2/engine/indicators/fuel_flow_kg_sec', + oilTemp: 'sim/cockpit2/engine/indicators/oil_temperature_deg_C', + oilPress: 'sim/cockpit2/engine/indicators/oil_pressure_psi', + egt: 'sim/cockpit2/engine/indicators/EGT_deg_C', + fuelQty: 'sim/cockpit2/fuel/fuel_quantity', + volts: 'sim/cockpit2/electrical/bus_volts', + amps: 'sim/cockpit2/electrical/battery_amps', + + // --- autopilot readouts (live values, so the panel reflects reality) --- + apState: 'sim/cockpit2/autopilot/autopilot_state', // bitfield of active modes + apHdgBug: 'sim/cockpit2/autopilot/heading_dial_deg_mag_pilot', + apAltBug: 'sim/cockpit2/autopilot/altitude_dial_ft_pilot', + apVsBug: 'sim/cockpit2/autopilot/vvi_dial_fpm', + apSpdBug: 'sim/cockpit2/autopilot/airspeed_dial_kts_mach', + apEngaged: 'sim/cockpit2/autopilot/servos_on', + navHdef: 'sim/cockpit2/radios/indicators/hsi_relative_bearing_vor_pilot', +}; + +// Datarefs the frontend may WRITE (e.g. turning the heading bug knob). +export const WRITABLE_DATAREFS = { + apHdgBug: 'sim/cockpit2/autopilot/heading_dial_deg_mag_pilot', + apAltBug: 'sim/cockpit2/autopilot/altitude_dial_ft_pilot', + apVsBug: 'sim/cockpit2/autopilot/vvi_dial_fpm', + apSpdBug: 'sim/cockpit2/autopilot/airspeed_dial_kts_mach', + xpdrMode: 'sim/cockpit2/radios/actuators/transponder_mode', // 0 off,1 stby,2 on,3 alt + xpdrCode: 'sim/cockpit2/radios/actuators/transponder_code', // 4-digit squawk +}; + +// Commands the frontend may TRIGGER (autopilot mode buttons etc.). +export const COMMANDS = { + apToggle: 'sim/autopilot/servos_toggle', + fdToggle: 'sim/autopilot/fdir_toggle', + hdg: 'sim/autopilot/heading', + nav: 'sim/autopilot/NAV', + apr: 'sim/autopilot/approach', + altHold: 'sim/autopilot/altitude_hold', + vs: 'sim/autopilot/vertical_speed', + flc: 'sim/autopilot/level_change', + vnav: 'sim/autopilot/vnav', + backCourse:'sim/autopilot/back_course', + noseUp: 'sim/autopilot/nose_up', + noseDown: 'sim/autopilot/nose_down', + altUp: 'sim/autopilot/altitude_up', + altDown: 'sim/autopilot/altitude_down', + hdgUp: 'sim/autopilot/heading_up', + hdgDown: 'sim/autopilot/heading_down', + xpdrIdent: 'sim/transponder/transponder_ident', +}; + +// Every clickable G1000 bezel control maps to a real X-Plane command. The PFD +// is unit n1, the MFD is unit n3 (the default C172 layout). Aliases are +// prefixed pfd_/mfd_ so the frontend just says e.g. command('mfd_fpl'). +const G1000_KEYS = [ + ...Array.from({ length: 12 }, (_, i) => `softkey${i + 1}`), + 'direct', 'menu', 'fpl', 'proc', 'clr', 'ent', 'cursor', + 'fms_outer_up', 'fms_outer_down', 'fms_inner_up', 'fms_inner_down', + 'range_up', 'range_down', 'pan_push', 'pan_up', 'pan_down', 'pan_left', 'pan_right', + 'hdg_up', 'hdg_down', 'hdg_sync', + 'alt_outer_up', 'alt_outer_down', 'alt_inner_up', 'alt_inner_down', + 'crs_up', 'crs_down', 'crs_sync', 'baro_up', 'baro_down', + 'nav_outer_up', 'nav_outer_down', 'nav_inner_up', 'nav_inner_down', 'nav12', 'nvol_up', 'nvol_dn', + 'com_outer_up', 'com_outer_down', 'com_inner_up', 'com_inner_down', 'com12', 'cvol_up', 'cvol_dn', + 'ap', 'fd', 'hdg', 'alt', 'nav', 'vnv', 'apr', 'bc', 'vs', 'flc', 'nose_up', 'nose_down', +]; +for (const [unit, prefix] of [['n1', 'pfd'], ['n3', 'mfd']]) { + for (const k of G1000_KEYS) COMMANDS[`${prefix}_${k}`] = `sim/GPS/g1000${unit}_${k}`; +} diff --git a/server/flightplan.js b/server/flightplan.js new file mode 100644 index 0000000..6b0e5ce --- /dev/null +++ b/server/flightplan.js @@ -0,0 +1,100 @@ +// Shared flight plan: one plan, synced to every connected tablet. Can resolve +// idents via navdata, and export an X-Plane .fms file the sim can load. + +import fs from 'node:fs'; +import path from 'node:path'; +import { lookup, xplaneRoot } from './navdata.js'; + +// waypoint: { id, lat, lon, type, alt? } +// activeLeg = index of the waypoint the active (magenta) leg flies TO. The leg +// runs from waypoints[activeLeg-1] to waypoints[activeLeg]. Defaults to 1. +let plan = { name: 'ACTIVE', waypoints: [], activeLeg: 1 }; + +const clampLeg = (i) => Math.max(1, Math.min(plan.waypoints.length - 1, i | 0)); + +export const getPlan = () => plan; + +export function setPlan(next) { + const wps = Array.isArray(next?.waypoints) + ? next.waypoints + .filter((w) => isFinite(w.lat) && isFinite(w.lon)) + .map((w) => ({ id: String(w.id || 'WPT'), lat: +w.lat, lon: +w.lon, type: w.type || 'WPT', alt: w.alt ?? null })) + : []; + const wantLeg = Number.isFinite(next?.activeLeg) ? next.activeLeg : 1; + plan = { name: next?.name || 'ACTIVE', waypoints: wps, activeLeg: Math.max(1, Math.min(wps.length - 1, wantLeg)) || 1 }; + return plan; +} + +export function setActiveLeg(index) { + if (plan.waypoints.length >= 2) plan.activeLeg = clampLeg(index); + return plan; +} + +// Add a waypoint by ident (resolved against navdata) or raw "lat,lon". +export function addWaypoint(input) { + const raw = String(input || '').trim(); + const m = raw.match(/^(-?\d+(?:\.\d+)?)[ ,]+(-?\d+(?:\.\d+)?)$/); + if (m) { + plan.waypoints.push({ id: 'USR', lat: +m[1], lon: +m[2], type: 'USR', alt: null }); + return { ok: true, plan }; + } + const hit = lookup(raw); + if (!hit) return { ok: false, error: `unknown ident: ${raw}` }; + plan.waypoints.push({ ...hit, alt: null }); + return { ok: true, plan }; +} + +export function removeWaypoint(index) { + if (index >= 0 && index < plan.waypoints.length) plan.waypoints.splice(index, 1); + if (plan.waypoints.length >= 2) plan.activeLeg = clampLeg(plan.activeLeg); + return plan; +} + +// ---- great-circle helpers (nm + degrees) ---- +const R_NM = 3440.065; +const rad = (d) => (d * Math.PI) / 180; +const deg = (r) => (r * 180) / Math.PI; + +export function legDistanceNm(a, b) { + const dLat = rad(b.lat - a.lat), dLon = rad(b.lon - a.lon); + const s = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2; + return 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(s))); +} + +export function legBearing(a, b) { + const y = Math.sin(rad(b.lon - a.lon)) * Math.cos(rad(b.lat)); + const x = Math.cos(rad(a.lat)) * Math.sin(rad(b.lat)) - + Math.sin(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.cos(rad(b.lon - a.lon)); + return (deg(Math.atan2(y, x)) + 360) % 360; +} + +// ---- X-Plane .fms (v1100) export ---- +function fmsType(t) { + return { APT: 1, NDB: 2, VOR: 3, WPT: 11, USR: 28 }[t] || 11; +} + +export function exportFms(name = 'WEBFPL') { + const wp = plan.waypoints; + if (wp.length < 2) return { ok: false, error: 'need at least 2 waypoints' }; + + const lines = ['I', '1100 Version', 'CYCLE 2501']; + lines.push(`ADEP ${wp[0].id}`); + lines.push(`ADES ${wp[wp.length - 1].id}`); + lines.push(`NUMENR ${wp.length}`); + for (const w of wp) { + const alt = w.alt ?? 0; + lines.push(`${fmsType(w.type)} ${w.id} ${alt.toFixed(6)} ${w.lat.toFixed(6)} ${w.lon.toFixed(6)}`); + } + const content = lines.join('\n') + '\n'; + + const root = xplaneRoot(); + const dir = root ? path.join(root, 'Output', 'FMS plans') : path.join(process.cwd(), 'fms-out'); + try { + fs.mkdirSync(dir, { recursive: true }); + const file = path.join(dir, `${name}.fms`); + fs.writeFileSync(file, content); + return { ok: true, file, intoXplane: !!root }; + } catch (e) { + return { ok: false, error: e.message }; + } +} diff --git a/server/navdata.js b/server/navdata.js new file mode 100644 index 0000000..405b199 --- /dev/null +++ b/server/navdata.js @@ -0,0 +1,226 @@ +// Reads X-Plane's own navigation data so the FMS can resolve real waypoint / +// VOR / NDB / airport identifiers to coordinates — the same database the sim +// uses. Runs on the X-Plane PC (where the bridge lives), so the files are local. +// +// Everything degrades gracefully: if X-Plane / the files can't be found, the +// FMS still works with map-clicks and raw "LAT,LON" entry. + +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; + +// Common install locations to probe. Override with XPLANE_ROOT. +function candidateRoots() { + const env = process.env.XPLANE_ROOT; + const home = process.env.HOME || process.env.USERPROFILE || ''; + return [ + env, + 'C:/X-Plane 12', 'D:/X-Plane 12', 'E:/X-Plane 12', + 'C:/X-Plane 11', 'D:/X-Plane 11', + path.join(home, 'X-Plane 12'), + path.join(home, 'Desktop', 'X-Plane 12'), + '/Applications/X-Plane 12', + ].filter(Boolean); +} + +function findRoot() { + for (const root of candidateRoots()) { + try { + if (fs.existsSync(path.join(root, 'Resources', 'default data'))) return root; + } catch { /* ignore */ } + } + return null; +} + +// alias -> { lat, lon, type } ; type: WPT | VOR | NDB | APT +const index = new Map(); +// Geographic stores for the moving map (bbox queries) and NEAREST search. +// Airports + navaids stay in flat arrays (small enough to scan); the far more +// numerous fixes go into 1°×1° buckets so a bbox query only scans nearby cells. +const airports = []; // { id, lat, lon, name, elev } +const navaids = []; // { id, lat, lon, type:'VOR'|'NDB', freq, name } +const fixCells = new Map(); // "ilat,ilon" -> [{ id, lat, lon, type:'FIX' }] +const rwyByApt = new Map(); // ICAO -> [{ n1, la1, lo1, n2, la2, lo2, w }] (runway ends + width m) +const state = { root: null, loaded: false, count: 0 }; + +function add(id, lat, lon, type) { + if (!id || !isFinite(lat) || !isFinite(lon)) return; + const key = id.toUpperCase(); + if (!index.has(key)) index.set(key, { id: key, lat, lon, type }); +} + +function pushFix(f) { + const k = `${Math.floor(f.lat)},${Math.floor(f.lon)}`; + let a = fixCells.get(k); + if (!a) { a = []; fixCells.set(k, a); } + a.push(f); +} + +const R_NM = 3440.065; // earth radius in nautical miles +const rad = (d) => (d * Math.PI) / 180; +function distNm(la1, lo1, la2, lo2) { + const dLat = rad(la2 - la1), dLon = rad(lo2 - lo1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(rad(la1)) * Math.cos(rad(la2)) * Math.sin(dLon / 2) ** 2; + return 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(a))); +} +function bearingDeg(la1, lo1, la2, lo2) { + const y = Math.sin(rad(lo2 - lo1)) * Math.cos(rad(la2)); + const x = Math.cos(rad(la1)) * Math.sin(rad(la2)) - Math.sin(rad(la1)) * Math.cos(rad(la2)) * Math.cos(rad(lo2 - lo1)); + return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; +} + +async function parseFixes(file) { + if (!fs.existsSync(file)) return; + const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity }); + for await (const line of rl) { + const t = line.trim(); + if (!t || t === '99' || /^[IA]\b/.test(t) || /Version/.test(t)) continue; + const p = t.split(/\s+/); + const lat = parseFloat(p[0]), lon = parseFloat(p[1]), id = p[2]; + add(id, lat, lon, 'WPT'); + if (id && isFinite(lat) && isFinite(lon)) pushFix({ id: id.toUpperCase(), lat, lon, type: 'FIX' }); + } +} + +async function parseNav(file) { + if (!fs.existsSync(file)) return; + const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity }); + for await (const line of rl) { + const t = line.trim(); + if (!t || t === '99' || /^[IA]\b/.test(t) || /Version/.test(t)) continue; + const p = t.split(/\s+/); + const code = parseInt(p[0], 10); + if (code !== 2 && code !== 3) continue; // 2 = NDB, 3 = VOR/DME + const lat = parseFloat(p[1]), lon = parseFloat(p[2]), id = p[7]; + const type = code === 2 ? 'NDB' : 'VOR'; + add(id, lat, lon, type); + if (id && isFinite(lat) && isFinite(lon)) { + // p[4] = frequency (VOR in 10 kHz e.g. 11630 → 116.30; NDB in kHz); + // name is everything after the airport/region columns. + navaids.push({ id: id.toUpperCase(), lat, lon, type, freq: parseInt(p[4], 10) || 0, name: p.slice(10).join(' ') }); + } + } +} + +// Airports: derive a reference point from each airport's first runway (row 100) +// in apt.dat. The "1" header row carries the ICAO but no coordinates. +async function parseAirports(file) { + if (!fs.existsSync(file)) return; + const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity }); + let icao = null, name = '', elev = 0, placed = false; + const place = (lat, lon) => { + if (!isFinite(lat) || !isFinite(lon)) return; + add(icao, lat, lon, 'APT'); + airports.push({ id: icao.toUpperCase(), lat, lon, name, elev }); + placed = true; + }; + for await (const line of rl) { + const p = line.trim().split(/\s+/); + const code = parseInt(p[0], 10); + if (code === 1 || code === 16 || code === 17) { // land/sea/heliport header + icao = p[4]; elev = parseInt(p[1], 10) || 0; name = p.slice(5).join(' '); placed = false; + } else if (icao && code === 100) { // land runway (both ends) + const r = { n1: p[8], la1: parseFloat(p[9]), lo1: parseFloat(p[10]), n2: p[17], la2: parseFloat(p[18]), lo2: parseFloat(p[19]), w: parseFloat(p[1]) }; + if (isFinite(r.la1) && isFinite(r.lo1) && isFinite(r.la2) && isFinite(r.lo2)) { + const key = icao.toUpperCase(); + let a = rwyByApt.get(key); if (!a) { a = []; rwyByApt.set(key, a); } a.push(r); + if (!placed) place((r.la1 + r.la2) / 2, (r.lo1 + r.lo2) / 2); + } + } else if (!placed && icao && (code === 101 || code === 102)) { // water/heli pad + place(parseFloat(p[code === 101 ? 4 : 5]), parseFloat(p[code === 101 ? 5 : 6])); + } + } +} + +export async function loadNavData() { + const root = findRoot(); + state.root = root; + if (!root) { + console.log('navdata: X-Plane root not found (set XPLANE_ROOT) — FMS works with map-clicks / LAT,LON only'); + state.loaded = true; + return; + } + console.log(`navdata: X-Plane at ${root} — parsing nav data ...`); + const dd = path.join(root, 'Resources', 'default data'); + const cd = path.join(root, 'Custom Data'); // user nav data overrides if present + const pick = (name) => (fs.existsSync(path.join(cd, name)) ? path.join(cd, name) : path.join(dd, name)); + try { + await parseFixes(pick('earth_fix.dat')); + await parseNav(pick('earth_nav.dat')); + // apt.dat is large; parse the global airports file in the background. + parseAirports(path.join(root, 'Global Scenery', 'Global Airports', 'Earth nav data', 'apt.dat')) + .then(() => { state.count = index.size; console.log(`navdata: airports done (${index.size} total entries)`); }) + .catch((e) => console.log('navdata: airport parse skipped:', e.message)); + } catch (e) { + console.log('navdata: parse error:', e.message); + } + state.count = index.size; + state.loaded = true; + console.log(`navdata: ${index.size} fixes/navaids ready`); +} + +export function lookup(id) { + return index.get(String(id).toUpperCase()) || null; +} + +export function search(q, limit = 20) { + const needle = String(q || '').toUpperCase().trim(); + if (!needle) return []; + const exact = [], prefix = []; + for (const v of index.values()) { + if (v.id === needle) exact.push(v); + else if (v.id.startsWith(needle)) prefix.push(v); + if (exact.length + prefix.length > 400) break; + } + return [...exact, ...prefix].slice(0, limit); +} + +// NEAREST: closest airports (default) or navaids to a point, with range/bearing. +export function nearest(lat, lon, { count = 15, type = 'apt' } = {}) { + if (!isFinite(lat) || !isFinite(lon)) return []; + const src = (type === 'vor' || type === 'ndb' || type === 'nav') ? navaids : airports; + return src + .filter((f) => (type === 'vor' || type === 'ndb') ? f.type.toLowerCase() === type : true) + .map((f) => ({ ...f, dist: distNm(lat, lon, f.lat, f.lon), brg: Math.round(bearingDeg(lat, lon, f.lat, f.lon)) })) + .sort((a, b) => a.dist - b.dist) + .slice(0, count) + .map((f) => ({ ...f, dist: +f.dist.toFixed(1) })); +} + +// BBOX: every feature inside a lat/lon window, for the moving map to draw. +// types ⊆ { apt, vor, ndb, fix }. Output is capped so a wide view stays light. +export function bbox(s, w, n, e, types = ['apt', 'vor', 'ndb'], limit = 800) { + const out = []; + const inB = (f) => f.lat >= s && f.lat <= n && f.lon >= w && f.lon <= e; + if (types.includes('apt')) for (const f of airports) { if (inB(f)) { out.push({ ...f, type: 'APT' }); if (out.length >= limit) return out; } } + for (const f of navaids) { if (types.includes(f.type.toLowerCase()) && inB(f)) { out.push(f); if (out.length >= limit) return out; } } + if (types.includes('fix')) { + for (let la = Math.floor(s); la <= Math.floor(n); la++) + for (let lo = Math.floor(w); lo <= Math.floor(e); lo++) { + const a = fixCells.get(`${la},${lo}`); + if (!a) continue; + for (const f of a) { if (inB(f)) { out.push(f); if (out.length >= limit) return out; } } + } + } + return out; +} + +// Runways of every airport within radiusNm — for the PFD's synthetic-vision view. +export function runwaysNear(lat, lon, radiusNm = 12) { + if (!isFinite(lat) || !isFinite(lon)) return []; + const out = []; + for (const a of airports) { + if (distNm(lat, lon, a.lat, a.lon) > radiusNm) continue; + const rs = rwyByApt.get(a.id); + if (rs) for (const r of rs) out.push({ apt: a.id, ...r }); + } + return out; +} + +export function navStatus() { + return { root: state.root, loaded: state.loaded, entries: index.size, airports: airports.length, navaids: navaids.length }; +} + +export function xplaneRoot() { + return state.root; +} diff --git a/server/procedures.js b/server/procedures.js new file mode 100644 index 0000000..d47042b --- /dev/null +++ b/server/procedures.js @@ -0,0 +1,141 @@ +// Parses X-Plane's CIFP procedure data (SIDs / STARs / approaches) on demand — +// one small file per airport in Resources/default data/CIFP/.dat, in the +// ARINC-424-derived "XP CIFP" format. Fix idents are resolved to coordinates +// via the shared navdata index; runway thresholds come from the RWY records. +// +// Used by the G1000 PROC page: list a destination's procedures, then load a +// chosen procedure+transition's leg fixes into the active flight plan. + +import fs from 'node:fs'; +import path from 'node:path'; +import { lookup, xplaneRoot } from './navdata.js'; + +// "N47274972" -> 47.4638.. / "W122183954" -> -122.3109.. +function parseCoord(s) { + if (!s || s.length < 8) return null; + const hemi = s[0]; + const neg = hemi === 'S' || hemi === 'W'; + const digits = s.slice(1); + // lat = DDMMSSss (8) ; lon = DDDMMSSss (9) + const degLen = (hemi === 'E' || hemi === 'W') ? 3 : 2; + const dd = parseInt(digits.slice(0, degLen), 10); + const mm = parseInt(digits.slice(degLen, degLen + 2), 10); + const ss = parseInt(digits.slice(degLen + 2, degLen + 4), 10); + const hh = parseInt(digits.slice(degLen + 4, degLen + 6) || '0', 10); + if (!isFinite(dd) || !isFinite(mm)) return null; + const val = dd + mm / 60 + (ss + hh / 100) / 3600; + return neg ? -val : val; +} + +function cifpFile(icao) { + const root = xplaneRoot(); + if (!root) return null; + const f = path.join(root, 'Resources', 'default data', 'CIFP', `${icao.toUpperCase()}.dat`); + return fs.existsSync(f) ? f : null; +} + +// Parse one airport's procedures into a structured summary + leg store. +// Returns null if the airport has no CIFP file. +export function parseProcedures(icao) { + const file = cifpFile(icao); + if (!file) return null; + + const runways = {}; // RW16C -> { lat, lon, elev } + const groups = { SID: {}, STAR: {}, APPCH: {} }; + // groups[type][procName] = { order:[trans...], legs:{ trans:[{fix,term,alt}] } } + + const ensure = (type, name, trans) => { + const g = groups[type]; + if (!g[name]) g[name] = { order: [], legs: {} }; + if (!(trans in g[name].legs)) { g[name].legs[trans] = []; g[name].order.push(trans); } + return g[name].legs[trans]; + }; + + for (const raw of fs.readFileSync(file, 'utf8').split('\n')) { + const line = raw.trim().replace(/;$/, ''); + if (!line) continue; + const colon = line.indexOf(':'); + if (colon < 0) continue; + const type = line.slice(0, colon); + const f = line.slice(colon + 1).split(','); + + if (type === 'RWY') { + // RWY:RW16C, , ,00429, ,ISZI,3, ;N47274972,W122183954,0000 + const id = f[0]; + const tail = line.split(';')[1] || ''; // "N47274972,W122183954,0000" + const [latS, lonS] = tail.split(','); + const lat = parseCoord(latS), lon = parseCoord(lonS); + if (lat != null && lon != null) runways[id] = { lat, lon, elev: parseInt(f[3], 10) || 0 }; + continue; + } + if (type !== 'SID' && type !== 'STAR' && type !== 'APPCH') continue; + + // f[0]=seqno, f[1]=route type, f[2]=proc, f[3]=transition, f[4]=fix, + // f[11]=path/termination, f[22]=alt flag (+/-), f[23]=altitude. + const procName = f[2]; // BANGR9 / CHINS5 / I16C + const trans = (f[3] || '').trim(); // RW16C / PDT / ERYKA / '' (common) + const fix = (f[4] || '').trim(); // OTLIE / ANVIL / RW16C + const term = (f[11] || '').trim(); // path/termination: IF TF CF DF VA CA ... + const altFlag = (f[22] || '').trim(); + const altVal = parseInt((f[23] || '').trim(), 10); + const alt = isFinite(altVal) && altVal > 0 ? altVal : null; + + const legs = ensure(type, procName, trans || '(common)'); + legs.push({ fix, term, alt, altFlag }); + } + + // Build the client-facing summary (names + their transitions). + const summarize = (g) => Object.entries(g).map(([name, v]) => ({ + name, transitions: v.order.filter((t) => t !== '(common)'), + })); + + return { + icao: icao.toUpperCase(), + runways: Object.keys(runways), + sids: summarize(groups.SID), + stars: summarize(groups.STAR), + approaches: summarize(groups.APPCH), + _groups: groups, + _rwy: runways, + }; +} + +// Resolve a chosen procedure+transition to a list of waypoints with coordinates. +// type: 'sid' | 'star' | 'approach'. Fixes are resolved via the navdata index; +// runway "fixes" (RWxx) and unresolved fixes fall back to the RWY threshold. +export function procedureLegs(icao, type, name, trans) { + const parsed = parseProcedures(icao); + if (!parsed) return []; + const TYPE = { sid: 'SID', star: 'STAR', approach: 'APPCH' }[String(type).toLowerCase()]; + const g = parsed._groups[TYPE]; + if (!g || !g[name]) return []; + const node = g[name]; + + // Compose the leg list: chosen transition first, then the common segment. + // (SID: runway/enroute transition then common climb-out; STAR: enroute entry + // then common arrival; approach: IAF transition then final-approach segment.) + const seq = []; + const wantTrans = trans && node.legs[trans] ? trans : node.order.find((t) => t !== '(common)'); + if (wantTrans && node.legs[wantTrans]) seq.push(...node.legs[wantTrans]); + if (node.legs['(common)']) seq.push(...node.legs['(common)']); + + const out = []; + const seen = new Set(); + for (const leg of seq) { + if (!leg.fix) continue; // heading/altitude legs w/o a fix + if (seen.has(leg.fix)) continue; // de-dupe repeated fixes + let pt = null; + const isRwy = /^RW/.test(leg.fix); + if (isRwy && parsed._rwy[leg.fix]) pt = parsed._rwy[leg.fix]; + else { + const hit = lookup(leg.fix); + if (hit) pt = { lat: hit.lat, lon: hit.lon }; + } + if (!pt) continue; // unresolved fix → skip + seen.add(leg.fix); + out.push({ id: leg.fix, lat: pt.lat, lon: pt.lon, type: isRwy ? 'APT' : 'WPT', alt: leg.alt }); + // An approach ends at the runway threshold — drop the missed-approach legs. + if (TYPE === 'APPCH' && isRwy) break; + } + return out; +} diff --git a/shot.mjs b/shot.mjs new file mode 100644 index 0000000..1be4c15 --- /dev/null +++ b/shot.mjs @@ -0,0 +1,17 @@ +import { chromium } from 'playwright'; + +const PORT = process.env.BRIDGE_PORT || 8099; +const base = `http://localhost:${PORT}`; +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1180, height: 820 }, deviceScaleFactor: 2 }); + +await page.goto(base, { waitUntil: 'networkidle' }); + +const tabs = [['pfd', 'PFD'], ['mfd', 'MFD'], ['map', 'Map'], ['fms', 'FMS'], ['ap', 'Autopilot']]; +for (const [tab, label] of tabs) { + await page.getByRole('button', { name: label, exact: true }).click(); + await page.waitForTimeout(tab === 'map' || tab === 'pfd' ? 4000 : 700); // tiles/terrain + await page.screenshot({ path: `/tmp/cockpit-${tab}.png` }); + console.log(`shot: /tmp/cockpit-${tab}.png`); +} +await browser.close(); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..c976c20 --- /dev/null +++ b/web/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + X-Plane Glass Cockpit + + + + + +
+ + + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..6d58eab --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1973 @@ +{ + "name": "xplane-glass-cockpit-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xplane-glass-cockpit-web", + "version": "0.1.0", + "dependencies": { + "leaflet": "^1.9.4", + "maplibre-gl": "^5.24.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz", + "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.2" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz", + "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.8.5", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.5.tgz", + "integrity": "sha512-EzEJmMt6thioRH7GI9LWS7ahXTcAhAPGWCe6oTP2Ps4YnsXOOAfeqx854lZaiDnwURfHmcCKV1mr6oo0i23x6w==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.11.tgz", + "integrity": "sha512-dKvjKdITw9d0y3ndGkSqLUEpWCizMtdq8NB06cHohH/JZ2sJoM7dClR9wzJLUWykjbw9RXDFmhjjNBnNW27mzw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz", + "integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@maplibre/geojson-vt": "^5.0.4", + "@types/geojson": "^7946.0.16", + "@types/supercluster": "^7.1.3", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", + "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", + "license": "ISC" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kdbush": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", + "license": "ISC" + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/maplibre-gl": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", + "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.1.0", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/geojson-vt": "^6.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.8.1", + "@maplibre/mlt": "^1.1.8", + "@maplibre/vt-pbf": "^4.3.0", + "@types/geojson": "^7946.0.16", + "earcut": "^3.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/pbf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz", + "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..e097ce6 --- /dev/null +++ b/web/package.json @@ -0,0 +1,21 @@ +{ + "name": "xplane-glass-cockpit-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "leaflet": "^1.9.4", + "maplibre-gl": "^5.24.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^5.4.11" + } +} diff --git a/web/public/icons/apple-touch-icon.png b/web/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..6769486 Binary files /dev/null and b/web/public/icons/apple-touch-icon.png differ diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png new file mode 100644 index 0000000..3289bb2 Binary files /dev/null and b/web/public/icons/icon-192.png differ diff --git a/web/public/icons/icon-512-maskable.png b/web/public/icons/icon-512-maskable.png new file mode 100644 index 0000000..9604b71 Binary files /dev/null and b/web/public/icons/icon-512-maskable.png differ diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png new file mode 100644 index 0000000..124ee1b Binary files /dev/null and b/web/public/icons/icon-512.png differ diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest new file mode 100644 index 0000000..15253fe --- /dev/null +++ b/web/public/manifest.webmanifest @@ -0,0 +1,16 @@ +{ + "name": "G1000 Glass Cockpit", + "short_name": "G1000", + "description": "X-Plane 12 G1000 glass cockpit — PFD / MFD / FMS over your LAN", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "landscape", + "background_color": "#000000", + "theme_color": "#000000", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..30a599c --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,31 @@ +// Minimal service worker: caches the app shell so the cockpit launches fast and +// survives brief network blips. Live data (the bridge WebSocket, /api, and map +// tiles) is never cached — only same-origin GET app assets. +const CACHE = 'g1000-shell-v1'; + +self.addEventListener('install', () => self.skipWaiting()); + +self.addEventListener('activate', (e) => { + e.waitUntil( + caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (e) => { + const url = new URL(e.request.url); + // Only same-origin GET app shell; skip the API and let the WS pass through. + if (e.request.method !== 'GET' || url.origin !== location.origin) return; + if (url.pathname.startsWith('/api') || url.pathname === '/ws') return; + + // Stale-while-revalidate: serve cache fast, refresh in the background. + e.respondWith( + caches.open(CACHE).then(async (cache) => { + const cached = await cache.match(e.request); + const network = fetch(e.request) + .then((res) => { if (res && res.ok) cache.put(e.request, res.clone()); return res; }) + .catch(() => cached); + return cached || network; + }) + ); +}); diff --git a/web/src/App.jsx b/web/src/App.jsx new file mode 100644 index 0000000..0aaee80 --- /dev/null +++ b/web/src/App.jsx @@ -0,0 +1,115 @@ +import React, { useState } 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'; + +// 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', +}; +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' }, +]; + +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; }); + // 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(true); + // 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 }); + // The NRST (nearest airports/navaids) window, toggled by the PFD NRST softkey. + const [nrst, setNrst] = useState(false); + // The TMR/REF (timer / references) window, toggled by the PFD TMR/REF softkey. + const [tmr, setTmr] = useState(false); + // MFD map mode (base layer), switched via the Map-Opt softkeys. + const [mapMode, setMapMode] = useState({ base: 'topo' }); + // Direct-To (D→) dialog — opened from the bezel on either GDU. + const [dto, setDto] = useState(false); + // PROC (procedures: SID/STAR/approach) dialog — opened from the bezel. + const [proc, setProc] = useState(false); + + const connKind = xp.xpConnected ? 'ok' : xp.connected ? 'warn' : 'bad'; + const connText = xp.xpConnected ? 'X-PLANE' : xp.connected ? 'NO SIM' : 'OFFLINE'; + + return ( +
+ + +
+ {tab === 'pfd' && ( + setSvt3d((v) => !v)} + inset={inset} onSetInset={setInset} insetMode={insetMode} onInsetMode={setInsetMode} + nrst={nrst} onToggleNrst={() => setNrst((v) => !v)} onDirect={() => setDto(true)} + tmr={tmr} onToggleTmr={() => setTmr((v) => !v)} onProc={() => setProc(true)}> + setNrst(false)} + tmr={tmr} onCloseTmr={() => setTmr(false)} flightPlan={xp.flightPlan} fp={xp.fp} /> + + )} + {tab === 'mfd' && ( + setDto(true)} onProc={() => setProc(true)}> + + + )} + {tab === 'map' && } + {tab === 'fms' && } + {tab === 'vfr' && } + {tab === 'ap' && } +
+ {dto && setDto(false)} />} + {proc && setProc(false)} />} +
+ ); +} diff --git a/web/src/api/useXplane.js b/web/src/api/useXplane.js new file mode 100644 index 0000000..93580d1 --- /dev/null +++ b/web/src/api/useXplane.js @@ -0,0 +1,88 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +// Single WebSocket connection to the bridge. Streams dataref values + the +// shared flight plan in; sends commands / dataref writes / flight-plan edits +// out. Auto-reconnects. +export function useXplane() { + const [values, setValues] = useState({}); + const [flightPlan, setFlightPlan] = useState({ name: 'ACTIVE', waypoints: [] }); + const [exportMsg, setExportMsg] = useState(null); + const [connected, setConnected] = useState(false); // socket to bridge + const [xpConnected, setXpConnected] = useState(false); // bridge <-> X-Plane + const wsRef = useRef(null); + + useEffect(() => { + let closed = false; + let retry; + // Coalesce incoming value bursts into a single React update per animation + // frame — keeps the gauges smooth instead of re-rendering ~20×/sec. + let pending = null; + let raf = 0; + const flush = () => { + raf = 0; + if (pending) { const p = pending; pending = null; setValues((prev) => ({ ...prev, ...p })); } + }; + + const connect = () => { + const proto = location.protocol === 'https:' ? 'wss' : 'ws'; + const ws = new WebSocket(`${proto}://${location.host}/ws`); + wsRef.current = ws; + + ws.onopen = () => setConnected(true); + ws.onmessage = (ev) => { + const msg = JSON.parse(ev.data); + if (msg.type === 'values') { + pending = pending ? Object.assign(pending, msg.data) : { ...msg.data }; + if (!raf) raf = requestAnimationFrame(flush); + } + else if (msg.type === 'status') setXpConnected(!!msg.xpConnected); + else if (msg.type === 'flightplan') setFlightPlan(msg.data); + else if (msg.type === 'fp_export_result') setExportMsg(msg); + }; + ws.onclose = () => { + setConnected(false); + setXpConnected(false); + if (!closed) retry = setTimeout(connect, 2000); + }; + ws.onerror = () => ws.close(); + }; + + connect(); + return () => { closed = true; clearTimeout(retry); wsRef.current?.close(); }; + }, []); + + const send = useCallback((obj) => { + const ws = wsRef.current; + if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); + }, []); + + const command = useCallback((name, duration = 0) => send({ type: 'command', name, duration }), [send]); + const setDataref = useCallback((name, value) => send({ type: 'setDataref', name, value }), [send]); + + // flight-plan actions + const fp = { + add: (ident) => send({ type: 'fp_add', ident }), + addLatLon: (lat, lon) => send({ type: 'fp_add', ident: `${lat},${lon}` }), + remove: (index) => send({ type: 'fp_remove', index }), + setActive: (index) => send({ type: 'fp_active', index }), + clear: () => send({ type: 'fp_clear' }), + set: (plan) => send({ type: 'fp_set', plan }), + export: (name) => send({ type: 'fp_export', name }), + }; + + return { values, flightPlan, exportMsg, connected, xpConnected, command, setDataref, fp }; +} + +// Search X-Plane's nav database (waypoints/VOR/NDB/airports) via the bridge. +export async function navSearch(q) { + if (!q) return []; + try { + const r = await fetch(`/api/nav/search?q=${encodeURIComponent(q)}`); + return r.ok ? r.json() : []; + } catch { + return []; + } +} + +// Convenience: read a numeric value with a fallback. +export const num = (v, d = 0) => (typeof v === 'number' && isFinite(v) ? v : d); diff --git a/web/src/components/AutopilotPanel.jsx b/web/src/components/AutopilotPanel.jsx new file mode 100644 index 0000000..2b4f82b --- /dev/null +++ b/web/src/components/AutopilotPanel.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { num } from '../api/useXplane.js'; + +// Autopilot / AFCS mode-control panel — styled like a Garmin GMC-710 mode +// controller: an annunciator bar on top (active = green, armed = white), a row +// of lit mode keys, and selectors (HDG / ALT / VS / IAS) with knob steppers. +// Buttons fire X-Plane's own autopilot commands; the sim stays the source of truth. + +const AP_BITS = { + fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, + nav: 1 << 8, apr: 1 << 9, vnav: 1 << 11, altHold: 1 << 14, bc: 1 << 18, +}; +const on = (s, b) => (num(s) & b) !== 0; + +export default function AutopilotPanel({ xp }) { + const { values: V, command, setDataref } = xp; + const s = num(V.apState); + const eng = num(V.apEngaged) > 0; + + const lateral = on(s, AP_BITS.apr) ? 'APR' : on(s, AP_BITS.nav) ? 'NAV' : on(s, AP_BITS.hdg) ? 'HDG' : 'ROL'; + const vertical = on(s, AP_BITS.flc) ? 'FLC' : on(s, AP_BITS.vs) ? 'VS' : on(s, AP_BITS.vnav) ? 'VNV' : on(s, AP_BITS.altHold) ? 'ALT' : 'PIT'; + + const Key = ({ label, cmd, active }) => ( + + ); + + const Sel = ({ label, value, unit, alias, step, dn, up, pad }) => ( +
+
{label}
+
{pad ? String(((Math.round(value) % 360) + 360) % 360).padStart(3, '0') : Math.round(value)}{unit}
+
+ + + +
+
+ ); + + return ( +
+
+ {/* annunciator bar */} +
+ AP + FD + + {lateral} + + {vertical} + {Math.round(num(V.apAltBug))}FT +
+ + {/* mode keys */} +
+ + + + + + + + + + +
+ + {/* selectors */} +
+ + + + +
+ +
+ PITCH + + +
+
+
+ ); +} diff --git a/web/src/components/Bezel.jsx b/web/src/components/Bezel.jsx new file mode 100644 index 0000000..3196b28 --- /dev/null +++ b/web/src/components/Bezel.jsx @@ -0,0 +1,225 @@ +import React, { useState } from 'react'; +import { num } from '../api/useXplane.js'; + +// The physical GDU bezel of X-Plane's "XPLANE 1000" (its G1000 clone): +// title bar, knob columns, the 12 softkeys along the bottom — and, on the MFD, +// the autopilot mode controller built into the left bezel (just like the sim). +// +// EVERY control is clickable and fires the matching real X-Plane command +// (sim/GPS/g1000n1_* on the PFD, g1000n3_* on the MFD) via xp.command(). +// +// The PFD softkeys are a two-level menu, exactly like the real unit: +// root → [INSET · PFD · CDI · DME · XPDR · IDENT · TMR/REF · NRST · CAUTION] +// PFD → [PATHWAY · SYN TERR · HRZN HDG · APTSIGNS · … · BACK] +// SYN TERR toggles the 3D synthetic-vision terrain on/off. +const PFD_MENU = { + root: ['', 'INSET', '', 'PFD', '', 'CDI', 'DME', 'XPDR', 'IDENT', 'TMR/REF', 'NRST', 'CAUTION'], + pfd: ['PATHWAY', 'SYN TERR', 'HRZN HDG', 'APTSIGNS', '', '', '', '', '', '', '', 'BACK'], + // XPDR submenu: standby/on/alt modes, VFR (1200), CODE entry, IDENT. + xpdr: ['STBY', 'ON', 'ALT', 'VFR', '', 'CODE', 'IDENT', '', '', '', '', 'BACK'], + // CODE entry turns the softkeys into the octal squawk keypad (digits 0–7). + xpdrcode: ['0', '1', '2', '3', '4', '5', '6', '7', 'BKSP', '', 'BACK', ''], + // INSET submenu: on/off, declutter, base layer, OFF, back. + inset: ['INSET', 'DCLTR', '', 'TOPO', 'TERRAIN', '', '', '', '', '', 'OFF', 'BACK'], +}; +// MFD softkeys are a two-level menu like the real unit. MAP opens the Map-Opt +// page; TOPO/TERRAIN/OSM switch the base map; BACK returns. (OSM is our tuned +// extra layer in an otherwise-empty slot.) +const MFD_MENU = { + root: ['SYSTEM', 'MAP', '', '', '', '', '', '', '', 'DCLTR', '', ''], + mapopt: ['TRAFFIC', 'PROFILE', 'TOPO', 'TERRAIN', 'AIRWAYS', '', 'NEXRAD', 'OSM', '', '', 'BACK', ''], + system: ['DEC FUEL', 'INC FUEL', 'RST FUEL', '', '', '', '', '', '', '', 'BACK', ''], +}; + +// autopilot_state bitfield (best-effort; tweak per aircraft) +const AP_BITS = { fd: 1 << 0, hdg: 1 << 1, vs: 1 << 4, flc: 1 << 6, nav: 1 << 8, apr: 1 << 9, vnav: 1 << 11, altHold: 1 << 14, bc: 1 << 18 }; + +export default function Bezel({ variant = 'pfd', xp, svt3d, onToggleSvt, inset, onSetInset, insetMode, onInsetMode, nrst, onToggleNrst, tmr, onToggleTmr, onDirect, onProc, mapMode, onMapMode, children }) { + const u = variant === 'mfd' ? 'mfd' : 'pfd'; // command prefix + const fire = (suffix) => xp && xp.command(`${u}_${suffix}`); + const [page, setPage] = useState('root'); // softkey menu page + const [squawk, setSquawk] = useState(''); // XPDR code being typed + + const menu = variant === 'mfd' ? MFD_MENU : PFD_MENU; + const keys = menu[page] || menu.root; + const setBase = (b) => onMapMode && onMapMode((m) => ({ ...m, base: m.base === b ? 'dark' : b })); + const xpdrMode = num(xp?.values?.xpdrMode); + const setMode = (m) => xp && xp.setDataref('xpdrMode', m); + + const typeDigit = (d) => { + const next = (squawk + d).slice(-4); + setSquawk(next); + if (next.length === 4) { // 4 octal digits → write & exit + xp && xp.setDataref('xpdrCode', parseInt(next, 10)); + setSquawk(''); setPage('xpdr'); + } + }; + + const onSoftkey = (i, label) => { + fire(`softkey${i + 1}`); // mirror to the in-sim G1000 + if (variant === 'mfd') { + if (label === 'MAP') setPage('mapopt'); + else if (label === 'SYSTEM') setPage('system'); + else if (label === 'BACK') setPage('root'); + else if (label === 'TOPO') setBase('topo'); + else if (label === 'TERRAIN') setBase('terrain'); + else if (label === 'OSM') setBase('osm'); + else if (label === 'DCLTR') onMapMode && onMapMode((m) => ({ ...m, dcltr: m.dcltr ? 0 : 1 })); + } else { + if (label === 'PFD') setPage('pfd'); + else if (label === 'BACK') setPage(page === 'xpdrcode' ? 'xpdr' : 'root'); + else if (label === 'SYN TERR') onToggleSvt && onToggleSvt(); + else if (label === 'INSET') { + if (page === 'root') { onSetInset && onSetInset(true); setPage('inset'); } + else onSetInset && onSetInset(!inset); // toggle from within the submenu + } + else if (label === 'OFF') { onSetInset && onSetInset(false); setPage('root'); } + else if (label === 'DCLTR') onInsetMode && onInsetMode((m) => ({ ...m, dcltr: m.dcltr ? 0 : 1 })); + else if (label === 'TOPO') onInsetMode && onInsetMode((m) => ({ ...m, base: 'topo' })); + else if (label === 'TERRAIN') onInsetMode && onInsetMode((m) => ({ ...m, base: 'terrain' })); + else if (label === 'NRST') onToggleNrst && onToggleNrst(); + else if (label === 'TMR/REF') onToggleTmr && onToggleTmr(); + else if (label === 'XPDR') setPage('xpdr'); + else if (label === 'STBY') setMode(1); + else if (label === 'ON') setMode(2); + else if (label === 'ALT') setMode(3); + else if (label === 'VFR') xp && xp.setDataref('xpdrCode', 1200); + else if (label === 'CODE') { setSquawk(''); setPage('xpdrcode'); } + else if (label === 'IDENT') xp && xp.command('xpdrIdent'); + else if (label === 'BKSP') setSquawk((s) => s.slice(0, -1)); + else if (page === 'xpdrcode' && /^[0-7]$/.test(label)) typeDigit(label); + } + }; + // which softkey is "lit" right now + const isOn = (label) => { + if (variant === 'mfd') return (label === 'TOPO' && mapMode?.base === 'topo') + || (label === 'TERRAIN' && mapMode?.base === 'terrain') || (label === 'OSM' && mapMode?.base === 'osm') + || (label === 'DCLTR' && mapMode?.dcltr > 0); + return (label === 'SYN TERR' && svt3d) || (label === 'INSET' && inset) || (label === 'NRST' && nrst) || (label === 'TMR/REF' && tmr) + || (label === 'STBY' && xpdrMode === 1) || (label === 'ON' && xpdrMode === 2) || (label === 'ALT' && xpdrMode === 3) + || (page === 'inset' && label === 'TOPO' && insetMode?.base === 'topo') + || (page === 'inset' && label === 'TERRAIN' && insetMode?.base === 'terrain') + || (label === 'DCLTR' && insetMode?.dcltr > 0); + }; + + return ( +
+
+ + + {variant === 'mfd' && xp && } + +
+ +
+
XPLANE 1000
+
{children}
+ {page === 'xpdrcode' && ( +
SQUAWK {squawk.padEnd(4, '_')}
+ )} +
+ {keys.map((s, i) => ( + + ))} +
+
+ +
+ + + +
+ D→MENU + FPLPROC + CLRENT +
+ +
+
+ ); +} + +function BtnG({ fire, cmd, onClick, children }) { + return ; +} + +// Autopilot mode controller (left bezel of the MFD). Buttons fire real X-Plane +// commands; active modes light up from autopilot_state / servos_on. +function APController({ xp }) { + const st = num(xp.values.apState); + const on = (bit) => (st & bit) !== 0; + const eng = num(xp.values.apEngaged) > 0; + const B = ({ label, cmd, active }) => ( + + ); + return ( +
+ + + + + + + + + + + + +
+ ); +} + +// Concentric G1000 knob. The outer ring rotates via the side arrows (‹ ›) and +// the mouse wheel; the inner ring via the top/bottom arrows (˄ ˅) and shift+wheel. +// Clicking the knob centre fires the push action (PUSH …). The RANGE knob also +// pans with a directional cross. +function Knob({ label, sub, outer, inner, push, big, joy, pan, fire }) { + const onWheel = (e) => { + if (!outer) return; + e.preventDefault(); + const set = (e.shiftKey && inner) ? inner : outer; + fire(e.deltaY < 0 ? set[0] : set[1]); + }; + return ( +
+ {label} +
+ {inner && } + {outer && } + + {outer && } + {inner && } +
+ {pan && ( +
+ + + + +
+ )} + {sub && {sub}} +
+ ); +} diff --git a/web/src/components/CDU.jsx b/web/src/components/CDU.jsx new file mode 100644 index 0000000..8222b35 --- /dev/null +++ b/web/src/components/CDU.jsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import { num, navSearch } from '../api/useXplane.js'; + +// FMS as an X-Plane-style CDU/FMC: a green screen showing the active flight plan +// as legs, six line-select keys per side, a scratchpad, and an alphanumeric +// keypad. Edits go through the shared flight plan (the same one the PFD/MFD use). +// +// LSK (left, per row): +// • scratchpad has an ident → insert that waypoint at the row +// • DEL armed → delete the leg at the row +// • otherwise → make that leg the active (magenta) leg (Direct-To) +// EXEC exports the plan to X-Plane as an .fms file. + +const R_NM = 3440.065, rad = (d) => d * Math.PI / 180, deg = (r) => r * 180 / Math.PI; +function distNm(a, b) { + const dLat = rad(b.lat - a.lat), dLon = rad(b.lon - a.lon); + const s = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2; + return 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(s))); +} +function brng(a, b) { + const y = Math.sin(rad(b.lon - a.lon)) * Math.cos(rad(b.lat)); + const x = Math.cos(rad(a.lat)) * Math.sin(rad(b.lat)) - Math.sin(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.cos(rad(b.lon - a.lon)); + return (deg(Math.atan2(y, x)) + 360) % 360; +} + +const ROWS = 5; // legs visible per page + +export default function CDU({ xp }) { + const { flightPlan, fp, exportMsg } = xp; + const wps = flightPlan.waypoints || []; + const active = Math.max(1, Math.min(wps.length - 1, flightPlan.activeLeg ?? 1)); + const [scr, setScr] = useState(''); + const [del, setDel] = useState(false); + const [msg, setMsg] = useState(''); + const [page, setPage] = useState(0); + + const pages = Math.max(1, Math.ceil((wps.length + 1) / ROWS)); + const start = page * ROWS; + + const type = (ch) => { setMsg(''); setScr((s) => (s + ch).slice(0, 8)); }; + const clr = () => { if (scr) setScr((s) => s.slice(0, -1)); else { setDel(false); setMsg(''); } }; + + // resolve an ident and splice it into the plan at `index` + const insertAt = async (ident, index) => { + const hits = await navSearch(ident); + const hit = hits[0]; + if (!hit) { setMsg('NOT IN DATABASE'); return; } + const next = wps.slice(); + next.splice(index, 0, { id: hit.id, lat: hit.lat, lon: hit.lon, type: hit.type || 'WPT', alt: null }); + fp.set({ name: 'ACTIVE', waypoints: next, activeLeg: flightPlan.activeLeg ?? 1 }); + setScr(''); + }; + + const lsk = (rowIdx) => { + const i = start + rowIdx; + if (scr) { insertAt(scr, Math.min(i, wps.length)); return; } + if (del) { if (i < wps.length) fp.remove(i); setDel(false); return; } + if (i >= 1 && i < wps.length) fp.setActive(i); + }; + + const exec = () => { if (wps.length >= 2) fp.export('WEBFPL'); else setMsg('NEED 2 WAYPOINTS'); }; + + // build the visible rows + const rows = []; + for (let r = 0; r < ROWS; r++) { + const i = start + r; + if (i < wps.length) { + const w = wps[i], prev = wps[i - 1]; + const d = prev ? distNm(prev, w) : 0; + const dtk = prev ? Math.round(brng(prev, w)) : null; + rows.push({ i, id: w.id, type: w.type, d, dtk, orig: i === 0, act: i === active }); + } else if (i === wps.length) { + rows.push({ i, empty: true }); + } else { + rows.push({ i, blank: true }); + } + } + + const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + const KEYS = [A.slice(0, 7), A.slice(7, 14), A.slice(14, 21), A.slice(21, 26).concat([' ']), ['1', '2', '3', '4', '5'], ['6', '7', '8', '9', '0']]; + + const Lsk = ({ side, r }) => + + + + + + +
+ {KEYS.map((rowK, ri) => ( +
+ {rowK.map((k) => ( + + ))} +
+ ))} +
+ + + ); +} diff --git a/web/src/components/DirectTo.jsx b/web/src/components/DirectTo.jsx new file mode 100644 index 0000000..d3af2d7 --- /dev/null +++ b/web/src/components/DirectTo.jsx @@ -0,0 +1,91 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { num, navSearch } from '../api/useXplane.js'; + +// G1000 Direct-To (D→) dialog. Type or pick a waypoint ident; ACTIVATE flies a +// direct magenta leg from the present position to it. We model that by setting +// the shared flight plan to [PPOS → target] (the map/HSI already draw the leg) +// and also firing the in-sim "direct" command so the real G1000 follows along. +const R_NM = 3440.065; +const rad = (d) => (d * Math.PI) / 180; +function distBrg(la1, lo1, la2, lo2) { + const dLat = rad(la2 - la1), dLon = rad(lo2 - lo1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(rad(la1)) * Math.cos(rad(la2)) * Math.sin(dLon / 2) ** 2; + const dist = 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(a))); + const y = Math.sin(rad(lo2 - lo1)) * Math.cos(rad(la2)); + const x = Math.cos(rad(la1)) * Math.sin(rad(la2)) - Math.sin(rad(la1)) * Math.cos(rad(la2)) * Math.cos(rad(lo2 - lo1)); + const brg = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; + return { dist, brg }; +} + +export default function DirectTo({ xp, onClose }) { + const { values, fp, command } = xp; + const [entry, setEntry] = useState(''); + const [hits, setHits] = useState([]); + const [sel, setSel] = useState(null); // chosen { id, lat, lon, type } + const inputRef = useRef(null); + + useEffect(() => { inputRef.current?.focus(); }, []); + + // Live ident search against X-Plane's nav database. + useEffect(() => { + const q = entry.trim(); + if (q.length < 2 || /[,\s]/.test(q)) { setHits([]); return; } + let alive = true; + navSearch(q).then((r) => alive && setHits(r.slice(0, 6))); + return () => { alive = false; }; + }, [entry]); + + const lat = num(values.lat), lon = num(values.lon); + const preview = sel && isFinite(lat) ? distBrg(lat, lon, sel.lat, sel.lon) : null; + + const activate = () => { + if (!sel) return; + fp.set({ name: 'ACTIVE', waypoints: [ + { id: 'PPOS', lat, lon, type: 'USR' }, + { id: sel.id, lat: sel.lat, lon: sel.lon, type: sel.type || 'WPT' }, + ] }); + command('direct'); // mirror to the in-sim G1000 + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +
D→ DIRECT TO
+
+ + { setEntry(e.target.value.toUpperCase()); setSel(null); }} + onKeyDown={(e) => { if (e.key === 'Enter' && sel) activate(); if (e.key === 'Escape') onClose(); }} + placeholder="IDENT (z.B. KSEA, SEA, ELN)" + autoCapitalize="characters" autoCorrect="off" spellCheck="false" + /> + {hits.length > 0 && ( +
+ {hits.map((h) => ( + + ))} +
+ )} + {sel && ( +
+ {sel.id} + {sel.type} + {preview && {String(Math.round(preview.brg)).padStart(3, '0')}° · {preview.dist.toFixed(1)} NM} +
+ )} +
+
+ + +
+
+
+ ); +} diff --git a/web/src/components/FMS.jsx b/web/src/components/FMS.jsx new file mode 100644 index 0000000..3357766 --- /dev/null +++ b/web/src/components/FMS.jsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from 'react'; +import { num, navSearch } from '../api/useXplane.js'; + +const R_NM = 3440.065; +const rad = (d) => (d * Math.PI) / 180; +const deg = (r) => (r * 180) / Math.PI; +function distNm(a, b) { + const dLat = rad(b.lat - a.lat), dLon = rad(b.lon - a.lon); + const s = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2; + return 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(s))); +} +function bearing(a, b) { + const y = Math.sin(rad(b.lon - a.lon)) * Math.cos(rad(b.lat)); + const x = Math.cos(rad(a.lat)) * Math.sin(rad(b.lat)) - + Math.sin(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.cos(rad(b.lon - a.lon)); + return (deg(Math.atan2(y, x)) + 360) % 360; +} + +export default function FMS({ xp }) { + const { flightPlan, fp, values, exportMsg } = xp; + const wps = flightPlan.waypoints || []; + const [entry, setEntry] = useState(''); + const [hits, setHits] = useState([]); + + // live ident search against X-Plane's nav database + useEffect(() => { + const q = entry.trim(); + if (q.length < 2 || /[,\s]/.test(q)) { setHits([]); return; } + let alive = true; + navSearch(q).then((r) => alive && setHits(r.slice(0, 6))); + return () => { alive = false; }; + }, [entry]); + + const add = (id) => { fp.add(id || entry.trim()); setEntry(''); setHits([]); }; + + let total = 0; + const rows = wps.map((w, i) => { + const prev = wps[i - 1]; + const d = prev ? distNm(prev, w) : 0; + const brg = prev ? bearing(prev, w) : null; + total += d; + return { w, i, d, brg }; + }); + + const gs = num(values.groundspeed) * 1.94384; + const ete = gs > 20 ? total / gs : null; // hours + const active = Math.max(1, Math.min(wps.length - 1, flightPlan?.activeLeg ?? 1)); + + return ( +
+
+ FLIGHT PLAN + + {total.toFixed(0)} NM{ete ? ` · ETE ${fmtHrs(ete)}` : ''} + +
+ +
+
+ #WPTDTKDIST +
+ {rows.length === 0 &&
Kein Flugplan — Wegpunkt eingeben oder auf der Map tippen.
} + {rows.map(({ w, i, d, brg }) => ( +
i >= 1 && fp.setActive(i)} title={i >= 1 ? 'Als aktives Bein setzen' : ''}> + {i + 1} + {w.id}{w.type} + {brg == null ? '—' : `${String(Math.round(brg)).padStart(3, '0')}°`} + {i === 0 ? 'ORIG' : `${d.toFixed(1)}`} + +
+ ))} +
+ +
+ {hits.length > 0 && ( +
+ {hits.map((h) => ( + + ))} +
+ )} +
+ setEntry(e.target.value.toUpperCase())} + onKeyDown={(e) => e.key === 'Enter' && add()} + placeholder="IDENT (z.B. KSEA, SEA) oder LAT,LON" + autoCapitalize="characters" autoCorrect="off" spellCheck="false" + /> + +
+
+ + +
+ {exportMsg && ( +
+ {exportMsg.ok + ? (exportMsg.intoXplane + ? `✓ Gespeichert in X-Plane: ${shorten(exportMsg.file)} — im Flieger-FMS unter „Load" wählen.` + : `✓ Datei geschrieben: ${shorten(exportMsg.file)} (X-Plane-Ordner nicht gefunden — XPLANE_ROOT setzen).`) + : `✗ ${exportMsg.error}`} +
+ )} +
+
+ ); +} + +function fmtHrs(h) { + const m = Math.round(h * 60); + return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}`; +} +function shorten(p) { + return p && p.length > 48 ? '…' + p.slice(-46) : p; +} diff --git a/web/src/components/MFD.jsx b/web/src/components/MFD.jsx new file mode 100644 index 0000000..3f44aaf --- /dev/null +++ b/web/src/components/MFD.jsx @@ -0,0 +1,210 @@ +import React, { useState } from 'react'; +import { num } from '../api/useXplane.js'; +import MapView from './MapView.jsx'; + +const arr = (v, i = 0, d = 0) => (Array.isArray(v) ? num(v[i], d) : num(v, d)); +const KG_PER_GAL = 2.72; // avgas +const navF = (v) => (num(v) / 100).toFixed(2); +const comF = (v) => (num(v) / 100).toFixed(3); + +// G1000 MFD — full-width NAV/COM bar on top, the engine instrument strip (EIS) +// down the left as real bar gauges, and the moving map (X-Plane nav data) with +// G1000 chrome (compass rose, range, NORTH UP, mode) filling the rest. +export default function MFD({ values: V, flightPlan, fp, mapMode }) { + const [rangeNm, setRangeNm] = useState(8); + return ( +
+ +
+ +
+ setRangeNm(rangeNm)} /> + +
+
+
+ ); +} + +/* ---------------- top NAV/COM bar ---------------- */ +function MfdTopBar({ V }) { + const gs = Math.round(num(V.groundspeed) * 1.94384); + const trk = String(Math.round(num(V.track)) % 360).padStart(3, '0'); + const swap = (x, y) => ; + return ( + + + {[300, 660].map((x) => )} + + {/* NAV1 / NAV2 */} + NAV1 + + {navF(V.nav1)} + {swap(150, 27)} + {navF(V.nav1Sb)} + NAV2 + {navF(V.nav2)} + {navF(V.nav2Sb)} + {/* centre: GS/DTK/TRK/ETE + active mode line */} + GS + {gs} + KT + DTK + TRK + {trk}° + ETE + NAV – DEFAULT NAV + {/* COM1 / COM2 */} + {comF(V.com1)} + {swap(818, 27)} + + {comF(V.com1Sb)} + COM1 + {comF(V.com2)} + {comF(V.com2Sb)} + COM2 + + ); +} + +/* ---------------- engine instrument strip (EIS) ---------------- */ +function EisStrip({ V }) { + const rpm = arr(V.engRpm); + const ffGph = (arr(V.fuelFlow) * 3600) / KG_PER_GAL; + const oilPsi = arr(V.oilPress); + const oilF = arr(V.oilTemp) * 9 / 5 + 32; + const egtF = arr(V.egt) * 9 / 5 + 32; + const fuelL = arr(V.fuelQty, 0) / KG_PER_GAL; + const fuelR = arr(V.fuelQty, 1) / KG_PER_GAL; + const volts = arr(V.volts, 0, 28); + const amps = arr(V.amps); + return ( + + + + + + + + + + ENG + 0.0 HRS + – ELECTRICAL – + M + BUS + E + {volts.toFixed(1)} + VOLTS + {volts.toFixed(1)} + M + BATT + S + {amps >= 0 ? '+' : ''}{amps.toFixed(1)} + AMPS + +0.0 + + ); +} + +function Bar({ y, label, val, min, max, value, zones }) { + const x0 = 8, x1 = 182, bw = x1 - x0; + const px = (v) => x0 + bw * Math.max(0, Math.min(1, (v - min) / (max - min))); + const p = px(value); + return ( + + {label} + {val != null && {val}} + + {zones.map((z, i) => )} + + + ); +} + +// Fuel quantity: one bar per the C172's two tanks, with L and R pointers on a +// shared 0–10–20–F (gal) scale; yellow/red caution zone at the low end. +function FuelBar({ y, left, right }) { + const x0 = 8, x1 = 182, bw = x1 - x0, max = 26.5; + const px = (g) => x0 + bw * Math.max(0, Math.min(1, g / max)); + const tick = (g, lbl) => ( + + + {lbl} + + ); + const ptr = (g, lbl) => ( + + + {lbl} + + ); + return ( + + FUEL QTY GAL + + + + + {tick(0, '0')}{tick(8.83, '10')}{tick(17.66, '20')}{tick(max, 'F')} + {ptr(left, 'L')}{ptr(right, 'R')} + + ); +} + +function RpmArc({ rpm }) { + const max = 2700, frac = Math.max(0, Math.min(1, rpm / max)); + const a0 = -210, a1 = 30, ang = a0 + (a1 - a0) * frac; + const cx = 95, cy = 62, r = 42; + const pt = (deg, rr) => [cx + rr * Math.cos((deg * Math.PI) / 180), cy + rr * Math.sin((deg * Math.PI) / 180)]; + const arc = (s, e, color, w) => { + const [x0, y0] = pt(s, r), [x1, y1] = pt(e, r); + return 180 ? 1 : 0} 1 ${x1} ${y1}`} fill="none" stroke={color} strokeWidth={w} />; + }; + const [nx, ny] = pt(ang, r - 2); + return ( + + {arc(a0, a1, '#2a2a2a', 7)} + {arc(a0, -30, '#0c0', 7)} + {arc(0, a1, '#c00', 7)} + + + RPM + {Math.round(rpm)} + + ); +} + +/* ---------------- map chrome overlay (compass rose / range / mode) ---------------- */ +const NICE = [0.5, 1, 1.5, 2, 2.5, 4, 5, 7.5, 10, 15, 20, 25, 40, 50, 75, 100, 150, 200, 250, 500]; +function niceRange(nm) { let r = NICE[0]; for (const s of NICE) if (nm >= s) r = s; return r; } + +function MapChrome({ V, rangeNm }) { + const gs = Math.round(num(V.groundspeed) * 1.94384); + const rng = niceRange(rangeNm); + const cx = 160, cy = 160, r = 150; + const ticks = []; + for (let d = 0; d < 360; d += 10) { + const a = ((d - 90) * Math.PI) / 180; + const big = d % 30 === 0; + const r2 = r - (big ? 12 : 7); + ticks.push(); + if (big) { + const lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10; + ticks.push({lbl}); + } + } + return ( +
+ {ticks} +
{gs} KTNORTH UP
+
{rng} NM
+
NAV
+
+ ); +} diff --git a/web/src/components/MapView.jsx b/web/src/components/MapView.jsx new file mode 100644 index 0000000..f61f281 --- /dev/null +++ b/web/src/components/MapView.jsx @@ -0,0 +1,208 @@ +import React, { useEffect, useRef, useState } from 'react'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import { num } from '../api/useXplane.js'; + +const PLANE_SVG = + ''; + +// A single nav feature rendered as G1000-style symbology: cyan airport, green +// VOR hexagon, brown NDB dot-ring, light fix triangle — with an optional label. +function navSymbol(f, label) { + const t = (f.type || '').toUpperCase(); + let g, color; + if (t === 'APT') { + color = '#19d3ff'; + g = ``; + } else if (t === 'VOR') { + color = '#19ff5a'; + g = ``; + } else if (t === 'NDB') { + color = '#d59a5a'; + g = ``; + } else { + color = '#cfe3ff'; + g = ``; + } + const lbl = label ? `${f.id}` : ''; + const html = ``; + return L.marker([f.lat, f.lon], { + icon: L.divIcon({ className: 'nav-divicon', html, iconSize: [18, 18], iconAnchor: [9, 9] }), + interactive: false, + }); +} + +// Selectable base layers — switched by the MFD's Map-Opt softkeys. 'dark' draws +// no tiles (pure black, like the G1000 with TOPO off); 'terrain' is a relief +// hillshade; 'topo' is shaded relief; 'osm' is our tuned street layer. +const TILES = { + topo: { url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', opts: { maxZoom: 17, subdomains: 'abc' } }, + osm: { url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', opts: { maxZoom: 19 } }, + terrain: { url: 'https://server.arcgisonline.com/ArcGIS/rest/services/Elevation/World_Hillshade/MapServer/tile/{z}/{y}/{x}', opts: { maxZoom: 16 } }, + dark: null, +}; + +export default function MapView({ values, flightPlan, fp, inset = false, hud = true, mapMode, dcltr = 0, onView }) { + const elRef = useRef(null); + const mapRef = useRef(null); + const acRef = useRef(null); + const routeRef = useRef(null); + const wpLayerRef = useRef(null); + const navLayerRef = useRef(null); + const navAbortRef = useRef(null); + const baseRef = useRef(null); + const [follow, setFollow] = useState(true); + const followRef = useRef(true); + followRef.current = follow; + + const lat = num(values.lat, 47.45); + const lon = num(values.lon, -122.31); + const track = num(values.track); + const gs = num(values.groundspeed) * 1.94384; // m/s -> kt + const base = mapMode?.base || 'topo'; + const dcltrRef = useRef(dcltr); + dcltrRef.current = dcltr; + + // Swap the base tile layer (and report it via the container's dark class). + const applyBase = (map, name) => { + if (baseRef.current) { map.removeLayer(baseRef.current); baseRef.current = null; } + const def = TILES[name]; + if (def) baseRef.current = L.tileLayer(def.url, def.opts).addTo(map); + if (baseRef.current) baseRef.current.bringToBack(); + elRef.current?.classList.toggle('dark', name === 'dark'); + }; + + // Report the current range (centre→top edge, in NM) for the G1000 range box. + const reportView = (map) => { + if (!onView) return; + const c = map.getCenter(), n = L.latLng(map.getBounds().getNorth(), c.lng); + onView({ rangeNm: map.distance(c, n) / 1852 }); + }; + + // create map once + useEffect(() => { + const map = L.map(elRef.current, { zoomControl: !inset, attributionControl: false, dragging: !inset, scrollWheelZoom: !inset }) + .setView([lat, lon], inset ? 10 : 9); + applyBase(map, base); + + navLayerRef.current = L.layerGroup().addTo(map); // real airports/navaids/fixes + routeRef.current = L.layerGroup().addTo(map); // flight-plan legs (white + magenta active) + wpLayerRef.current = L.layerGroup().addTo(map); + + // Pull X-Plane's own nav data for the current view and draw it as G1000-style + // vector symbology (cyan airports, green VOR hexagons, NDB dot-rings, fixes). + const refreshNav = async () => { + const layer = navLayerRef.current; + if (!layer) return; + const z = map.getZoom(); + if (z < 6 || dcltrRef.current > 0) { layer.clearLayers(); return; } + const types = z >= 10 ? 'apt,vor,ndb,fix' : z >= 8 ? 'apt,vor,ndb' : 'apt'; + const b = map.getBounds(); + const url = `/api/nav/bbox?s=${b.getSouth()}&w=${b.getWest()}&n=${b.getNorth()}&e=${b.getEast()}&types=${types}&limit=${z >= 10 ? 500 : 250}`; + try { + navAbortRef.current?.abort(); + navAbortRef.current = new AbortController(); + const res = await fetch(url, { signal: navAbortRef.current.signal }); + if (!res.ok) return; + const feats = await res.json(); + layer.clearLayers(); + const labels = z >= 8; + for (const f of feats) navSymbol(f, labels).addTo(layer); + } catch { /* aborted or offline — leave as is */ } + }; + map.on('moveend', () => { refreshNav(); reportView(map); }); + map.on('zoomend', () => reportView(map)); + setTimeout(() => { refreshNav(); reportView(map); }, 300); + + const icon = L.divIcon({ className: 'ac-divicon', html: PLANE_SVG, iconSize: [34, 34], iconAnchor: [17, 17] }); + acRef.current = L.marker([lat, lon], { icon, interactive: false, zIndexOffset: 1000 }).addTo(map); + + // tap the map to drop a user waypoint (full map only) + if (!inset && fp) map.on('click', (e) => fp.addLatLon(+e.latlng.lat.toFixed(5), +e.latlng.lng.toFixed(5))); + if (!inset) map.on('dragstart', () => setFollow(false)); + + mapRef.current = map; + // leaflet needs a nudge once its container has real size + setTimeout(() => map.invalidateSize(), 200); + return () => { map.remove(); mapRef.current = null; }; + }, []); // eslint-disable-line + + // swap base layer when the MFD changes map mode + useEffect(() => { + const map = mapRef.current; + if (map) applyBase(map, base); + }, [base]); // eslint-disable-line + + // declutter: hide nav symbology, or repopulate it, when the level changes + useEffect(() => { + const map = mapRef.current; + if (!map) return; + if (dcltr > 0) navLayerRef.current?.clearLayers(); + else map.fire('moveend'); // triggers refreshNav to redraw symbols + }, [dcltr]); // eslint-disable-line + + // update aircraft position + heading + useEffect(() => { + const ac = acRef.current, map = mapRef.current; + if (!ac || !map) return; + ac.setLatLng([lat, lon]); + const el = ac.getElement()?.querySelector('svg'); + if (el) el.style.transform = `rotate(${track}deg)`; + if (followRef.current) map.panTo([lat, lon], { animate: true, duration: 0.5 }); + }, [lat, lon, track]); + + // redraw route + waypoints when the plan changes. Like the real G1000, the + // active leg (to waypoint `activeLeg`) is magenta; all other legs are white. + useEffect(() => { + const route = routeRef.current, layer = wpLayerRef.current; + if (!route || !layer) return; + route.clearLayers(); + layer.clearLayers(); + const wps = flightPlan?.waypoints || []; + const active = Math.max(1, Math.min(wps.length - 1, flightPlan?.activeLeg ?? 1)); + for (let i = 1; i < wps.length; i++) { + const seg = [[wps[i - 1].lat, wps[i - 1].lon], [wps[i].lat, wps[i].lon]]; + const isActive = i === active; + L.polyline(seg, { color: isActive ? '#ff20ff' : '#ffffff', weight: isActive ? 4 : 2.5, opacity: 0.95 }).addTo(route); + } + wps.forEach((w, i) => { + const isActiveWp = i === active; // the waypoint the active leg flies to + L.circleMarker([w.lat, w.lon], { + radius: 6, color: '#fff', weight: 2, + fillColor: isActiveWp ? '#ff20ff' : '#0a0a0a', fillOpacity: 1, + }) + .bindTooltip(`${i + 1}. ${w.id}`, { permanent: true, direction: 'right', className: 'wp-label' }) + .addTo(layer); + }); + }, [flightPlan]); + + if (inset) { + return ( +
+
+
+ ); + } + return ( +
+
+ {hud && ( + <> +
+
GS{Math.round(gs)} kt
+
TRK{String(Math.round(track) % 360).padStart(3, '0')}°
+
POS{lat.toFixed(3)}, {lon.toFixed(3)}
+ {num(values.gpsDistNm) > 0 && ( +
→WPT{num(values.gpsDistNm).toFixed(1)} nm
+ )} +
+ +
Tippe auf die Karte, um einen Wegpunkt hinzuzufügen
+ + )} +
+ ); +} diff --git a/web/src/components/Nearest.jsx b/web/src/components/Nearest.jsx new file mode 100644 index 0000000..08cd4e8 --- /dev/null +++ b/web/src/components/Nearest.jsx @@ -0,0 +1,77 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { num } from '../api/useXplane.js'; + +// The G1000 "Nearest" window. On the PFD it pops up over the right side when you +// press the NRST softkey; it lists the closest airports / VORs / NDBs to the +// aircraft with bearing + distance, straight from X-Plane's own nav data +// (/api/nav/nearest). Tabs switch the feature type, like turning the FMS knob +// through the NRST page group on the real unit. +const TABS = [ + { id: 'apt', label: 'APT' }, + { id: 'vor', label: 'VOR' }, + { id: 'ndb', label: 'NDB' }, +]; + +// VOR freq comes in 10 kHz units (11630 → 116.30); NDB in kHz (e.g. 350). +const freqStr = (f, type) => { + const n = num(f); + if (!n) return ''; + return type === 'vor' ? (n / 100).toFixed(2) : String(n); +}; + +export default function Nearest({ values, onClose }) { + const [type, setType] = useState('apt'); + const [rows, setRows] = useState([]); + const lastRef = useRef(null); + const lat = num(values.lat), lon = num(values.lon); + + useEffect(() => { + let abort = new AbortController(); + let timer; + const load = async () => { + if (!isFinite(lat) || !isFinite(lon) || (lat === 0 && lon === 0)) return; + try { + const r = await fetch(`/api/nav/nearest?lat=${lat}&lon=${lon}&type=${type}&count=9`, { signal: abort.signal }); + if (r.ok) setRows(await r.json()); + } catch { /* aborted / offline */ } + }; + load(); + // Refresh as the aircraft moves (cheap server scan). + timer = setInterval(load, 5000); + return () => { abort.abort(); clearInterval(timer); }; + }, [type, Math.round(lat * 50), Math.round(lon * 50)]); // re-key on ~1nm moves + + return ( +
+
+ NEAREST +
+ {TABS.map((t) => ( + + ))} +
+ {onClose && } +
+
+ {type === 'apt' ? 'IDENT' : 'IDENT'} + BRG + DIS + {type === 'apt' ? 'ELEV' : 'FREQ'} +
+
+ {rows.length === 0 &&
— no data —
} + {rows.map((f, i) => ( +
+ {f.id} + {String(num(f.brg)).padStart(3, '0')}° + {num(f.dist).toFixed(1)}nm + + {type === 'apt' ? `${Math.round(num(f.elev))}ft` : freqStr(f.freq, type)} + + {f.name && {f.name}} +
+ ))} +
+
+ ); +} diff --git a/web/src/components/PFD.jsx b/web/src/components/PFD.jsx new file mode 100644 index 0000000..72c4354 --- /dev/null +++ b/web/src/components/PFD.jsx @@ -0,0 +1,589 @@ +import React, { useRef, useState, useLayoutEffect, Suspense, lazy } from 'react'; +import { num } from '../api/useXplane.js'; +import MapView from './MapView.jsx'; +import Nearest from './Nearest.jsx'; +import TimerRef from './TimerRef.jsx'; +// Lazy-load the heavy WebGL terrain engine only when the PFD is shown. +const SVT = lazy(() => import('./SVT.jsx')); + +// Garmin G1000 (GDU 1040) PFD as used by the default Cessna 172. Every field is +// driven by the same X-Plane datarefs the in-sim G1000 uses, so values match. +const W = 1000; +const H = 780; +const PITCH_PX = 9; + +const arr0 = (v, d = 0) => (Array.isArray(v) ? num(v[0], d) : num(v, d)); +const navF = (v) => (num(v) / 100).toFixed(2); // NAV: 11170 -> 111.70 +const comF = (v) => (num(v) / 100).toFixed(3); // COM: 11810 -> 118.100 + +// Great-circle bearing + distance (nm) from aircraft to a point. +const D2R = Math.PI / 180, R2D = 180 / Math.PI, R_NM = 3440.065; +function brgDist(la1, lo1, la2, lo2) { + const dLat = (la2 - la1) * D2R, dLon = (lo2 - lo1) * D2R; + const a = Math.sin(dLat / 2) ** 2 + Math.cos(la1 * D2R) * Math.cos(la2 * D2R) * Math.sin(dLon / 2) ** 2; + const dist = 2 * R_NM * Math.asin(Math.min(1, Math.sqrt(a))); + const y = Math.sin(dLon) * Math.cos(la2 * D2R); + const x = Math.cos(la1 * D2R) * Math.sin(la2 * D2R) - Math.sin(la1 * D2R) * Math.cos(la2 * D2R) * Math.cos(dLon); + const brg = (Math.atan2(y, x) * R2D + 360) % 360; + return { brg, dist }; +} +// GPS guidance to the active flight-plan leg, computed from our own plan. +function activeNav(V, fp) { + const wps = fp?.waypoints || []; + if (wps.length < 1) return null; + const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1)); + const wp = wps[ai]; + const lat = num(V.lat), lon = num(V.lon); + if (!wp || (!lat && !lon)) return null; + const { brg, dist } = brgDist(lat, lon, wp.lat, wp.lon); + const prev = wps[ai - 1]; + const dtk = prev ? brgDist(prev.lat, prev.lon, wp.lat, wp.lon).brg : brg; + const gs = num(V.groundspeed) * 1.94384; + const ete = gs > 20 ? (dist / gs) * 3600 : null; // seconds + // Cross-track deviation from the prev→wp leg, for the GPS CDI (dots; full + // scale 2 nm enroute = 1 nm/dot). + = right of course → CDI bar deflects left. + let def = 0, xtk = 0; + if (prev) { + const leg = brgDist(prev.lat, prev.lon, lat, lon); // prev → aircraft + xtk = Math.asin(Math.sin(leg.dist / R_NM) * Math.sin((leg.brg - dtk) * D2R)) * R_NM; + def = Math.max(-2.5, Math.min(2.5, -xtk / 1.0)); + } + return { id: wp.id, brg, dist, dtk, ete, xtk, def }; +} +function fmtEte(s) { + if (s == null) return '--:--'; + const m = Math.round(s / 60); + return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}`; +} +// VNAV: nearest downstream waypoint with a lower altitude constraint, and the +// vertical speed required to meet it at the current groundspeed. +function vnavInfo(V, fp) { + const wps = fp?.waypoints || []; + const ai = Math.max(1, Math.min(wps.length - 1, fp?.activeLeg ?? 1)); + const alt = num(V.altitude); + const lat = num(V.lat), lon = num(V.lon); + const gs = num(V.groundspeed) * 1.94384; + if (gs < 40 || (!lat && !lon)) return null; + let cum = 0, prevLat = lat, prevLon = lon; + for (let i = ai; i < wps.length; i++) { + cum += brgDist(prevLat, prevLon, wps[i].lat, wps[i].lon).dist; + prevLat = wps[i].lat; prevLon = wps[i].lon; + const tgt = num(wps[i].alt); + if (tgt > 0 && tgt < alt - 50) { + const tMin = (cum / gs) * 60; + const vsReq = tMin > 0 ? (tgt - alt) / tMin : 0; + return { wptId: wps[i].id, tgtAlt: tgt, dist: cum, vsReq }; + } + } + return null; +} + +// Attitude SYMBOLOGY clip (pitch ladder, roll arc, FD) — only the upper region, +// so the ladder doesn't bleed into the tapes/HSI. +const ATT = { x: W / 2 - 290, y: 270 - 175, w: 580, h: 385 }; +// The synthetic terrain fills the WHOLE display below the radio bar, exactly +// like the real G1000 — the tapes and HSI sit translucently on top of it. +const SVT_BOX = { x: 0, y: 74, w: W, h: H - 74 }; +// The INSET moving map sits in the bottom-left corner (toggled by INSET softkey). +const INSET_BOX = { x: 6, y: 556, w: 300, h: 172 }; + +export default function PFD({ values: V, svt = true, inset = false, insetMode, nrst = false, onCloseNrst, tmr = false, onCloseTmr, flightPlan, fp }) { + const wrapRef = useRef(null); + const svgRef = useRef(null); + const [box, setBox] = useState(null); + const [insetBox, setInsetBox] = useState(null); + + // Map SVG-space regions to on-screen pixels, accounting for the SVG's + // letterboxing (xMidYMid meet) — used for both the SVT canvas and the inset. + useLayoutEffect(() => { + const measure = () => { + const svg = svgRef.current, wrap = wrapRef.current; + if (!svg || !wrap) return; + const r = svg.getBoundingClientRect(), wr = wrap.getBoundingClientRect(); + const scale = Math.min(r.width / W, r.height / H); + const offX = (r.width - W * scale) / 2 + (r.left - wr.left); + const offY = (r.height - H * scale) / 2 + (r.top - wr.top); + const map = (b) => ({ left: offX + b.x * scale, top: offY + b.y * scale, width: b.w * scale, height: b.h * scale }); + setBox(map(SVT_BOX)); + setInsetBox(map(INSET_BOX)); + }; + measure(); + const ro = new ResizeObserver(measure); + if (svgRef.current) ro.observe(svgRef.current); + window.addEventListener('resize', measure); + return () => { ro.disconnect(); window.removeEventListener('resize', measure); }; + }, []); + + const nav = activeNav(V, flightPlan); + const vnav = vnavInfo(V, flightPlan); + + return ( +
+ {svt && box && ( +
+ }> +
+ )} + {inset && insetBox && ( +
+ +
+ )} + + + + + + + + + + {!svt && } + + {nav && } + {vnav && } + + + + + + + + + {nrst && } + {tmr && } +
+ ); +} + +/* ---------------- top NAV/COM radio bar ---------------- */ +// Matches the XPLANE 1000: NAV cyan (active boxed), COM green active / +// cyan-boxed standby, a centre flight-plan cell with DIS/BRG, ⇄ swap arrows. +const SWAP = '⇔'; +function RadioBar({ V }) { + const swap = (x, y) => {SWAP}; + return ( + + + {/* cell dividers */} + {[330, 560, 690].map((x) => )} + + + {/* NAV1 / NAV2 (left) */} + NAV1 + + {navF(V.nav1)} + {swap(176, 28)} + {navF(V.nav1Sb)} + NAV2 + {navF(V.nav2)} + {swap(176, 60)} + {navF(V.nav2Sb)} + + {/* centre: active leg + DIS/BRG */} + {'→'} + + + DIS + {V.gpsDistNm != null ? num(V.gpsDistNm).toFixed(1) : '_._'} + NM + BRG + + {/* COM1 / COM2 (right) */} + {comF(V.com1)} + {swap(848, 28)} + + {comF(V.com1Sb)} + COM1 + {comF(V.com2)} + {swap(848, 60)} + {comF(V.com2Sb)} + COM2 + + ); +} + +/* ---------------- attitude + flight director ---------------- */ +function Attitude({ V, svt }) { + const pitch = num(V.pitch), roll = num(V.roll), slip = num(V.slip); + const fdP = num(V.fdPitch), fdR = num(V.fdRoll); + const cx = W / 2, cy = 270; + const off = pitch * PITCH_PX; + + return ( + + + + + + + + {/* sky/ground only when SVT is off — otherwise the 3D terrain shows */} + {!svt && } + {!svt && } + {!svt && } + {pitchLadder(cx, cy)} + + + {/* flight director command bars (magenta) */} + + + + + {rollArc(cx, cy, roll, slip)} + {/* fixed aircraft reference (yellow) */} + + + + + {/* flight path marker (green) — track/AOA based; offset approximated */} + {(() => { + const fpx = Math.max(-120, Math.min(120, (num(V.track) - num(V.heading)) * 6)); + return ( + + + + + + + ); + })()} + {!svt && } + + ); +} + +function pitchLadder(cx, cy) { + const m = []; + for (let d = -90; d <= 90; d += 2.5) { + if (d === 0) continue; + const y = cy - d * PITCH_PX; + const ten = d % 10 === 0, five = d % 5 === 0; + const half = ten ? 70 : five ? 40 : 22; + m.push(); + if (ten) m.push( + + {Math.abs(d)} + {Math.abs(d)} + + ); + } + return {m}; +} + +function rollArc(cx, cy, roll, slip) { + const r = 165; + const ticks = [-60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60]; + return ( + + {ticks.map((t) => { + const a = (t - 90) * (Math.PI / 180); + const big = t % 30 === 0 || t === 0; + const r2 = r - (big ? 18 : 11); + return ; + })} + + + + + + + ); +} + +/* ---------------- airspeed tape ---------------- */ +// V-speed reference marks for the C172 (KIAS), shown below the tape like the +// XPLANE 1000: Vy=74 (Y), Vx=62 (X), best glide=68 (G). +const VSPEEDS = [{ s: 74, l: 'Y' }, { s: 62, l: 'X' }, { s: 68, l: 'G' }]; +function AirspeedTape({ V }) { + const ias = num(V.airspeed), tas = num(V.tas), spdBug = num(V.apSpdBug); + const x = 60, top = 95, h = 350, cy = top + h / 2, px = 3.6; + const W2 = 84, sx = x + W2 - 7; // colour strip at the right inner edge + const ticks = []; + const lo = Math.floor((ias - 50) / 10) * 10; + for (let s = lo; s <= ias + 50; s += 10) { + if (s < 0) continue; + const y = cy + (ias - s) * px; + ticks.push( + {s}); + } + const yOf = (s) => Math.max(top, Math.min(top + h, cy + (ias - s) * px)); + const band = (a, b, color) => ; + const bugY = Math.max(top, Math.min(top + h, cy + (ias - spdBug) * px)); + const valid = ias >= 20; + return ( + + + {/* V-speed colour strip (white flap arc, green normal, yellow caution, red Vne) */} + {band(33, 85, '#e8e8e8')} + {band(48, 129, '#16c116')} + {band(129, 163, '#e0d000')} + + {ticks} + {/* selected-airspeed bug (cyan) */} + + {/* current-speed readout box (points right toward the tape) */} + + {valid ? Math.round(ias) : '- - -'} + {/* V-speed reference list below the tape */} + {VSPEEDS.map((v, i) => ( + + {v.s} + + {v.l} + + ))} + {/* TAS box at the very bottom */} + + TAS + {Math.round(tas)} + + ); +} + +/* ---------------- altitude tape + VSI + baro ---------------- */ +function AltitudeTape({ V }) { + const alt = num(V.altitude), vs = num(V.vspeed), altBug = num(V.apAltBug), baro = num(V.baro, 29.92); + const x = W - 70 - 84, W2 = 84, top = 95, h = 350, cy = top + h / 2, px = 0.42; + const ticks = []; + const lo = Math.floor((alt - 420) / 100) * 100; + for (let a = lo; a <= alt + 420; a += 100) { + const y = cy + (alt - a) * px; + ticks.push( + {a}); + } + const bugY = Math.max(top, Math.min(top + h, cy + (alt - altBug) * px)); + // rolling readout: leading hundreds (static) + a two-digit drum that *rolls* + // through 20-ft steps, so you always see the value you're between — exactly + // like the mechanical tens drum on the real GDU 1040. + const hi = Math.floor(alt / 100); + const STEP = 20, ROW = 25; // drum increment + row height + const base = Math.floor(alt / STEP) * STEP; // nearest 20 ft at/below current + const frac = (alt - base) / STEP; // 0..1 position between rows + const selStr = altBug > 0 ? String(Math.round(altBug)) : '- - - - -'; + // drum geometry + const drumX = x + W2 + 4, drumW = 26, drumCx = drumX + drumW / 2; + return ( + + {/* selected altitude (cyan) above the tape */} + + {selStr} + + {ticks} + {/* selected-altitude bug (cyan) on the tape */} + + {/* current-altitude readout (points left toward the tape): static hundreds + + a rolling tens/units drum that scrolls through 20-ft steps, so two + values are visible at once with the pointer between them (GDU 1040). */} + + + {hi} + + {[-1, 0, 1, 2].map((k) => { + const v = base + k * STEP; + const s = String(((v % 100) + 100) % 100).padStart(2, '0'); + return {s}; + })} + + {/* baro */} + + {baro.toFixed(2)} IN + {/* VSI to the right */} + + + ); +} +function VSI({ x, cy, h, vs, bug }) { + const max = 2000, top = cy - h / 2 + 10, bot = cy + h / 2 - 10; + const yOf = (v) => cy - (Math.max(-max, Math.min(max, v)) / max) * (h / 2 - 10); + return ( + + + {[2000, 1000, 0, -1000, -2000].map((v) => ( + + + {v !== 0 && {Math.abs(v) / 1000}} + + ))} + {[500, 1500, -500, -1500].map((v) => )} + {bug !== 0 && } + + {Math.abs(vs) >= 100 ? Math.round(vs / 10) * 10 : ''} + + ); +} + +/* ---------------- HSI compass rose ---------------- */ +function HSI({ V, nav }) { + const hdg = ((num(V.heading) % 360) + 360) % 360; + const bug = num(V.apHdgBug); + // With an active flight-plan leg the CDI follows OUR GPS guidance (desired + // track + cross-track); otherwise it mirrors the sim's nav source. + const crs = nav ? nav.dtk : num(V.obsCrs, 360); + const def = nav ? nav.def : num(V.hsiDef); + const toFrom = nav ? 1 : num(V.hsiToFrom); + const cx = W / 2, cy = 630, r = 130; + + const ticks = []; + for (let d = 0; d < 360; d += 5) { + const a = ((d - hdg - 90) * Math.PI) / 180; + const big = d % 30 === 0; + const r2 = r - (big ? 18 : 10); + ticks.push(); + if (big) { + const lr = r - 36, la = a; + const lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10; + ticks.push({lbl}); + } + } + const bugA = bug - hdg, crsA = crs - hdg; + const defPx = Math.max(-2, Math.min(2, def)) * 26; + + return ( + + + {ticks} + {/* lubber line + heading box */} + + + {String(Math.round(hdg) % 360).padStart(3, '0')} + {/* heading bug (cyan) */} + + + + {/* GPS source label */} + GPS + ENR + {/* course pointer + CDI (magenta = GPS source) */} + + + + + {/* CDI deviation bar */} + + {[-2, -1, 1, 2].map((d) => )} + {toFrom > 0 && } + + {/* cyan bearing pointer to the active flight-plan waypoint (BRG) */} + {nav && ( + + + + + + )} + + + ); +} + +/* ---------------- VNAV box (descent target + required vertical speed) ---------------- */ +function VnavBox({ vnav }) { + const vs = Math.round(vnav.vsReq / 10) * 10; + return ( + + + VNV + {vnav.tgtAlt}FT + @{vnav.wptId} + VS + {vs >= 0 ? '+' : ''}{vs} + + ); +} + +/* ---------------- glideslope (vertical deviation) for ILS approaches ---------------- */ +// Localizer freqs: 108.10–111.95 MHz with an odd 100-kHz digit → an ILS (has GS). +function isILS(navHz) { + const f = num(navHz) / 100; + return f >= 108.1 && f <= 111.95 && Math.floor(f * 10) % 2 === 1; +} +function GlideSlope({ V }) { + if (!isILS(V.nav1)) return null; + const cx = 792, cy = 280, h = 120, step = h / 2.5; // ±2.5 dots + // +vdef = above glideslope → diamond rides high → "fly down". + const def = Math.max(-2.5, Math.min(2.5, num(V.gsDef))); + const dy = cy - def * step; + return ( + + + GS + + + {[-2, -1, 1, 2].map((d) => ( + + ))} + + + ); +} + +/* ---------------- GPS nav status block (top, when a leg is active) ---------------- */ +function NavStatus({ nav }) { + return ( + + + {nav.id} + DTK + {String(Math.round(nav.dtk)).padStart(3, '0')}° + DIS + {nav.dist.toFixed(1)}nm + ETE + {fmtEte(nav.ete)} + + ); +} + +/* ---------------- HDG/CRS boxes flanking the HSI ---------------- */ +function HdgCrsBoxes({ V, nav }) { + const hdgSel = String(Math.round(num(V.apHdgBug)) % 360).padStart(3, '0'); + // In GPS mode (active leg) the CRS field shows the desired track. + const crsVal = nav ? nav.dtk : num(V.obsCrs, 360); + const crs = String(Math.round(crsVal) % 360 || 360).padStart(3, '0'); + return ( + + + HDG + {hdgSel}° + + CRS + {crs}° + + ); +} + +/* ---------------- bottom data line: OAT / ISA / XPDR / LCL ---------------- */ +function DataStrip({ V }) { + const oatC = num(V.oat); + const oatF = Math.round(oatC * 9 / 5 + 32); + const stdC = 15 - 1.98 * (num(V.altitude) / 1000); // ISA temp at altitude + const isaF = Math.round((oatC - stdC) * 9 / 5); + const xpdr = String(num(V.xpdrCode, 1200)).padStart(4, '0'); + const mode = ['OFF', 'STBY', 'ON', 'ALT', 'TEST'][num(V.xpdrMode)] || 'ALT'; + const now = new Date(); + const lcl = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; + return ( + + + {/* OAT + ISA (left) */} + + OAT {oatF}°F + + ISA {isaF >= 0 ? '+' : ''}{isaF}°F + {/* XPDR + LCL (right) — code/mode in green text, like the real GDU */} + XPDR + {xpdr}{mode} + LCL {lcl} + + ); +} diff --git a/web/src/components/Proc.jsx b/web/src/components/Proc.jsx new file mode 100644 index 0000000..b373f7b --- /dev/null +++ b/web/src/components/Proc.jsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import { num } from '../api/useXplane.js'; + +// G1000 PROC dialog. Pick a destination/airport, a category (Departure / Arrival +// / Approach), then a procedure + transition; LOAD inserts the procedure's leg +// fixes into the active flight plan. Procedures come from X-Plane's own CIFP data +// via /api/nav/procs and /api/nav/proc (resolved to coordinates server-side). +const CATS = [ + { id: 'approach', label: 'APPROACH', key: 'approaches', t: 'approach' }, + { id: 'arrival', label: 'ARRIVAL', key: 'stars', t: 'star' }, + { id: 'departure', label: 'DEPARTURE', key: 'sids', t: 'sid' }, +]; + +export default function Proc({ xp, onClose }) { + const { flightPlan, fp, values } = xp; + // Default airport: the plan's destination if it's an airport, else blank. + const wps = flightPlan?.waypoints || []; + const destGuess = [...wps].reverse().find((w) => w.type === 'APT')?.id || ''; + const [icao, setIcao] = useState(destGuess); + const [query, setQuery] = useState(destGuess); + const [procs, setProcs] = useState(null); + const [err, setErr] = useState(''); + const [cat, setCat] = useState('approach'); + const [selProc, setSelProc] = useState(null); // { name, transitions } + const [selTrans, setSelTrans] = useState(''); + const [legs, setLegs] = useState([]); + + // Fetch the procedure summary whenever the airport changes. + useEffect(() => { + const id = icao.trim().toUpperCase(); + if (id.length < 3) { setProcs(null); return; } + let alive = true; + setErr(''); setProcs(null); setSelProc(null); setSelTrans(''); setLegs([]); + fetch(`/api/nav/procs?icao=${id}`).then((r) => r.ok ? r.json() : Promise.reject(r.status)) + .then((d) => { if (alive) setProcs(d); }) + .catch(() => { if (alive) setErr(`keine Prozeduren für ${id}`); }); + return () => { alive = false; }; + }, [icao]); + + // Preview the resolved legs when a procedure+transition is chosen. + useEffect(() => { + if (!procs || !selProc) { setLegs([]); return; } + const c = CATS.find((c) => c.id === cat); + let alive = true; + const t = encodeURIComponent(selTrans || ''); + fetch(`/api/nav/proc?icao=${procs.icao}&type=${c.t}&name=${encodeURIComponent(selProc.name)}&trans=${t}`) + .then((r) => r.ok ? r.json() : []).then((d) => { if (alive) setLegs(d); }); + return () => { alive = false; }; + }, [procs, cat, selProc, selTrans]); + + const catList = procs ? (procs[CATS.find((c) => c.id === cat).key] || []) : []; + + const load = () => { + if (!legs.length) return; + const existing = wps.slice(); + // Departures go to the front, arrivals/approaches to the end. + const merged = cat === 'departure' ? [...legs, ...existing] : [...existing, ...legs]; + fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: cat === 'departure' ? 1 : existing.length || 1 }); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +
PROCEDURES
+
+
+ + setQuery(e.target.value.toUpperCase())} + onKeyDown={(e) => e.key === 'Enter' && setIcao(query)} + placeholder="ICAO (z.B. KSEA)" autoCapitalize="characters" autoCorrect="off" spellCheck="false" /> + +
+ {err &&
{err}
} + +
+ {CATS.map((c) => ( + + ))} +
+ +
+
+
{procs ? `${catList.length}` : '—'} PROC
+ {catList.map((p) => ( + + ))} + {procs && catList.length === 0 &&
keine
} +
+
+
TRANS
+ {selProc?.transitions.map((t) => ( + + ))} + {selProc && selProc.transitions.length === 0 &&
} +
+
+
{legs.length} FIXES
+ {legs.map((l, i) => ( +
+ {l.id}{l.alt ? {l.alt}ft : null} +
+ ))} +
+
+
+
+ + +
+
+
+ ); +} diff --git a/web/src/components/SVT.jsx b/web/src/components/SVT.jsx new file mode 100644 index 0000000..0108799 --- /dev/null +++ b/web/src/components/SVT.jsx @@ -0,0 +1,177 @@ +import React, { useEffect, useRef } from 'react'; +import maplibregl from 'maplibre-gl'; +import 'maplibre-gl/dist/maplibre-gl.css'; +import { num } from '../api/useXplane.js'; + +// Synthetic Vision background: real-world 3D terrain (elevation tiles) rendered +// in WebGL, with the camera placed at the aircraft and oriented by heading and +// pitch. Bank (roll) is applied as a CSS rotation of the whole canvas. This is +// the SVT *concept* using real-world DEM data — not X-Plane's own scenery. +// +// Free public elevation tiles: AWS "terrarium" (no API key needed). +const STYLE = { + version: 8, + glyphs: 'https://fonts.openmaptiles.org/{fontstack}/{range}.pbf', // for runway-number labels + sources: { + dem: { + type: 'raster-dem', + tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'], + encoding: 'terrarium', + tileSize: 256, + maxzoom: 11, // coarser cap = far fewer tiles to fetch + attribution: 'Elevation: Mapzen/AWS', + }, + }, + layers: [ + // background shows above the horizon = the sky + { id: 'bg', type: 'background', paint: { 'background-color': '#4a93da' } }, + { + id: 'relief', + type: 'color-relief', + source: 'dem', + paint: { + 'color-relief-color': [ + 'interpolate', ['linear'], ['elevation'], + -50, '#3d6ea5', 0, '#2e6b3a', 300, '#5a8f3c', 800, '#9aa84a', + 1500, '#b08f4e', 2500, '#8d6b4a', 3500, '#b9b0a6', 4500, '#ffffff', + ], + }, + }, + { id: 'hill', type: 'hillshade', source: 'dem', paint: { 'hillshade-exaggeration': 0.55 } }, + ], + terrain: { source: 'dem', exaggeration: 1.3 }, +}; + +// Build runway surfaces (+ threshold number labels) from the bridge's runway +// list. Each runway becomes a ground-draped quad plus two rotated number tags. +function runwayGeoJSON(list) { + const feats = []; + for (const r of list) { + const midLat = (r.la1 + r.la2) / 2; + const mLat = 111320, mLon = 111320 * Math.cos((midLat * Math.PI) / 180); + const dx = (r.lo2 - r.lo1) * mLon, dy = (r.la2 - r.la1) * mLat; + const len = Math.hypot(dx, dy) || 1; + const hw = (r.w || 30) / 2; + const dLon = ((-dy / len) * hw) / mLon, dLat = ((dx / len) * hw) / mLat; + const c1 = [r.lo1 + dLon, r.la1 + dLat], c2 = [r.lo2 + dLon, r.la2 + dLat]; + const c3 = [r.lo2 - dLon, r.la2 - dLat], c4 = [r.lo1 - dLon, r.la1 - dLat]; + feats.push({ type: 'Feature', geometry: { type: 'Polygon', coordinates: [[c1, c2, c3, c4, c1]] }, properties: {} }); + const brg = (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360; + feats.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [r.lo1, r.la1] }, properties: { num: r.n1, rot: brg } }); + feats.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [r.lo2, r.la2] }, properties: { num: r.n2, rot: (brg + 180) % 360 } }); + } + return { type: 'FeatureCollection', features: feats }; +} + +export default function SVT({ values }) { + const elRef = useRef(null); + const mapRef = useRef(null); + const dataRef = useRef(values); + dataRef.current = values; + + useEffect(() => { + let map; + try { + map = new maplibregl.Map({ + container: elRef.current, + style: STYLE, + center: [num(values.lon, -122.31), num(values.lat, 47.45)], + zoom: 11.5, + pitch: 72, + bearing: num(values.heading), + maxPitch: 76, // lower max pitch = nearer horizon = less distant terrain + pixelRatio: 1, // don't render at 2× on retina — big perf/bandwidth win + renderWorldCopies: false, + maxTileCacheSize: 40, + attributionControl: false, + interactive: false, + preserveDrawingBuffer: true, + fadeDuration: 0, + }); + mapRef.current = map; + } catch (e) { + // WebGL unavailable → the CSS gradient fallback stays visible. + console.warn('SVT: WebGL init failed', e?.message); + return; + } + + // Runways from X-Plane's nav data, draped on the terrain with their numbers. + let rwyTimer; + const addRunways = () => { + if (map.getSource('runways')) return; + map.addSource('runways', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); + map.addLayer({ id: 'rwy-fill', type: 'fill', source: 'runways', filter: ['==', ['geometry-type'], 'Polygon'], paint: { 'fill-color': '#33373b', 'fill-opacity': 0.9 } }); + map.addLayer({ id: 'rwy-line', type: 'line', source: 'runways', filter: ['==', ['geometry-type'], 'Polygon'], paint: { 'line-color': '#e8edf2', 'line-width': 1.6 } }); + map.addLayer({ + id: 'rwy-num', type: 'symbol', source: 'runways', filter: ['==', ['geometry-type'], 'Point'], + layout: { + 'text-field': ['get', 'num'], 'text-font': ['Open Sans Bold'], 'text-size': 15, + 'text-rotate': ['get', 'rot'], 'text-rotation-alignment': 'map', 'text-keep-upright': false, + 'text-allow-overlap': true, 'text-ignore-placement': true, + }, + paint: { 'text-color': '#fff', 'text-halo-color': '#000', 'text-halo-width': 1.4 }, + }); + let last = null; + const refresh = async () => { + const v = dataRef.current, lat = num(v.lat), lon = num(v.lon); + if (!isFinite(lat) || !isFinite(lon)) return; + if (last && Math.abs(last[0] - lat) < 0.02 && Math.abs(last[1] - lon) < 0.02) return; + last = [lat, lon]; + try { + const res = await fetch(`/api/nav/runways?lat=${lat}&lon=${lon}&radius=15`); + if (!res.ok) return; + map.getSource('runways')?.setData(runwayGeoJSON(await res.json())); + } catch { /* offline */ } + }; + refresh(); + rwyTimer = setInterval(refresh, 4000); + }; + + // Terrain awareness (TAWS): recolour the relief relative to aircraft + // altitude — terrain within 1000 ft below = yellow, within 100 ft below or + // above = red, otherwise normal. Stops are in metres (terrarium elevation). + let lastBandM = null; + const updateTerrainAwareness = (altFt) => { + const altM = altFt * 0.3048; + if (lastBandM != null && Math.abs(altM - lastBandM) < 12) return; + lastBandM = altM; + const yellowLo = altM - 305, redLo = altM - 30; // 1000 ft / 100 ft below + const s = []; // [elevation, color] pairs, strictly increasing inputs + const push = (e, c) => { if (!s.length || e > s[s.length - 2]) s.push(e, c); }; + push(-150, '#2f6a3c'); push(150, '#4f8a3e'); push(900, '#9a8a4a'); + push(yellowLo - 1, '#7d6a3a'); + push(yellowLo, '#e6c200'); push(redLo - 1, '#e6c200'); + push(redLo, '#e03030'); push(redLo + 4000, '#ff2a2a'); + try { map.setPaintProperty('relief', 'color-relief-color', ['interpolate', ['linear'], ['elevation'], ...s]); } catch { /* not ready */ } + }; + + let raf; + const tick = () => { + const v = dataRef.current; + // Keep the view close: higher zoom floor + capped pitch bounds the area. + const zoom = Math.max(10.5, Math.min(12.5, 12.5 - num(v.altitude) / 3500)); + try { + map.jumpTo({ + center: [num(v.lon, -122.31), num(v.lat, 47.45)], + bearing: num(v.heading), + pitch: Math.max(58, Math.min(76, 72 + num(v.pitch))), + zoom, + }); + updateTerrainAwareness(num(v.altitude, 5500)); + } catch { /* style not ready yet */ } + raf = requestAnimationFrame(tick); + }; + map.on('load', () => { addRunways(); raf = requestAnimationFrame(tick); }); + + return () => { cancelAnimationFrame(raf); clearInterval(rwyTimer); map.remove(); mapRef.current = null; }; + }, []); // eslint-disable-line + + // Bank: rotate the whole terrain canvas opposite to aircraft roll; scale up so + // the corners stay covered while rotated. + const roll = num(values.roll); + return ( +
+
+
+ ); +} diff --git a/web/src/components/TimerRef.jsx b/web/src/components/TimerRef.jsx new file mode 100644 index 0000000..da0ea70 --- /dev/null +++ b/web/src/components/TimerRef.jsx @@ -0,0 +1,90 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { num } from '../api/useXplane.js'; + +// G1000 TMR/REF window (PFD). The real unit shows a generic timer plus the +// reference V-speeds and barometric minimums. This implements the timer +// (count-up or count-down with START/STOP/RESET) and the V-speed / minimums +// references with simple on/off bugs. Self-contained — no sim dependency. +const VSPEEDS = [ + { key: 'vr', label: 'Vr', def: 55 }, + { key: 'vx', label: 'Vx', def: 62 }, + { key: 'vy', label: 'Vy', def: 74 }, + { key: 'vg', label: 'Vg', def: 68 }, // best glide +]; + +function fmt(sec) { + const s = Math.max(0, Math.floor(sec)); + const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60; + const pad = (n) => String(n).padStart(2, '0'); + return h > 0 ? `${pad(h)}:${pad(m)}:${pad(ss)}` : `${pad(m)}:${pad(ss)}`; +} + +export default function TimerRef({ values, onClose }) { + const [dir, setDir] = useState('up'); // 'up' | 'dn' + const [running, setRunning] = useState(false); + const [elapsed, setElapsed] = useState(0); // seconds + const [target, setTarget] = useState(300); // count-down start (s) + const [vbugs, setVbugs] = useState({}); // key -> bool (shown on tape, future) + const [minsOn, setMinsOn] = useState(false); + const [mins, setMins] = useState(500); // baro minimums (ft) + const tickRef = useRef(null); + + useEffect(() => { + if (!running) return; + const t0 = Date.now() - elapsed * 1000; + tickRef.current = setInterval(() => setElapsed((Date.now() - t0) / 1000), 250); + return () => clearInterval(tickRef.current); + }, [running]); // eslint-disable-line + + const shown = dir === 'dn' ? Math.max(0, target - elapsed) : elapsed; + const alt = num(values.altitude); + const belowMins = minsOn && alt > 0 && alt <= mins; + + const reset = () => { setRunning(false); setElapsed(0); }; + + return ( +
+
+ TIMER / REFERENCES + {onClose && } +
+
+
{fmt(shown)}
+
+ + +
+
+ + +
+ {dir === 'dn' && ( +
+ + + {fmt(target)} + +
+ )} + +
REFERENCES — V-SPEEDS
+
+ {VSPEEDS.map((v) => ( + + ))} +
+ +
+ + + {mins} FT + +
+ {belowMins &&
MINIMUMS
} +
+
+ ); +} diff --git a/web/src/components/VFR.jsx b/web/src/components/VFR.jsx new file mode 100644 index 0000000..36bc079 --- /dev/null +++ b/web/src/components/VFR.jsx @@ -0,0 +1,304 @@ +import React from 'react'; +import { num } from '../api/useXplane.js'; + +// Classic analog "six-pack" VFR panel: airspeed, attitude, altimeter, turn +// coordinator, heading indicator, vertical speed — round steam gauges driven by +// the same X-Plane datarefs. For steam/VFR aircraft (and just because it looks +// great). Each gauge is a self-contained SVG on a dark instrument panel. + +const arr0 = (v, d = 0) => (Array.isArray(v) ? num(v[0], d) : num(v, d)); +const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); +// point on a dial: ang in degrees, 0 = up (12 o'clock), clockwise positive. +const pt = (cx, cy, r, ang) => { + const a = (ang - 90) * Math.PI / 180; + return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; +}; + +function Bezel({ title, children }) { + return ( +
+ + + + + {children} + + {title} +
+ ); +} + +const Needle = ({ ang, len = 70, w = 4, color = '#fff', tail = 14 }) => ( + + + +); + +// generic tick ring +function ticks(min, max, a0, a1, step, big = 1, r = 84, lab) { + const out = []; + let i = 0; + for (let v = min; v <= max + 1e-6; v += step, i++) { + const ang = a0 + ((v - min) / (max - min)) * (a1 - a0); + const isBig = i % big === 0; + const [x1, y1] = pt(100, 100, r, ang); + const [x2, y2] = pt(100, 100, r - (isBig ? 12 : 7), ang); + out.push(); + if (lab && isBig) { + const [lx, ly] = pt(100, 100, r - 24, ang); + out.push({lab(v)}); + } + } + return out; +} + +/* ---------- Airspeed ---------- */ +function ASI({ V }) { + const kt = num(V.airspeed); + const A0 = -150, A1 = 150, MIN = 0, MAX = 200; + const ang = A0 + (clamp(kt, MIN, MAX) - MIN) / (MAX - MIN) * (A1 - A0); + const arc = (lo, hi, color, rr, wdt) => { + const [x1, y1] = pt(100, 100, rr, A0 + (lo / MAX) * (A1 - A0)); + const [x2, y2] = pt(100, 100, rr, A0 + (hi / MAX) * (A1 - A0)); + const large = ((hi - lo) / MAX) * 300 > 180 ? 1 : 0; + return ; + }; + return ( + + {arc(33, 85, '#fff', 70, 4)} {/* white flap arc */} + {arc(48, 129, '#21d04a', 78, 5)} {/* green normal */} + {arc(129, 163, '#e6c200', 78, 5)}{/* yellow caution */} + {(() => { const [x, y] = pt(100, 100, 78, A0 + (163 / MAX) * (A1 - A0)); const [x2, y2] = pt(100, 100, 70, A0 + (163 / MAX) * (A1 - A0)); return ; })()} + {ticks(0, 200, A0, A1, 10, 2, 84, (v) => (v % 20 === 0 && v >= 40 ? v : ''))} + KT + + + + ); +} + +/* ---------- Attitude ---------- */ +function AI({ V }) { + const pitch = num(V.pitch), roll = num(V.roll); + const PPD = 2.0; // px per degree pitch + const off = clamp(pitch, -25, 25) * PPD; + return ( + + + + + + + + + + + {[-20, -10, 10, 20].map((d) => ( + + + {d % 20 === 0 && {Math.abs(d)}} + + ))} + + + {/* bank arc + pointer */} + + {[-60, -30, -20, -10, 0, 10, 20, 30, 60].map((b) => { const [x1, y1] = pt(100, 100, 84, b); const [x2, y2] = pt(100, 100, 78, b); return ; })} + + + + {/* fixed aircraft reference */} + + + + + ); +} + +/* ---------- Altimeter (3-pointer) ---------- */ +function ALT({ V }) { + const alt = num(V.altitude), baro = num(V.baro, 29.92); + const a100 = (alt % 1000) / 1000 * 360; + const a1000 = (alt % 10000) / 10000 * 360; + const a10000 = (alt % 100000) / 100000 * 360; + return ( + + {ticks(0, 1000, 0, 360, 100, 1, 84, (v) => (v < 1000 ? v / 100 : ''))} + {ticks(0, 1000, 0, 360, 20, 5, 84)} + + {baro.toFixed(2)} + {/* 10000 ft thin pointer */} + + {/* 1000 ft short fat */} + + {/* 100 ft long */} + + + + ); +} + +/* ---------- Turn coordinator ---------- */ +function TC({ V }) { + const roll = num(V.roll), slip = num(V.slip); + const bank = clamp(roll, -30, 30); // little-plane bank (approx turn rate) + const ballX = 100 + clamp(slip, -8, 8) * 3.0; + return ( + + {/* standard-rate marks */} + {[-25, 25].map((b) => { const [x1, y1] = pt(100, 100, 80, b); const [x2, y2] = pt(100, 100, 66, b); return ; })} + L + R + {/* little airplane */} + + + + + + 2 MIN + {/* inclinometer (slip ball) */} + + + + + ); +} + +/* ---------- Heading indicator ---------- */ +function HI({ V }) { + const hdg = ((num(V.heading) % 360) + 360) % 360; + const card = []; + for (let d = 0; d < 360; d += 5) { + const big = d % 30 === 0; + const [x1, y1] = pt(100, 100, 84, d); + const [x2, y2] = pt(100, 100, 84 - (big ? 12 : 7), d); + card.push(); + if (big) { + const [lx, ly] = pt(100, 100, 62, d); + const lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10; + card.push({lbl}); + } + } + return ( + + {card} + {/* fixed aircraft */} + + + + + ); +} + +/* ---------- Vertical speed ---------- */ +function VSI({ V }) { + const vs = clamp(num(V.vspeed), -2000, 2000); + // 0 at 9 o'clock (270°), climb sweeps up (toward 0/up), descent down. + const ang = 270 + (vs / 2000) * 160; // -2000→110°, 0→270°, +2000→430°(=70°) + return ( + + {ticks(-2000, 2000, 110, 430, 500, 1, 84, (v) => Math.abs(v) / 1000)} + {ticks(-2000, 2000, 110, 430, 100, 5, 84)} + FPM x1000 + 0 + + + + ); +} + +/* ---------- small gauges for the engine/fuel cluster ---------- */ +const KG_GAL = 2.72; + +function SmallBezel({ title, children }) { + return ( +
+ + + + {children} + + {title} +
+ ); +} + +// dual half-dial gauge: left needle (lower-left sweep) + right needle (lower-right) +function Dual({ title, l, r }) { + const sg = (v, min, max, a0, a1) => a0 + (clamp(v, min, max) - min) / (max - min) * (a1 - a0); + const sp = (cx, cy, rr, ang) => { const a = (ang - 90) * Math.PI / 180; return [cx + rr * Math.cos(a), cy + rr * Math.sin(a)]; }; + const band = (lo, hi, min, max, a0, a1, color) => { + const [x1, y1] = sp(60, 60, 46, sg(lo, min, max, a0, a1)); + const [x2, y2] = sp(60, 60, 46, sg(hi, min, max, a0, a1)); + return ; + }; + const La = sg(l.value, l.min, l.max, -150, -20), Ra = sg(r.value, r.min, r.max, 20, 150); + return ( + + {l.green && band(l.green[0], l.green[1], l.min, l.max, -150, -20, '#21d04a')} + {r.green && band(r.green[0], r.green[1], r.min, r.max, 20, 150, '#21d04a')} + {l.tag} + {r.tag} + + + + + ); +} + +/* ---------- tachometer ---------- */ +function Tach({ V }) { + const rpm = arr0(V.engRpm); + const A0 = -150, A1 = 150; + const ang = A0 + clamp(rpm, 0, 3500) / 3500 * (A1 - A0); + return ( + + {ticks(0, 3500, A0, A1, 500, 1, 84, (v) => v / 100)} + {(() => { const [x1, y1] = pt(100, 100, 84, A0 + 2700 / 3500 * (A1 - A0)); const [x2, y2] = pt(100, 100, 72, A0 + 2700 / 3500 * (A1 - A0)); return ; })()} + {(() => { const [x1, y1] = pt(100, 100, 80, A0 + 2100 / 3500 * (A1 - A0)); const [x2, y2] = pt(100, 100, 80, A0 + 2600 / 3500 * (A1 - A0)); return ; })()} + RPM x100 + {Math.round(rpm)} + + + + ); +} + +/* ---------- digital OAT / Volts plate ---------- */ +function Clock({ V }) { + const oatC = num(V.oat), oatF = Math.round(oatC * 9 / 5 + 32); + const volts = arr0(V.volts, 28); + return ( +
+
{oatF}°FO.A.T.
+
{volts.toFixed(1)}VOLT
+
+ ); +} + +export default function VFR({ values: V }) { + const fuelL = arr0(V.fuelQty, 0) / KG_GAL, fuelR = (Array.isArray(V.fuelQty) ? num(V.fuelQty[1]) : 0) / KG_GAL; + const oilF = arr0(V.oilTemp) * 9 / 5 + 32, oilP = arr0(V.oilPress); + const egtF = arr0(V.egt) * 9 / 5 + 32, ffGph = (arr0(V.fuelFlow) * 3600) / KG_GAL; + const amps = arr0(V.amps); + return ( +
+
+
+ + + + + +
+
+
+ + +
+
+
+
+
+ ); +} diff --git a/web/src/main.jsx b/web/src/main.jsx new file mode 100644 index 0000000..0857bd6 --- /dev/null +++ b/web/src/main.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.jsx'; +import './styles.css'; + +createRoot(document.getElementById('root')).render(); diff --git a/web/src/styles.css b/web/src/styles.css new file mode 100644 index 0000000..c71de41 --- /dev/null +++ b/web/src/styles.css @@ -0,0 +1,491 @@ +:root { + --bg: #0a0a0a; + --panel: #141414; + --accent: #00e676; + --amber: #ffcc00; + /* App chrome (everything that is NOT a G1000 instrument): same clean + macOS-dark look as the desktop launcher. */ + --ui-font: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; + --c-bg: #1c1c1e; + --c-surface: #2c2c2e; + --c-fill: #3a3a3c; + --c-line: #48484a; + --c-line-soft: #38383a; + --c-txt: #ffffff; + --c-txt2: #ebebf5; + --c-mut: #8e8e93; + --c-green: #30d158; + --c-amber: #ffd60a; + --c-red: #ff453a; + color-scheme: dark; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +html, body, #root { height: 100%; } +body { + background: var(--bg); + color: #eee; + font-family: -apple-system, system-ui, "Segoe UI", monospace; + overscroll-behavior: none; + -webkit-user-select: none; + user-select: none; + touch-action: manipulation; +} + +.app { display: flex; flex-direction: row; height: 100vh; height: 100dvh; } + +/* collapsible left nav rail — macOS-dark app chrome (Inter) */ +.sidebar { + display: flex; flex-direction: column; gap: 8px; + background: var(--c-bg); border-right: 1px solid var(--c-line-soft); + font-family: var(--ui-font); + width: 58px; flex: 0 0 58px; transition: width .18s ease, flex-basis .18s ease; + padding: 10px 8px; + padding-top: max(10px, env(safe-area-inset-top)); + padding-left: max(8px, env(safe-area-inset-left)); +} +.app.nav-wide .sidebar { width: 176px; flex-basis: 176px; } + +.sb-top { + display: flex; align-items: center; justify-content: space-between; gap: 6px; + background: var(--c-surface); color: var(--c-txt); + border: 1px solid var(--c-line-soft); border-radius: 10px; + padding: 9px 10px; cursor: pointer; font-family: inherit; min-height: 38px; +} +.sb-top:hover { background: #34343a; } +.brand { font-weight: 700; font-size: 17px; letter-spacing: .3px; white-space: nowrap; } +.brand span { color: var(--c-green); font-weight: 500; } +.sb-chev { color: var(--c-mut); font-size: 12px; } +.app.nav-narrow .brand span { display: none; } +.app.nav-narrow .sb-chev { display: none; } +.app.nav-narrow .sb-top { justify-content: center; padding: 9px 0; } + +.snav { display: flex; flex-direction: column; gap: 5px; flex: 1; margin-top: 4px; } +.snav-i { + display: flex; align-items: center; gap: 13px; + background: transparent; color: var(--c-mut); + border: 1px solid transparent; border-radius: 10px; + padding: 10px 11px; font-size: 14px; font-weight: 500; font-family: inherit; + cursor: pointer; width: 100%; text-align: left; overflow: hidden; + transition: background .12s, color .12s; +} +.snav-i:hover { background: var(--c-surface); color: var(--c-txt2); } +.snav-i.active { background: rgba(48,209,88,.16); color: var(--c-green); border-color: rgba(48,209,88,.35); } +.snav-ic { flex: 0 0 22px; } +.snav-lbl { white-space: nowrap; } +.app.nav-narrow .snav-lbl { display: none; } +.app.nav-narrow .snav-i { justify-content: center; padding: 10px 0; gap: 0; } + +.sb-conn { display: flex; align-items: center; gap: 11px; padding: 9px 11px; font-size: 11px; font-weight: 600; letter-spacing: .8px; color: var(--c-mut); } +.sb-conn .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; background: var(--c-mut); } +.sb-conn.ok .dot { background: var(--c-green); box-shadow: 0 0 8px var(--c-green); } +.sb-conn.warn .dot { background: var(--c-amber); } +.sb-conn.bad .dot { background: var(--c-red); } +.app.nav-narrow .sb-conn { justify-content: center; padding: 9px 0; } + +/* main screen */ +.screen { flex: 1; display: flex; align-items: center; justify-content: center; padding: 10px; overflow: hidden; min-width: 0; } + +/* VFR analog panel (C172 steam gauges on a metal panel) */ +.vfr-panel { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; + background: linear-gradient(150deg, #44474d 0%, #303338 40%, #232529 100%); + overflow: auto; padding: 18px; } +.vfr-layout { display: flex; align-items: center; gap: clamp(14px, 3vw, 40px); } +.vfr-cluster { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px 14px; align-content: center; max-width: 270px; } +.vfr-cluster .vfr-clock { grid-column: 1 / -1; } +.vfr-main { display: flex; flex-direction: column; align-items: center; gap: clamp(10px, 2vw, 26px); } +.vfr-grid { display: grid; grid-template-columns: repeat(3, minmax(140px, 215px)); gap: clamp(10px, 2.2vw, 26px); } +.vfr-tach { width: clamp(130px, 16vw, 180px); } +.vfr-gauge, .vfr-sg { display: flex; flex-direction: column; align-items: center; gap: 6px; } +.vfr-gauge svg, .vfr-sg svg { width: 100%; height: auto; filter: drop-shadow(0 4px 12px rgba(0,0,0,.55)); } +.vfr-name, .vfr-sname { font-family: var(--ui-font); letter-spacing: 1.2px; color: #c9d0d7; font-weight: 600; } +.vfr-name { font-size: 11px; } .vfr-sname { font-size: 9px; } +.vfr-clock { background: #0c0d0f; border: 1px solid #2a2f36; border-radius: 6px; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; } +.vc-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; } +.vc-row b { font-family: 'Saira Condensed', monospace; color: #46e0c0; font-size: 18px; } +.vc-row span { color: #7d8893; font-size: 9px; letter-spacing: 1px; } + +/* Garmin-like condensed type on every PFD/MFD readout (CSS overrides the + monospace presentation attributes inside the SVG). */ +.g1000 text, .g1000 tspan { font-family: 'Saira Condensed', 'Saira Semi Condensed', sans-serif; font-weight: 600; } + +/* PFD */ +.pfd, .g1000 { width: 100%; height: 100%; max-height: 100%; display: block; } +.pfd-wrap { position: relative; width: 100%; height: 100%; } +.pfd-wrap .g1000 { position: relative; z-index: 1; } +/* SVT (synthetic vision) terrain behind the attitude window */ +.svt-pos { position: absolute; z-index: 0; overflow: hidden; } +.svt-fallback { + width: 100%; height: 100%; overflow: hidden; + background: linear-gradient(#1f86d6 0%, #6fb2e6 49%, #7a5230 51%, #4a2d12 100%); +} +/* INSET moving map, bottom-left corner of the PFD (toggled by INSET softkey) */ +.pfd-inset { position: absolute; z-index: 2; overflow: hidden; border: 2px solid #c8d0d8; box-shadow: 0 0 0 1px #000; } +.pfd-inset .mapwrap, .pfd-inset .leaflet-host { width: 100%; height: 100%; } +.mapwrap.inset .leaflet-control-container { display: none; } +/* NRST (nearest) window — pops over the right side of the PFD */ +.nrst-window { + position: absolute; z-index: 4; top: 9%; right: 1.5%; width: 41%; max-width: 440px; + background: rgba(8, 10, 12, 0.94); border: 1px solid #4a5560; border-radius: 3px; + color: #fff; font-family: 'Roboto Mono', monospace; box-shadow: 0 4px 18px rgba(0,0,0,0.6); +} +.nrst-head { display: flex; align-items: center; gap: 8px; padding: 5px 8px; background: #11161b; border-bottom: 1px solid #2c343c; } +.nrst-title { color: #39d3c0; font-size: 13px; font-weight: bold; letter-spacing: 1px; } +.nrst-tabs { display: flex; gap: 3px; margin-left: auto; } +.nrst-tabs button { background: #1c242c; color: #9fb0bd; border: 1px solid #2c343c; border-radius: 2px; font: inherit; font-size: 11px; padding: 2px 9px; cursor: pointer; } +.nrst-tabs button.on { background: #0c9; color: #04201c; border-color: #0c9; font-weight: bold; } +.nrst-x { background: none; border: none; color: #9fb0bd; cursor: pointer; font-size: 14px; padding: 0 2px; } +.nrst-cols, .nrst-row { display: grid; grid-template-columns: 1.3fr 0.8fr 1fr 1.1fr; align-items: baseline; padding: 2px 8px; column-gap: 4px; } +.nrst-cols { color: #6f808d; font-size: 10px; border-bottom: 1px solid #222; padding-bottom: 4px; } +.nrst-row { font-size: 14px; border-bottom: 1px solid #161b20; } +.nrst-row .c-id { color: #0ff; font-weight: bold; } +.nrst-row .c-brg, .nrst-row .c-dis { color: #fff; text-align: right; } +.nrst-row .c-dis u { color: #6f808d; font-size: 9px; text-decoration: none; margin-left: 1px; } +.nrst-row .c-xtra { color: #39d3c0; text-align: right; } +.nrst-row .c-name { grid-column: 1 / -1; color: #8b9aa6; font-size: 10px; margin-top: -1px; } +.nrst-list { max-height: 62vh; overflow-y: auto; } +.nrst-empty { color: #6f808d; text-align: center; padding: 12px; font-size: 12px; } +/* XPDR squawk-entry readout above the softkey keypad */ +.squawk-entry { text-align: center; color: #9fb0bd; font-family: 'Roboto Mono', monospace; font-size: 13px; letter-spacing: 1px; padding: 3px 0; } +.squawk-entry b { color: #19ff19; font-size: 18px; letter-spacing: 5px; margin-left: 6px; } +/* TMR/REF window — left side of the PFD */ +.tmr-window { + position: absolute; z-index: 4; top: 9%; left: 1.5%; width: 30%; max-width: 320px; + background: rgba(8, 10, 12, 0.94); border: 1px solid #4a5560; border-radius: 3px; + color: #fff; font-family: 'Roboto Mono', monospace; box-shadow: 0 4px 18px rgba(0,0,0,0.6); +} +.tmr-body { padding: 8px 10px; } +.tmr-clock { font-size: 34px; font-weight: bold; text-align: center; color: #fff; letter-spacing: 2px; } +.tmr-dir { display: flex; gap: 4px; justify-content: center; margin: 4px 0; } +.tmr-dir button { background: #1c242c; color: #9fb0bd; border: 1px solid #2c343c; font: inherit; font-size: 11px; padding: 2px 12px; cursor: pointer; } +.tmr-dir button.on { background: #0c9; color: #04201c; font-weight: bold; } +.tmr-ctl { display: flex; gap: 6px; margin: 6px 0; } +.tmr-ctl .fbtn { flex: 1; } +.tmr-target { display: flex; align-items: center; gap: 6px; justify-content: center; color: #cfd6dd; font-size: 13px; } +.tmr-target label { color: #6f808d; font-size: 10px; } +.tmr-target button { background: #1c242c; color: #fff; border: 1px solid #2c343c; width: 24px; cursor: pointer; } +.tmr-sec { color: #39d3c0; font-size: 10px; margin: 8px 0 4px; border-top: 1px solid #222; padding-top: 6px; } +.tmr-vspeeds { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; } +.tmr-vspeeds button { display: flex; align-items: baseline; gap: 5px; background: #141a20; border: 1px solid #222b33; color: #cfd6dd; font: inherit; padding: 4px 6px; cursor: pointer; } +.tmr-vspeeds button.on { border-color: #0c9; background: #0a1f1b; } +.tmr-vspeeds button i { color: #0a8; font-style: normal; } .tmr-vspeeds button b { color: #fff; margin-left: auto; } .tmr-vspeeds button u { color: #6f808d; font-size: 9px; text-decoration: none; } +.tmr-mins { display: flex; align-items: center; gap: 6px; margin-top: 8px; } +.tmr-mins button:first-child { background: #1c242c; color: #9fb0bd; border: 1px solid #2c343c; font: inherit; font-size: 11px; padding: 3px 8px; cursor: pointer; } +.tmr-mins button:first-child.on { background: #0c9; color: #04201c; font-weight: bold; } +.tmr-mins > button:not(:first-child) { background: #1c242c; color: #fff; border: 1px solid #2c343c; width: 24px; cursor: pointer; } +.tmr-mins span { color: #fff; margin-left: auto; } .tmr-mins span.alert { color: #ffd24a; font-weight: bold; } +.tmr-minalert { margin-top: 6px; text-align: center; background: #ffd24a; color: #1a1400; font-weight: bold; padding: 3px; border-radius: 2px; letter-spacing: 2px; } +/* Modal dialogs (Direct-To, …) */ +.dlg-backdrop { position: fixed; inset: 0; z-index: 20; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; } +.dlg { background: #0c1015; border: 1px solid #3a4651; border-radius: 5px; min-width: 320px; color: #fff; font-family: 'Roboto Mono', monospace; box-shadow: 0 8px 30px rgba(0,0,0,0.7); } +.dlg-head { background: #11161b; padding: 8px 12px; border-bottom: 1px solid #2c343c; color: #fff; font-weight: bold; letter-spacing: 1px; border-radius: 5px 5px 0 0; } +.dto-arrow { color: #e040fb; margin-right: 8px; } +.dto-body { padding: 12px; } +.dto-lbl { color: #6f808d; font-size: 11px; display: block; margin-bottom: 4px; } +.dto-input { width: 100%; box-sizing: border-box; background: #05080b; border: 1px solid #2c343c; color: #0ff; font: inherit; font-size: 20px; letter-spacing: 3px; padding: 6px 10px; text-transform: uppercase; } +.dto-hits { display: flex; flex-direction: column; gap: 3px; margin-top: 6px; } +.dto-hits button { display: flex; align-items: baseline; gap: 8px; background: #141a20; border: 1px solid #222b33; color: #cfd6dd; font: inherit; padding: 5px 8px; cursor: pointer; text-align: left; } +.dto-hits button.on { border-color: #0c9; background: #0a1f1b; } +.dto-hits button b { color: #0ff; } .dto-hits button i { color: #0a8; font-style: normal; font-size: 11px; } .dto-hits button span { color: #6f808d; font-size: 11px; margin-left: auto; } +.dto-sel { display: flex; align-items: baseline; gap: 10px; margin-top: 10px; padding-top: 8px; border-top: 1px solid #222; } +.dto-sel .dto-id { color: #0ff; font-size: 20px; font-weight: bold; } +.dto-sel .dto-type { color: #0a8; font-size: 11px; } +.dto-sel .dto-vec { color: #e040fb; margin-left: auto; font-weight: bold; } +.dlg-actions { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid #2c343c; } +.dlg-actions .fbtn { flex: 1; } +/* PROC dialog */ +.dlg.proc { width: 640px; max-width: 92vw; } +.proc-body { padding: 12px; } +.proc-apt { display: flex; align-items: center; gap: 8px; } +.proc-apt label { color: #6f808d; font-size: 11px; } +.proc-apt input { flex: 1; background: #05080b; border: 1px solid #2c343c; color: #0ff; font: inherit; font-size: 18px; letter-spacing: 2px; padding: 5px 10px; text-transform: uppercase; } +.proc-apt .fbtn { flex: 0 0 auto; } +.proc-err { color: #ffae42; font-size: 12px; margin-top: 6px; } +.proc-tabs { display: flex; gap: 4px; margin: 10px 0 6px; } +.proc-tabs button { flex: 1; background: #1c242c; color: #9fb0bd; border: 1px solid #2c343c; font: inherit; font-size: 12px; padding: 5px; cursor: pointer; } +.proc-tabs button.on { background: #0c9; color: #04201c; font-weight: bold; border-color: #0c9; } +.proc-cols { display: grid; grid-template-columns: 1fr 1fr 1.4fr; gap: 6px; height: 300px; } +.proc-list, .proc-preview { background: #05080b; border: 1px solid #1c242c; overflow-y: auto; display: flex; flex-direction: column; } +.proc-coltitle { position: sticky; top: 0; background: #11161b; color: #6f808d; font-size: 10px; padding: 4px 8px; border-bottom: 1px solid #222; } +.proc-list button { background: none; border: none; border-bottom: 1px solid #11161b; color: #cfd6dd; font: inherit; font-size: 14px; text-align: left; padding: 6px 8px; cursor: pointer; } +.proc-list button:hover { background: #11161b; } +.proc-list button.on { background: #0a1f1b; color: #0ff; font-weight: bold; } +.proc-empty { color: #6f808d; font-size: 11px; padding: 8px; } +.proc-leg { display: flex; align-items: baseline; gap: 8px; padding: 5px 8px; border-bottom: 1px solid #11161b; font-size: 13px; } +.proc-leg b { color: #0ff; } .proc-leg u { color: #39d3c0; font-size: 10px; text-decoration: none; margin-left: auto; } +/* G1000 vector nav symbology drawn from X-Plane's own nav data */ +.nav-divicon { background: none; border: none; } +.nav-sym { position: relative; width: 18px; height: 18px; } +.nav-lbl { position: absolute; left: 19px; top: 1px; font: 700 11px/1 monospace; white-space: nowrap; + text-shadow: 0 0 2px #000, 0 0 2px #000, 1px 1px 1px #000; } +/* Bank pivots about the aircraft reference (attitude centre), which sits ~28% + down the full-screen terrain box — so terrain roll tracks the attitude. */ +.svt-canvas { width: 100%; height: 100%; transform-origin: 50% 28%; } +.svt-canvas .maplibregl-canvas { width: 100% !important; height: 100% !important; } + +/* ---- GDU-1040 bezel ---- */ +.bezel { + width: 100%; height: 100%; display: flex; align-items: stretch; gap: 0; + background: linear-gradient(150deg, #3a3c40, #202123 55%, #2c2d30); + border-radius: 18px; padding: 12px; box-shadow: inset 0 1px 0 #4a4c50, 0 8px 30px #000; + font-family: 'Saira Semi Condensed', sans-serif; +} +.bezel-core { flex: 1; display: flex; flex-direction: column; min-width: 0; } +.bezel-title { text-align: center; color: #c9ced3; font-size: 14px; font-weight: 700; letter-spacing: 3px; padding: 2px 0 6px; } +.bezel-screen { + flex: 1; background: #000; border-radius: 6px; overflow: hidden; position: relative; + border: 2px solid #0a0a0a; box-shadow: inset 0 0 18px #000, inset 0 0 2px #1a3a5a; + display: flex; min-height: 0; +} +.bezel-screen > * { width: 100%; height: 100%; } +.softkeys { display: grid; grid-template-columns: repeat(12, 1fr); gap: 6px; padding: 8px 2px 2px; } +.softkey { + height: 30px; display: flex; align-items: center; justify-content: center; + background: linear-gradient(#202224, #131416); border: 1px solid #000; border-top: 1px solid #45474b; + border-radius: 4px; color: #cfd6dc; font-size: 12px; font-weight: 600; letter-spacing: .3px; + box-shadow: 0 1px 2px #000; cursor: pointer; font-family: inherit; +} +.softkey:not(.empty):hover { background: linear-gradient(#2a2c2f, #1a1b1e); border-top-color: #5a5d61; } +.softkey:not(.empty):active { background: #0d0e10; box-shadow: inset 0 1px 3px #000; } +.softkey.empty { color: transparent; cursor: default; } +.softkey.on { background: #e8edf2; color: #0a0c0e; border-top-color: #fff; font-weight: 800; } +.softkey.caution { color: #1a1b1e; background: linear-gradient(#ffd23a, #e0b400); border-top-color: #fff2a8; font-weight: 800; } + +.bezel-knobs { display: flex; flex-direction: column; align-items: center; justify-content: space-around; padding: 4px 6px; gap: 6px; } +.bezel-knobs.left { width: 88px; } .bezel-knobs.right { width: 100px; } +.knob-wrap { display: flex; flex-direction: column; align-items: center; gap: 2px; position: relative; } +.knob-lbl { color: #d2d7dc; font-size: 12px; font-weight: 800; letter-spacing: 1px; } +.knob-sub { color: #8b9197; font-size: 8.5px; font-weight: 600; letter-spacing: .3px; text-align: center; } +.knob-extra { position: absolute; right: -10px; top: 6px; width: 20px; height: 16px; background: #1a1b1e; border: 1px solid #000; border-radius: 3px; color: #cfd6dc; font-size: 11px; text-align: center; line-height: 16px; } +.knob.outer { + width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: relative; + background: radial-gradient(circle at 35% 30%, #55585d, #2a2c2f 70%); box-shadow: 0 2px 5px #000, inset 0 1px 0 #6a6d72; +} +.knob-wrap.big .knob.outer { width: 58px; height: 58px; } +.knob.inner { width: 26px; height: 26px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #44474b, #1c1e20); box-shadow: inset 0 1px 0 #5a5d61; } +.knob.joy .joy-cross { position: absolute; color: #6a6d72; font-size: 22px; font-weight: 700; pointer-events: none; } +.knob.outer { cursor: pointer; border: none; padding: 0; } +.knob.outer:active { box-shadow: 0 1px 2px #000, inset 0 2px 4px #000; } + +/* concentric knob + its rotation arrows */ +.knob-cluster { position: relative; display: flex; align-items: center; justify-content: center; padding: 14px; } +.knob-arrow { + position: absolute; width: 16px; height: 16px; padding: 0; line-height: 1; + background: #1a1b1e; border: 1px solid #000; border-radius: 3px; color: #b9c0c6; + font-size: 11px; font-weight: 800; cursor: pointer; display: flex; align-items: center; justify-content: center; +} +.knob-arrow:hover { background: #2a2c2f; color: #fff; } +.knob-arrow:active { background: #000; } +.knob-arrow.left { left: -2px; } .knob-arrow.right { right: -2px; } +.knob-arrow.top { top: -2px; } .knob-arrow.bottom { bottom: -2px; } + +.pan-pad { display: grid; grid-template-columns: repeat(2, 14px); gap: 2px; margin-top: 3px; } +.pan-pad button { + width: 14px; height: 12px; padding: 0; background: #1a1b1e; border: 1px solid #000; + border-radius: 2px; color: #b9c0c6; font-size: 7px; cursor: pointer; +} +.pan-pad button:active { background: #000; } + +.bezel-btn { + background: linear-gradient(#26282b, #16171a); border: 1px solid #000; border-top: 1px solid #3e4044; + border-radius: 5px; color: #cfd6dc; font-size: 11px; font-weight: 700; text-align: center; + padding: 6px 4px; min-width: 40px; line-height: 1.1; cursor: pointer; font-family: inherit; +} +.bezel-btn:hover { background: linear-gradient(#303236, #1c1d20); border-top-color: #55585d; } +.bezel-btn:active { background: #0d0e10; box-shadow: inset 0 1px 3px #000; } +.bezel-btn.sm { font-size: 10px; padding: 5px 3px; min-width: 0; } +.bezel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; width: 100%; } + +/* autopilot mode controller (MFD left bezel) */ +.ap-controller { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; width: 100%; } +.ap-key { + background: linear-gradient(#26282b, #131416); border: 1px solid #000; border-top: 1px solid #3e4044; + border-radius: 4px; color: #d6dbe0; font-size: 11px; font-weight: 700; letter-spacing: .2px; + padding: 7px 2px; line-height: 1; font-family: 'Saira Semi Condensed', sans-serif; +} +.ap-key:active { transform: scale(0.95); } +.ap-key.on { background: #0a8f3c; color: #fff; border-color: #0c0; box-shadow: 0 0 8px rgba(0,200,80,.6); } + +/* G1000 MFD: top NAV/COM bar, engine strip (EIS) left, moving map right */ +.mfd-g1000 { display: flex; flex-direction: column; width: 100%; height: 100%; background: #000; } +.mfd-topbar { width: 100%; height: auto; flex-shrink: 0; display: block; border-bottom: 1px solid #3a3a3a; } +.mfd-body { flex: 1; display: flex; min-height: 0; } +.eis-svg { width: 178px; flex-shrink: 0; height: 100%; background: #0a0a0a; border-right: 1px solid #222; } +.mfd-map { flex: 1; position: relative; min-width: 0; } +.leaflet-host.dark { background: #000; } +/* G1000 chrome over the map */ +.map-chrome { position: absolute; inset: 0; pointer-events: none; z-index: 650; } +.map-rose { position: absolute; left: 50%; top: 50%; width: min(74%, 380px); height: min(74%, 380px); transform: translate(-50%, -50%); } +.mc-tr { position: absolute; right: 6px; top: 6px; display: flex; gap: 6px; align-items: center; + font: 600 12px/1 monospace; color: #fff; background: #000a; padding: 4px 6px; } +.mc-tr span { color: #cfe3ff; } +.mc-range { position: absolute; right: 6px; bottom: 36px; font: 700 14px/1 monospace; color: #0ff; + background: #000; border: 1px solid #555; padding: 4px 8px; } +.mc-mode { position: absolute; right: 6px; bottom: 6px; display: flex; align-items: center; gap: 3px; + font: 600 12px/1 monospace; color: #0ff; background: #000a; padding: 4px 6px; } +.mc-mode em { width: 8px; height: 8px; border: 1px solid #0ff; display: inline-block; } +.mc-mode em.on { background: #0ff; } + +/* Autopilot — GMC-710-style AFCS mode controller (app chrome look) */ +.afcs { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; + background: radial-gradient(120% 100% at 50% 0%, #1f2227, #0c0d0f); padding: 20px; font-family: var(--ui-font); } +.afcs-unit { width: min(880px, 100%); background: linear-gradient(#202329, #15171b); + border: 1px solid #34383f; border-radius: 16px; padding: 16px; box-shadow: 0 14px 40px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.04); display: flex; flex-direction: column; gap: 14px; } + +.afcs-ann { display: flex; align-items: center; gap: 10px; background: #07080a; border: 1px solid #2a2f36; + border-radius: 10px; padding: 10px 14px; font-family: 'Saira Semi Condensed', var(--ui-font); } +.ann { color: #4a525b; font-size: 17px; font-weight: 700; letter-spacing: 1px; } +.ann.on { color: #2ee06a; text-shadow: 0 0 10px rgba(46,224,106,.5); } +.ann.mode { color: #2ee06a; text-shadow: 0 0 10px rgba(46,224,106,.5); font-size: 18px; } +.ann-sep { width: 1px; height: 18px; background: #2a2f36; } +.ann-gap { flex: 1; } +.ann.val { color: #fff; font-size: 18px; } .ann.val i { color: #7d8893; font-style: normal; font-size: 11px; margin-left: 2px; } + +.afcs-keys { display: grid; grid-template-columns: repeat(10, 1fr); gap: 8px; } +.apk { background: linear-gradient(#2b2f36, #1c2026); color: #cdd4db; border: 1px solid #3a3f47; + border-radius: 9px; padding: 14px 4px; font-family: var(--ui-font); font-size: 14px; font-weight: 700; + letter-spacing: .5px; cursor: pointer; transition: all .12s; } +.apk:hover { border-color: #4a525b; } +.apk:active { transform: translateY(1px); } +.apk.on { background: linear-gradient(#1f8f47, #146b34); color: #fff; border-color: #2ee06a; + box-shadow: 0 0 16px rgba(46,224,106,.45), inset 0 1px 0 rgba(255,255,255,.15); } +.apk.sm { padding: 9px 12px; font-size: 12px; } + +.afcs-sels { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } +.apsel { background: #07080a; border: 1px solid #2a2f36; border-radius: 12px; padding: 10px; text-align: center; } +.apsel-lbl { color: #2ee06a; font-size: 12px; font-weight: 700; letter-spacing: 1.5px; } +.apsel-val { font-family: 'Saira Condensed', monospace; font-size: 34px; font-weight: 700; color: #fff; line-height: 1.1; margin: 2px 0 6px; } +.apsel-val span { font-size: 14px; color: #7d8893; margin-left: 3px; } +.apsel-knob { display: flex; align-items: center; justify-content: center; gap: 10px; } +.apsel-knob button { width: 46px; height: 40px; background: linear-gradient(#2b2f36, #1c2026); color: #cdd4db; + border: 1px solid #3a3f47; border-radius: 8px; font-size: 20px; cursor: pointer; } +.apsel-knob button:active { background: #146b34; color: #fff; } +.knobdot { width: 30px; height: 30px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #3a3f47, #15171b); border: 1px solid #444; } + +.afcs-pitch { display: flex; align-items: center; gap: 10px; } +.afcs-pitch span { color: #7d8893; font-size: 11px; letter-spacing: 1.5px; font-weight: 600; } + +/* FMS as an X-Plane-style CDU/FMC */ +.cdu { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; + background: radial-gradient(120% 100% at 50% 0%, #1c1f24, #0b0c0e); padding: 16px; font-family: var(--ui-font); } +.cdu-unit { width: min(560px, 100%); background: linear-gradient(#23262c, #15171b); border: 1px solid #34383f; + border-radius: 18px; padding: 14px; box-shadow: 0 16px 44px rgba(0,0,0,.55); display: flex; flex-direction: column; gap: 12px; } +.cdu-screenwrap { display: flex; align-items: stretch; gap: 8px; } +.cdu-lsks { display: flex; flex-direction: column; justify-content: space-around; padding: 38px 0 30px; gap: 6px; } +.cdu-lsk { width: 18px; height: 26px; border-radius: 4px; background: linear-gradient(#3a3f47, #23262c); border: 1px solid #4a525b; cursor: pointer; } +.cdu-lsk:active { background: #146b34; } +.cdu-screen { flex: 1; background: #04140a; border: 1px solid #0b3; border-radius: 8px; padding: 10px 12px; + font-family: 'Saira Semi Condensed', monospace; color: #34e06a; min-height: 240px; + box-shadow: inset 0 0 30px rgba(0,80,30,.4); display: flex; flex-direction: column; } +.cdu-hdr { display: flex; justify-content: space-between; color: #9fffc0; font-size: 13px; letter-spacing: 1px; border-bottom: 1px solid #084d24; padding-bottom: 4px; } +.cdu-cols { display: grid; grid-template-columns: 1fr 60px 60px; color: #1f9d52; font-size: 10px; margin-top: 4px; } +.cdu-row { display: grid; grid-template-columns: 1fr 60px 60px; align-items: baseline; font-size: 17px; padding: 3px 0; border-bottom: 1px solid #06250f; } +.cdu-row.act { background: rgba(255,32,255,.12); } +.cdu-row.act .cdu-wpt, .cdu-row.act .cdu-dtk, .cdu-row.act .cdu-dist { color: #ff5bff; } +.cdu-wpt { color: #fff; font-weight: 600; } .cdu-wpt i { color: #1f9d52; font-style: normal; font-size: 11px; margin-left: 7px; } +.cdu-dtk, .cdu-dist { color: #34e06a; text-align: right; } +.cdu-add { color: #1f9d52; font-size: 14px; } .cdu-empty { color: #084d24; } +.cdu-scratch { margin-top: auto; border-top: 1px solid #084d24; padding-top: 6px; display: flex; justify-content: space-between; min-height: 22px; } +.cdu-sp { color: #fff; font-size: 16px; letter-spacing: 2px; background: #06250f; padding: 1px 8px; border-radius: 3px; min-width: 90px; } +.cdu-msg { color: #ffd24a; font-size: 13px; } .cdu-msg.ok { color: #34e06a; } + +.cdu-fn { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px; } +.cdu-pad { display: flex; flex-direction: column; gap: 7px; } +.cdu-padrow { display: grid; grid-template-columns: repeat(7, 1fr); gap: 7px; } +.cdu-padrow:nth-child(n+5) { grid-template-columns: repeat(5, 1fr); max-width: 72%; margin: 0 auto; width: 100%; } +.cdu-k { background: linear-gradient(#2b2f36, #1c2026); color: #dfe5ec; border: 1px solid #3a3f47; border-radius: 8px; + padding: 11px 4px; font-family: var(--ui-font); font-size: 14px; font-weight: 600; cursor: pointer; } +.cdu-k:hover { border-color: #4a525b; } .cdu-k:active { transform: translateY(1px); background: #146b34; color: #fff; } +.cdu-k.fn { font-size: 12px; letter-spacing: .5px; color: #9fb0bd; } +.cdu-k.fn.arm { background: #7d5a10; color: #fff; border-color: #ffd24a; } +.cdu-k.fn.exec { background: linear-gradient(#1f8f47, #146b34); color: #fff; border-color: #2ee06a; } + +/* MFD */ +.mfd { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; justify-content: center; width: 100%; } +.rose { width: min(58vh, 440px); height: auto; } +.mfd-data { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.mfd-cell { + background: var(--panel); border: 1px solid #262626; border-radius: 12px; + padding: 14px 18px; min-width: 150px; display: flex; flex-direction: column; gap: 4px; +} +.mfd-cell .k { color: var(--accent); font-size: 13px; font-weight: 700; letter-spacing: 1px; } +.mfd-cell .v { font-size: 30px; font-weight: 800; font-family: monospace; } + +/* ---- Map ---- */ +.mapwrap { position: relative; width: 100%; height: 100%; } +.leaflet-host { position: absolute; inset: 0; background: #1a1a1a; } +.leaflet-container { font-family: monospace; } +.ac-divicon svg { transition: transform .2s linear; transform-origin: 50% 50%; filter: drop-shadow(0 0 3px #000); } +.wp-label { background: #000a; border: none; color: #ff20ff; font-weight: 700; font-family: monospace; } +.map-hud { + position: absolute; top: 10px; left: 10px; z-index: 500; + display: flex; flex-direction: column; gap: 6px; +} +.hud-cell { + background: #000c; border: 1px solid #333; border-radius: 8px; padding: 5px 10px; + display: flex; gap: 10px; align-items: baseline; min-width: 150px; +} +.hud-cell span { color: var(--accent); font-size: 11px; font-weight: 700; letter-spacing: 1px; } +.hud-cell b { font-family: monospace; font-size: 18px; margin-left: auto; } +.follow-btn { + position: absolute; right: 10px; top: 10px; z-index: 500; + background: #000c; color: #888; border: 1px solid #333; border-radius: 8px; + padding: 8px 14px; font-weight: 700; font-family: monospace; +} +.follow-btn.on { color: var(--accent); border-color: var(--accent); } +.map-hint { + position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); z-index: 500; + background: #000a; color: #ccc; padding: 5px 12px; border-radius: 8px; font-size: 12px; +} + +/* ---- FMS (CDU style) ---- */ +.fms { + width: min(720px, 100%); height: 100%; display: flex; flex-direction: column; + background: #04140a; border: 1px solid #0a3; border-radius: 12px; overflow: hidden; + font-family: monospace; color: #29f06a; +} +.fms-head { + display: flex; justify-content: space-between; padding: 12px 16px; + background: #062; color: #d6ffe6; font-weight: 800; letter-spacing: 1px; font-size: 15px; +} +.fms-total { color: #bff8d4; } +.fms-rows { flex: 1; overflow-y: auto; padding: 6px 0; } +.fms-row { + display: grid; grid-template-columns: 34px 1fr 64px 64px 40px; align-items: center; + gap: 8px; padding: 10px 16px; border-bottom: 1px solid #0a2a18; font-size: 17px; +} +.fms-colhead { color: #0a6; font-size: 12px; letter-spacing: 1px; border-bottom: 1px solid #0a3; } +.fms-empty { padding: 24px 16px; color: #0a6; text-align: center; } +.fms-row .wid { font-weight: 800; color: #d6ffe6; } +.fms-row .wtype { font-style: normal; color: #0a8; font-size: 11px; margin-left: 8px; } +.fms-row .dtk, .fms-row .dist { color: #29f06a; } +.fms-row.orig .dist { color: #ffae42; } +.fms-row { cursor: pointer; } +.fms-row.active { background: rgba(255, 32, 255, 0.12); border-left: 3px solid #ff20ff; } +.fms-row.active .wid { color: #ff7bff; } +.fms-row.active .dtk, .fms-row.active .dist { color: #ff20ff; } +.fms-row .del { background: none; border: none; color: #c44; font-size: 18px; cursor: pointer; } +.fms-scratch { background: #021008; border-top: 2px solid #0a3; padding: 10px; } +.fms-hits { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; } +.fms-hits button { + text-align: left; background: #08240f; border: 1px solid #0a3; border-radius: 6px; + padding: 8px 12px; color: #29f06a; font-family: monospace; +} +.fms-hits button b { color: #d6ffe6; } .fms-hits button i { color: #0a8; font-style: normal; margin: 0 8px; } +.fms-hits button span { color: #0a7; font-size: 12px; } +.fms-input { display: flex; gap: 8px; } +.fms-input input { + flex: 1; background: #000; border: 1px solid #0a5; border-radius: 8px; color: #ffae42; + font-family: monospace; font-size: 18px; padding: 12px; letter-spacing: 1px; +} +.fbtn { + background: #08240f; color: #29f06a; border: 1px solid #0a5; border-radius: 8px; + padding: 12px 16px; font-weight: 800; font-family: monospace; letter-spacing: 1px; +} +.fbtn.add { background: #0a5; color: #021008; } +.fbtn.export { background: #ffae42; color: #2a1500; border-color: #ffae42; flex: 1; } +.fbtn:disabled { opacity: .4; } +.fms-actions { display: flex; gap: 8px; margin-top: 8px; } +.fms-export { margin-top: 8px; font-size: 13px; padding: 8px; border-radius: 6px; } +.fms-export.ok { background: #06330f; color: #9f9; } +.fms-export.err { background: #330606; color: #f99; } diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..d484717 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// During `npm run dev`, proxy the bridge WebSocket so you can develop the UI +// with hot-reload while the bridge runs on :8080. +export default defineConfig({ + plugins: [react()], + server: { + host: true, // expose dev server on the LAN too + proxy: { + '/ws': { target: 'ws://localhost:8080', ws: true }, + '/api': { target: 'http://localhost:8080' }, + }, + }, +});