Seed products DB + Settings screen + build fixes

GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.

Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
This commit is contained in:
4aiman 2026-05-15 14:36:22 +03:00
commit f717204ac8
6 changed files with 447 additions and 69 deletions

View file

@ -945,6 +945,38 @@ body {
z-index: 1;
}
/* Floating recording bar */
.rec-floating-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
padding-top: calc(8px + var(--safe-area-top));
background: rgba(187, 134, 252, 0.15);
border-bottom: 1px solid var(--primary);
cursor: pointer;
order: 0;
}
.rec-bar-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #f44336;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
#rec-bar-text {
font-size: 13px;
color: var(--primary);
font-weight: 500;
}
/* Recording screen */
.rec-screen {
display: flex;

View file

@ -4,6 +4,23 @@
const GPS = {};
// ---------------------------------------------------------------------------
// Configurable thresholds (can be changed in settings)
GPS.config = {
minSpeedMs: 0.42, // 1.5 km/h — below this = stationary
minAccuracyM: 20, // skip fixes less accurate than this
minDistanceM: 1, // skip points closer than this
};
// Load saved config
try {
const saved = localStorage.getItem("baldav_gps_config");
if (saved) Object.assign(GPS.config, JSON.parse(saved));
} catch {}
GPS.saveConfig = function() {
localStorage.setItem("baldav_gps_config", JSON.stringify(GPS.config));
};
// Haversine distance (meters)
// ---------------------------------------------------------------------------
GPS.distance = function(lat1, lon1, lat2, lon2) {
@ -114,6 +131,197 @@ GPS.recording = {
activityType: "bike",
};
// Recording states: waiting_gps → ready → recording (auto on first movement)
// Pause: speed < 1 m/s for 5+ seconds → stop counting distance/time
// Start native GPS service on Android (foreground service survives background)
GPS._startNativeService = async function() {
try {
// Send config along with start command: start:minSpeed:maxAcc:minDist
const cmd = `start:${GPS.config.minSpeedMs}:${GPS.config.minAccuracyM}:${GPS.config.minDistanceM}`;
await window.__TAURI__.core.invoke("gps_service_command", { cmd });
console.log("[GPS] Native service started with config:", cmd);
GPS.recording.nativeService = true;
} catch (e) {
console.log("[GPS] No native service (desktop?), using WebView GPS");
GPS.recording.nativeService = false;
}
};
GPS._stopNativeService = async function() {
try {
await window.__TAURI__.core.invoke("gps_service_command", { cmd: "stop" });
} catch {}
};
// Read points from native service file
GPS._readNativePoints = async function() {
try {
const raw = await window.__TAURI__.core.invoke("gps_service_read_points");
if (!raw) return [];
return raw.trim().split("\n").filter(Boolean).map(line => {
const d = JSON.parse(line);
return {
lat: d.lat, lon: d.lon, altitude: d.alt,
speed_ms: d.speed, accuracy: d.acc,
ts: new Date(d.ts).toISOString(),
};
});
} catch { return []; }
};
// Core point processing — used by both WebView GPS and native service polling
GPS._processIncomingPoint = async function(p) {
if (!GPS.recording.active) return;
// State: waiting_gps → ready (got first accurate fix)
if (GPS.recording.state === "waiting_gps") {
GPS.recording.state = "ready";
if (GPS.recording.onStateChange) GPS.recording.onStateChange("ready", p);
}
// Manual pause — show position but don't process
if (GPS.recording.manualPaused) {
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
return;
}
// Determine if moving
const speed = p.speed_ms != null ? p.speed_ms : 0;
let isMoving = speed > GPS.config.minSpeedMs;
if (!isMoving && GPS.recording.lastMovingPoint) {
const d = GPS.distance(GPS.recording.lastMovingPoint.lat, GPS.recording.lastMovingPoint.lon, p.lat, p.lon);
const dt = (new Date(p.ts) - new Date(GPS.recording.lastMovingPoint.ts)) / 1000;
if (d > GPS.config.minDistanceM * 3 && dt < 5 && dt > 0) isMoving = true;
}
// State: ready → recording (first movement detected)
if (GPS.recording.state === "ready" && isMoving) {
GPS.recording.state = "recording";
GPS.recording.startTime = new Date();
const now = GPS.recording.startTime;
GPS.recording.sessionId = await DB.createGpsSession(
now.toISOString().slice(0, 10),
now.toTimeString().slice(0, 5),
GPS.recording.activityType
);
GPS._persistState();
if (GPS.recording.onStateChange) GPS.recording.onStateChange("recording", p);
}
// If not yet recording, just update position display
if (GPS.recording.state !== "recording") {
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
return;
}
// Auto-pause detection
if (!isMoving) {
if (!GPS.recording.pauseStart) {
GPS.recording.pauseStart = new Date(p.ts);
}
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
return;
}
// Resumed from auto-pause
if (GPS.recording.pauseStart) {
const pauseMs = new Date(p.ts) - GPS.recording.pauseStart;
GPS.recording.totalPauseMs += pauseMs;
GPS.recording.pauseStart = null;
if (GPS.recording.onStateChange) GPS.recording.onStateChange("resumed", p);
}
// Save moving point
GPS.recording.lastMovingPoint = p;
GPS.recording.points.push(p);
if (GPS.recording.sessionId) {
await DB.addGpsPoint(GPS.recording.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms);
}
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
};
// Persist recording state to localStorage for crash recovery
GPS._persistState = function() {
try {
localStorage.setItem("baldav_gps_active", JSON.stringify({
sessionId: GPS.recording.sessionId,
activityType: GPS.recording.activityType,
weightKg: GPS.recording.weightKg,
startTime: GPS.recording.startTime?.toISOString(),
totalPauseMs: GPS.recording.totalPauseMs,
manualPaused: GPS.recording.manualPaused,
pauses: GPS.recording.pauses || [],
pointCount: GPS.recording.points.length,
}));
} catch {}
};
GPS._clearPersistedState = function() {
localStorage.removeItem("baldav_gps_active");
};
// Manual pause/resume
GPS.pauseRecording = function() {
if (!GPS.recording.active || GPS.recording.manualPaused) return;
GPS.recording.manualPaused = true;
const last = GPS.recording.lastMovingPoint || GPS.recording.points[GPS.recording.points.length - 1];
GPS.recording._currentPause = {
startTs: Date.now(),
startLat: last?.lat, startLon: last?.lon,
};
GPS._persistState();
if (GPS.recording.onStateChange) GPS.recording.onStateChange("manual_paused");
};
GPS.resumeRecording = function() {
if (!GPS.recording.active || !GPS.recording.manualPaused) return;
GPS.recording.manualPaused = false;
const last = GPS.recording.lastMovingPoint || GPS.recording.points[GPS.recording.points.length - 1];
if (GPS.recording._currentPause) {
GPS.recording._currentPause.endTs = Date.now();
GPS.recording._currentPause.endLat = last?.lat;
GPS.recording._currentPause.endLon = last?.lon;
const dur = GPS.recording._currentPause.endTs - GPS.recording._currentPause.startTs;
GPS.recording.totalPauseMs += dur;
if (!GPS.recording.pauses) GPS.recording.pauses = [];
GPS.recording.pauses.push(GPS.recording._currentPause);
GPS.recording._currentPause = null;
}
GPS._persistState();
if (GPS.recording.onStateChange) GPS.recording.onStateChange("manual_resumed");
};
// Recover orphaned recording session on app startup
GPS.recoverSession = async function() {
const saved = localStorage.getItem("baldav_gps_active");
if (!saved) return;
try {
const s = JSON.parse(saved);
if (!s.sessionId) { GPS._clearPersistedState(); return; }
// Import any remaining native points
const nativePoints = await GPS._readNativePoints();
if (nativePoints.length > (s.pointCount || 0)) {
for (let i = s.pointCount || 0; i < nativePoints.length; i++) {
const p = nativePoints[i];
await DB.addGpsPoint(s.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms);
}
}
// Finalize the session
const pts = await DB.getGpsPoints(s.sessionId);
if (pts.length > 0) {
const stats = GPS.computeStats(pts, s.weightKg || 80);
await DB.finishGpsSession(s.sessionId, new Date().toTimeString().slice(0, 5),
stats.distance, stats.kcal, stats.avgSpeed, stats.maxSpeed, stats.elevGain, stats.elevLoss);
console.log("[GPS] Recovered session", s.sessionId, "with", pts.length, "points");
}
GPS._clearPersistedState();
await GPS._stopNativeService();
} catch (e) {
console.error("[GPS] Recovery error:", e);
GPS._clearPersistedState();
}
};
GPS.startRecording = async function(activityType, weightKg) {
if (GPS.recording.active) return;
if (!navigator.geolocation) throw new Error("Geolocation not available");
@ -121,34 +329,46 @@ GPS.startRecording = async function(activityType, weightKg) {
GPS.recording.active = true;
GPS.recording.activityType = activityType || "bike";
GPS.recording.points = [];
GPS.recording.startTime = new Date();
GPS.recording.startTime = null;
GPS.recording.weightKg = weightKg ?? 80;
GPS.recording.state = "waiting_gps";
GPS.recording.sessionId = null;
GPS.recording.pauseStart = null;
GPS.recording.totalPauseMs = 0;
GPS.recording.lastMovingPoint = null;
GPS.recording.manualPaused = false;
GPS.recording.pauses = [];
GPS.recording._currentPause = null;
GPS.recording.nativeService = false;
GPS.recording._pollTimer = null;
GPS.recording._lastNativeCount = 0;
const now = GPS.recording.startTime;
GPS.recording.sessionId = await DB.createGpsSession(
now.toISOString().slice(0, 10),
now.toTimeString().slice(0, 5),
GPS.recording.activityType
);
// Start native foreground service (Android)
await GPS._startNativeService();
// Poll native service output file
if (GPS.recording.nativeService) {
GPS.recording._pollTimer = setInterval(async () => {
if (!GPS.recording.active) { clearInterval(GPS.recording._pollTimer); return; }
const nativePoints = await GPS._readNativePoints();
for (let i = GPS.recording._lastNativeCount; i < nativePoints.length; i++) {
await GPS._processIncomingPoint(nativePoints[i]);
}
GPS.recording._lastNativeCount = nativePoints.length;
}, 2000);
}
GPS.recording.watchId = navigator.geolocation.watchPosition(
async (pos) => {
// Skip inaccurate fixes (>100m accuracy = unreliable)
if (pos.coords.accuracy > 100) return;
const p = {
if (pos.coords.accuracy > GPS.config.minAccuracyM) return;
await GPS._processIncomingPoint({
lat: pos.coords.latitude,
lon: pos.coords.longitude,
altitude: pos.coords.altitude,
speed_ms: pos.coords.speed,
accuracy: pos.coords.accuracy,
ts: new Date(pos.timestamp).toISOString(),
};
GPS.recording.points.push(p);
await DB.addGpsPoint(
GPS.recording.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms
);
if (GPS.recording.onPoint) GPS.recording.onPoint(p);
});
},
(err) => {
console.error("GPS error:", err.message);
@ -162,11 +382,31 @@ GPS.stopRecording = async function() {
if (!GPS.recording.active) return null;
GPS.recording.active = false;
if (GPS.recording._pollTimer) clearInterval(GPS.recording._pollTimer);
if (GPS.recording.watchId != null) {
navigator.geolocation.clearWatch(GPS.recording.watchId);
GPS.recording.watchId = null;
}
// Stop native service
if (GPS.recording.nativeService) {
// Import any remaining points from native service
const nativePoints = await GPS._readNativePoints();
if (nativePoints.length > GPS.recording._lastNativeCount && GPS.recording.sessionId) {
for (let i = GPS.recording._lastNativeCount; i < nativePoints.length; i++) {
const p = nativePoints[i];
GPS.recording.points.push(p);
await DB.addGpsPoint(GPS.recording.sessionId, p.ts, p.lat, p.lon, p.altitude, p.speed_ms);
}
}
await GPS._stopNativeService();
}
if (!GPS.recording.sessionId) {
return null;
}
const stats = GPS.computeStats(GPS.recording.points, GPS.recording.weightKg);
const endTime = new Date().toTimeString().slice(0, 5);
@ -177,9 +417,13 @@ GPS.stopRecording = async function() {
);
const sid = GPS.recording.sessionId;
const pauses = GPS.recording.pauses || [];
GPS.recording.sessionId = null;
GPS.recording.points = [];
return { sessionId: sid, stats };
GPS.recording.pauses = [];
GPS.recording.manualPaused = false;
GPS._clearPersistedState();
return { sessionId: sid, stats, pauses };
};
// ---------------------------------------------------------------------------

View file

@ -27,6 +27,9 @@ async function init() {
// Wait for DB migrations
if (DB._migrationsReady) await DB._migrationsReady;
// Recover any orphaned GPS recording session
await GPS.recoverSession();
// Import seed products if needed
try {
app.innerHTML = `<div class="loading">Проверка продуктов...</div>`;
@ -69,10 +72,33 @@ function renderScreen(name) {
today: renderToday,
products: renderProducts,
gps: renderGpsScreen,
recording: renderRecordingInline,
supplements: renderSupplements,
settings: renderSettings,
};
app.innerHTML = "";
// Floating recording bar (visible on all screens except recording)
if (GPS.recording.active && name !== "recording") {
const bar = document.createElement("div");
bar.className = "rec-floating-bar";
bar.onclick = () => renderScreen("recording");
bar.innerHTML = `<span class="rec-bar-dot"></span><span id="rec-bar-text">Запись...</span>`;
app.appendChild(bar);
// Update bar every second
if (window._recBarTimer) clearInterval(window._recBarTimer);
window._recBarTimer = setInterval(() => {
if (!GPS.recording.active) { clearInterval(window._recBarTimer); return; }
const pts = GPS.recording.points;
const stats = GPS.computeStats(pts, GPS.recording.weightKg);
const sec = GPS.recording.startTime ? Math.round((Date.now() - GPS.recording.startTime.getTime() - GPS.recording.totalPauseMs) / 1000) : 0;
const el = document.querySelector("#rec-bar-text");
if (el) el.textContent = `${GPS.formatDistance(stats.distance)} | ${GPS.formatDuration(sec)}${GPS.recording.manualPaused ? " | Пауза" : ""}`;
}, 1000);
} else if (window._recBarTimer) {
clearInterval(window._recBarTimer);
}
app.appendChild(buildNav(name));
const content = document.createElement("div");
content.className = "screen-content";
@ -86,7 +112,7 @@ function buildNav(active) {
[
{ id: "today", label: "Сегодня" },
{ id: "products", label: "Продукты" },
{ id: "gps", label: "Трек" },
{ id: GPS.recording.active ? "recording" : "gps", label: GPS.recording.active ? "Запись" : "Трек" },
{ id: "supplements", label: "Добавки" },
{ id: "settings", label: "Настройки" },
].forEach(item => {
@ -742,20 +768,15 @@ async function renderGpsScreen(container) {
// Recording button
container.querySelector("#gps-rec-btn").onclick = async () => {
if (GPS.recording.active) {
showRecordingScreen();
renderScreen("recording");
} else {
const type = container.querySelector("#gps-type").value;
const wkg = state.latestWeight?.weight_kg ?? 80;
await GPS.startRecording(type, wkg);
showRecordingScreen();
renderScreen("recording");
}
};
if (GPS.recording.active) {
// If already recording, go straight to recording screen
showRecordingScreen();
}
// Session click → detail view
container.querySelectorAll(".gps-session-item").forEach(item => {
item.onclick = () => {
@ -814,8 +835,10 @@ async function renderGpsScreen(container) {
};
}
function showRecordingScreen() {
showModal(`
function renderRecordingInline(container) {
if (!GPS.recording.active) { renderScreen("gps"); return; }
container.innerHTML = `
<div class="rec-screen">
<div id="rec-map" class="rec-map"></div>
<div class="rec-stats">
@ -825,16 +848,11 @@ function showRecordingScreen() {
<div class="rec-stat"><span id="rec-elev" class="rec-val">—</span><span class="rec-lbl">Высота</span></div>
</div>
<div class="rec-bottom">
<div class="rec-status" id="rec-status">GPS...</div>
<div class="rec-status" id="rec-status">${GPS.recording.state === "waiting_gps" ? "Поиск GPS..." : GPS.recording.state === "ready" ? "Начните движение..." : ""}</div>
<button id="rec-pause" class="btn-secondary">${GPS.recording.manualPaused ? "Продолжить" : "Пауза"}</button>
<button id="rec-stop" class="btn-primary gps-rec recording">Стоп</button>
</div>
</div>`,
() => {});
// Hide modal OK/Cancel buttons
const overlay = document.querySelector(".modal-overlay");
const btns = overlay.querySelector(".modal-btns");
if (btns) btns.style.display = "none";
</div>`;
setTimeout(() => {
const mapEl = document.querySelector("#rec-map");
@ -853,15 +871,20 @@ function showRecordingScreen() {
const trailLine = L.polyline([], { color: "#bb86fc", weight: 5 }).addTo(map);
let posMarker = null;
// Center on user
navigator.geolocation?.getCurrentPosition(
pos => {
map.setView([pos.coords.latitude, pos.coords.longitude], 15);
document.querySelector("#rec-status").textContent = "GPS OK";
},
() => { document.querySelector("#rec-status").textContent = "GPS не найден"; },
{ enableHighAccuracy: true, timeout: 10000 }
);
// State changes
GPS.recording.onStateChange = (state, p) => {
if (state === "ready") {
statusEl.textContent = "GPS OK. Начните движение...";
statusEl.style.color = "#03dac6";
map.setView([p.lat, p.lon], 16);
} else if (state === "recording") {
statusEl.textContent = "";
statusEl.style.color = "";
} else if (state === "resumed") {
statusEl.textContent = "";
statusEl.style.color = "";
}
};
// Live point updates
const distEl = document.querySelector("#rec-dist");
@ -887,17 +910,23 @@ function showRecordingScreen() {
const stats = GPS.computeStats(pts, GPS.recording.weightKg);
if (distEl) distEl.textContent = GPS.formatDistance(stats.distance);
// Speed: from device speed or computed from distance
let curSpeed = 0;
if (p.speed_ms != null && p.speed_ms >= 0) {
curSpeed = p.speed_ms * 3.6;
} else if (pts.length >= 2) {
// Fallback: compute from last 2 points
const prev = pts[pts.length - 2];
const d = GPS.distance(prev.lat, prev.lon, p.lat, p.lon);
const dt = (new Date(p.ts) - new Date(prev.ts)) / 1000;
if (dt > 0) curSpeed = (d / dt) * 3.6;
// Show current state
const recState = GPS.recording.state;
if (recState === "waiting_gps") {
statusEl.textContent = "Поиск GPS...";
statusEl.style.color = "#ffb74d";
map.setView([p.lat, p.lon], 15);
return;
}
if (recState === "ready") {
// Position marker only
if (posMarker) map.removeLayer(posMarker);
posMarker = L.circleMarker([p.lat, p.lon], { radius: 6, color: "#4285f4", fillColor: "#4285f4", fillOpacity: 1, weight: 2 }).addTo(map);
return;
}
// Speed
const curSpeed = (p.speed_ms != null && p.speed_ms >= 0) ? p.speed_ms * 3.6 : 0;
speedBuf.push(curSpeed);
if (speedBuf.length > 5) speedBuf.shift();
const sorted = speedBuf.slice().sort((a, b) => a - b);
@ -908,8 +937,8 @@ function showRecordingScreen() {
const alt = p.altitude;
if (alt != null && elevEl) elevEl.textContent = Math.round(alt) + " м";
// Pause detection — only if we have enough data and speed is actually low
if (speedBuf.length >= 3 && median < 1) {
// Pause indicator
if (GPS.recording.pauseStart) {
statusEl.textContent = "Пауза";
statusEl.style.color = "#ffb74d";
} else {
@ -918,22 +947,36 @@ function showRecordingScreen() {
}
};
// Timer
// Timer — excludes pauses, starts only when recording
const timer = setInterval(() => {
if (!GPS.recording.active) { clearInterval(timer); return; }
const sec = Math.round((Date.now() - GPS.recording.startTime.getTime()) / 1000);
if (!GPS.recording.startTime) return; // not started yet
const totalMs = Date.now() - GPS.recording.startTime.getTime();
const activeMs = totalMs - GPS.recording.totalPauseMs -
(GPS.recording.pauseStart ? Date.now() - GPS.recording.pauseStart.getTime() : 0);
const sec = Math.max(0, Math.round(activeMs / 1000));
if (timeEl) timeEl.textContent = GPS.formatDuration(sec);
}, 1000);
// Pause button
container.querySelector("#rec-pause").onclick = () => {
if (GPS.recording.manualPaused) {
GPS.resumeRecording();
container.querySelector("#rec-pause").textContent = "Пауза";
statusEl.textContent = "";
statusEl.style.color = "";
} else {
GPS.pauseRecording();
container.querySelector("#rec-pause").textContent = "Продолжить";
statusEl.textContent = "Пауза (ручная)";
statusEl.style.color = "#ffb74d";
}
};
// Stop button
document.querySelector("#rec-stop").onclick = async () => {
const result = await GPS.stopRecording();
container.querySelector("#rec-stop").onclick = async () => {
await GPS.stopRecording();
clearInterval(timer);
// Ask for name
// TODO: custom naming modal
// Close modal
document.querySelector(".modal-overlay")?.remove();
history.back();
renderScreen("gps");
};
@ -1640,6 +1683,31 @@ async function renderSettings(container) {
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">GPS (запись треков)</div>
<div class="settings-item">
<div class="settings-item-left">
<div class="settings-item-title">Мин. скорость (км/ч)</div>
<div class="settings-item-desc">Ниже = пауза</div>
</div>
<input id="s-min-speed" type="number" step="0.1" value="${(GPS.config.minSpeedMs * 3.6).toFixed(1)}" style="width:60px;text-align:right;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg);padding:4px 6px;">
</div>
<div class="settings-item">
<div class="settings-item-left">
<div class="settings-item-title">Макс. точность (м)</div>
<div class="settings-item-desc">Фиксы точнее этого = пропуск</div>
</div>
<input id="s-min-acc" type="number" step="1" value="${GPS.config.minAccuracyM}" style="width:60px;text-align:right;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg);padding:4px 6px;">
</div>
<div class="settings-item">
<div class="settings-item-left">
<div class="settings-item-title">Мин. расстояние (м)</div>
<div class="settings-item-desc">Точки ближе = пропуск</div>
</div>
<input id="s-min-dist" type="number" step="0.5" value="${GPS.config.minDistanceM}" style="width:60px;text-align:right;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg);padding:4px 6px;">
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Разрешения</div>
<div class="settings-item">
@ -1673,6 +1741,18 @@ async function renderSettings(container) {
offToggle.classList.toggle("on", newVal);
};
// GPS config inputs
const gpsInputs = ["s-min-speed", "s-min-acc", "s-min-dist"];
for (const id of gpsInputs) {
const el = container.querySelector("#" + id);
if (el) el.onchange = () => {
GPS.config.minSpeedMs = parseFloat(container.querySelector("#s-min-speed").value) / 3.6;
GPS.config.minAccuracyM = parseFloat(container.querySelector("#s-min-acc").value);
GPS.config.minDistanceM = parseFloat(container.querySelector("#s-min-dist").value);
GPS.saveConfig();
};
}
// GPS permission check — works on both desktop and mobile
const gpsStatus = container.querySelector("#s-gps-status");
const gpsToggle = container.querySelector("#s-gps-toggle");

View file

@ -1,6 +1,6 @@
[package]
name = "baldav"
version = "0.1.28"
version = "0.1.39"
edition = "2021"
[lib]

View file

@ -20,6 +20,28 @@ fn get_app_info() -> AppInfo {
}
}
// 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();
let cmd_path = data_dir.join("gps_command.txt");
std::fs::write(&cmd_path, &cmd).map_err(|e| e.to_string())?;
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())?;
let pts_path = data_dir.join("gps_live_points.jsonl");
if pts_path.exists() {
std::fs::read_to_string(&pts_path).map_err(|e| e.to_string())
} else {
Ok(String::new())
}
}
#[tauri::command]
fn import_seed_products(app: tauri::AppHandle) -> Result<String, String> {
// Find seed DB — try multiple locations
@ -356,7 +378,7 @@ pub fn run() {
.add_migrations("sqlite:baldav.db", migrations)
.build(),
)
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, get_app_info])
.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");
}

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.28",
"version": "0.1.39",
"identifier": "dev.baldav.app",
"build": {
"frontendDist": "../frontend"