Seed products DB + Settings screen + build fixes

Products seed: 9299 products bundled in products_seed.db, Kotlin
copies from assets to filesDir on Android, Rust imports into main
DB on first launch with version check for future updates.

Settings: profile button, offline toggle, GPS permission, app info
with git hash and build date from build.rs.

Build: version synced to Cargo.toml, unified import (GPX/FTB by
extension), chart zoom Ctrl+scroll only, scroll wheel zoom disabled
on detail map.
This commit is contained in:
4aiman 2026-05-14 17:35:29 +03:00
commit 2ca2181e09
7 changed files with 199 additions and 17 deletions

View file

@ -0,0 +1 @@
extra argument: "SELECT"

Binary file not shown.

View file

@ -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 = `<div class="loading">Проверка продуктов...</div>`;
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) {
<div class="settings-section">
<div class="settings-section-title">О приложении</div>
<div class="settings-item">
<div class="settings-item" id="s-about">
<div class="settings-item-left">
<div class="settings-item-title">BalDav</div>
<div class="settings-item-desc">Версия ${document.querySelector("meta[name=version]")?.content || "dev"}</div>
<div class="settings-item-desc" id="s-version">...</div>
</div>
</div>
</div>
@ -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() {

View file

@ -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"] }

View file

@ -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())
}

View file

@ -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<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,
@ -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");
}

View file

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