diff --git a/frontend/css/styles.css b/frontend/css/styles.css index af0ad1a..cfe0fd7 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1062,6 +1062,15 @@ select { .rec-panel { flex-shrink: 0; } +.draw-screen { + display: flex; + flex-direction: column; + height: 100%; +} +.draw-map { + flex: 1; + min-height: 200px; +} .rec-map-wrap { flex: 1; diff --git a/frontend/js/db.js b/frontend/js/db.js index f95ecdc..e30e488 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -476,11 +476,19 @@ async function getActivitiesForDate(date) { return select("SELECT * FROM activity_log WHERE date = ?1 ORDER BY time ASC", [date]); } -async function addActivity(date, time, type, desc, minutes, kcal) { - await execute( - "INSERT INTO activity_log (date, time, type, desc, minutes, kcal) VALUES (?1,?2,?3,?4,?5,?6)", - [date, time, type ?? "manual", desc, minutes ?? null, kcal ?? null] - ); +async function addActivity(date, time, type, desc, minutes, kcal, opts) { + // Build column list dynamically — new columns may not exist yet on first run + const cols = ["date", "time", "type", "desc", "minutes", "kcal"]; + const vals = [date, time, type ?? "manual", desc, minutes ?? null, kcal ?? null]; + if (opts) { + const actCols = (await select("PRAGMA table_info(activity_log)")).map(c => c.name); + if (actCols.includes("activity_type")) { cols.push("activity_type"); vals.push(opts.activity_type ?? "manual"); } + if (actCols.includes("sets") && opts.sets) { cols.push("sets"); vals.push(opts.sets); } + if (actCols.includes("reps") && opts.reps) { cols.push("reps"); vals.push(opts.reps); } + if (actCols.includes("distance_m") && opts.distance_m) { cols.push("distance_m"); vals.push(opts.distance_m); } + } + const placeholders = cols.map((_, i) => `?${i + 1}`).join(","); + await execute(`INSERT INTO activity_log (${cols.join(",")}) VALUES (${placeholders})`, vals); } async function deleteActivity(id) { @@ -1544,6 +1552,16 @@ async function runMigrations() { // Spatial index for fast 2m proximity checks await execute(`CREATE INDEX IF NOT EXISTS idx_road_lat_lon ON road_points(lat, lon)`); + // Activity types: extend activity_log with structured fields + const actCols = (await select("PRAGMA table_info(activity_log)")).map(c => c.name); + if (!actCols.includes("activity_type")) { + await execute("ALTER TABLE activity_log ADD COLUMN activity_type TEXT"); + await execute("UPDATE activity_log SET activity_type = 'manual' WHERE activity_type IS NULL"); + } + if (!actCols.includes("sets")) await execute("ALTER TABLE activity_log ADD COLUMN sets INTEGER"); + if (!actCols.includes("reps")) await execute("ALTER TABLE activity_log ADD COLUMN reps INTEGER"); + if (!actCols.includes("distance_m")) await execute("ALTER TABLE activity_log ADD COLUMN distance_m REAL"); + // Water content column for hydration tracking if (!productCols.includes("water_pct")) { await execute("ALTER TABLE products ADD COLUMN water_pct REAL"); diff --git a/frontend/js/main.js b/frontend/js/main.js index 8757f4e..9b634cb 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -9,6 +9,13 @@ const app = document.getElementById("app"); const UPDATE_BASE_URL = "https://4aiman.top/baldav"; +const ACTIVITY_MET = { squats: 6.0, jumprope: 12.3, stretch: 2.3, swim: 8.0, manual: 3.0 }; +const ACTIVITY_NAMES = { squats: "Приседания", jumprope: "Скакалка", stretch: "Растяжки", swim: "Бассейн", manual: "Другое" }; +function activityKcal(type, minutes, weightKg) { + const met = ACTIVITY_MET[type] || 3.0; + return Math.round(met * 3.5 * weightKg * minutes / 200); +} + let state = { screen: "today", date: todayIso(), @@ -393,15 +400,21 @@ async function renderToday(container) { ${s.avg_speed_kmh ? s.avg_speed_kmh.toFixed(1) + " км/ч" : ""} `; }).join("")} - ${activities.map(a => - `
  • + ${activities.map(a => { + const at = a.activity_type || "manual"; + const name = ACTIVITY_NAMES[at] || a.desc || "—"; + let detail = ""; + if (at === "squats" && a.sets && a.reps) detail = `${a.sets}x${a.reps}, `; + if (at === "swim" && a.distance_m) detail = `${a.distance_m} м, `; + if (a.minutes) detail += `${a.minutes} мин`; + if (at === "manual" && a.desc) detail = esc(a.desc) + (a.minutes ? `, ${a.minutes} мин` : ""); + return `
  • ${a.time ?? ""} - ${esc(a.desc ?? "—")} - ${a.minutes ? `${a.minutes} мин` : ""} - ${a.kcal != null ? "−" + Math.round(a.kcal) + " кк" : ""} + ${at !== "manual" ? name : ""}${detail ? ` ${detail}` : ""} + ${a.kcal && !isNaN(a.kcal) ? "−" + Math.round(a.kcal) + " кк" : ""} -
  • ` - ).join("")} + `; + }).join("")} ${!gpsSessions.length && !activities.length ? `
  • нет данных
  • ` : ""} @@ -966,7 +979,7 @@ async function renderStatsCharts(container, days, foodTotals, bmr) { // Modals // --------------------------------------------------------------------------- -function showModal(html, onSubmit) { +function showModal(html, onSubmit, onMount) { const overlay = document.createElement("div"); overlay.className = "modal-overlay"; overlay.innerHTML = ` @@ -1005,6 +1018,7 @@ function showModal(html, onSubmit) { alert("Ошибка: " + e); } }; + if (onMount) onMount(overlay); } function showWeightModal() { @@ -1062,7 +1076,7 @@ function showFoodModal() {
    - +
    @@ -1194,7 +1208,7 @@ function showEditFoodModal(entry) {
    - +
    ${!entry.product_id && !entry.recipe_id ? `
    @@ -1270,31 +1284,89 @@ function showEditFoodModal(entry) { } function showActivityModal() { + const types = Object.entries(ACTIVITY_NAMES); showModal(`

    Активность

    + + +
    +
    -
    - - + +
    - - + + +
    +
    + +
    `, async overlay => { + const atype = overlay.querySelector("#m-atype").value; const desc = overlay.querySelector("#m-desc").value.trim(); const min = parseInt(overlay.querySelector("#m-min").value) || null; - const kcal = parseFloat(overlay.querySelector("#m-kcal").value) || null; + const sets = parseInt(overlay.querySelector("#m-sets").value) || null; + const reps = parseInt(overlay.querySelector("#m-reps").value) || null; + const dist = parseFloat(overlay.querySelector("#m-dist").value) || null; const time = overlay.querySelector("#m-time").value; - if (!desc) return false; - await DB.addActivity(state.date, time, "manual", desc, min, kcal); + let kcalRaw = overlay.querySelector("#m-kcal").value.trim(); + let kcal = kcalRaw ? parseFloat(kcalRaw) : null; + if (kcal === null || isNaN(kcal)) { + if (min && atype !== "manual") { + kcal = activityKcal(atype, min, parseFloat(state.latestWeight?.weight_kg) || 75); + } else { + kcal = null; + } + } + const label = atype === "manual" ? desc : ACTIVITY_NAMES[atype]; + if (!label) return false; + await DB.addActivity(state.date, time, "manual", label, min, kcal, + { activity_type: atype, sets, reps, distance_m: dist }); + }, overlay => { + // Dynamic field visibility based on type + const sel = overlay.querySelector("#m-atype"); + const update = () => { + const t = sel.value; + overlay.querySelector("#m-desc-row").style.display = t === "manual" ? "" : "none"; + overlay.querySelector("#m-sets-row").style.display = t === "squats" ? "" : "none"; + overlay.querySelector("#m-dist-row").style.display = t === "swim" ? "" : "none"; + // Auto-calc kcal when minutes change + const minEl = overlay.querySelector("#m-min"); + const kcalEl = overlay.querySelector("#m-kcal"); + if (t !== "manual" && minEl.value && !kcalEl.value) { + const w = parseFloat(state.latestWeight?.weight_kg) || 75; + kcalEl.placeholder = activityKcal(t, parseInt(minEl.value) || 0, w) || "авто"; + } + }; + sel.onchange = update; + overlay.querySelector("#m-min").oninput = () => { + const t = sel.value; + if (t !== "manual") { + const w = parseFloat(state.latestWeight?.weight_kg) || 75; + const m = parseInt(overlay.querySelector("#m-min").value) || 0; + overlay.querySelector("#m-kcal").placeholder = m ? activityKcal(t, m, w) : "авто"; + } + }; + update(); }); } @@ -1494,7 +1566,7 @@ async function renderProducts(container) {

    Добавить в дневник

    - +
    @@ -2577,8 +2649,8 @@ function showDrawRouteScreen(container) { let polyline = null; container.innerHTML = ` -
    -
    +
    +
    0 мДистанция
    0Точек
    @@ -3376,29 +3448,30 @@ async function showSessionDetail(sessionId) { const tRange = tN - t0 || 1; // Split into segments by GPS gap threshold - // If accelerometer shows movement on both sides of gap → keep connected (don't break) + // Manual routes: no splitting — points are connected as-is (synthetic timestamps have large gaps) + const isManualRoute = session.source === "manual"; const gapSec = GPS.config.gapThresholdSec ?? 30; const segments = []; // array of arrays of [lat, lon] const gapLines = []; // dashed lines for accel-confirmed gaps let curSeg = [[points[0].lat, points[0].lon]]; for (let i = 1; i < points.length; i++) { - const gap = (new Date(points[i].ts) - new Date(points[i-1].ts)) / 1000; - if (gap > gapSec) { - const accelBefore = points[i-1].accel_rms; - const accelAfter = points[i].accel_rms; - // If both sides show movement → bridge the gap with dashed line - if (accelBefore != null && accelAfter != null && accelBefore > 2 && accelAfter > 2) { - // Keep in same segment — person was moving, GPS just lost signal - curSeg.push([points[i].lat, points[i].lon]); - gapLines.push([[points[i-1].lat, points[i-1].lon], [points[i].lat, points[i].lon]]); - } else { - segments.push(curSeg); - curSeg = []; - curSeg.push([points[i].lat, points[i].lon]); + if (!isManualRoute) { + const gap = (new Date(points[i].ts) - new Date(points[i-1].ts)) / 1000; + if (gap > gapSec) { + const accelBefore = points[i-1].accel_rms; + const accelAfter = points[i].accel_rms; + if (accelBefore != null && accelAfter != null && accelBefore > 2 && accelAfter > 2) { + curSeg.push([points[i].lat, points[i].lon]); + gapLines.push([[points[i-1].lat, points[i-1].lon], [points[i].lat, points[i].lon]]); + } else { + segments.push(curSeg); + curSeg = []; + curSeg.push([points[i].lat, points[i].lon]); + } + continue; } - } else { - curSeg.push([points[i].lat, points[i].lon]); } + curSeg.push([points[i].lat, points[i].lon]); } segments.push(curSeg); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f496aac..348bc7f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -139,7 +139,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "baldav" -version = "0.1.279" +version = "0.1.289" dependencies = [ "base64 0.22.1", "btleplug", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 88f40a9..faa9bc5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "baldav" -version = "0.1.279" +version = "0.1.289" edition = "2021" [lib] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b906abb..4a478eb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json", "productName": "BalDav", - "version": "0.1.279", + "version": "0.1.289", "identifier": "dev.baldav.app", "build": { "frontendDist": "../frontend"