diff --git a/frontend/data/_seed_inserts.sql b/frontend/data/_seed_inserts.sql new file mode 100644 index 0000000..6104ea5 --- /dev/null +++ b/frontend/data/_seed_inserts.sql @@ -0,0 +1 @@ +extra argument: "SELECT" diff --git a/frontend/data/products_seed.db b/frontend/data/products_seed.db new file mode 100644 index 0000000..a43eeb2 Binary files /dev/null and b/frontend/data/products_seed.db differ diff --git a/frontend/js/main.js b/frontend/js/main.js index 20a55e4..57227d2 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -27,6 +27,19 @@ async function init() { // Wait for DB migrations if (DB._migrationsReady) await DB._migrationsReady; + // Import seed products if needed + try { + app.innerHTML = `
Проверка продуктов...
`; + const result = await window.__TAURI__.core.invoke("import_seed_products"); + console.log("[SEED]", result); + if (result.startsWith("imported:")) { + const [_, count, total] = result.split(":"); + console.log(`[SEED] Imported ${count}/${total} products`); + } + } catch (e) { + console.error("[SEED] error:", e); + } + // Apply safe area insets (Android status bar / nav bar) try { const insets = await window.__TAURI__.core.invoke("get_safe_insets"); @@ -1640,10 +1653,10 @@ async function renderSettings(container) {
О приложении
-
+
BalDav
-
Версия ${document.querySelector("meta[name=version]")?.content || "dev"}
+
...
@@ -1660,23 +1673,45 @@ async function renderSettings(container) { offToggle.classList.toggle("on", newVal); }; - // GPS permission check + // GPS permission check — works on both desktop and mobile const gpsStatus = container.querySelector("#s-gps-status"); const gpsToggle = container.querySelector("#s-gps-toggle"); - if (navigator.permissions && navigator.permissions.query) { - try { - const result = await navigator.permissions.query({ name: "geolocation" }); - gpsStatus.textContent = result.state === "granted" ? "Разрешено" : result.state === "denied" ? "Запрещено" : "Не запрошено"; - gpsToggle.classList.toggle("on", result.state === "granted"); - } catch { gpsStatus.textContent = "Недоступно"; } - } - gpsToggle.onclick = () => { + + // Check GPS availability + if (!navigator.geolocation) { + gpsStatus.textContent = "Недоступно"; + } else { + // Try to actually get position to check real permission + gpsStatus.textContent = "Проверка..."; navigator.geolocation.getCurrentPosition( () => { gpsStatus.textContent = "Разрешено"; gpsToggle.classList.add("on"); }, - () => { gpsStatus.textContent = "Запрещено"; gpsToggle.classList.remove("on"); }, - { enableHighAccuracy: true, timeout: 5000 } + (err) => { + if (err.code === 1) gpsStatus.textContent = "Нажмите для запроса"; + else gpsStatus.textContent = "Ошибка: " + err.message; + }, + { enableHighAccuracy: false, timeout: 3000, maximumAge: 60000 } + ); + } + + gpsToggle.onclick = () => { + gpsStatus.textContent = "Запрашиваю..."; + navigator.geolocation.getCurrentPosition( + () => { gpsStatus.textContent = "Разрешено"; gpsToggle.classList.add("on"); }, + (err) => { + gpsStatus.textContent = err.code === 1 ? "Запрещено" : "Ошибка: " + err.message; + gpsToggle.classList.remove("on"); + }, + { enableHighAccuracy: true, timeout: 10000 } ); }; + + // App version info + try { + const info = await window.__TAURI__.core.invoke("get_app_info"); + container.querySelector("#s-version").textContent = `v${info.version} (${info.git_hash}) ${info.build_date}`; + } catch { + container.querySelector("#s-version").textContent = "dev"; + } } function showProfileModal() { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01e1b50..8886a24 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "baldav" -version = "0.1.0" +version = "0.1.28" edition = "2021" [lib] @@ -15,3 +15,4 @@ tauri = { version = "2", features = [] } tauri-plugin-sql = { version = "2", features = ["sqlite"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +rusqlite = { version = "0.31", features = ["bundled"] } diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e..e50037e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,25 @@ +use std::process::Command; + fn main() { + // Pass git hash and build info to Rust via env vars + let git_hash = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|_| "unknown".into()); + + let build_date = chrono_free_date(); + + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + println!("cargo:rustc-env=BUILD_DATE={}", build_date); + tauri_build::build() } + +fn chrono_free_date() -> String { + Command::new("date") + .args(["+%Y-%m-%d %H:%M"]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|_| "unknown".into()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4d12753..515e097 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,128 @@ +#[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(), + } +} + +#[tauri::command] +fn import_seed_products(app: tauri::AppHandle) -> Result { + // 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::().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>(0)?, + row.get::<_, Option>(1)?, + row.get::<_, Option>(2)?, + row.get::<_, Option>(3)?, + row.get::<_, Option>(4)?, + row.get::<_, Option>(5)?, + row.get::<_, Option>(6)?, + row.get::<_, Option>(7)?, + row.get::<_, Option>(8)?, + row.get::<_, Option>(9)?, + row.get::<_, Option>(10)?, + row.get::<_, Option>(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, @@ -236,7 +356,7 @@ pub fn run() { .add_migrations("sqlite:baldav.db", migrations) .build(), ) - .invoke_handler(tauri::generate_handler![get_safe_insets]) + .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, get_app_info]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bac6449..d3f56e0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json", "productName": "BalDav", - "version": "0.1.0", + "version": "0.1.28", "identifier": "dev.baldav.app", "build": { "frontendDist": "../frontend" @@ -22,7 +22,10 @@ "targets": "all", "icon": [ "icons/icon.png" - ] + ], + "resources": { + "../frontend/data/products_seed.db": "data/products_seed.db" + } }, "plugins": { "sql": {