2026-05-14 17:35:29 +03:00
|
|
|
#[allow(unused_imports)]
|
2026-05-13 10:21:24 +03:00
|
|
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
2026-05-14 14:17:57 +03:00
|
|
|
#[allow(unused_imports)]
|
|
|
|
|
use tauri::Manager;
|
2026-05-21 00:06:50 +03:00
|
|
|
use tauri::Emitter;
|
2026-05-14 14:17:57 +03:00
|
|
|
use serde::Serialize;
|
2026-05-13 10:21:24 +03:00
|
|
|
|
2026-05-14 17:35:29 +03:00
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct AppInfo {
|
|
|
|
|
version: String,
|
|
|
|
|
git_hash: String,
|
|
|
|
|
build_date: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn get_app_info() -> AppInfo {
|
|
|
|
|
AppInfo {
|
|
|
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
|
|
|
git_hash: env!("GIT_HASH").to_string(),
|
|
|
|
|
build_date: env!("BUILD_DATE").to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.
Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00
|
|
|
// GPS service control — writes command file, Kotlin reads and starts/stops service
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn gps_service_command(app: tauri::AppHandle, cmd: String) -> Result<String, String> {
|
|
|
|
|
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
|
|
|
|
std::fs::create_dir_all(&data_dir).ok();
|
2026-05-16 10:49:55 +03:00
|
|
|
// Write to app_data_dir
|
Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.
Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00
|
|
|
let cmd_path = data_dir.join("gps_command.txt");
|
|
|
|
|
std::fs::write(&cmd_path, &cmd).map_err(|e| e.to_string())?;
|
2026-05-16 10:49:55 +03:00
|
|
|
// Also write to app root dir (Android: Kotlin watches there)
|
|
|
|
|
if let Some(root) = data_dir.parent().and_then(|p| p.parent()) {
|
|
|
|
|
let alt = root.join("gps_command.txt");
|
|
|
|
|
let _ = std::fs::write(&alt, &cmd);
|
|
|
|
|
}
|
Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.
Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00
|
|
|
Ok(format!("sent:{}", cmd))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read GPS points from service's JSONL file
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn gps_service_read_points(app: tauri::AppHandle) -> Result<String, String> {
|
|
|
|
|
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
2026-05-16 10:49:55 +03:00
|
|
|
// Also check parent dir (Android: app_data_dir may be files/dev.baldav.app but service writes to app root)
|
Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.
Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00
|
|
|
let pts_path = data_dir.join("gps_live_points.jsonl");
|
2026-05-16 10:49:55 +03:00
|
|
|
let alt_path = data_dir.parent().and_then(|p| p.parent()).map(|p| p.join("gps_live_points.jsonl"));
|
Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.
Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00
|
|
|
if pts_path.exists() {
|
|
|
|
|
std::fs::read_to_string(&pts_path).map_err(|e| e.to_string())
|
2026-05-16 10:49:55 +03:00
|
|
|
} else if let Some(ref alt) = alt_path {
|
|
|
|
|
if alt.exists() {
|
|
|
|
|
std::fs::read_to_string(alt).map_err(|e| e.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(String::new())
|
|
|
|
|
}
|
Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.
Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00
|
|
|
} else {
|
|
|
|
|
Ok(String::new())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 21:21:13 +03:00
|
|
|
#[tauri::command]
|
|
|
|
|
fn save_file(app: tauri::AppHandle, filename: String, content: String) -> Result<String, String> {
|
|
|
|
|
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
|
|
|
|
std::fs::create_dir_all(&data_dir).ok();
|
|
|
|
|
|
|
|
|
|
// Write content to temp file for Kotlin to read
|
|
|
|
|
let content_path = data_dir.join("save_content.txt");
|
|
|
|
|
std::fs::write(&content_path, &content).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// Send save command to Kotlin (triggers SAF file picker on Android)
|
|
|
|
|
let cmd_path = data_dir.join("gps_command.txt");
|
|
|
|
|
std::fs::write(&cmd_path, format!("save:{}", filename)).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// Also write to app root for Kotlin FileObserver
|
|
|
|
|
if let Some(root) = data_dir.parent().and_then(|p| p.parent()) {
|
|
|
|
|
let _ = std::fs::write(root.join("save_content.txt"), &content);
|
|
|
|
|
let _ = std::fs::write(root.join("gps_command.txt"), format!("save:{}", filename));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok("save_requested".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 17:35:29 +03:00
|
|
|
#[tauri::command]
|
|
|
|
|
fn import_seed_products(app: tauri::AppHandle) -> Result<String, String> {
|
|
|
|
|
// Find seed DB — try multiple locations
|
|
|
|
|
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
|
|
|
|
let seed_in_data = data_dir.join("products_seed.db");
|
|
|
|
|
let seed_in_files = data_dir.join("files").join("products_seed.db");
|
|
|
|
|
|
2026-05-21 00:06:50 +03:00
|
|
|
// Always try to refresh seed DB from bundled assets (resource_dir).
|
|
|
|
|
// This ensures app updates deliver new seed data even if an old copy exists.
|
|
|
|
|
let resource_dir = app.path().resource_dir().unwrap_or_default();
|
|
|
|
|
let seed_resource = resource_dir.join("data").join("products_seed.db");
|
|
|
|
|
if seed_resource.exists() {
|
|
|
|
|
// Overwrite the local copy with the bundled one (may be newer)
|
|
|
|
|
let _ = std::fs::copy(&seed_resource, &seed_in_data);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 17:35:29 +03:00
|
|
|
let seed_path = if seed_in_data.exists() {
|
|
|
|
|
seed_in_data
|
|
|
|
|
} else if seed_in_files.exists() {
|
|
|
|
|
seed_in_files
|
|
|
|
|
} else {
|
2026-05-21 00:06:50 +03:00
|
|
|
return Err(format!("Seed DB not found. Checked: {:?}, {:?}", seed_in_data, seed_in_files));
|
2026-05-14 17:35:29 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let seed_db = rusqlite::Connection::open(&seed_path).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
let seed_version: i64 = seed_db.query_row(
|
|
|
|
|
"SELECT version FROM seed_meta LIMIT 1", [], |r| r.get(0)
|
|
|
|
|
).map_err(|e| e.to_string())?;
|
|
|
|
|
|
2026-05-21 00:06:50 +03:00
|
|
|
// tauri-plugin-sql may store DB in app_config_dir (desktop: ~/.config/...)
|
|
|
|
|
// or app_data_dir (desktop: ~/.local/share/...). Check both.
|
|
|
|
|
let config_dir = app.path().app_config_dir().unwrap_or_default();
|
2026-05-14 17:35:29 +03:00
|
|
|
let main_path = {
|
2026-05-21 00:06:50 +03:00
|
|
|
let p_config = config_dir.join("baldav.db");
|
|
|
|
|
let p_data = data_dir.join("baldav.db");
|
|
|
|
|
let p_parent = data_dir.join("..").join("baldav.db");
|
|
|
|
|
if p_config.exists() { p_config }
|
|
|
|
|
else if p_data.exists() { p_data }
|
|
|
|
|
else if p_parent.exists() { p_parent }
|
|
|
|
|
else { p_data }
|
2026-05-14 17:35:29 +03:00
|
|
|
};
|
|
|
|
|
let main_db = rusqlite::Connection::open(&main_path).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
let current_version: i64 = main_db.query_row(
|
|
|
|
|
"SELECT COALESCE((SELECT value FROM profile WHERE key='seed_version'), '0')",
|
|
|
|
|
[], |r| {
|
|
|
|
|
let s: String = r.get(0)?;
|
|
|
|
|
Ok(s.parse::<i64>().unwrap_or(0))
|
|
|
|
|
}
|
|
|
|
|
).unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
if current_version >= seed_version {
|
|
|
|
|
return Ok(format!("up-to-date:v{}", current_version));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
2026-05-21 00:06:50 +03:00
|
|
|
// Emit initial progress
|
|
|
|
|
let _ = app.emit("seed_progress", serde_json::json!({"current": 0, "total": total}));
|
|
|
|
|
|
|
|
|
|
// Ensure all columns exist in main DB (including micronutrients from ION import)
|
|
|
|
|
let extra_cols = [
|
|
|
|
|
"composition TEXT", "name_lower TEXT", "category TEXT",
|
|
|
|
|
"sodium_mg REAL", "potassium_mg REAL", "calcium_mg REAL",
|
|
|
|
|
"magnesium_mg REAL", "phosphorus_mg REAL", "iron_mg REAL",
|
|
|
|
|
"vit_a_mcg REAL", "vit_b1_mg REAL", "vit_b2_mg REAL",
|
|
|
|
|
"vit_b3_mg REAL", "vit_c_mg REAL", "vit_e_mg REAL", "carotene_mcg REAL",
|
|
|
|
|
"nutrients_guessed INTEGER DEFAULT 0",
|
|
|
|
|
];
|
|
|
|
|
for col in &extra_cols {
|
|
|
|
|
let _ = main_db.execute(&format!("ALTER TABLE products ADD COLUMN {}", col), []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read ALL columns from seed DB including micronutrients
|
2026-05-14 17:35:29 +03:00
|
|
|
let mut stmt = seed_db.prepare(
|
2026-05-21 00:06:50 +03:00
|
|
|
"SELECT name, brand, kcal_per_100g, protein_per_100g, fat_per_100g,
|
|
|
|
|
carbs_per_100g, fiber_per_100g, composition, source, store_id,
|
|
|
|
|
name_lower, weight_g, category,
|
|
|
|
|
sodium_mg, potassium_mg, calcium_mg, magnesium_mg, phosphorus_mg,
|
|
|
|
|
iron_mg, vit_a_mcg, vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_c_mg,
|
|
|
|
|
vit_e_mg, carotene_mcg, COALESCE(nutrients_guessed, 0)
|
|
|
|
|
FROM products"
|
2026-05-14 17:35:29 +03:00
|
|
|
).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
let mut inserted = 0usize;
|
2026-05-21 00:06:50 +03:00
|
|
|
let mut processed = 0usize;
|
|
|
|
|
|
2026-05-14 17:35:29 +03:00
|
|
|
let rows = stmt.query_map([], |row| {
|
2026-05-21 00:06:50 +03:00
|
|
|
// 26 columns: 0-12 strings/basic, 13-25 micronutrients
|
2026-05-14 17:35:29 +03:00
|
|
|
Ok((
|
2026-05-21 00:06:50 +03:00
|
|
|
row.get::<_, Option<String>>(0)?, // name
|
|
|
|
|
row.get::<_, Option<String>>(1)?, // brand
|
|
|
|
|
row.get::<_, Option<f64>>(2)?, // kcal
|
|
|
|
|
row.get::<_, Option<f64>>(3)?, // protein
|
|
|
|
|
row.get::<_, Option<f64>>(4)?, // fat
|
|
|
|
|
row.get::<_, Option<f64>>(5)?, // carbs
|
|
|
|
|
row.get::<_, Option<f64>>(6)?, // fiber
|
|
|
|
|
row.get::<_, Option<String>>(7)?, // composition
|
|
|
|
|
row.get::<_, Option<String>>(8)?, // source
|
|
|
|
|
row.get::<_, Option<String>>(9)?, // store_id
|
|
|
|
|
row.get::<_, Option<String>>(10)?, // name_lower
|
|
|
|
|
row.get::<_, Option<f64>>(11)?, // weight_g
|
|
|
|
|
row.get::<_, Option<String>>(12)?, // category
|
|
|
|
|
row.get::<_, Option<f64>>(13)?, // sodium_mg
|
|
|
|
|
row.get::<_, Option<f64>>(14)?, // potassium_mg
|
|
|
|
|
row.get::<_, Option<f64>>(15)?, // calcium_mg
|
|
|
|
|
row.get::<_, Option<f64>>(16)?, // magnesium_mg
|
|
|
|
|
row.get::<_, Option<f64>>(17)?, // phosphorus_mg
|
|
|
|
|
row.get::<_, Option<f64>>(18)?, // iron_mg
|
|
|
|
|
row.get::<_, Option<f64>>(19)?, // vit_a_mcg
|
|
|
|
|
row.get::<_, Option<f64>>(20)?, // vit_b1_mg
|
|
|
|
|
row.get::<_, Option<f64>>(21)?, // vit_b2_mg
|
|
|
|
|
row.get::<_, Option<f64>>(22)?, // vit_b3_mg
|
|
|
|
|
row.get::<_, Option<f64>>(23)?, // vit_c_mg
|
|
|
|
|
row.get::<_, Option<f64>>(24)?, // vit_e_mg
|
|
|
|
|
row.get::<_, Option<f64>>(25)?, // carotene_mcg
|
|
|
|
|
row.get::<_, Option<i64>>(26)?, // nutrients_guessed
|
2026-05-14 17:35:29 +03:00
|
|
|
))
|
|
|
|
|
}).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
main_db.execute("BEGIN", []).map_err(|e| e.to_string())?;
|
|
|
|
|
for row in rows {
|
|
|
|
|
let r = row.map_err(|e| e.to_string())?;
|
|
|
|
|
main_db.execute(
|
2026-05-21 00:06:50 +03:00
|
|
|
"INSERT OR IGNORE INTO products (
|
|
|
|
|
name, brand, kcal_per_100g, protein_per_100g, fat_per_100g,
|
|
|
|
|
carbs_per_100g, fiber_per_100g, composition, source, store_id,
|
|
|
|
|
name_lower, weight_g, category,
|
|
|
|
|
sodium_mg, potassium_mg, calcium_mg, magnesium_mg, phosphorus_mg,
|
|
|
|
|
iron_mg, vit_a_mcg, vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_c_mg,
|
|
|
|
|
vit_e_mg, carotene_mcg, nutrients_guessed
|
|
|
|
|
) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24,?25,?26,?27)",
|
|
|
|
|
rusqlite::params![
|
|
|
|
|
r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8, r.9,
|
|
|
|
|
r.10, r.11, r.12, r.13, r.14, r.15, r.16, r.17, r.18, r.19,
|
|
|
|
|
r.20, r.21, r.22, r.23, r.24, r.25, r.26
|
|
|
|
|
],
|
2026-05-14 17:35:29 +03:00
|
|
|
).map_err(|e| e.to_string())?;
|
|
|
|
|
inserted += 1;
|
2026-05-21 00:06:50 +03:00
|
|
|
processed += 1;
|
|
|
|
|
|
|
|
|
|
// Emit progress every 200 rows
|
|
|
|
|
if processed % 200 == 0 {
|
|
|
|
|
let _ = app.emit("seed_progress", serde_json::json!({"current": processed, "total": total}));
|
|
|
|
|
}
|
2026-05-14 17:35:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main_db.execute(
|
|
|
|
|
"INSERT OR REPLACE INTO profile (key, value) VALUES ('seed_version', ?1)",
|
|
|
|
|
[seed_version.to_string()],
|
|
|
|
|
).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
main_db.execute("COMMIT", []).map_err(|e| e.to_string())?;
|
|
|
|
|
|
2026-05-21 00:06:50 +03:00
|
|
|
// Sync enriched data for EXISTING products (INSERT OR IGNORE skips them).
|
|
|
|
|
// ATTACH seed DB and run bulk UPDATE to copy nutrients_guessed flag and micronutrients.
|
|
|
|
|
let seed_str = seed_path.to_str().unwrap_or_default().to_string();
|
|
|
|
|
if let Ok(_) = main_db.execute("ATTACH DATABASE ?1 AS seed", rusqlite::params![seed_str]) {
|
|
|
|
|
// Sync nutrients_guessed flag
|
|
|
|
|
let _ = main_db.execute_batch(
|
|
|
|
|
"UPDATE main.products SET nutrients_guessed = 1
|
|
|
|
|
WHERE nutrients_guessed IS NULL OR nutrients_guessed = 0
|
|
|
|
|
AND name_lower IN (SELECT s.name_lower FROM seed.products s WHERE s.nutrients_guessed = 1)"
|
|
|
|
|
);
|
|
|
|
|
// Sync micronutrients for products that have them in seed but not in main
|
|
|
|
|
let _ = main_db.execute_batch(
|
|
|
|
|
"UPDATE main.products SET
|
|
|
|
|
sodium_mg = COALESCE(sodium_mg, (SELECT s.sodium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
potassium_mg = COALESCE(potassium_mg, (SELECT s.potassium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
calcium_mg = COALESCE(calcium_mg, (SELECT s.calcium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
magnesium_mg = COALESCE(magnesium_mg, (SELECT s.magnesium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
phosphorus_mg= COALESCE(phosphorus_mg,(SELECT s.phosphorus_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
iron_mg = COALESCE(iron_mg, (SELECT s.iron_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
vit_a_mcg = COALESCE(vit_a_mcg, (SELECT s.vit_a_mcg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
vit_b1_mg = COALESCE(vit_b1_mg, (SELECT s.vit_b1_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
vit_b2_mg = COALESCE(vit_b2_mg, (SELECT s.vit_b2_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
vit_b3_mg = COALESCE(vit_b3_mg, (SELECT s.vit_b3_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
vit_c_mg = COALESCE(vit_c_mg, (SELECT s.vit_c_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
vit_e_mg = COALESCE(vit_e_mg, (SELECT s.vit_e_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1)),
|
|
|
|
|
carotene_mcg = COALESCE(carotene_mcg, (SELECT s.carotene_mcg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1))
|
|
|
|
|
WHERE main.products.sodium_mg IS NULL
|
|
|
|
|
AND main.products.name_lower IN (SELECT s2.name_lower FROM seed.products s2 WHERE s2.sodium_mg IS NOT NULL)"
|
|
|
|
|
);
|
|
|
|
|
let _ = main_db.execute("DETACH DATABASE seed", []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Final progress
|
|
|
|
|
let _ = app.emit("seed_progress", serde_json::json!({"current": total, "total": total}));
|
|
|
|
|
|
2026-05-14 17:35:29 +03:00
|
|
|
Ok(format!("imported:{}:{}", inserted, total))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 14:17:57 +03:00
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct SafeInsets {
|
|
|
|
|
top: i32,
|
|
|
|
|
bottom: i32,
|
|
|
|
|
left: i32,
|
|
|
|
|
right: i32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "android")]
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn get_safe_insets(app: tauri::AppHandle) -> SafeInsets {
|
|
|
|
|
let data_dir = app.path().app_data_dir().unwrap_or_default();
|
|
|
|
|
let insets_file = data_dir.join("safe_insets.json");
|
|
|
|
|
for _ in 0..10 {
|
|
|
|
|
if let Ok(content) = std::fs::read_to_string(&insets_file) {
|
|
|
|
|
if let Ok(insets) = serde_json::from_str::<serde_json::Value>(&content) {
|
|
|
|
|
return SafeInsets {
|
|
|
|
|
top: insets["top"].as_i64().unwrap_or(0) as i32,
|
|
|
|
|
bottom: insets["bottom"].as_i64().unwrap_or(0) as i32,
|
|
|
|
|
left: insets["left"].as_i64().unwrap_or(0) as i32,
|
|
|
|
|
right: insets["right"].as_i64().unwrap_or(0) as i32,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
|
|
|
}
|
|
|
|
|
SafeInsets { top: 0, bottom: 0, left: 0, right: 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn get_safe_insets() -> SafeInsets {
|
|
|
|
|
SafeInsets { top: 0, bottom: 0, left: 0, right: 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
2026-05-13 10:21:24 +03:00
|
|
|
pub fn run() {
|
2026-05-14 14:17:57 +03:00
|
|
|
let migrations: Vec<Migration> = vec![
|
2026-05-13 10:21:24 +03:00
|
|
|
Migration {
|
|
|
|
|
version: 1,
|
|
|
|
|
description: "full schema v1",
|
|
|
|
|
sql: r#"
|
|
|
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS profile (
|
|
|
|
|
key TEXT PRIMARY KEY,
|
|
|
|
|
value TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS products (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
brand TEXT,
|
|
|
|
|
category TEXT,
|
|
|
|
|
weight_g REAL,
|
|
|
|
|
kcal_per_100g REAL,
|
|
|
|
|
protein_per_100g REAL,
|
|
|
|
|
fat_per_100g REAL,
|
|
|
|
|
carbs_per_100g REAL,
|
|
|
|
|
fiber_per_100g REAL,
|
|
|
|
|
vit_a_mcg REAL,
|
|
|
|
|
vit_c_mg REAL,
|
|
|
|
|
vit_d_mcg REAL,
|
|
|
|
|
vit_e_mg REAL,
|
|
|
|
|
vit_k_mcg REAL,
|
|
|
|
|
vit_b1_mg REAL,
|
|
|
|
|
vit_b2_mg REAL,
|
|
|
|
|
vit_b3_mg REAL,
|
|
|
|
|
vit_b6_mg REAL,
|
|
|
|
|
vit_b12_mcg REAL,
|
|
|
|
|
folate_mcg REAL,
|
|
|
|
|
calcium_mg REAL,
|
|
|
|
|
iron_mg REAL,
|
|
|
|
|
magnesium_mg REAL,
|
|
|
|
|
zinc_mg REAL,
|
|
|
|
|
copper_mg REAL,
|
|
|
|
|
selenium_mcg REAL,
|
|
|
|
|
potassium_mg REAL,
|
|
|
|
|
sodium_mg REAL,
|
|
|
|
|
source TEXT,
|
|
|
|
|
store_id TEXT,
|
|
|
|
|
image_path TEXT,
|
|
|
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS supplements (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
daily_dose_g REAL,
|
|
|
|
|
vit_a_mcg REAL,
|
|
|
|
|
vit_c_mg REAL,
|
|
|
|
|
vit_d_mcg REAL,
|
|
|
|
|
vit_e_mg REAL,
|
|
|
|
|
vit_k_mcg REAL,
|
|
|
|
|
vit_b1_mg REAL,
|
|
|
|
|
vit_b2_mg REAL,
|
|
|
|
|
vit_b3_mg REAL,
|
|
|
|
|
vit_b6_mg REAL,
|
|
|
|
|
vit_b12_mcg REAL,
|
|
|
|
|
folate_mcg REAL,
|
|
|
|
|
calcium_mg REAL,
|
|
|
|
|
iron_mg REAL,
|
|
|
|
|
magnesium_mg REAL,
|
|
|
|
|
zinc_mg REAL,
|
|
|
|
|
copper_mg REAL,
|
|
|
|
|
selenium_mcg REAL,
|
|
|
|
|
potassium_mg REAL,
|
|
|
|
|
sodium_mg REAL
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS recipes (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
cooking_method TEXT,
|
|
|
|
|
cooking_temp_c INTEGER,
|
|
|
|
|
servings INTEGER DEFAULT 1,
|
|
|
|
|
notes TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
|
|
|
|
product_id INTEGER NOT NULL REFERENCES products(id),
|
|
|
|
|
amount_g REAL NOT NULL
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS weight_log (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
date TEXT NOT NULL,
|
|
|
|
|
time TEXT,
|
|
|
|
|
weight_kg REAL NOT NULL
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sleep_log (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
date TEXT NOT NULL,
|
|
|
|
|
bed_time TEXT,
|
|
|
|
|
asleep_time TEXT,
|
|
|
|
|
wake_time TEXT,
|
|
|
|
|
nap_minutes INTEGER DEFAULT 0,
|
|
|
|
|
notes TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS food_log (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
date TEXT NOT NULL,
|
|
|
|
|
time TEXT,
|
|
|
|
|
product_id INTEGER REFERENCES products(id),
|
|
|
|
|
recipe_id INTEGER REFERENCES recipes(id),
|
|
|
|
|
amount_g REAL NOT NULL,
|
|
|
|
|
kcal REAL,
|
|
|
|
|
protein_g REAL,
|
|
|
|
|
fat_g REAL,
|
|
|
|
|
carbs_g REAL,
|
|
|
|
|
fiber_g REAL,
|
|
|
|
|
note TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS supplement_log (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
date TEXT NOT NULL,
|
|
|
|
|
time TEXT,
|
|
|
|
|
supplement_id INTEGER NOT NULL REFERENCES supplements(id),
|
|
|
|
|
dose_g REAL
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS gps_sessions (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
date TEXT NOT NULL,
|
|
|
|
|
start_time TEXT,
|
|
|
|
|
end_time TEXT,
|
|
|
|
|
activity_type TEXT,
|
|
|
|
|
total_distance_m REAL,
|
|
|
|
|
total_kcal REAL,
|
|
|
|
|
avg_speed_kmh REAL,
|
|
|
|
|
max_speed_kmh REAL,
|
|
|
|
|
elevation_gain_m REAL,
|
|
|
|
|
elevation_loss_m REAL,
|
|
|
|
|
source TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS gps_points (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
session_id INTEGER NOT NULL REFERENCES gps_sessions(id) ON DELETE CASCADE,
|
|
|
|
|
ts TEXT NOT NULL,
|
|
|
|
|
lat REAL NOT NULL,
|
|
|
|
|
lon REAL NOT NULL,
|
|
|
|
|
altitude_gps_m REAL,
|
|
|
|
|
altitude_srtm_m REAL,
|
|
|
|
|
speed_ms REAL,
|
|
|
|
|
heart_rate INTEGER
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS activity_log (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
date TEXT NOT NULL,
|
|
|
|
|
time TEXT,
|
|
|
|
|
type TEXT,
|
|
|
|
|
desc TEXT,
|
|
|
|
|
minutes INTEGER,
|
|
|
|
|
kcal REAL,
|
|
|
|
|
gps_session_id INTEGER REFERENCES gps_sessions(id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_food_log_date ON food_log(date);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_activity_log_date ON activity_log(date);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_weight_log_date ON weight_log(date);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sleep_log_date ON sleep_log(date);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gps_points_sess ON gps_points(session_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_products_name ON products(name COLLATE NOCASE);
|
|
|
|
|
"#,
|
|
|
|
|
kind: MigrationKind::Up,
|
|
|
|
|
},
|
|
|
|
|
Migration {
|
|
|
|
|
version: 2,
|
|
|
|
|
description: "add composition column to products",
|
|
|
|
|
sql: "ALTER TABLE products ADD COLUMN composition TEXT;",
|
|
|
|
|
kind: MigrationKind::Up,
|
|
|
|
|
},
|
2026-05-14 14:17:57 +03:00
|
|
|
Migration {
|
|
|
|
|
version: 3,
|
|
|
|
|
description: "reserved",
|
|
|
|
|
sql: "SELECT 1;",
|
|
|
|
|
kind: MigrationKind::Up,
|
|
|
|
|
},
|
2026-05-13 10:21:24 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
tauri::Builder::default()
|
|
|
|
|
.plugin(
|
|
|
|
|
tauri_plugin_sql::Builder::default()
|
|
|
|
|
.add_migrations("sqlite:baldav.db", migrations)
|
|
|
|
|
.build(),
|
|
|
|
|
)
|
2026-05-16 21:21:13 +03:00
|
|
|
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, get_app_info, gps_service_command, gps_service_read_points, save_file])
|
2026-05-13 10:21:24 +03:00
|
|
|
.run(tauri::generate_context!())
|
|
|
|
|
.expect("error while running tauri application");
|
|
|
|
|
}
|