baldav/src-tauri/src/lib.rs

398 lines
15 KiB
Rust
Raw Normal View History

#[allow(unused_imports)]
use tauri_plugin_sql::{Migration, MigrationKind};
#[allow(unused_imports)]
use tauri::Manager;
use serde::Serialize;
#[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(),
}
}
// 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 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");
let seed_path = if seed_in_data.exists() {
seed_in_data
} else if seed_in_files.exists() {
seed_in_files
} else {
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() {
std::fs::copy(&seed_resource, &seed_in_data.clone()).map_err(|e| e.to_string())?;
seed_in_data
} 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())?;
// Read seed version
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())?;
// Open main DB — check multiple locations (Tauri may put it in different dirs)
let main_path = {
let p1 = data_dir.join("baldav.db");
let p2 = data_dir.join("..").join("baldav.db");
if p1.exists() { p1 } else if p2.exists() { p2 } else { p1 }
};
let main_db = rusqlite::Connection::open(&main_path).map_err(|e| e.to_string())?;
// Check current seed version in main DB
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));
}
// Count products in seed
let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0))
.map_err(|e| e.to_string())?;
// Import products in batches
let mut stmt = seed_db.prepare(
"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 FROM products"
).map_err(|e| e.to_string())?;
let mut inserted = 0usize;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, Option<String>>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, Option<f64>>(2)?,
row.get::<_, Option<f64>>(3)?,
row.get::<_, Option<f64>>(4)?,
row.get::<_, Option<f64>>(5)?,
row.get::<_, Option<f64>>(6)?,
row.get::<_, Option<String>>(7)?,
row.get::<_, Option<String>>(8)?,
row.get::<_, Option<String>>(9)?,
row.get::<_, Option<String>>(10)?,
row.get::<_, Option<f64>>(11)?,
))
}).map_err(|e| e.to_string())?;
// Ensure columns exist
let _ = main_db.execute("ALTER TABLE products ADD COLUMN composition TEXT", []);
let _ = main_db.execute("ALTER TABLE products ADD COLUMN name_lower TEXT", []);
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(
"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) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
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],
).map_err(|e| e.to_string())?;
inserted += 1;
}
// Update seed version
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())?;
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, get_app_info, gps_service_command, gps_service_read_points])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}