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; }