ui: bulk-delete tracks, HR/VR record + detail layout

- Tracks list: the long-press multi-select (used for merge) now also offers Удалить -
  bulk delete of the selected tracks (1+; merge still needs 2+), same selection flow.
- HR/VR record screen: heart rate fills the main area (like the map on a GPS ride),
  Подключить/Стоп moved into a bottom bar, consistent with the GPS screen.
- HR/VR workout detail: 'Удалить зарядку' pinned to the bottom (was floating in the
  scroll flow and stretched).
This commit is contained in:
4aiman 2026-10-30 23:07:53 +03:00
commit eb430ffb78
2 changed files with 48 additions and 13 deletions

View file

@ -751,7 +751,12 @@ select {
.full-width { width: 100%; } .full-width { width: 100%; }
/* HR-only workout (зарядка) recording screen: big heart rate, timer, kcal, no map. */ /* HR-only workout (зарядка) recording screen: big heart rate, timer, kcal, no map. */
.workout-screen { padding: 24px 16px; display: flex; flex-direction: column; align-items: center; gap: 8px; } .workout-screen { display: flex; flex-direction: column; height: 100%; }
.wk-main { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 16px; }
.wk-panel { flex-shrink: 0; display: flex; flex-direction: column; padding: 10px 16px; padding-bottom: calc(10px + var(--safe-area-bottom, 0px)); }
/* Workout detail (in a modal): fill the scrollable body so the delete button sits at the bottom. */
.wkd-wrap { display: flex; flex-direction: column; min-height: 100%; }
.wkd-del { margin-top: auto; flex: none; align-self: stretch; }
.wk-hr-big { font-size: 92px; font-weight: 700; line-height: 1; color: var(--primary); } .wk-hr-big { font-size: 92px; font-weight: 700; line-height: 1; color: var(--primary); }
.wk-hr-lbl { color: var(--muted); margin-top: -4px; } .wk-hr-lbl { color: var(--muted); margin-top: -4px; }
.wk-stats { display: flex; gap: 18px; margin: 14px 0; flex-wrap: wrap; justify-content: center; } .wk-stats { display: flex; gap: 18px; margin: 14px 0; flex-wrap: wrap; justify-content: center; }

View file

@ -2934,6 +2934,7 @@ async function renderGpsScreen(container) {
<div id="gps-sel-bar" class="gps-sel-bar" hidden> <div id="gps-sel-bar" class="gps-sel-bar" hidden>
<span id="gps-sel-count">Выбрано 0</span> <span id="gps-sel-count">Выбрано 0</span>
<button id="gps-sel-merge" class="btn-primary" disabled>Объединить</button> <button id="gps-sel-merge" class="btn-primary" disabled>Объединить</button>
<button id="gps-sel-delete" class="btn-secondary" style="color:#cf6679" disabled>Удалить</button>
<button id="gps-sel-cancel" class="btn-secondary">Отмена</button> <button id="gps-sel-cancel" class="btn-secondary">Отмена</button>
</div> </div>
</div>`; </div>`;
@ -2991,11 +2992,13 @@ async function renderGpsScreen(container) {
let selBar = container.querySelector("#gps-sel-bar"); let selBar = container.querySelector("#gps-sel-bar");
let selCount = container.querySelector("#gps-sel-count"); let selCount = container.querySelector("#gps-sel-count");
let selMergeBtn = container.querySelector("#gps-sel-merge"); let selMergeBtn = container.querySelector("#gps-sel-merge");
let selDeleteBtn = container.querySelector("#gps-sel-delete");
let refreshSel = () => { let refreshSel = () => {
container.querySelectorAll(".gps-session-item").forEach(it => it.classList.toggle("selected", selected.has(it.dataset.sid))); container.querySelectorAll(".gps-session-item").forEach(it => it.classList.toggle("selected", selected.has(it.dataset.sid)));
if (selBar) selBar.hidden = !selMode; if (selBar) selBar.hidden = !selMode;
if (selCount) selCount.textContent = "Выбрано " + selected.size; if (selCount) selCount.textContent = "Выбрано " + selected.size;
if (selMergeBtn) selMergeBtn.disabled = selected.size < 2; if (selMergeBtn) selMergeBtn.disabled = selected.size < 2; // merge needs 2+
if (selDeleteBtn) selDeleteBtn.disabled = selected.size < 1; // delete works from 1
}; };
let exitSel = () => { selMode = false; selected.clear(); refreshSel(); }; let exitSel = () => { selMode = false; selected.clear(); refreshSel(); };
let toggleSel = (sid) => { let toggleSel = (sid) => {
@ -3060,6 +3063,27 @@ async function renderGpsScreen(container) {
selMergeBtn.disabled = false; selMergeBtn.textContent = "Объединить"; selMergeBtn.disabled = false; selMergeBtn.textContent = "Объединить";
} }
}; };
if (selDeleteBtn) selDeleteBtn.onclick = async () => {
if (selected.size < 1) return;
let ids = [...selected];
let n = ids.length;
let word = (n % 10 === 1 && n % 100 !== 11) ? "трек"
: (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) ? "трека" : "треков";
if (!(await appConfirm(`Удалить ${n} ${word}? Это необратимо.`, "Удалить"))) return;
try {
selDeleteBtn.disabled = true; selDeleteBtn.textContent = "Удаляю...";
let done = 0;
for (let id of ids) {
try { await DB.deleteGpsSession(id); done++; } catch (e) { console.error("[bulk delete]", id, e); }
}
showToast(`Удалено: ${done}`);
exitSel();
renderScreen("gps");
} catch (e) {
showToast("Ошибка удаления: " + (e.message || e));
selDeleteBtn.disabled = false; selDeleteBtn.textContent = "Удалить";
}
};
// Pagination: "Показать ещё" loads the next page from the DB and appends it. // Pagination: "Показать ещё" loads the next page from the DB and appends it.
let sessOffset = sessions.length; let sessOffset = sessions.length;
@ -4777,6 +4801,7 @@ function renderWorkoutDetail(session) {
let max = hrs.length ? Math.max(...hrs) : null; let max = hrs.length ? Math.max(...hrs) : null;
let kcal = Math.round(session.total_kcal || 0); let kcal = Math.round(session.total_kcal || 0);
let html = ` let html = `
<div class="wkd-wrap">
<div class="sd-section"><div class="sd-title">${session.activity_type === "vr" ? "Руки/VR" : "Зарядка"} · ${session.date} ${session.start_time ?? ""}</div> <div class="sd-section"><div class="sd-title">${session.activity_type === "vr" ? "Руки/VR" : "Зарядка"} · ${session.date} ${session.start_time ?? ""}</div>
<div class="sd-row"> <div class="sd-row">
<div class="sd-stat"><span class="sd-val">${GPS.formatDuration(durSec)}</span><span class="sd-lbl">Время</span></div> <div class="sd-stat"><span class="sd-val">${GPS.formatDuration(durSec)}</span><span class="sd-lbl">Время</span></div>
@ -4790,7 +4815,8 @@ function renderWorkoutDetail(session) {
<div class="sd-section"><div class="sd-title">Пульс</div> <div class="sd-section"><div class="sd-title">Пульс</div>
<div class="chart-wrap"><canvas id="wk-hr-chart" height="150"></canvas></div> <div class="chart-wrap"><canvas id="wk-hr-chart" height="150"></canvas></div>
</div> </div>
<button id="wk-del" class="btn-secondary full-width" style="margin-top:8px;color:#cf6679">Удалить зарядку</button>`; <button id="wk-del" class="btn-secondary full-width wkd-del" style="color:#cf6679">Удалить зарядку</button>
</div>`;
showModal(html, () => {}, (overlay) => { showModal(html, () => {}, (overlay) => {
let ctx = overlay.querySelector("#wk-hr-chart"); let ctx = overlay.querySelector("#wk-hr-chart");
if (ctx && samples.length) { if (ctx && samples.length) {
@ -4817,17 +4843,21 @@ function renderWorkoutDetail(session) {
function renderWorkoutInline(container) { function renderWorkoutInline(container) {
container.innerHTML = ` container.innerHTML = `
<div class="workout-screen"> <div class="workout-screen">
<div class="wk-hr-big" id="wk-hr">--</div> <div class="wk-main">
<div class="wk-hr-lbl">уд/мин</div> <div class="wk-hr-big" id="wk-hr">--</div>
<div class="wk-stats"> <div class="wk-hr-lbl">уд/мин</div>
<div class="wk-stat"><span id="wk-time" class="wk-val">0:00</span><span class="wk-lbl">Время</span></div> <div class="wk-status" id="wk-status">Подключите пульсометр</div>
<div class="wk-stat"><span id="wk-kcal" class="wk-val">0</span><span class="wk-lbl">ккал</span></div> </div>
<div class="wk-stat"><span id="wk-avg" class="wk-val">--</span><span class="wk-lbl">средний</span></div> <div class="wk-panel">
<div class="wk-stat"><span id="wk-max" class="wk-val">--</span><span class="wk-lbl">макс</span></div> <div class="wk-stats">
<div class="wk-stat"><span id="wk-time" class="wk-val">0:00</span><span class="wk-lbl">Время</span></div>
<div class="wk-stat"><span id="wk-kcal" class="wk-val">0</span><span class="wk-lbl">ккал</span></div>
<div class="wk-stat"><span id="wk-avg" class="wk-val">--</span><span class="wk-lbl">средний</span></div>
<div class="wk-stat"><span id="wk-max" class="wk-val">--</span><span class="wk-lbl">макс</span></div>
</div>
<button id="wk-connect" class="btn-secondary full-width">Подключить пульсометр</button>
<button id="wk-stop" class="btn-primary gps-rec recording full-width">Стоп</button>
</div> </div>
<div class="wk-status" id="wk-status">Подключите пульсометр</div>
<button id="wk-connect" class="btn-secondary full-width">Подключить пульсометр</button>
<button id="wk-stop" class="btn-primary gps-rec recording full-width">Стоп</button>
</div>`; </div>`;
let hrEl = container.querySelector("#wk-hr"), timeEl = container.querySelector("#wk-time"); let hrEl = container.querySelector("#wk-hr"), timeEl = container.querySelector("#wk-time");