Activity types (squats/jumprope/stretch/swim), road map, tile import, fixes
Activity types:
- New types: squats, jump rope, stretching, swimming pool
- MET-based kcal auto-calculation (6.0/12.3/2.3/8.0)
- Dynamic modal fields per type (sets/reps, distance, minutes)
- Fix: state.latestWeight is object {weight_kg}, not number
Road map:
- road_points table with 2m dedup, activity type (bike/walk)
- Rust rebuild_roads + get_road_points (bypass SQL plugin 28K limit)
- Segment detection by 100m gap, bike=blue walk=brown
Tile system:
- CSP fix: a.tile.openstreetmap.org matches *.tile pattern
- pack_tiles.js tool, import from file/router, ureq body limit bypass
- Tile errors: done(null,tile) prevents crash cascade
Other fixes:
- Manual routes: skip gap segmentation (synthetic timestamps)
- Draw route: own CSS class, not affected by landscape rotation
- Orientation lock button (A/L), hysteresis 65/35 degrees
- GPS placeholder only when state=waiting_gps
- Food amount inputmode=text for formula support (4*30)
- showModal onMount callback
This commit is contained in:
parent
6b250c0c5d
commit
4ed6e6f039
6 changed files with 145 additions and 45 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<span class="log-kcal" style="color:var(--primary)">${s.avg_speed_kmh ? s.avg_speed_kmh.toFixed(1) + " км/ч" : ""}</span>
|
||||
</li>`;
|
||||
}).join("")}
|
||||
${activities.map(a =>
|
||||
`<li class="log-item">
|
||||
${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 `<li class="log-item">
|
||||
<span class="log-time">${a.time ?? ""}</span>
|
||||
<span class="log-desc">${esc(a.desc ?? "—")}
|
||||
${a.minutes ? `<small>${a.minutes} мин</small>` : ""}</span>
|
||||
<span class="log-kcal">${a.kcal != null ? "−" + Math.round(a.kcal) + " кк" : ""}</span>
|
||||
<span class="log-desc">${at !== "manual" ? name : ""}${detail ? ` <small>${detail}</small>` : ""}</span>
|
||||
<span class="log-kcal">${a.kcal && !isNaN(a.kcal) ? "−" + Math.round(a.kcal) + " кк" : ""}</span>
|
||||
<button class="del-btn" data-table="activity" data-id="${a.id}">✕</button>
|
||||
</li>`
|
||||
).join("")}
|
||||
</li>`;
|
||||
}).join("")}
|
||||
${!gpsSessions.length && !activities.length ? `<li class="log-empty">нет данных</li>` : ""}
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -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() {
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label id="m-amount-label">Количество (г)</label>
|
||||
<input id="m-amount" type="text" inputmode="numeric" placeholder="100 или 4*30" value="100" />
|
||||
<input id="m-amount" type="text" inputmode="text" placeholder="100 или 4*30" value="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Время</label>
|
||||
|
|
@ -1194,7 +1208,7 @@ function showEditFoodModal(entry) {
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label id="m-amount-label">Количество (${(entry.water_pct && entry.water_pct > 80) ? "мл" : "г"})</label>
|
||||
<input id="m-amount" type="text" inputmode="numeric" value="${entry.amount_g || 100}" />
|
||||
<input id="m-amount" type="text" inputmode="text" value="${entry.amount_g || 100}" />
|
||||
</div>
|
||||
${!entry.product_id && !entry.recipe_id ? `
|
||||
<div class="form-group" id="m-kcal-group">
|
||||
|
|
@ -1270,31 +1284,89 @@ function showEditFoodModal(entry) {
|
|||
}
|
||||
|
||||
function showActivityModal() {
|
||||
const types = Object.entries(ACTIVITY_NAMES);
|
||||
showModal(`
|
||||
<h3>Активность</h3>
|
||||
<div class="form-group">
|
||||
<label>Тип</label>
|
||||
<select id="m-atype" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg)">
|
||||
${types.map(([k,v]) => `<option value="${k}">${v}</option>`).join("")}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="m-desc-row">
|
||||
<label>Описание</label>
|
||||
<input id="m-desc" type="text" placeholder="прогулка" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Минуты</label>
|
||||
<input id="m-min" type="number" inputmode="numeric" placeholder="40" />
|
||||
<div class="form-group" id="m-sets-row" style="display:none">
|
||||
<label>Подходы x Повторения</label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input id="m-sets" type="number" inputmode="numeric" placeholder="4" style="width:50%" />
|
||||
<input id="m-reps" type="number" inputmode="numeric" placeholder="15" style="width:50%" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="m-dist-row" style="display:none">
|
||||
<label>Дистанция (м)</label>
|
||||
<input id="m-dist" type="number" inputmode="numeric" placeholder="800" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ккал расход (примерно)</label>
|
||||
<input id="m-kcal" type="number" inputmode="numeric" placeholder="150" />
|
||||
<label>Минуты</label>
|
||||
<input id="m-min" type="number" inputmode="numeric" placeholder="20" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ккал расход</label>
|
||||
<input id="m-kcal" type="number" inputmode="numeric" placeholder="авто" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Время</label>
|
||||
<input id="m-time" type="time" value="${nowTime()}" />
|
||||
</div>`,
|
||||
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) {
|
|||
<h3>Добавить в дневник</h3>
|
||||
<div class="form-group">
|
||||
<label>Количество (г)</label>
|
||||
<input id="m-amount" type="text" inputmode="numeric" placeholder="100 или 4*30" value="100" />
|
||||
<input id="m-amount" type="text" inputmode="text" placeholder="100 или 4*30" value="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Время</label>
|
||||
|
|
@ -2577,8 +2649,8 @@ function showDrawRouteScreen(container) {
|
|||
let polyline = null;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rec-screen">
|
||||
<div id="draw-map" class="rec-map"></div>
|
||||
<div class="draw-screen">
|
||||
<div id="draw-map" class="draw-map"></div>
|
||||
<div class="rec-stats">
|
||||
<div class="rec-stat"><span id="draw-dist" class="rec-val">0 м</span><span class="rec-lbl">Дистанция</span></div>
|
||||
<div class="rec-stat"><span id="draw-pts" class="rec-val">0</span><span class="rec-lbl">Точек</span></div>
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
|
|
@ -139,7 +139,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||
|
||||
[[package]]
|
||||
name = "baldav"
|
||||
version = "0.1.279"
|
||||
version = "0.1.289"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"btleplug",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "baldav"
|
||||
version = "0.1.279"
|
||||
version = "0.1.289"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue