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": {