Initial commit: offline flight planner (Rust, PFPX-class)
Clean-room reproduction of PFPX: route generation + IFPS validation + OFP. Independent design — official ICAO/EUROCONTROL data only (RAD, FRA points, IFPUV oracle); no community FPL sources. Workspace crates: core (routing/discover/rad/navdata/perf/export), cli, server, rad (Annex parser), gui (Tauri v2 + React + MapLibre). Route discovery: oracle-in-the-loop repair against Eurocontrol IFPUV. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "flightplanner-gui"
|
||||
version = "0.1.0"
|
||||
description = "Flight Planner desktop GUI (Tauri)"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix keeps the lib name distinct from the bin on Windows.
|
||||
# See https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "gui_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
flightplanner-core = { workspace = true }
|
||||
flightplanner-rad = { path = "../../crates/rad" }
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-start-dragging"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,388 @@
|
||||
//! Tauri backend for the Flight Planner GUI.
|
||||
//!
|
||||
//! The `plan` command just delegates to the shared engine in `core::api`, so the
|
||||
//! local (in-process) path runs the exact same code as the custom HTTP server.
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use flightplanner_core::api::{self, CachedRouteDto, PlanRequest, PlanResult};
|
||||
use flightplanner_core::error::CoreError;
|
||||
use flightplanner_core::rad::RadData;
|
||||
use flightplanner_core::routing::discover::{DiscoverResult, IfpsErr, IfpsValidator, IfpsVerdict};
|
||||
use tauri::{Emitter, State};
|
||||
|
||||
/// The RAD loaded once at startup (None if the file isn't present).
|
||||
struct RadState(Option<Arc<RadData>>);
|
||||
|
||||
/// Run route + optional fuel plan locally. Errors are stringified for the UI.
|
||||
#[tauri::command]
|
||||
fn plan(req: PlanRequest, rad: State<RadState>) -> Result<PlanResult, String> {
|
||||
api::plan(&req, rad.0.as_deref()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Authoritative IFPS validator backed by the IFPUV scraper (`validate.mjs`).
|
||||
/// Emits a `discover-progress` event before each ~25 s round-trip so the UI can
|
||||
/// show what's being checked.
|
||||
struct NodeIfps {
|
||||
dir: String,
|
||||
app: Option<tauri::AppHandle>,
|
||||
step: AtomicUsize,
|
||||
}
|
||||
|
||||
impl IfpsValidator for NodeIfps {
|
||||
fn validate(&self, adep: &str, ades: &str, route: &str, fl: i32) -> Result<IfpsVerdict, CoreError> {
|
||||
let step = self.step.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if let Some(app) = &self.app {
|
||||
let _ = app.emit(
|
||||
"discover-progress",
|
||||
serde_json::json!({
|
||||
"step": step,
|
||||
"adep": adep, "ades": ades, "route": route, "fl": fl,
|
||||
}),
|
||||
);
|
||||
}
|
||||
let payload = serde_json::json!({
|
||||
"adep": adep, "ades": ades, "route": route, "level": format!("F{:03}", fl),
|
||||
});
|
||||
let out = std::process::Command::new("node")
|
||||
.arg("validate.mjs")
|
||||
.arg(payload.to_string())
|
||||
.current_dir(&self.dir)
|
||||
.output()?; // io::Error → CoreError::Io
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let line = stdout
|
||||
.lines()
|
||||
.rev()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.ok_or_else(|| {
|
||||
CoreError::Other(format!(
|
||||
"IFPUV validator produced no result. stderr: {}",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
))
|
||||
})?;
|
||||
#[derive(serde::Deserialize)]
|
||||
struct V {
|
||||
accepted: Option<bool>,
|
||||
#[serde(default)]
|
||||
errors: Vec<IfpsErr>,
|
||||
}
|
||||
let v: V = serde_json::from_str(line)?; // serde error → CoreError::Json
|
||||
Ok(IfpsVerdict { accepted: v.accepted.unwrap_or(false), errors: v.errors })
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an IFPS-valid route via the oracle loop (live IFPUV, ~1–3 min the
|
||||
/// first time), store it, and return the discovery result. Runs off-thread.
|
||||
#[tauri::command]
|
||||
async fn generate_validated(
|
||||
app: tauri::AppHandle,
|
||||
req: PlanRequest,
|
||||
rad: State<'_, RadState>,
|
||||
) -> Result<DiscoverResult, String> {
|
||||
let rad = rad.0.clone();
|
||||
let dir = std::env::var("FP_IFPUV_DIR")
|
||||
.unwrap_or_else(|_| r"C:\Users\Alexandre\flightplanner\tools\ifpuv".to_string());
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let validator = NodeIfps { dir, app: Some(app), step: AtomicUsize::new(0) };
|
||||
api::discover_route(&req, rad.as_deref(), &validator).map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
/// Stored routes for a city pair from our own route database (local file).
|
||||
#[tauri::command]
|
||||
fn recent_routes(db_path: String, dep: String, dest: String) -> Result<Vec<CachedRouteDto>, String> {
|
||||
api::recent_routes(&db_path, &dep, &dest, 10).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Total number of stored routes.
|
||||
#[tauri::command]
|
||||
fn route_db_count(db_path: String) -> Result<i64, String> {
|
||||
api::route_db_count(&db_path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Run a Playwright IFPUV script (`script`) with the JSON `payload` and return
|
||||
/// its single JSON result line. Round-trips the public Eurocontrol validator.
|
||||
fn run_ifpuv(script: &str, payload: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let dir = std::env::var("FP_IFPUV_DIR")
|
||||
.unwrap_or_else(|_| r"C:\Users\Alexandre\flightplanner\tools\ifpuv".to_string());
|
||||
let out = std::process::Command::new("node")
|
||||
.arg(script)
|
||||
.arg(payload.to_string())
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.map_err(|e| format!("cannot launch Node.js validator: {e}"))?;
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let line = stdout
|
||||
.lines()
|
||||
.rev()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.ok_or_else(|| {
|
||||
format!("validator produced no result. stderr: {}", String::from_utf8_lossy(&out.stderr).trim())
|
||||
})?;
|
||||
serde_json::from_str::<serde_json::Value>(line).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Authoritative IFPS validation: builds an ICAO FPL from `payload` and submits
|
||||
/// it to the public Eurocontrol IFPUV. Returns `{ accepted, errors:[{code,msg}], raw, fpl }`.
|
||||
/// Network round-trip, ~20–30 s (guest GWT app load).
|
||||
#[tauri::command]
|
||||
async fn ifps_validate(payload: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
tauri::async_runtime::spawn_blocking(move || run_ifpuv("validate.mjs", payload))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
/// Oracle-driven IFPS auto-correction: validates and mechanically fixes the
|
||||
/// tractable errors (RAD level caps, unknown designators) against the IFPUV,
|
||||
/// looping until accepted. Returns `{ accepted, level, route, iterations, log, errors }`.
|
||||
#[tauri::command]
|
||||
async fn ifps_autofix(payload: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
tauri::async_runtime::spawn_blocking(move || run_ifpuv("autofix.mjs", payload))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
/// Load the RAD workbook once (env `FP_RAD`, else the project default).
|
||||
fn load_rad() -> Option<RadData> {
|
||||
let path = std::env::var("FP_RAD")
|
||||
.unwrap_or_else(|_| r"C:\Users\Alexandre\flightplanner\rad\RAD_current.xlsx".to_string());
|
||||
if !std::path::Path::new(&path).exists() {
|
||||
eprintln!("RAD: file not found ({path}) — RAD checks disabled");
|
||||
return None;
|
||||
}
|
||||
match flightplanner_rad::parse(&path) {
|
||||
Ok(mut rad) => {
|
||||
let (f, o, c) = rad.dct_counts();
|
||||
// Official EUROCONTROL FRA points (separate file) for the FRA-points router.
|
||||
let fra_path = std::env::var("FP_FRA_POINTS")
|
||||
.unwrap_or_else(|_| r"C:\Users\Alexandre\flightplanner\rad\fra-points.xlsx".to_string());
|
||||
rad.fra_points = flightplanner_rad::parse_fra_points(&fra_path).unwrap_or_default();
|
||||
eprintln!(
|
||||
"RAD loaded: {} areas, {} DCT ({f} forbidden / {o} conditional / {c} compulsory), {} level caps, {} FRA points",
|
||||
rad.areas.len(),
|
||||
rad.dct.len(),
|
||||
rad.level_caps.len(),
|
||||
rad.fra_points.len(),
|
||||
);
|
||||
Some(rad)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("RAD: parse failed ({e}) — RAD checks disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(RadState(load_rad().map(Arc::new)))
|
||||
.invoke_handler(tauri::generate_handler![plan, recent_routes, route_db_count, ifps_validate, ifps_autofix, generate_validated])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// End-to-end smoke test of the shared `plan` engine against the real DB.
|
||||
/// Skips (passes) when the DB isn't present so the test stays portable.
|
||||
#[test]
|
||||
fn plan_command_end_to_end() {
|
||||
let db = "C:\\Users\\Alexandre\\flightplanner\\real.db";
|
||||
let dir = "C:\\Users\\Alexandre\\flightplanner\\data\\aircraft";
|
||||
if !std::path::Path::new(db).exists() {
|
||||
return;
|
||||
}
|
||||
// Load the RAD so the FRA graph router is exercised (as in the real app).
|
||||
let rad = load_rad();
|
||||
let res = api::plan(
|
||||
&PlanRequest {
|
||||
from: "LFPG".into(),
|
||||
to: "EGLL".into(),
|
||||
db_path: db.into(),
|
||||
aircraft: Some("A320".into()),
|
||||
aircraft_dir: dir.into(),
|
||||
cifp_dir: Some("C:\\Users\\Alexandre\\flightplanner\\navdata\\CIFP".into()),
|
||||
cruise_fl: Some(360),
|
||||
alternate: None,
|
||||
payload_kg: Some(16_000.0),
|
||||
},
|
||||
rad.as_ref(),
|
||||
)
|
||||
.expect("plan should succeed");
|
||||
|
||||
eprintln!("ROUTE: {}", res.route_string);
|
||||
assert!(!res.legs.is_empty(), "route should have legs");
|
||||
assert!(res.route_string.starts_with("LFPG"), "{}", res.route_string);
|
||||
assert!(res.ofp.contains("OPERATIONAL FLIGHT PLAN"), "OFP present");
|
||||
let fuel = res.fuel.expect("A320 ⇒ fuel plan");
|
||||
assert!(fuel.block_fuel_kg > fuel.trip_fuel_kg, "block > trip");
|
||||
let mass = fuel.mass.expect("OpenAP ⇒ mass breakdown");
|
||||
assert!(mass.takeoff_kg > mass.landing_kg, "TOW > LDW");
|
||||
let last = res.legs.last().unwrap();
|
||||
assert!((last.cum_dist_nm - res.total_nm).abs() < 1.0, "cum dist ends at total");
|
||||
}
|
||||
|
||||
/// Plan several European pairs at once (RAD loaded once) and print each
|
||||
/// route + the enroute item-15 (fixes joined by DCT) ready for the IFPUV
|
||||
/// oracle. Run: `cargo test -p flightplanner-gui plan_pairs -- --nocapture`.
|
||||
#[test]
|
||||
fn plan_pairs() {
|
||||
let db = "C:\\Users\\Alexandre\\flightplanner\\real.db";
|
||||
let dir = "C:\\Users\\Alexandre\\flightplanner\\data\\aircraft";
|
||||
let cifp = "C:\\Users\\Alexandre\\flightplanner\\navdata\\CIFP";
|
||||
if !std::path::Path::new(db).exists() {
|
||||
return;
|
||||
}
|
||||
let rad = load_rad();
|
||||
let pairs_env = std::env::var("FP_TEST_PAIRS")
|
||||
.unwrap_or_else(|_| "LFPG-EGLL,EGLL-LFPG,LFPG-EDDF,EHAM-LSZH,LFPG-LEMD,EDDM-LEBL".into());
|
||||
let pairs: Vec<(String, String)> = pairs_env
|
||||
.split(',')
|
||||
.filter_map(|p| p.split_once('-').map(|(a, b)| (a.trim().to_uppercase(), b.trim().to_uppercase())))
|
||||
.collect();
|
||||
for (from, to) in &pairs {
|
||||
let (from, to) = (from.as_str(), to.as_str());
|
||||
let res = api::plan(
|
||||
&PlanRequest {
|
||||
from: from.into(),
|
||||
to: to.into(),
|
||||
db_path: db.into(),
|
||||
aircraft: Some("A320".into()),
|
||||
aircraft_dir: dir.into(),
|
||||
cifp_dir: Some(cifp.into()),
|
||||
cruise_fl: Some(360),
|
||||
alternate: None,
|
||||
payload_kg: Some(16_000.0),
|
||||
},
|
||||
rad.as_ref(),
|
||||
);
|
||||
match res {
|
||||
Ok(r) => {
|
||||
// item 15 = enroute fix chain (each leg's `to` except the destination).
|
||||
let n = r.legs.len();
|
||||
let item15: Vec<String> =
|
||||
r.legs.iter().take(n.saturating_sub(1)).map(|l| l.to.clone()).collect();
|
||||
eprintln!("PAIR {from}->{to}\n ROUTE : {}\n ITEM15: {}", r.route_string, item15.join(" DCT "));
|
||||
}
|
||||
Err(e) => eprintln!("PAIR {from}->{to} ERROR: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch coverage: run discovery over several European pairs and report the
|
||||
/// no-error rate + which error codes block the failures. Ignored (very slow,
|
||||
/// many live IFPUV round-trips). Pairs via env `FP_BATCH_PAIRS` (comma-sep
|
||||
/// `LFPG-EGLL,...`). Run: `cargo test -p flightplanner-gui batch_coverage -- --ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore = "live IFPUV batch, many minutes"]
|
||||
fn batch_coverage() {
|
||||
let db = "C:\\Users\\Alexandre\\flightplanner\\real.db";
|
||||
let dir = "C:\\Users\\Alexandre\\flightplanner\\data\\aircraft";
|
||||
let cifp = "C:\\Users\\Alexandre\\flightplanner\\navdata\\CIFP";
|
||||
if !std::path::Path::new(db).exists() {
|
||||
return;
|
||||
}
|
||||
let rad = load_rad();
|
||||
let ifdir = std::env::var("FP_IFPUV_DIR")
|
||||
.unwrap_or_else(|_| "C:\\Users\\Alexandre\\flightplanner\\tools\\ifpuv".to_string());
|
||||
let validator = NodeIfps { dir: ifdir, app: None, step: std::sync::atomic::AtomicUsize::new(0) };
|
||||
let pairs_env = std::env::var("FP_BATCH_PAIRS").unwrap_or_else(|_| {
|
||||
"LFPG-EGLL,LFPG-LFMN,LFPG-EDDF,EHAM-LSZH,LEMD-LEBL,EDDM-LEBL".into()
|
||||
});
|
||||
let pairs: Vec<(String, String)> = pairs_env
|
||||
.split(',')
|
||||
.filter_map(|p| p.split_once('-').map(|(a, b)| (a.trim().to_uppercase(), b.trim().to_uppercase())))
|
||||
.collect();
|
||||
|
||||
let mut ok = 0usize;
|
||||
let mut hist: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
for (from, to) in &pairs {
|
||||
let req = PlanRequest {
|
||||
from: from.clone(),
|
||||
to: to.clone(),
|
||||
db_path: db.into(),
|
||||
aircraft: Some("A320".into()),
|
||||
aircraft_dir: dir.into(),
|
||||
cifp_dir: Some(cifp.into()),
|
||||
cruise_fl: Some(360),
|
||||
alternate: None,
|
||||
payload_kg: Some(16_000.0),
|
||||
};
|
||||
match api::discover_route(&req, rad.as_ref(), &validator) {
|
||||
Ok(r) => {
|
||||
let codes: Vec<&str> = r.errors.iter().map(|e| e.code.as_str()).collect();
|
||||
eprintln!(
|
||||
"BATCH {from}->{to}: {} F{:03} iters={} errs={} [{}]",
|
||||
if r.accepted { "OK " } else { "FAIL" },
|
||||
r.fl,
|
||||
r.iterations,
|
||||
r.errors.len(),
|
||||
codes.join(",")
|
||||
);
|
||||
if r.accepted {
|
||||
ok += 1;
|
||||
}
|
||||
for e in &r.errors {
|
||||
*hist.entry(e.code.clone()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("BATCH {from}->{to}: ERROR {e}"),
|
||||
}
|
||||
}
|
||||
let mut codes: Vec<(String, usize)> = hist.into_iter().collect();
|
||||
codes.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
eprintln!("BATCH COVERAGE: {ok}/{} no-error ({:.0}%)", pairs.len(), 100.0 * ok as f64 / pairs.len().max(1) as f64);
|
||||
eprintln!("BATCH blocking codes: {codes:?}");
|
||||
}
|
||||
|
||||
/// Live end-to-end: run the oracle discovery loop against the real IFPUV.
|
||||
/// Ignored by default (network + several ~25 s round-trips). Run with:
|
||||
/// `cargo test -p flightplanner-gui discover_e2e -- --ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore = "live IFPUV round-trips (minutes) + network"]
|
||||
fn discover_e2e() {
|
||||
let db = "C:\\Users\\Alexandre\\flightplanner\\real.db";
|
||||
let dir = "C:\\Users\\Alexandre\\flightplanner\\data\\aircraft";
|
||||
let cifp = "C:\\Users\\Alexandre\\flightplanner\\navdata\\CIFP";
|
||||
if !std::path::Path::new(db).exists() {
|
||||
return;
|
||||
}
|
||||
let rad = load_rad();
|
||||
let ifdir = std::env::var("FP_IFPUV_DIR")
|
||||
.unwrap_or_else(|_| "C:\\Users\\Alexandre\\flightplanner\\tools\\ifpuv".to_string());
|
||||
let validator = NodeIfps { dir: ifdir, app: None, step: std::sync::atomic::AtomicUsize::new(0) };
|
||||
let pairs_env = std::env::var("FP_TEST_PAIRS").unwrap_or_else(|_| "LFPG-EGLL,LFPG-LFMN".into());
|
||||
let pairs: Vec<(String, String)> = pairs_env
|
||||
.split(',')
|
||||
.filter_map(|p| p.split_once('-').map(|(a, b)| (a.trim().to_uppercase(), b.trim().to_uppercase())))
|
||||
.collect();
|
||||
for (from, to) in &pairs {
|
||||
let (from, to) = (from.as_str(), to.as_str());
|
||||
let req = PlanRequest {
|
||||
from: from.into(),
|
||||
to: to.into(),
|
||||
db_path: db.into(),
|
||||
aircraft: Some("A320".into()),
|
||||
aircraft_dir: dir.into(),
|
||||
cifp_dir: Some(cifp.into()),
|
||||
cruise_fl: Some(360),
|
||||
alternate: None,
|
||||
payload_kg: Some(16_000.0),
|
||||
};
|
||||
match api::discover_route(&req, rad.as_ref(), &validator) {
|
||||
Ok(r) => eprintln!(
|
||||
"== {from}->{to} accepted={} fl={} nm={:.0}\n route : {}\n item15: {}\n log:\n {}",
|
||||
r.accepted, r.fl, r.total_nm, r.route_string, r.item15, r.log.join("\n ")
|
||||
),
|
||||
Err(e) => eprintln!("== {from}->{to} ERROR: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
gui_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Flight Planner",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.flightplanner.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Flight Planner",
|
||||
"width": 1180,
|
||||
"height": 820,
|
||||
"minWidth": 900,
|
||||
"minHeight": 640,
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||