diff --git a/frontend/js/gps.js b/frontend/js/gps.js
index 34d29b5..e21d4bd 100644
--- a/frontend/js/gps.js
+++ b/frontend/js/gps.js
@@ -23,6 +23,7 @@ GPS.config = {
useFusedLocation: true, // fused-провайдер (Google Play services) — как Яндекс/Google Maps.
// raw GPS_PROVIDER на устройстве приложению не отдаёт фиксы; fused смешивает GPS+ГЛОНАСС+сеть.
autoFinishIdleMin: 15, // авто-завершить тренировку после N мин без движения (забыл «Конец»)
+ trackColor: '#bb86fc', // цвет линии текущего/записанного трека (по умолчанию фиолетовый темы)
roadColorWalk: '#8B5E3C', // цвет наложения проезженных/пройденных дорог: ходьба
roadColorBike: '#2266aa', // ... вело
roadColorRun: '#e67e22', // ... бег
diff --git a/frontend/js/main.js b/frontend/js/main.js
index 25b96e1..3e7e029 100644
--- a/frontend/js/main.js
+++ b/frontend/js/main.js
@@ -180,7 +180,7 @@ async function init() {
let result = await window.__TAURI__.core.invoke("import_seed_products");
if (unlisten) unlisten();
- } catch (e) { console.error("[SEED]", e); alert("Seed error: " + e); }
+ } catch (e) { console.error("[SEED]", e); appAlert("Seed error: " + e); }
// Apply safe area insets (Android status bar / nav bar)
try {
@@ -1028,7 +1028,7 @@ function showModal(html, onSubmit, onMount) {
if (ok !== false) { closeModal(); if (ok !== "norender") renderScreen(state.screen); }
} catch (e) {
console.error("[showModal] onSubmit error:", e);
- alert("Ошибка: " + e);
+ appAlert("Ошибка: " + e);
}
};
if (onMount) onMount(overlay);
@@ -1103,6 +1103,36 @@ function appPrompt(message, defaultValue = "", okText = "OK") {
});
}
+// In-app info dialog (replaces native alert()). One OK button. Returns Promise.
+function appAlert(message, okText = "OK") {
+ return new Promise(resolve => {
+ let ov = document.createElement("div");
+ ov.className = "modal-overlay confirm-overlay";
+ ov.innerHTML = `
+
+
${message}
+
+
+
+
`;
+ document.body.appendChild(ov);
+ let settled = false;
+ let base = winStack.length;
+ winPush("alert", () => { ov.remove(); if (!settled) { settled = true; resolve(); } });
+ let finish = () => {
+ if (settled) return;
+ settled = true;
+ let count = winStack.length - base;
+ for (let i = 0; i < count; i++) winStack.pop().close();
+ _skipPops = count;
+ if (count > 0) history.go(-count);
+ ov.remove();
+ resolve();
+ };
+ ov.querySelector("#cf-ok").onclick = () => finish();
+ });
+}
+
// Close the currently-open track-detail modal (plus its fullscreen-map child, if any),
// balance the history stack, then reopen it fresh. Mirrors the recalc/refine pattern
// but pops down to the detail modal so it also works while the map is fullscreen —
@@ -1650,7 +1680,7 @@ async function renderProducts(container) {
let pid = parseInt(btn.dataset.pid);
let refs = await DB.getProductRefs(pid);
if (refs.food > 0 || refs.recipes > 0) {
- alert(`Нельзя удалить: продукт используется в ${refs.food ? refs.food + " записях еды" : ""}${refs.food && refs.recipes ? " и " : ""}${refs.recipes ? refs.recipes + " рецептах" : ""}`);
+ await appAlert(`Нельзя удалить: продукт используется в ${refs.food ? refs.food + " записях еды" : ""}${refs.food && refs.recipes ? " и " : ""}${refs.recipes ? refs.recipes + " рецептах" : ""}`);
return;
}
await DB.deleteProduct(pid);
@@ -3238,7 +3268,7 @@ async function showRouteEditor(container, routeId) {
0Точек
- тап карты — точка · тащи — двигать · тап точки — удалить
+ тап карты — точка · тап линии — вставить · тащи — двигать · тап точки — удалить
@@ -3268,11 +3298,17 @@ async function showRouteEditor(container, routeId) {
function updatePolyline() {
let latlngs = points.map(p => [p.lat, p.lon]);
if (polyline) map.removeLayer(polyline);
- if (latlngs.length >= 2) polyline = L.polyline(latlngs, { color, weight: 4 }).addTo(map);
+ if (latlngs.length >= 2) {
+ polyline = L.polyline(latlngs, { color, weight: 4, interactive: true }).addTo(map);
+ // Tap the LINE to insert a point at that spot — a distinct target from
+ // markers/empty-map, so it doesn't clash with add-at-end / drag / delete.
+ polyline.on("click", (e) => { L.DomEvent.stop(e); insertAt(e.latlng); });
+ }
}
- // Draggable + deletable point. Markers stay parallel to `points` (same index),
- // so drag updates points[idx] and click (delete) splices both.
- function addMarker(p) {
+ // Draggable + deletable point marker. Markers stay parallel to `points` (same
+ // index) — drag updates points[idx], click (delete) splices both. Returns the
+ // marker; caller pushes (end) or splices (mid-insert) into `markers`.
+ function makeMarker(p) {
let m = L.marker([p.lat, p.lon], { draggable: true, icon: dotIcon });
m.on("drag", () => {
let idx = markers.indexOf(m);
@@ -3287,11 +3323,31 @@ async function showRouteEditor(container, routeId) {
updatePolyline(); updateStats();
});
m.addTo(map);
- markers.push(m);
+ return m;
+ }
+ // Insert a point where the line was tapped: pick the nearest segment [i,i+1]
+ // (planar lat/lon distance — fine at city scale) and splice at i+1.
+ function insertAt(latlng) {
+ if (points.length < 2) return;
+ let best = -1, bestD = Infinity;
+ for (let i = 0; i < points.length - 1; i++) {
+ let a = points[i], b = points[i + 1];
+ let dx = b.lon - a.lon, dy = b.lat - a.lat;
+ let t = (dx || dy) ? ((latlng.lng - a.lon) * dx + (latlng.lat - a.lat) * dy) / (dx * dx + dy * dy) : 0;
+ t = Math.max(0, Math.min(1, t));
+ let cx = a.lon + t * dx, cy = a.lat + t * dy;
+ let d = Math.hypot(latlng.lng - cx, latlng.lat - cy);
+ if (d < bestD) { bestD = d; best = i; }
+ }
+ if (best < 0) return;
+ let np = { lat: latlng.lat, lon: latlng.lng };
+ points.splice(best + 1, 0, np);
+ markers.splice(best + 1, 0, makeMarker(np));
+ updatePolyline(); updateStats();
}
if (points.length) {
- points.forEach(p => addMarker(p));
+ points.forEach(p => markers.push(makeMarker(p)));
updatePolyline(); updateStats();
map.fitBounds(L.latLngBounds(points.map(p => [p.lat, p.lon])).pad(0.1));
} else if (navigator.geolocation) {
@@ -3301,7 +3357,7 @@ async function showRouteEditor(container, routeId) {
map.on("click", e => {
let p = { lat: e.latlng.lat, lon: e.latlng.lng };
- points.push(p); addMarker(p);
+ points.push(p); markers.push(makeMarker(p));
updatePolyline(); updateStats();
});
@@ -3389,7 +3445,7 @@ function showDrawRouteScreen(container) {
let latlngs = points.map(p => [p.lat, p.lon]);
if (polyline) map.removeLayer(polyline);
if (latlngs.length >= 2) {
- polyline = L.polyline(latlngs, { color: "#bb86fc", weight: 4 }).addTo(map);
+ polyline = L.polyline(latlngs, { color: GPS.config.trackColor || "#bb86fc", weight: 4 }).addTo(map);
}
}
@@ -3398,7 +3454,7 @@ function showDrawRouteScreen(container) {
let { lat, lng } = e.latlng;
points.push({ lat, lon: lng });
let marker = L.circleMarker([lat, lng], {
- radius: 6, color: "#bb86fc", fillColor: "#bb86fc", fillOpacity: 1, weight: 2
+ radius: 6, color: GPS.config.trackColor || "#bb86fc", fillColor: GPS.config.trackColor || "#bb86fc", fillOpacity: 1, weight: 2
}).addTo(map);
markers.push(marker);
updatePolyline();
@@ -3550,7 +3606,7 @@ async function renderRecordingInline(container) {
setTimeout(() => map.invalidateSize(), 200);
let borderLine = L.polyline([], { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
- let trailLine = L.polyline([], { color: "#bb86fc", weight: 5 }).addTo(map);
+ let trailLine = L.polyline([], { color: GPS.config.trackColor || "#bb86fc", weight: 5 }).addTo(map);
// Map orientation: a mode button picks the source — magnetic compass or GPS course.
// Manual 90° steps (↑) rotate the whole screen on top. Two on-map arrows always show
@@ -3786,14 +3842,18 @@ async function renderRecordingInline(container) {
smoothedHeading = (smoothedHeading + diff * 0.3 + 360) % 360;
}
// DEBUG overlay: raw sensor values on screen (to verify calibration).
- let dbg = document.getElementById("orient-dbg");
- if (!dbg) {
- dbg = document.createElement("div");
- dbg.id = "orient-dbg";
- dbg.style.cssText = "position:absolute;bottom:4px;left:4px;z-index:1002;background:rgba(0,0,0,0.85);color:#0f0;font:11px monospace;padding:3px 5px;border-radius:4px;pointer-events:none;white-space:nowrap;";
- mapEl.parentElement.appendChild(dbg);
+ // Only in debug builds — window.__BALDAV_DEBUG is set in init() from
+ // get_app_info().debug (cfg!(debug_assertions)); silent in release.
+ if (window.__BALDAV_DEBUG) {
+ let dbg = document.getElementById("orient-dbg");
+ if (!dbg) {
+ dbg = document.createElement("div");
+ dbg.id = "orient-dbg";
+ dbg.style.cssText = "position:absolute;bottom:4px;left:4px;z-index:1002;background:rgba(0,0,0,0.85);color:#0f0;font:11px monospace;padding:3px 5px;border-radius:4px;pointer-events:none;white-space:nowrap;";
+ mapEl.parentElement.appendChild(dbg);
+ }
+ dbg.textContent = "a=" + Math.round(e.alpha || 0) + " raw=" + Math.round(raw) + " off=" + Math.round(northOffset) + " h=" + Math.round(heading) + " sm=" + Math.round(smoothedHeading);
}
- dbg.textContent = "a=" + Math.round(e.alpha || 0) + " raw=" + Math.round(raw) + " off=" + Math.round(northOffset) + " h=" + Math.round(heading) + " sm=" + Math.round(smoothedHeading);
updateArrows(); // keep the orange compass arrow live even in GPS mode
if (!compassFrozen) applyBearing();
}
@@ -4534,7 +4594,7 @@ async function showSessionDetail(sessionId) {
segments.forEach(seg => {
if (seg.length < 2) { allCoords.push(...seg); return; }
L.polyline(seg, { color: "#ffffff", weight: 8, opacity: 0.8 }).addTo(map);
- L.polyline(seg, { color: "#bb86fc", weight: 5 }).addTo(map);
+ L.polyline(seg, { color: GPS.config.trackColor || "#bb86fc", weight: 5 }).addTo(map);
L.circleMarker(seg[0], { radius: 5, color: "#4caf50", fillColor: "#4caf50", fillOpacity: 1 }).addTo(map);
L.circleMarker(seg[seg.length - 1], { radius: 5, color: "#f44336", fillColor: "#f44336", fillOpacity: 1 }).addTo(map);
allCoords.push(...seg);
@@ -4542,7 +4602,7 @@ async function showSessionDetail(sessionId) {
// Draw dashed lines for accel-confirmed gaps (GPS lost signal but person was moving)
for (let gl of gapLines) {
- L.polyline(gl, { color: "#bb86fc", weight: 3, opacity: 0.5, dashArray: "8, 8" }).addTo(map);
+ L.polyline(gl, { color: GPS.config.trackColor || "#bb86fc", weight: 3, opacity: 0.5, dashArray: "8, 8" }).addTo(map);
}
// Invisible but tappable hit-targets on top of track
@@ -5725,6 +5785,10 @@ async function renderSettings(container) {
+
+
Цвет текущего трека
Линия записи/трека (чтобы не путать с дорогами)
+
+
@@ -5910,7 +5974,7 @@ async function renderSettings(container) {
btn.textContent = prev;
if (!filePath) return; // cancelled
} else {
- filePath = prompt("Путь к файлу тайлов (.db):");
+ filePath = await appPrompt("Путь к файлу тайлов (.db):", "");
if (!filePath) return;
}
let origText = btn.textContent;
@@ -6003,13 +6067,15 @@ async function renderSettings(container) {
GPS.config.roadColorBike = container.querySelector("#s-road-color-bike").value;
GPS.config.roadColorWalk = container.querySelector("#s-road-color-walk").value;
GPS.config.roadColorRun = container.querySelector("#s-road-color-run").value;
+ let trackColorEl = container.querySelector("#s-track-color");
+ if (trackColorEl) GPS.config.trackColor = trackColorEl.value;
let fusedEl = container.querySelector("#s-use-fused");
if (fusedEl) GPS.config.useFusedLocation = fusedEl.checked;
let gpxzEl = container.querySelector("#s-gpxz-key");
if (gpxzEl) GPS.config.gpxzKey = gpxzEl.value.trim();
GPS.saveConfig();
};
- let gpsInputs = ["s-min-speed", "s-min-acc", "s-min-dist", "s-max-speed", "s-gap-threshold", "s-air-drag", "s-air-density", "s-pedal-eff", "s-alt-smooth", "s-walk-max-speed", "s-walk-min-speed", "s-road-color-bike", "s-road-color-walk", "s-road-color-run", "s-gpxz-key"];
+ let gpsInputs = ["s-min-speed", "s-min-acc", "s-min-dist", "s-max-speed", "s-gap-threshold", "s-air-drag", "s-air-density", "s-pedal-eff", "s-alt-smooth", "s-walk-max-speed", "s-walk-min-speed", "s-track-color", "s-road-color-bike", "s-road-color-walk", "s-road-color-run", "s-gpxz-key"];
for (let id of gpsInputs) {
let el = container.querySelector("#" + id);
if (el) { el.onchange = readGpsConfig; el.oninput = readGpsConfig; }