baldav/src-tauri/src/lib.rs
4aiman 184b86b18c Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
  commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
  Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00

691 lines
31 KiB
Rust

#[allow(unused_imports)]
use tauri_plugin_sql::{Migration, MigrationKind};
#[allow(unused_imports)]
use tauri::Manager;
use serde::Serialize;
#[allow(unused, dead_code)]
mod ble;
// Simple HTTP proxy for sync requests (WebView blocks mixed http/https content)
#[tauri::command]
fn http_request(url: String, method: String, body: Option<String>) -> Result<String, String> {
let mut resp = match method.to_uppercase().as_str() {
"POST" => {
let b = body.unwrap_or_default();
ureq::post(&url)
.header("Content-Type", "application/json")
.send(b.as_bytes())
.map_err(|e| e.to_string())?
}
_ => ureq::get(&url).call().map_err(|e| e.to_string())?,
};
resp.body_mut().read_to_string().map_err(|e| e.to_string())
}
// Download binary file from URL to local path (for tile DB import from router)
// read_to_vec() has a 10MB limit, so use as_reader() for unlimited binary reads
#[tauri::command]
fn download_file(url: String, dest: String) -> Result<String, String> {
use std::io::Read;
let resp = ureq::get(&url).call().map_err(|e| format!("fetch: {e}"))?;
let mut reader = resp.into_body().into_reader();
let mut data = Vec::new();
reader.read_to_end(&mut data).map_err(|e| format!("read: {e}"))?;
std::fs::write(&dest, &data).map_err(|e| format!("write: {e}"))?;
Ok(format!("{}", data.len()))
}
// Rebuild road_points from all GPS tracks with 2m deduplication.
// Runs entirely in Rust/SQLite — no Tauri SQL plugin row limits.
#[tauri::command]
fn rebuild_roads(app: tauri::AppHandle, min_speed_ms: f64) -> Result<String, String> {
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let db_path = {
let p1 = data_dir.join("../../baldav.db");
let p2 = data_dir.join("baldav.db");
let p3 = std::path::PathBuf::from(
data_dir.to_str().unwrap_or("").replace("/files/dev.baldav.app", "")
).join("baldav.db");
if p1.exists() { p1 } else if p2.exists() { p2 } else { p3 }
};
let db = rusqlite::Connection::open(&db_path).map_err(|e| format!("open: {e}"))?;
db.execute("DROP TABLE IF EXISTS road_points", []).ok();
db.execute("CREATE TABLE road_points (id INTEGER PRIMARY KEY, lat REAL NOT NULL, lon REAL NOT NULL, atype TEXT)", [])
.map_err(|e| format!("create: {e}"))?;
db.execute("CREATE INDEX idx_road_lat_lon ON road_points(lat, lon)", [])
.map_err(|e| format!("index: {e}"))?;
// 2m in degrees: lat ~0.000018, lon ~0.000027 at 47N
let d_lat: f64 = 0.000018;
let d_lon: f64 = 0.000027;
let mut read_stmt = db.prepare(
"SELECT p.lat, p.lon, s.activity_type FROM gps_points p
JOIN gps_sessions s ON s.id = p.session_id
WHERE p.speed_ms >= ?1
ORDER BY p.session_id, p.seq"
).map_err(|e| format!("prepare read: {e}"))?;
let points: Vec<(f64, f64, String)> = read_stmt.query_map(rusqlite::params![min_speed_ms], |row| {
Ok((row.get::<_, f64>(0)?, row.get::<_, f64>(1)?, row.get::<_, String>(2).unwrap_or_default()))
}).map_err(|e| format!("query: {e}"))?
.filter_map(|r| r.ok())
.collect();
drop(read_stmt);
db.execute("BEGIN", []).map_err(|e| format!("begin: {e}"))?;
let mut inserted = 0u32;
for (lat, lon, atype) in &points {
let exists: bool = db.query_row(
"SELECT EXISTS(SELECT 1 FROM road_points WHERE lat BETWEEN ?1 AND ?2 AND lon BETWEEN ?3 AND ?4)",
rusqlite::params![lat - d_lat, lat + d_lat, lon - d_lon, lon + d_lon],
|r| r.get(0),
).unwrap_or(false);
if !exists {
db.execute(
"INSERT INTO road_points (lat, lon, atype) VALUES (?1, ?2, ?3)",
rusqlite::params![lat, lon, atype],
).map_err(|e| format!("insert: {e}"))?;
inserted += 1;
}
}
db.execute("COMMIT", []).map_err(|e| format!("commit: {e}"))?;
Ok(format!("{{\"total_points\":{},\"road_points\":{}}}", points.len(), inserted))
}
// Read road_points in batches — bypasses Tauri SQL plugin's ~28K row limit
#[tauri::command]
fn get_road_points(app: tauri::AppHandle, offset: u32, batch_size: u32) -> Result<String, String> {
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
// Find main DB — same path resolution as import_seed_products
let db_path = {
let p1 = data_dir.join("../../baldav.db");
let p2 = data_dir.join("baldav.db");
let p3 = std::path::PathBuf::from(
data_dir.to_str().unwrap_or("").replace("/files/dev.baldav.app", "")
).join("baldav.db");
if p1.exists() { p1 } else if p2.exists() { p2 } else { p3 }
};
let db = rusqlite::Connection::open(&db_path).map_err(|e| format!("open: {e}"))?;
let total: u32 = db.query_row(
"SELECT count(*) FROM road_points", [], |r| r.get(0)
).unwrap_or(0);
let mut stmt = db.prepare(
"SELECT lat, lon, COALESCE(atype,'bike') FROM road_points ORDER BY id LIMIT ?1 OFFSET ?2"
).map_err(|e| format!("prepare: {e}"))?;
let mut points = Vec::new();
let rows = stmt.query_map(rusqlite::params![batch_size, offset], |row| {
let lat: f64 = row.get(0)?;
let lon: f64 = row.get(1)?;
let atype: String = row.get(2)?;
Ok((lat, lon, atype))
}).map_err(|e| format!("query: {e}"))?;
for row in rows {
let (lat, lon, atype) = row.map_err(|e| format!("row: {e}"))?;
let t = if atype == "walk" { "w" } else { "b" };
points.push(format!("[{:.7},{:.7},\"{}\"]", lat, lon, t));
}
let has_more = offset + batch_size < total;
Ok(format!("{{\"points\":[{}],\"total\":{},\"has_more\":{}}}", points.join(","), total, has_more))
}
#[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(),
}
}
// Read update check result written by Kotlin (update_info.json)
#[tauri::command]
fn get_update_info(app: tauri::AppHandle) -> Result<String, String> {
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let paths = [
data_dir.join("update_info.json"),
data_dir.parent().and_then(|p| p.parent()).map(|r| r.join("update_info.json")).unwrap_or_default(),
];
for p in &paths {
if p.exists() {
if let Ok(s) = std::fs::read_to_string(p) {
let s = s.trim().to_string();
if !s.is_empty() { return Ok(s); }
}
}
}
Ok("{}".to_string())
}
// 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();
// Write to app_data_dir
let cmd_path = data_dir.join("gps_command.txt");
std::fs::write(&cmd_path, &cmd).map_err(|e| e.to_string())?;
// 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);
}
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())?;
// Also check parent dir (Android: app_data_dir may be files/dev.baldav.app but service writes to app root)
let pts_path = data_dir.join("gps_live_points.jsonl");
let alt_path = data_dir.parent().and_then(|p| p.parent()).map(|p| p.join("gps_live_points.jsonl"));
if pts_path.exists() {
std::fs::read_to_string(&pts_path).map_err(|e| e.to_string())
} 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())
}
} else {
Ok(String::new())
}
}
#[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())
}
// Read tiles from a SQLite file in batches for import into IndexedDB
#[tauri::command]
fn import_tiles(path: String, offset: u32, batch_size: u32) -> Result<String, String> {
let db = rusqlite::Connection::open(&path).map_err(|e| format!("open: {e}"))?;
let total: u32 = db.query_row("SELECT count(*) FROM tiles", [], |r| r.get(0))
.map_err(|e| format!("count: {e}"))?;
let mut stmt = db.prepare("SELECT key, data FROM tiles LIMIT ?1 OFFSET ?2")
.map_err(|e| format!("prepare: {e}"))?;
let mut tiles = Vec::new();
let rows = stmt.query_map(rusqlite::params![batch_size, offset], |row| {
let key: String = row.get(0)?;
let data: Vec<u8> = row.get(1)?;
Ok((key, data))
}).map_err(|e| format!("query: {e}"))?;
for row in rows {
let (key, data) = row.map_err(|e| format!("row: {e}"))?;
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
tiles.push(format!("[\"{}\",\"{}\"]", key, b64));
}
let has_more = offset + batch_size < total;
Ok(format!("{{\"tiles\":[{}],\"total\":{},\"has_more\":{}}}", tiles.join(","), total, has_more))
}
#[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");
// 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);
}
let seed_path = if seed_in_data.exists() {
seed_in_data
} else if seed_in_files.exists() {
seed_in_files
} else {
return Err(format!("Seed DB not found. Checked: {:?}, {:?}", seed_in_data, seed_in_files));
};
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())?;
// 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();
// tauri-plugin-sql stores DB in different locations depending on platform.
// Android: often in app root (/data/user/0/pkg/baldav.db), not in files/ subdir.
let main_path = {
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");
// Android app root: THREE levels up from app_data_dir (files/dev.baldav.app/ → files/ → root/)
let p_root = data_dir.join("..").join("..").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 if p_root.exists() { p_root }
else { p_data }
};
let main_db = rusqlite::Connection::open(&main_path).map_err(|e| e.to_string())?;
// Ensure all columns exist in main DB
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",
"user_edited INTEGER DEFAULT 0",
];
for col in &extra_cols {
let _ = main_db.execute(&format!("ALTER TABLE products ADD COLUMN {}", col), []);
}
// Dedup ALWAYS runs (even if seed version matches) — fixes legacy duplicates.
let _ = main_db.execute(
"CREATE INDEX IF NOT EXISTS idx_products_name_lower ON products(name_lower)", []);
main_db.execute(
"UPDATE food_log SET product_id = (
SELECT MIN(p2.id) FROM products p2
WHERE p2.name_lower = (SELECT p3.name_lower FROM products p3 WHERE p3.id = food_log.product_id)
AND p2.source = (SELECT p3.source FROM products p3 WHERE p3.id = food_log.product_id)
AND COALESCE(p2.store_id,'') = COALESCE((SELECT p3.store_id FROM products p3 WHERE p3.id = food_log.product_id),'')
) WHERE product_id NOT IN (
SELECT MIN(id) FROM products WHERE name_lower IS NOT NULL
GROUP BY name_lower, source, COALESCE(store_id,'')
)", []).map_err(|e| format!("dedup food_log: {}", e))?;
main_db.execute(
"UPDATE recipe_ingredients SET product_id = (
SELECT MIN(p2.id) FROM products p2
WHERE p2.name_lower = (SELECT p3.name_lower FROM products p3 WHERE p3.id = recipe_ingredients.product_id)
AND p2.source = (SELECT p3.source FROM products p3 WHERE p3.id = recipe_ingredients.product_id)
AND COALESCE(p2.store_id,'') = COALESCE((SELECT p3.store_id FROM products p3 WHERE p3.id = recipe_ingredients.product_id),'')
) WHERE product_id NOT IN (
SELECT MIN(id) FROM products WHERE name_lower IS NOT NULL
GROUP BY name_lower, source, COALESCE(store_id,'')
)", []).map_err(|e| format!("dedup recipe_ingredients: {}", e))?;
// Reassign product_units (also has FK to products)
main_db.execute(
"DELETE FROM product_units WHERE product_id NOT IN (
SELECT MIN(id) FROM products WHERE name_lower IS NOT NULL
GROUP BY name_lower, source, COALESCE(store_id,'')
)", []).map_err(|e| format!("dedup product_units: {}", e))?;
main_db.execute(
"DELETE FROM products WHERE name_lower IS NOT NULL
AND id NOT IN (
SELECT MIN(id) FROM products WHERE name_lower IS NOT NULL
GROUP BY name_lower, source, COALESCE(store_id,'')
)", []).map_err(|e| format!("dedup delete: {}", e))?;
let _ = main_db.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_products_dedup
ON products(name_lower, source, COALESCE(store_id, ''))
WHERE name_lower IS NOT NULL", []);
// Check seed version — skip import if already up to date
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));
}
// ATTACH seed DB for import
let seed_str = seed_path.to_str().unwrap_or_default().to_string();
main_db.execute("ATTACH DATABASE ?1 AS seed", rusqlite::params![seed_str])
.map_err(|e| e.to_string())?;
let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0))
.map_err(|e| e.to_string())?;
main_db.execute("BEGIN", []).map_err(|e| e.to_string())?;
// Bulk INSERT new products from seed (INSERT OR IGNORE now works with unique index)
let inserted = main_db.execute(
"INSERT OR IGNORE INTO main.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
)
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 seed.products",
[]
).map_err(|e| e.to_string())?;
// Seed index for fast UPDATE lookups
let _ = main_db.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_seed_name_lower ON seed.products(name_lower);"
);
// Update KBJU + micronutrients for existing store products (seed already attached above)
// 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)"
);
// Update KBJU + micronutrients for store products that user hasn't manually edited.
// This ensures seed corrections (e.g. bread 268→243) flow through to app DB.
let _ = main_db.execute_batch(
"UPDATE main.products SET
kcal_per_100g = (SELECT s.kcal_per_100g FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1),
protein_per_100g = (SELECT s.protein_per_100g FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1),
fat_per_100g = (SELECT s.fat_per_100g FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1),
carbs_per_100g = (SELECT s.carbs_per_100g FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1),
fiber_per_100g = (SELECT s.fiber_per_100g FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1),
nutrients_guessed= COALESCE((SELECT s.nutrients_guessed FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), nutrients_guessed),
sodium_mg = COALESCE((SELECT s.sodium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), sodium_mg),
potassium_mg = COALESCE((SELECT s.potassium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), potassium_mg),
calcium_mg = COALESCE((SELECT s.calcium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), calcium_mg),
magnesium_mg = COALESCE((SELECT s.magnesium_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), magnesium_mg),
phosphorus_mg = COALESCE((SELECT s.phosphorus_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), phosphorus_mg),
iron_mg = COALESCE((SELECT s.iron_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), iron_mg),
vit_a_mcg = COALESCE((SELECT s.vit_a_mcg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), vit_a_mcg),
vit_b1_mg = COALESCE((SELECT s.vit_b1_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), vit_b1_mg),
vit_b2_mg = COALESCE((SELECT s.vit_b2_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), vit_b2_mg),
vit_b3_mg = COALESCE((SELECT s.vit_b3_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), vit_b3_mg),
vit_c_mg = COALESCE((SELECT s.vit_c_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), vit_c_mg),
vit_e_mg = COALESCE((SELECT s.vit_e_mg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), vit_e_mg),
carotene_mcg = COALESCE((SELECT s.carotene_mcg FROM seed.products s WHERE s.name_lower = main.products.name_lower LIMIT 1), carotene_mcg)
WHERE (main.products.user_edited IS NULL OR main.products.user_edited = 0)
AND main.products.source IN ('magnit', '5ka', 'kdv', 'ion')
AND main.products.name_lower IN (SELECT s2.name_lower FROM seed.products s2)"
);
// Update seed version AFTER all inserts+updates succeed
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())?;
let _ = main_db.execute("DETACH DATABASE seed", []);
Ok(format!("imported:{}:{}", inserted, total))
}
#[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)]
pub fn run() {
let migrations: Vec<Migration> = vec![
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,
},
Migration {
version: 3,
description: "reserved",
sql: "SELECT 1;",
kind: MigrationKind::Up,
},
];
tauri::Builder::default()
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:baldav.db", migrations)
.build(),
)
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, rebuild_roads, get_road_points, get_app_info, gps_service_command, gps_service_read_points, save_file, get_update_info, http_request, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}