Desktop: synchronous track-detail render + text session ids
showSessionDetail rendered its map/charts/buttons inside setTimeout blocks guarded by renderToken. On WebKitGTK the deferred render was voided, so the track detail showed no map, charts, or fullscreen button (while the synchronously-rendered roads view worked). - Remove all setTimeout/requestAnimationFrame/IIFE from showSessionDetail; render map, charts and wire buttons synchronously right after the modal markup exists (like showRoadsMap). Early `return`s become `if` guards; goals/food-cost keep real `await`. Force layout (offsetHeight) + call invalidateSize so Leaflet reads the real container size in the modal. - Fix large i64 session-id corruption from tauri-plugin-sql 2.4.0 (ids come back off by 1 as JS numbers). Read/pass session ids as text via CAST(id AS TEXT) AS id_str everywhere they are reused: Trek list, Today click, compare modal/view, goal-marker prev sessions, map default-center, and importGpxSession (whose points were orphaned by the corrupted id). SQLite coerces the string to the INTEGER column. Point ids (max ~135k) are small and unaffected.
This commit is contained in:
parent
d4ef9a1f9f
commit
5d6025ea9c
2 changed files with 81 additions and 110 deletions
|
|
@ -721,22 +721,24 @@ async function finishGpsSession(id, endTime, distM, kcal, avgSpd, maxSpd, elevGa
|
|||
);
|
||||
}
|
||||
|
||||
// NOTE: tauri-plugin-sql corrupts large i64 ids when returned as JS numbers
|
||||
// (off by ±1). Always read/pass session ids via the text `id_str` column.
|
||||
async function getGpsSessions(limit) {
|
||||
return select(
|
||||
"SELECT * FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1",
|
||||
"SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1",
|
||||
[limit ?? 50]
|
||||
);
|
||||
}
|
||||
|
||||
async function getGpsSessionsForDate(date) {
|
||||
return select(
|
||||
"SELECT * FROM gps_sessions WHERE date = ?1 ORDER BY start_time ASC",
|
||||
"SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE date = ?1 ORDER BY start_time ASC",
|
||||
[date]
|
||||
);
|
||||
}
|
||||
|
||||
async function getGpsSession(id) {
|
||||
const rows = await select("SELECT * FROM gps_sessions WHERE id = ?1", [id]);
|
||||
const rows = await select("SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE id = ?1", [id]);
|
||||
return rows.length ? rows[0] : null;
|
||||
}
|
||||
|
||||
|
|
@ -760,9 +762,11 @@ async function importGpxSession(session, points) {
|
|||
session.max_speed_kmh, session.elevation_gain_m, session.elevation_loss_m,
|
||||
session.source ?? "gpx_import"]
|
||||
);
|
||||
// Get id of just-inserted session by matching unique fields
|
||||
// Get id of just-inserted session by matching unique fields.
|
||||
// CAST to TEXT — numeric i64 id is corrupted by the SQL plugin; use string
|
||||
// sid for point inserts (SQLite coerces it to the INTEGER session_id column).
|
||||
const rows = await select(
|
||||
"SELECT id FROM gps_sessions WHERE date=?1 AND start_time=?2 AND source=?3 ORDER BY id DESC LIMIT 1",
|
||||
"SELECT CAST(id AS TEXT) AS id FROM gps_sessions WHERE date=?1 AND start_time=?2 AND source=?3 ORDER BY id DESC LIMIT 1",
|
||||
[session.date, session.start_time, session.source ?? "gpx_import"]
|
||||
);
|
||||
const sid = rows[0].id;
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ async function renderToday(container) {
|
|||
let dist = s.total_distance_m ? (s.total_distance_m / 1000).toFixed(1) + " км" : "";
|
||||
let kcal = s.total_kcal ? Math.round(s.total_kcal) + " кк" : "";
|
||||
let dur = s.end_time && s.start_time ? s.start_time + "–" + s.end_time : s.start_time || "";
|
||||
return `<li class="log-item" data-gps-session="${s.id}" style="cursor:pointer">
|
||||
return `<li class="log-item" data-gps-session="${s.id_str ?? s.id}" style="cursor:pointer">
|
||||
<span class="log-time">${dur}</span>
|
||||
<span class="log-desc">${dist}${dist && kcal ? " / " : ""}${kcal}
|
||||
${s.elevation_gain_m ? `<small>+${Math.round(s.elevation_gain_m)}м</small>` : ""}</span>
|
||||
|
|
@ -593,15 +593,7 @@ async function renderToday(container) {
|
|||
|
||||
// GPS session clicks → open track details
|
||||
container.querySelectorAll("[data-gps-session]").forEach(el => {
|
||||
el.onclick = () => {
|
||||
let sid = el.dataset.gpsSession;
|
||||
renderScreen("recording");
|
||||
// After rendering, trigger session detail view
|
||||
setTimeout(() => {
|
||||
let sessionEl = document.querySelector(`[data-session-id="${sid}"]`);
|
||||
if (sessionEl) sessionEl.click();
|
||||
}, 300);
|
||||
};
|
||||
el.onclick = () => showSessionDetail(el.dataset.gpsSession);
|
||||
});
|
||||
|
||||
// Stats charts section
|
||||
|
|
@ -2375,7 +2367,7 @@ async function renderGpsScreen(container) {
|
|||
<div class="gps-sessions">
|
||||
<h3>История</h3>
|
||||
${sessions.length ? sessions.map(s => `
|
||||
<div class="gps-session-item" data-sid="${s.id}">
|
||||
<div class="gps-session-item" data-sid="${s.id_str ?? s.id}">
|
||||
<div class="gps-session-head">
|
||||
<span>${s.date} ${s.start_time ?? ""}</span>
|
||||
<span class="gps-session-type">${s.activity_type ?? ""}</span>
|
||||
|
|
@ -2423,12 +2415,10 @@ async function renderGpsScreen(container) {
|
|||
}
|
||||
};
|
||||
|
||||
// Session click → detail view
|
||||
// Session click → detail view. Keep sid as a STRING (numeric id from the
|
||||
// SQL plugin is corrupted off-by-1 for large i64 values).
|
||||
container.querySelectorAll(".gps-session-item").forEach(item => {
|
||||
item.onclick = () => {
|
||||
let sid = parseInt(item.dataset.sid);
|
||||
showSessionDetail(sid);
|
||||
};
|
||||
item.onclick = () => showSessionDetail(item.dataset.sid);
|
||||
});
|
||||
|
||||
// Three-dot menu
|
||||
|
|
@ -2583,7 +2573,7 @@ async function showAddGoalMarkerModal() {
|
|||
// Try to get last GPS point from recent sessions
|
||||
DB.getGpsSessions(1).then(async sessions => {
|
||||
if (sessions.length > 0) {
|
||||
let pts = await DB.getGpsPoints(sessions[0].id);
|
||||
let pts = await DB.getGpsPoints(sessions[0].id_str ?? sessions[0].id);
|
||||
if (pts.length > 0) {
|
||||
let last = pts[pts.length - 1];
|
||||
defaultLat = last.lat;
|
||||
|
|
@ -3538,12 +3528,12 @@ async function showSessionDetail(sessionId) {
|
|||
<button id="sd-delete" class="btn-secondary full-width" style="margin-top:8px;color:#cf6679">Удалить тренировку</button>
|
||||
`, () => {});
|
||||
|
||||
// Render map
|
||||
setTimeout(() => {
|
||||
if (renderToken !== _sessionDetailRender) return;
|
||||
let mapEl = document.querySelector("#sd-map");
|
||||
if (!mapEl || !points.length) return;
|
||||
if (renderToken !== _sessionDetailRender) return;
|
||||
|
||||
// Render map
|
||||
let mapEl = document.querySelector("#sd-map");
|
||||
if (mapEl && points.length) {
|
||||
void mapEl.offsetHeight; // force layout so Leaflet reads the real container size
|
||||
L.Icon.Default.imagePath = "lib/";
|
||||
let map = L.map(mapEl, { attributionControl: false, zoomControl: false, scrollWheelZoom: false, dragging: false, tap: false, touchZoom: true });
|
||||
mapEl._leafletMap = map; // save ref for winStack collapse
|
||||
|
|
@ -3621,7 +3611,8 @@ async function showSessionDetail(sessionId) {
|
|||
}).bindPopup(info, { closeButton: false, className: "point-popup" }).addTo(map);
|
||||
}
|
||||
|
||||
// Fit map to all points
|
||||
// Ensure Leaflet has correct container size, then fit
|
||||
map.invalidateSize();
|
||||
if (allCoords.length > 1) {
|
||||
map.fitBounds(L.latLngBounds(allCoords).pad(0.1));
|
||||
}
|
||||
|
|
@ -3639,7 +3630,7 @@ async function showSessionDetail(sessionId) {
|
|||
fsDiv.innerHTML = "✕";
|
||||
map.dragging.enable();
|
||||
map.scrollWheelZoom.enable();
|
||||
setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100);
|
||||
map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05));
|
||||
// Push fullscreen map onto winStack
|
||||
let parentOverlay = mapEl.closest(".modal-overlay");
|
||||
winPush("fsMap", () => {
|
||||
|
|
@ -3647,7 +3638,7 @@ async function showSessionDetail(sessionId) {
|
|||
fsDiv.innerHTML = "⛶";
|
||||
map.dragging.disable();
|
||||
map.scrollWheelZoom.disable();
|
||||
setTimeout(() => { map.invalidateSize(); if (mapEl._bounds) map.fitBounds(mapEl._bounds); }, 100);
|
||||
map.invalidateSize(); if (mapEl._bounds) map.fitBounds(mapEl._bounds);
|
||||
});
|
||||
winStack[winStack.length - 1].overlay = parentOverlay;
|
||||
}
|
||||
|
|
@ -3689,7 +3680,7 @@ async function showSessionDetail(sessionId) {
|
|||
map.on("click", () => {
|
||||
if (!mapEl.classList.contains("sd-map-full")) toggleMapFs();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Downsample arrays for smooth chart rendering (max ~500 points)
|
||||
function downsample(arr, maxPts) {
|
||||
|
|
@ -3712,14 +3703,11 @@ async function showSessionDetail(sessionId) {
|
|||
);
|
||||
|
||||
// Render speed chart
|
||||
setTimeout(() => {
|
||||
if (renderToken !== _sessionDetailRender) return;
|
||||
let ctx = document.querySelector("#sd-speed-chart");
|
||||
if (!ctx || !smoothed.length) return;
|
||||
// Update maxSpeed display to match smoothed graph
|
||||
let speedCtx = document.querySelector("#sd-speed-chart");
|
||||
if (speedCtx && smoothed.length) {
|
||||
let maxEl = document.querySelector("#sd-max-speed");
|
||||
if (maxEl) maxEl.textContent = graphMaxSpeed + " км/ч";
|
||||
new Chart(ctx, {
|
||||
new Chart(speedCtx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: dsTimeLabels,
|
||||
|
|
@ -3760,16 +3748,12 @@ async function showSessionDetail(sessionId) {
|
|||
},
|
||||
},
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Render elevation chart
|
||||
setTimeout(() => {
|
||||
if (renderToken !== _sessionDetailRender) return;
|
||||
let ctx = document.querySelector("#sd-elev-chart");
|
||||
if (!ctx || !elevations.length) return;
|
||||
let elevLabels = points.filter(p => p.altitude_gps_m != null)
|
||||
.map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1));
|
||||
new Chart(ctx, {
|
||||
let elevCtx = document.querySelector("#sd-elev-chart");
|
||||
if (elevCtx && elevations.length) {
|
||||
new Chart(elevCtx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: dsElevLabels,
|
||||
|
|
@ -3811,23 +3795,20 @@ async function showSessionDetail(sessionId) {
|
|||
},
|
||||
},
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// Render HR chart
|
||||
setTimeout(() => {
|
||||
if (renderToken !== _sessionDetailRender) return;
|
||||
let hrData = points.map(p => p.heart_rate || null);
|
||||
if (!hrData.some(v => v)) { /* no HR data — hide chart */ return; }
|
||||
let hrData = points.map(p => p.heart_rate || null);
|
||||
let hrCtx = document.querySelector("#sd-hr-chart");
|
||||
if (hrData.some(v => v) && hrCtx) {
|
||||
let wrap = document.querySelector("#sd-hr-wrap");
|
||||
if (wrap) wrap.style.display = "";
|
||||
let ctx = document.querySelector("#sd-hr-chart");
|
||||
if (!ctx) return;
|
||||
let dsHr = downsample(hrData, MAX_CHART_PTS);
|
||||
let dsHrLabels = downsample(
|
||||
points.map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1)),
|
||||
MAX_CHART_PTS
|
||||
);
|
||||
new Chart(ctx, {
|
||||
new Chart(hrCtx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: dsHrLabels,
|
||||
|
|
@ -3873,11 +3854,10 @@ async function showSessionDetail(sessionId) {
|
|||
},
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Delete button
|
||||
setTimeout(() => {
|
||||
if (renderToken !== _sessionDetailRender) return;
|
||||
// Buttons + bike selector
|
||||
{
|
||||
let exp = document.querySelector("#sd-export");
|
||||
if (exp) exp.onclick = () => GPS.exportGPX(sessionId);
|
||||
|
||||
|
|
@ -3957,18 +3937,14 @@ async function showSessionDetail(sessionId) {
|
|||
history.back();
|
||||
renderScreen("gps");
|
||||
};
|
||||
}, 100);
|
||||
|
||||
// IDEA 2: Goal markers — show closest approach for each marker
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
let goalSection = document.querySelector("#sd-goals-section");
|
||||
let goalsList = document.querySelector("#sd-goals-list");
|
||||
if (!goalSection || !goalsList || points.length < 2) return;
|
||||
|
||||
let markers = await DB.getGoalMarkers();
|
||||
if (markers.length === 0) return;
|
||||
}
|
||||
|
||||
// Goal markers — closest approach for each marker
|
||||
try {
|
||||
let goalSection = document.querySelector("#sd-goals-section");
|
||||
let goalsList = document.querySelector("#sd-goals-list");
|
||||
let markers = (goalSection && goalsList && points.length >= 2) ? await DB.getGoalMarkers() : [];
|
||||
if (markers.length > 0) {
|
||||
// For each marker, find closest point in this track
|
||||
let results = [];
|
||||
for (let marker of markers) {
|
||||
|
|
@ -3984,8 +3960,9 @@ async function showSessionDetail(sessionId) {
|
|||
let bestPrevDist = Infinity;
|
||||
let bestPrevDate = null;
|
||||
for (let ps of prevSessions) {
|
||||
if (ps.id === sessionId) continue;
|
||||
let psPts = await DB.getGpsPoints(ps.id);
|
||||
let psId = ps.id_str ?? String(ps.id);
|
||||
if (psId === String(sessionId)) continue;
|
||||
let psPts = await DB.getGpsPoints(psId);
|
||||
if (psPts.length < 2) continue;
|
||||
for (let pp of psPts) {
|
||||
let d = GPS.distance(pp.lat, pp.lon, marker.lat, marker.lon);
|
||||
|
|
@ -4029,23 +4006,17 @@ async function showSessionDetail(sessionId) {
|
|||
${progressHtml}
|
||||
</div>`;
|
||||
}).join("");
|
||||
} catch (e) { console.error("[goals]", e); }
|
||||
}, 500);
|
||||
}
|
||||
} catch (e) { console.error("[goals]", e); }
|
||||
|
||||
// IDEA 8: Food cost of workout
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
let costSection = document.querySelector("#sd-food-cost-section");
|
||||
let costEl = document.querySelector("#sd-food-cost");
|
||||
if (!costSection || !costEl) return;
|
||||
let sessionKcal = session.total_kcal ?? stats.kcal;
|
||||
if (!sessionKcal || sessionKcal <= 0) return;
|
||||
|
||||
let foods = await DB.getFrequentFoods(7);
|
||||
if (foods.length === 0) return;
|
||||
|
||||
// Build "cost" descriptions: N portions of food
|
||||
let parts = [];
|
||||
// Food cost of workout
|
||||
try {
|
||||
let costSection = document.querySelector("#sd-food-cost-section");
|
||||
let costEl = document.querySelector("#sd-food-cost");
|
||||
let sessionKcal = session.total_kcal ?? stats.kcal;
|
||||
let foods = (costSection && costEl && sessionKcal > 0) ? await DB.getFrequentFoods(7) : [];
|
||||
// Build "cost" descriptions: N portions of food
|
||||
let parts = [];
|
||||
for (let f of foods) {
|
||||
if (!f.kcal || f.kcal <= 0) continue;
|
||||
// kcal per one serving (as user ate it)
|
||||
|
|
@ -4061,19 +4032,17 @@ async function showSessionDetail(sessionId) {
|
|||
});
|
||||
}
|
||||
|
||||
if (parts.length === 0) return;
|
||||
|
||||
if (parts.length > 0) {
|
||||
// Take first 3, build readable string
|
||||
let display = parts.slice(0, 3);
|
||||
let lines = display.map(p =>
|
||||
`${p.portions} порц. ${esc(p.name)} (по ${p.amountStr || "?"}, ${p.servingKcal} кк/порц.)`
|
||||
);
|
||||
|
||||
costSection.style.display = "";
|
||||
costEl.innerHTML = `<div class="food-cost-total">${Math.round(sessionKcal)} ккал — это как:</div>
|
||||
<div class="food-cost-items">${lines.map(l => `<div class="food-cost-line">${l}</div>`).join('<div class="food-cost-or">или</div>')}</div>`;
|
||||
} catch (e) { console.error("[food-cost]", e); }
|
||||
}, 600);
|
||||
}
|
||||
} catch (e) { console.error("[food-cost]", e); }
|
||||
|
||||
} catch(e) { console.error("[showSessionDetail] error:", e, JSON.stringify(e)); }
|
||||
}
|
||||
|
|
@ -4087,7 +4056,7 @@ async function showCompareModal(sessionId, currentSession, currentPoints) {
|
|||
let sessions = await DB.getGpsSessions(100);
|
||||
// Filter: same activity type, similar distance (+-30%), not the same session
|
||||
let similar = sessions.filter(s =>
|
||||
s.id !== sessionId &&
|
||||
(s.id_str ?? String(s.id)) !== String(sessionId) &&
|
||||
s.activity_type === currentSession.activity_type &&
|
||||
s.total_distance_m != null &&
|
||||
s.total_distance_m > dist * 0.7 && s.total_distance_m < dist * 1.3
|
||||
|
|
@ -4105,31 +4074,29 @@ async function showCompareModal(sessionId, currentSession, currentPoints) {
|
|||
${similar.map(s => {
|
||||
let d = s.total_distance_m != null ? GPS.formatDistance(Math.round(s.total_distance_m)) : "?";
|
||||
let spd = s.avg_speed_kmh != null ? s.avg_speed_kmh + " км/ч" : "";
|
||||
return `<div class="compare-item" data-sid="${s.id}">
|
||||
return `<div class="compare-item" data-sid="${s.id_str ?? s.id}">
|
||||
<div style="font-weight:600">${s.date} ${s.start_time ?? ""}</div>
|
||||
<div style="font-size:12px;color:var(--muted)">${d} | ${spd}</div>
|
||||
</div>`;
|
||||
}).join("")}
|
||||
</div>`, () => {});
|
||||
|
||||
setTimeout(() => {
|
||||
let overlay = [...document.querySelectorAll(".modal-overlay")].pop();
|
||||
if (!overlay) return;
|
||||
// Hide save/cancel buttons — this is a selection modal
|
||||
let btns = overlay.querySelector(".modal-btns");
|
||||
if (btns) btns.style.display = "none";
|
||||
let overlay = [...document.querySelectorAll(".modal-overlay")].pop();
|
||||
if (!overlay) return;
|
||||
// Hide save/cancel buttons — this is a selection modal
|
||||
let btns = overlay.querySelector(".modal-btns");
|
||||
if (btns) btns.style.display = "none";
|
||||
|
||||
overlay.querySelectorAll(".compare-item").forEach(item => {
|
||||
item.onclick = async () => {
|
||||
let compId = parseInt(item.dataset.sid);
|
||||
// Close this modal, open comparison view
|
||||
if (winStack.length > 0) winStack.pop().close();
|
||||
_skipPops = 1;
|
||||
history.back();
|
||||
await showComparisonView(sessionId, compId, currentSession, currentPoints);
|
||||
};
|
||||
});
|
||||
}, 100);
|
||||
overlay.querySelectorAll(".compare-item").forEach(item => {
|
||||
item.onclick = async () => {
|
||||
let compId = item.dataset.sid; // keep as string (large i64 id)
|
||||
// Close this modal, open comparison view
|
||||
if (winStack.length > 0) winStack.pop().close();
|
||||
_skipPops = 1;
|
||||
history.back();
|
||||
await showComparisonView(sessionId, compId, currentSession, currentPoints);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function showComparisonView(currentId, compareId, currentSession, currentPoints) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue