Timers: add enable checkbox; collapse settings + hide display readouts when off

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-24 19:08:28 -05:00
parent a011a89100
commit 333477afdb

View file

@ -56,6 +56,7 @@
.display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:8px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); } .display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:8px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); }
.display .big { font-family:"Courier New",monospace; font-weight:700; font-size:40px; color:#ffd166; letter-spacing:3px; text-shadow:0 0 12px rgba(255,209,102,.5); } .display .big { font-family:"Courier New",monospace; font-weight:700; font-size:40px; color:#ffd166; letter-spacing:3px; text-shadow:0 0 12px rgba(255,209,102,.5); }
.display .dtimers { font-family:"Courier New",monospace; font-size:13px; color:#4dd0e1; margin:3px 0; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; } .display .dtimers { font-family:"Courier New",monospace; font-size:13px; color:#4dd0e1; margin:3px 0; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.display .dtimers[hidden] { display:none; }
.display .ctx { font-family:"Courier New",monospace; font-size:12px; color:#4dd0e1; min-height:15px; line-height:1.3; } .display .ctx { font-family:"Courier New",monospace; font-size:12px; color:#4dd0e1; min-height:15px; line-height:1.3; }
.display .ctx.muted-cue { color:#ffb454; } .display .ctx.muted-cue { color:#ffb454; }
.knob { margin-bottom:10px; } .knob { margin-bottom:10px; }
@ -192,7 +193,7 @@
<div style="flex:0 0 190px; min-width:170px"> <div style="flex:0 0 190px; min-width:170px">
<div class="display"> <div class="display">
<div class="big" id="bpmDisplay">120</div> <div class="big" id="bpmDisplay">120</div>
<div class="dtimers"> <div class="dtimers" id="dtimers">
<span title="elapsed (stopwatch)"><span id="elapsedVal">0:00</span></span> <span title="elapsed (stopwatch)"><span id="elapsedVal">0:00</span></span>
<span id="countWrap" title="countdown" hidden><span id="countVal" class="tval">0:00</span></span> <span id="countWrap" title="countdown" hidden><span id="countVal" class="tval">0:00</span></span>
</div> </div>
@ -228,8 +229,8 @@
<label style="font-size:12px">every <input type="number" class="num" id="rampEvery" min="1" max="16" value="4"> bars</label> <label style="font-size:12px">every <input type="number" class="num" id="rampEvery" min="1" max="16" value="4"> bars</label>
</div> </div>
</div> </div>
<div class="fbox" id="timerBox"> <div class="fbox toggleable" id="timerBox">
<div class="fhead"><span class="ftitle">Timers</span><span class="hint" style="margin:0">run while playing</span></div> <label class="fhead"><input type="checkbox" id="timersOn"><span class="ftitle">Timers</span><span class="hint" style="margin:0">run while playing</span></label>
<div class="fbody"> <div class="fbody">
<div class="row" style="gap:10px; align-items:center"> <div class="row" style="gap:10px; align-items:center">
<label style="font-size:12px">Elapsed (stopwatch)</label> <label style="font-size:12px">Elapsed (stopwatch)</label>
@ -649,7 +650,7 @@ function renderLaneStrip(m) {
/* ========================================================================= /* =========================================================================
PRESETS (localStorage) PRESETS (localStorage)
========================================================================= */ ========================================================================= */
const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs: "metronome.logs", seeded: "metronome.seeded", continue: "metronome.continue" }; const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs: "metronome.logs", seeded: "metronome.seeded", continue: "metronome.continue", timers: "metronome.timers" };
function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } } function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } }
function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { console.warn("localStorage unavailable", e); return false; } } function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { console.warn("localStorage unavailable", e); return false; } }
@ -676,6 +677,7 @@ let activeItem = -1; // selected / loaded item in the active set list
let nowPlaying = null; // { at, name } for duration logging let nowPlaying = null; // { at, name } for duration logging
let historyName = null; // item whose past-session history is shown let historyName = null; // item whose past-session history is shown
let continueMode = lsGet(LS.continue, false); // auto-advance to next item when countdown ends let continueMode = lsGet(LS.continue, false); // auto-advance to next item when countdown ends
let timersOn = lsGet(LS.timers, true); // master switch for the elapsed/countdown timers
function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs }; } function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs }; }
function applySetup(s) { function applySetup(s) {
@ -694,6 +696,7 @@ function syncPracticeUI() {
function refreshFeatureBoxes() { function refreshFeatureBoxes() {
$("trainerBox").classList.toggle("on", trainer.on); $("trainerBox").classList.toggle("on", trainer.on);
$("rampBox").classList.toggle("on", ramp.on); $("rampBox").classList.toggle("on", ramp.on);
$("timerBox").classList.toggle("on", timersOn);
} }
function fmtDur(sec) { sec = Math.round(sec); const m = Math.floor(sec / 60); return m + ":" + String(sec % 60).padStart(2, "0"); } function fmtDur(sec) { sec = Math.round(sec); const m = Math.floor(sec / 60); return m + ":" + String(sec % 60).padStart(2, "0"); }
function getSL() { return setlists[activeSL]; } function getSL() { return setlists[activeSL]; }
@ -996,7 +999,7 @@ function tickTimers() {
const now = Date.now(); const now = Date.now();
const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump
timers.last = now; timers.last = now;
if (state.running) { if (timersOn && state.running) {
timers.elapsedMs += dt; timers.elapsedMs += dt;
if (timers.totalMs > 0) { if (timers.totalMs > 0) {
const before = timers.remainingMs; const before = timers.remainingMs;
@ -1011,6 +1014,8 @@ function tickTimers() {
renderTimers(); renderTimers();
} }
function renderTimers() { function renderTimers() {
$("dtimers").hidden = !timersOn;
if (!timersOn) return;
$("elapsedVal").textContent = fmtClock(timers.elapsedMs); $("elapsedVal").textContent = fmtClock(timers.elapsedMs);
const off = timers.totalMs <= 0; const off = timers.totalMs <= 0;
$("countWrap").hidden = off; // hide countdown when off $("countWrap").hidden = off; // hide countdown when off
@ -1090,6 +1095,7 @@ $("countTime").addEventListener("input", (e) => { timers.totalMs = parseTime(e.t
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); }); $("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); }); $("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
$("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); }); $("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); });
$("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); });
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; }); $("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });
document.addEventListener("click", (e) => { const m = $("trayMenu"); if (m && !m.hidden && !m.contains(e.target) && e.target.id !== "trayMenuBtn") m.hidden = true; }); document.addEventListener("click", (e) => { const m = $("trayMenu"); if (m && !m.hidden && !m.contains(e.target) && e.target.id !== "trayMenuBtn") m.hidden = true; });
$("newSetlistBtn").addEventListener("click", newSetlist); $("newSetlistBtn").addEventListener("click", newSetlist);
@ -1155,6 +1161,7 @@ renderLog();
updateCtx(); updateCtx();
refreshFeatureBoxes(); refreshFeatureBoxes();
$("continueMode").checked = continueMode; $("continueMode").checked = continueMode;
$("timersOn").checked = timersOn;
$("appVersion").textContent = "v" + APP_VERSION; $("appVersion").textContent = "v" + APP_VERSION;
requestAnimationFrame(drawLoop); requestAnimationFrame(drawLoop);
</script> </script>