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:
parent
d238af55da
commit
2ca2181e09
7 changed files with 199 additions and 17 deletions
1
frontend/data/_seed_inserts.sql
Normal file
1
frontend/data/_seed_inserts.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
extra argument: "SELECT"
|
||||
BIN
frontend/data/products_seed.db
Normal file
BIN
frontend/data/products_seed.db
Normal file
Binary file not shown.
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue