diff --git a/src/index.html b/src/index.html index 60329cb..aa0a9d7 100644 --- a/src/index.html +++ b/src/index.html @@ -38,9 +38,21 @@
00:00
- +
—
+
+ + +

+
+
мощность @@ -59,16 +71,20 @@
-
- - - +
+
+ — + +
+
+
+
+
+ +
+ + +
diff --git a/src/main.js b/src/main.js index 7eb5121..885d932 100644 --- a/src/main.js +++ b/src/main.js @@ -122,9 +122,10 @@ async function connect(deviceId, btn, category, card) { } // --- Переход на экран упражнения --- -startBtn.addEventListener("click", () => { +startBtn.addEventListener("click", async () => { connectScreen.hidden = true; workoutScreen.hidden = false; + await loadWorkouts(); }); // --- Телеметрия --- @@ -182,62 +183,113 @@ listen("connection", (event) => { applyStatus(sel, category, status); }); -// --- Таймер --- +// --- Воркаут --- const timerEl = document.querySelector("#timer"); -const timerBtn = document.querySelector("#timer-btn"); -let running = false; -let startTs = 0; -let elapsed = 0; -let raf = 0; - -function formatTime(ms) { - const sec = Math.floor(ms / 1000); +const workoutSelect = document.querySelector("#workout-select"); +const ftpInput = document.querySelector("#ftp-input"); +const workoutMeta = document.querySelector("#workout-meta"); +const workoutName = document.querySelector("#workout-name"); +const workoutStep = document.querySelector("#workout-step"); +const workoutTarget = document.querySelector("#workout-target"); +const workoutBar = document.querySelector("#workout-bar"); +const workoutStartBtn = document.querySelector("#workout-start"); +const workoutPauseBtn = document.querySelector("#workout-pause"); +const workoutStopBtn = document.querySelector("#workout-stop"); + +let workouts = []; +let workoutPaused = false; + +function formatClock(sec) { const m = Math.floor(sec / 60); - const s = sec % 60; + const s = Math.floor(sec % 60); return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } -function tick() { - elapsed = performance.now() - startTs; - timerEl.textContent = formatTime(elapsed); - if (running) raf = requestAnimationFrame(tick); +async function loadWorkouts() { + try { + workouts = await invoke("list_workouts"); + workoutSelect.replaceChildren(); + const empty = el("option", null, "— выбери воркаут —"); + empty.value = ""; + workoutSelect.append(empty); + for (const w of workouts) { + const opt = el("option", null, `${w.date.slice(0, 10)} · ${w.name} · ${w.duration_min} мин`); + opt.value = w.path; + workoutSelect.append(opt); + } + ftpInput.value = await invoke("get_ftp"); + } catch (e) { + console.error(e); + } } -timerBtn.addEventListener("click", async () => { - running = !running; - if (running) { - startTs = performance.now() - elapsed; - timerBtn.textContent = "стоп"; - tick(); - try { - await invoke("trainer_start"); - } catch (e) { - console.error(e); - } - } else { - cancelAnimationFrame(raf); - timerBtn.textContent = "старт"; - try { - await invoke("trainer_stop"); - } catch (e) { - console.error(e); - } +function selectedWorkout() { + return workouts.find((w) => w.path === workoutSelect.value); +} + +workoutSelect.addEventListener("change", () => { + const w = selectedWorkout(); + workoutName.textContent = w ? w.name : "—"; + workoutMeta.textContent = w ? `${w.duration_min} мин · load ${w.load ?? "—"}` : ""; +}); + +async function setRunning(active) { + workoutStartBtn.disabled = active; + workoutPauseBtn.disabled = !active; + workoutStopBtn.disabled = !active; + workoutSelect.disabled = active; + ftpInput.disabled = active; + if (!active) { + workoutPaused = false; + workoutPauseBtn.textContent = "пауза"; + workoutStep.textContent = "—"; + workoutTarget.textContent = ""; + workoutBar.style.width = "0%"; + timerEl.textContent = "00:00"; + } +} + +workoutStartBtn.addEventListener("click", async () => { + const w = selectedWorkout(); + const ftp = Number(ftpInput.value); + if (!w || !Number.isFinite(ftp) || ftp <= 0) return; + try { + await invoke("set_ftp", { ftp }); + await invoke("start_workout", { path: w.path }); + await setRunning(true); + } catch (e) { + console.error(e); + } +}); + +workoutPauseBtn.addEventListener("click", async () => { + workoutPaused = !workoutPaused; + workoutPauseBtn.textContent = workoutPaused ? "продолжить" : "пауза"; + try { + await invoke("pause_workout", { paused: workoutPaused }); + } catch (e) { + console.error(e); } }); -// --- Целевая мощность/каденс --- -document.querySelector("#apply-btn").addEventListener("click", async () => { - const powerRaw = document.querySelector("#power-input").value.trim(); - const cadenceRaw = document.querySelector("#cadence-input").value.trim(); - if (powerRaw === "" || cadenceRaw === "") return; - const watts = Number(powerRaw); - const rpm = Number(cadenceRaw); - if (!Number.isFinite(watts) || !Number.isFinite(rpm)) return; - if (watts < 0 || rpm < 0 || !Number.isInteger(watts) || !Number.isInteger(rpm)) return; +workoutStopBtn.addEventListener("click", async () => { try { - await invoke("set_target_power", { watts }); - await invoke("set_target_cadence", { rpm }); + await invoke("stop_workout"); } catch (e) { console.error(e); } + await setRunning(false); +}); + +listen("workout-progress", (event) => { + const p = event.payload; + workoutStep.textContent = p.step_name || `шаг ${p.step_index + 1}`; + workoutTarget.textContent = p.target_watts != null ? `${p.target_watts} W` : ""; + timerEl.textContent = formatClock(p.elapsed_secs); + const pct = p.total_secs > 0 ? (p.elapsed_secs / p.total_secs) * 100 : 0; + workoutBar.style.width = `${Math.min(100, pct)}%`; +}); + +listen("workout-finished", () => { + setRunning(false); }); diff --git a/src/styles.css b/src/styles.css index ddbee3d..be6e2df 100644 --- a/src/styles.css +++ b/src/styles.css @@ -367,3 +367,70 @@ body { .field__input:focus { border-color: var(--c-text); } + +/* --- Выбор воркаута --- */ + +.workout-name { + font-size: var(--text-sm); + font-weight: var(--fw-medium); + color: var(--c-text-muted); + text-align: right; +} + +.workout-picker { + display: grid; + grid-template-columns: 1fr auto; + gap: var(--space-4); + align-items: end; +} + +.workout-meta { + grid-column: 1 / -1; + font-size: var(--text-sm); + color: var(--c-text-muted); + font-variant-numeric: tabular-nums; +} + +/* --- Прогресс воркаута --- */ + +.workout-progress { + display: grid; + gap: var(--space-2); +} + +.workout-progress__head { + display: flex; + justify-content: space-between; + align-items: baseline; + font-size: var(--text-sm); +} + +.workout-progress__step { + font-weight: var(--fw-medium); +} + +.workout-progress__target { + color: var(--c-text-muted); + font-variant-numeric: tabular-nums; +} + +.workout-progress__track { + height: 6px; + background: var(--c-surface-2); + border-radius: var(--radius-full); + overflow: hidden; +} + +.workout-progress__bar { + height: 100%; + width: 0%; + background: var(--c-accent); + border-radius: var(--radius-full); + transition: width var(--duration-fast) var(--ease); +} + +/* --- Кнопки воркаута --- */ + +.controls--workout { + grid-template-columns: 1fr 1fr 1fr; +}