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:
parent
2ca2181e09
commit
f717204ac8
6 changed files with 447 additions and 69 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "baldav"
|
||||
version = "0.1.28"
|
||||
version = "0.1.39"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue