@@ -228,7 +268,7 @@
========================================================================= */
const state = { bpm: 120, volume: 0.7, running: false };
const trainer = { on: false, playBars: 2, muteBars: 2 };
-const ramp = { on: false, amount: 2, everyBars: 4 };
+const ramp = { on: false, startBpm: 80, amount: 5, everyBars: 4 };
let meters = []; // array of meter-lane objects
let meterSeq = 0; // id counter
@@ -392,6 +432,7 @@ function scheduler() {
function start() {
ensureAudio(); audioCtx.resume();
state.running = true;
+ if (ramp.on) setBpm(ramp.startBpm); // ramp begins from its start BPM
const t0 = audioCtx.currentTime + 0.08;
for (const m of meters) { m.tick = 0; m.nextTime = t0; m.vq = []; m.vqPtr = 0; m.currentBeat = -1; m.currentBar = 0; }
masterBeat = 0; masterBeatTime = t0; muteWindows = [];
@@ -546,13 +587,13 @@ function applyLanes(lanes) {
}
function savePreset(name) {
const all = lsGet(LS.presets, {});
- all[name] = { bpm: state.bpm, lanes: snapshotLanes() };
+ all[name] = currentSetup();
lsSet(LS.presets, all); refreshPresetList(name);
}
function loadPreset(name) {
const all = lsGet(LS.presets, {}); const p = all[name]; if (!p) return;
const wasRunning = state.running; if (wasRunning) stop();
- setBpm(p.bpm); applyLanes(p.lanes); updateCtx();
+ applySetup(p);
if (wasRunning) start();
}
function deletePreset(name) { const all = lsGet(LS.presets, {}); delete all[name]; lsSet(LS.presets, all); refreshPresetList(); }
@@ -573,9 +614,19 @@ let setlists = lsGet(LS.setlists, []);
let activeSL = 0; // index of the selected set list
let playingItem = -1; // index of the item currently playing in the active set list
let nowPlaying = null; // { at, name } for duration logging
+let historyName = null; // name of the item whose past-session history is shown
-function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes() }; }
-function applySetup(s) { setBpm(s.bpm); applyLanes(s.lanes); updateCtx(); }
+function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp } }; }
+function applySetup(s) {
+ setBpm(s.bpm); applyLanes(s.lanes);
+ if (s.trainer) Object.assign(trainer, s.trainer);
+ if (s.ramp) Object.assign(ramp, s.ramp);
+ syncPracticeUI(); updateCtx();
+}
+function syncPracticeUI() {
+ $("trainerOn").checked = trainer.on; $("playBars").value = trainer.playBars; $("muteBars").value = trainer.muteBars;
+ $("rampOn").checked = ramp.on; $("rampStart").value = ramp.startBpm; $("rampAmt").value = ramp.amount; $("rampEvery").value = ramp.everyBars;
+}
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 saveSetlists() { lsSet(LS.setlists, setlists); }
@@ -602,12 +653,20 @@ function playItem(i) {
const sl = getSL(); if (!sl || !sl.items[i]) return;
logFinalize(); // close out the previously-playing item
applySetup(sl.items[i]);
- if (!state.running) start();
+ if (state.running) stop(); // clean restart so ramp start-BPM + bar counter reset
+ start();
playingItem = i;
+ historyName = sl.items[i].name;
nowPlaying = { at: Date.now(), name: sl.items[i].name };
- renderItems();
+ renderItems(); renderLog();
}
function nextItem() { const sl = getSL(); if (sl && playingItem + 1 < sl.items.length) playItem(playingItem + 1); }
+function updateItem(i) { // load → adjust → save back to the item
+ const sl = getSL(); if (!sl || !sl.items[i]) return;
+ const nm = sl.items[i].name;
+ sl.items[i] = { name: nm, ...currentSetup() };
+ saveSetlists(); renderItems();
+}
// Start/stop button + Space route through here so internal restarts don't log.
function toggleTransport() {
@@ -633,14 +692,17 @@ function renderItems() {
if (!sl.items.length) { box.innerHTML = '
No items yet — set up the metronome and “Add current settings”.
'; return; }
sl.items.forEach((it, i) => {
const row = document.createElement("div"); row.className = "ex-item";
+ const playing = (i === playingItem && state.running);
if (i === playingItem) row.style.borderColor = "#2e7d32";
- row.innerHTML = `
+ row.innerHTML = `
${i + 1}. ${it.name}
${it.bpm}bpm · ${it.lanes.map((l) => l.groupsStr).join("/")}
+
`;
- row.querySelector('[data-act=play]').onclick = () => playItem(i);
+ row.querySelector('[data-act=play]').onclick = () => (i === playingItem && state.running) ? toggleTransport() : playItem(i);
+ row.querySelector('[data-act=save]').onclick = () => updateItem(i);
row.querySelector('[data-act=up]').onclick = () => moveItem(i, -1);
row.querySelector('[data-act=down]').onclick = () => moveItem(i, 1);
row.querySelector('[data-act=del]').onclick = () => removeItem(i);
@@ -655,15 +717,16 @@ function logFinalize() {
logs.unshift({ at: nowPlaying.at, name: nowPlaying.name, durationSec: (Date.now() - nowPlaying.at) / 1000, bpm: state.bpm, lanes: snapshotLanes() });
lsSet(LS.logs, logs); nowPlaying = null; renderLog();
}
+// Show history for the item being (or last) played, so the user can compare
+// today's BPM/duration against previous days for that specific task.
function renderLog() {
- const box = $("logView"); const logs = lsGet(LS.logs, []); box.innerHTML = "";
- if (!logs.length) { box.innerHTML = '
No practice logged yet — play set-list items to record what you do.
'; return; }
- logs.forEach((e) => {
- const div = document.createElement("div"); div.className = "log-item";
- div.innerHTML = `
${new Date(e.at).toLocaleString()} — ${e.name}
-
${fmtDur(e.durationSec)} @ ${e.bpm}bpm · ${(e.lanes || []).map((l) => l.groupsStr + "/" + l.sound).join(", ")}
`;
- box.appendChild(div);
- });
+ const box = $("logView"); box.innerHTML = "";
+ if (!historyName) { box.innerHTML = '
Play a set-list item to see its history — compare BPM & duration across days.
'; return; }
+ const logs = lsGet(LS.logs, []).filter((e) => e.name === historyName);
+ let html = `
History — ${historyName}
`;
+ if (!logs.length) html += '
No past sessions for this item yet.
';
+ else html += logs.map((e) => `
${new Date(e.at).toLocaleString()} · ${fmtDur(e.durationSec)} @ ${e.bpm}bpm
`).join("");
+ box.innerHTML = html;
}
function clearLog() { if (confirm("Clear the practice log? (set lists & presets are kept)")) { lsSet(LS.logs, []); renderLog(); } }
@@ -732,6 +795,13 @@ function syncStartBtn() {
else { startBtn.textContent = "▶ Start"; startBtn.classList.remove("on"); }
}
function toggleShortcuts(show) { const o = $("shortcutsOverlay"); o.hidden = (show === undefined) ? !o.hidden : !show; }
+function applyTheme(t) {
+ document.documentElement.dataset.theme = t;
+ try { localStorage.setItem("metronome.theme", t); } catch (e) {}
+ $("themeBtn").textContent = t === "light" ? "🌙" : "☀"; // icon = theme you'd switch TO
+}
+$("themeBtn").addEventListener("click", () => applyTheme(document.documentElement.dataset.theme === "light" ? "dark" : "light"));
+applyTheme(document.documentElement.dataset.theme === "light" ? "light" : "dark");
$("startBtn").addEventListener("click", () => toggleTransport());
let _taps = [];
function tapTempo() {
@@ -753,6 +823,7 @@ $("trainerOn").addEventListener("change", (e) => trainer.on = e.target.checked);
$("playBars").addEventListener("input", (e) => trainer.playBars = +e.target.value);
$("muteBars").addEventListener("input", (e) => trainer.muteBars = +e.target.value);
$("rampOn").addEventListener("change", (e) => ramp.on = e.target.checked);
+$("rampStart").addEventListener("input", (e) => ramp.startBpm = +e.target.value);
$("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value);
$("rampEvery").addEventListener("input", (e) => ramp.everyBars = +e.target.value);
$("addMeterBtn").addEventListener("click", () => addMeter("4", 1, "claves"));
@@ -764,6 +835,8 @@ $("presetSelect").addEventListener("change", (e) => {
$("delPresetBtn").addEventListener("click", () => { const n = $("presetSelect").value; if (n && n !== "__save__" && confirm('Delete preset "' + n + '"?')) deletePreset(n); });
$("routineToggle").addEventListener("click", () => $("routineTray").classList.toggle("open"));
$("routineClose").addEventListener("click", () => $("routineTray").classList.remove("open"));
+$("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; });
$("newSetlistBtn").addEventListener("click", newSetlist);
$("delSetlistBtn").addEventListener("click", deleteSetlist);
$("setlistSelect").addEventListener("change", (e) => { activeSL = +e.target.value; playingItem = -1; renderSetlists(); });
@@ -773,10 +846,10 @@ $("addItemBtn").addEventListener("click", () => { addItem($("itemName").value.tr
$("helpBtn").addEventListener("click", () => toggleShortcuts());
$("shortcutsClose").addEventListener("click", () => toggleShortcuts(false));
$("shortcutsOverlay").addEventListener("click", (e) => { if (e.target.id === "shortcutsOverlay") toggleShortcuts(false); });
-$("exportBtn").addEventListener("click", exportAll);
-$("importBtn").addEventListener("click", () => $("importFile").click());
+$("exportBtn").addEventListener("click", () => { $("trayMenu").hidden = true; exportAll(); });
+$("importBtn").addEventListener("click", () => { $("trayMenu").hidden = true; $("importFile").click(); });
$("importFile").addEventListener("change", (e) => { if (e.target.files[0]) importAll(e.target.files[0]); e.target.value = ""; });
-$("clearLogBtn").addEventListener("click", clearLog);
+$("clearLogBtn").addEventListener("click", () => { $("trayMenu").hidden = true; clearLog(); });
window.addEventListener("keydown", (e) => {
const t = e.target;
if (t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;