-
-
-
-
-
+
+
Now loaded
+
Free play
+
+
@@ -271,7 +279,8 @@
| ⇧↑ ⇧↓ | Tempo ±10 BPM |
| A | Add meter lane |
| R | Set lists tray |
-
| N | Next set-list item |
+
| N | Load next set-list item |
+
| ⌥↑ ⌥↓ | Reorder selected item |
| 1–9 | Mute lane 1–9 |
| ? | This help |
| Esc | Close tray / help |
@@ -621,24 +630,7 @@ function applyLanes(lanes) {
const cb = m.el.querySelector(`#m${m.id}_mute`); if (cb) cb.checked = m.mute;
}
}
-function savePreset(name) {
- const all = lsGet(LS.presets, {});
- 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();
- applySetup(p);
- if (wasRunning) start();
-}
-function deletePreset(name) { const all = lsGet(LS.presets, {}); delete all[name]; lsSet(LS.presets, all); refreshPresetList(); }
-function refreshPresetList(sel) {
- const all = lsGet(LS.presets, {}); const list = $("presetSelect");
- list.innerHTML = '
';
- Object.keys(all).sort().forEach((n) => { const o = document.createElement("option"); o.value = n; o.textContent = n; list.appendChild(o); });
- list.value = sel || "";
-}
+// (Presets removed — set-list items are now the single "saved setup" mechanism.)
/* =========================================================================
SET LISTS + PRACTICE LOG
@@ -647,10 +639,10 @@ function refreshPresetList(sel) {
Each played item is logged (timestamp, name, duration, BPM, conditions).
========================================================================= */
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 activeSL = 0; // selected set list
+let activeItem = -1; // selected / loaded item 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
+let historyName = null; // item whose past-session history is shown
function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp } }; }
function applySetup(s) {
@@ -670,44 +662,66 @@ function saveSetlists() { lsSet(LS.setlists, setlists); }
// --- set list CRUD ---
function newSetlist() {
setlists.push({ title: "Set list " + (setlists.length + 1), description: "", items: [] });
- activeSL = setlists.length - 1; playingItem = -1; saveSetlists(); renderSetlists();
+ activeSL = setlists.length - 1; activeItem = -1; saveSetlists(); renderSetlists();
}
function deleteSetlist() {
if (!setlists.length || !confirm("Delete this set list?")) return;
- setlists.splice(activeSL, 1); activeSL = Math.max(0, activeSL - 1); playingItem = -1; saveSetlists(); renderSetlists();
+ setlists.splice(activeSL, 1); activeSL = Math.max(0, activeSL - 1); activeItem = -1; saveSetlists(); renderSetlists();
}
function addItem(name) {
const sl = getSL(); if (!sl) return;
sl.items.push({ name: name || ("Item " + (sl.items.length + 1)), ...currentSetup() });
+ activeItem = sl.items.length - 1; saveSetlists(); renderItems();
+}
+function removeItem(i) {
+ const sl = getSL(); if (!sl) return;
+ sl.items.splice(i, 1);
+ if (activeItem === i) activeItem = -1; else if (activeItem > i) activeItem--;
saveSetlists(); renderItems();
}
-function removeItem(i) { const sl = getSL(); sl.items.splice(i, 1); if (playingItem >= sl.items.length) playingItem = -1; saveSetlists(); renderItems(); }
-function moveItem(i, d) { const sl = getSL(); const j = i + d; if (j < 0 || j >= sl.items.length) return; [sl.items[i], sl.items[j]] = [sl.items[j], sl.items[i]]; saveSetlists(); renderItems(); }
+function moveItem(i, d) { const sl = getSL(); const j = i + d; if (j < 0 || j >= sl.items.length) return; [sl.items[i], sl.items[j]] = [sl.items[j], sl.items[i]]; saveSetlists(); }
+function moveActiveItem(d) { // keyboard reorder of the selected item (Alt+↑/↓)
+ const sl = getSL(); if (!sl || activeItem < 0) return;
+ const j = activeItem + d; if (j < 0 || j >= sl.items.length) return;
+ moveItem(activeItem, d); activeItem = j; renderItems();
+}
-// --- play / advance ---
-function playItem(i) {
+// --- select / advance: clicking an item LOADS it; the transport is the only play/stop ---
+function loadItem(i) {
const sl = getSL(); if (!sl || !sl.items[i]) return;
- logFinalize(); // close out the previously-playing item
+ const wasRunning = state.running;
+ if (wasRunning) logFinalize(); // close out the previous segment
applySetup(sl.items[i]);
- 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 };
+ activeItem = i; historyName = sl.items[i].name;
+ if (wasRunning) { stop(); start(); nowPlaying = { at: Date.now(), name: sl.items[i].name }; } // keep playing the new item
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
+function nextItem() { const sl = getSL(); if (sl && activeItem + 1 < sl.items.length) loadItem(activeItem + 1); }
+function updateItem(i) { // overwrite item with current settings (keeps its name)
const sl = getSL(); if (!sl || !sl.items[i]) return;
- const nm = sl.items[i].name;
- sl.items[i] = { name: nm, ...currentSetup() };
+ sl.items[i] = { name: sl.items[i].name, ...currentSetup() };
saveSetlists(); renderItems();
}
-// Start/stop button + Space route through here so internal restarts don't log.
+// Start/stop go through here so internal restarts don't create stray log entries.
function toggleTransport() {
- if (state.running) { logFinalize(); playingItem = -1; renderItems(); stop(); }
- else start();
+ if (state.running) { logFinalize(); stop(); }
+ else { start(); const sl = getSL(); if (activeItem >= 0 && sl && sl.items[activeItem]) nowPlaying = { at: Date.now(), name: sl.items[activeItem].name }; }
+ renderItems();
+}
+
+// --- now-playing info on the main screen (replaces the old preset dropdown) ---
+function renderNowPlaying() {
+ const sl = getSL(); const it = (sl && activeItem >= 0) ? sl.items[activeItem] : null;
+ if (!it) {
+ $("npName").textContent = "Free play";
+ $("npSub").textContent = "No set-list item loaded — edit the lanes freely.";
+ $("npDesc").textContent = (sl && sl.description) ? "“" + sl.title + "” — " + sl.description : "";
+ return;
+ }
+ $("npName").textContent = (activeItem + 1) + ". " + it.name;
+ $("npSub").textContent = it.bpm + " BPM · " + it.lanes.map((l) => l.sound + " " + l.groupsStr + (l.poly ? "~" : "") + (l.mute ? " (muted)" : "")).join(" · ");
+ $("npDesc").textContent = (sl.title || "") + (sl.description ? " — " + sl.description : "");
}
// --- render ---
@@ -724,26 +738,24 @@ function renderSetlists() {
}
function renderItems() {
const box = $("itemList"); box.innerHTML = ""; const sl = getSL();
- if (!sl) { box.innerHTML = '
Create a set list, then add the current settings as items.
'; return; }
- if (!sl.items.length) { box.innerHTML = '
No items yet — set up the metronome and “Add current settings”.
'; return; }
+ if (!sl) { box.innerHTML = '
Create a set list, then “Add current settings” to capture items.
'; renderNowPlaying(); return; }
+ if (!sl.items.length) { box.innerHTML = '
No items yet — set up the metronome and “Add current settings”.
'; renderNowPlaying(); 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 = `
-
${i + 1}. ${it.name}
-
${it.bpm}bpm · ${it.lanes.map((l) => l.groupsStr).join("/")}
-
-
-
-
`;
- 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);
+ const row = document.createElement("div");
+ row.className = "ex-item" + (i === activeItem ? " active" : "");
+ row.title = "Click to load into the player · Alt+↑/↓ to reorder";
+ row.innerHTML = `
${i + 1}. ${it.name}
+
${it.bpm} · ${it.lanes.map((l) => l.groupsStr).join("/")}
+
+
+
+ `;
+ row.onclick = () => loadItem(i);
+ row.querySelector('[data-act=save]').onclick = (e) => { e.stopPropagation(); updateItem(i); };
+ row.querySelector('[data-act=del]').onclick = (e) => { e.stopPropagation(); removeItem(i); };
box.appendChild(row);
});
+ renderNowPlaying();
}
// --- practice log (flat entries, one per played item) ---
@@ -786,9 +798,9 @@ function importAll(file) {
try {
const d = JSON.parse(reader.result);
if (d.presets) lsSet(LS.presets, d.presets);
- if (d.setlists) { lsSet(LS.setlists, d.setlists); setlists = d.setlists; activeSL = 0; playingItem = -1; }
+ if (d.setlists) { lsSet(LS.setlists, d.setlists); setlists = d.setlists; activeSL = 0; activeItem = -1; }
if (d.logs) lsSet(LS.logs, d.logs);
- refreshPresetList(); renderSetlists(); renderLog();
+ renderSetlists(); renderLog();
alert("Imported " + Object.keys(d.presets || {}).length + " presets, " + (d.setlists || []).length + " set lists, " + (d.logs || []).length + " log entries.");
} catch (e) { alert("Import failed: " + e.message); }
};
@@ -889,7 +901,7 @@ function applyHashShare() {
if (h.startsWith("#sl=")) {
const sl = codeToSetlist(decodeURIComponent(h.slice(4)));
setlists.push(sl); activeSL = setlists.length - 1; saveSetlists(); renderSetlists();
- if (sl.items[0]) applySetup(sl.items[0]);
+ if (sl.items[0]) { applySetup(sl.items[0]); activeItem = 0; historyName = sl.items[0].name; }
history.replaceState(null, "", location.pathname);
alert("Imported set list: " + sl.title + " (" + sl.items.length + " items)");
return true;
@@ -990,19 +1002,13 @@ $("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"));
-$("presetSelect").addEventListener("change", (e) => {
- const v = e.target.value;
- if (v === "__save__") { const n = (prompt("Save current settings as preset:") || "").trim(); if (n) savePreset(n); else refreshPresetList(); }
- else if (v) loadPreset(v);
-});
-$("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(); });
+$("setlistSelect").addEventListener("change", (e) => { activeSL = +e.target.value; activeItem = -1; renderSetlists(); });
$("slTitle").addEventListener("input", (e) => { const sl = getSL(); if (sl) { sl.title = e.target.value; saveSetlists(); const o = $("setlistSelect").options[activeSL]; if (o) o.textContent = sl.title || ("Set list " + (activeSL + 1)); } });
$("slDesc").addEventListener("input", (e) => { const sl = getSL(); if (sl) { sl.description = e.target.value; saveSetlists(); } });
$("addItemBtn").addEventListener("click", () => { addItem($("itemName").value.trim()); $("itemName").value = ""; });
@@ -1023,8 +1029,9 @@ $("shareOpen").addEventListener("click", () => window.open($("shareUrl").value,
window.addEventListener("keydown", (e) => {
const t = e.target;
if (t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
- if (e.metaKey || e.ctrlKey || e.altKey) return;
const k = e.key;
+ if (e.altKey && (k === "ArrowUp" || k === "ArrowDown")) { e.preventDefault(); moveActiveItem(k === "ArrowUp" ? -1 : 1); return; } // reorder selected item
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
if (e.code === "Space") { e.preventDefault(); toggleTransport(); }
else if (k === "t" || k === "T") { tapTempo(); }
else if (k === "ArrowUp") { e.preventDefault(); setBpm(state.bpm + (e.shiftKey ? 10 : 1)); }
@@ -1044,7 +1051,7 @@ window.addEventListener("keydown", (e) => {
// seed the demo set list once (first run only; not re-added if the user deletes it)
if (!lsGet(LS.seeded, false)) {
if (!setlists.length) {
- setlists.push({ title: "✨ Demos", description: "Tap ▶ on an item to hear it — meters, polyrhythms, odd time, subdivisions & practice tools.", items: DEMOS.map(([n, p]) => ({ name: n, ...patchToSetup(p) })) });
+ setlists.push({ title: "✨ Demos", description: "Click an item to load it, then press Space — meters, polyrhythms, odd time, subdivisions & practice tools.", items: DEMOS.map(([n, p]) => ({ name: n, ...patchToSetup(p) })) });
activeSL = 0; saveSetlists();
}
lsSet(LS.seeded, true);
@@ -1054,7 +1061,6 @@ if (!applyHashShare()) {
addMeter("4", 1, "kick"); // reference bar
addMeter("5", 1, "claves", null, true); // 5 fit into the bar → true 5:4 polyrhythm
}
-refreshPresetList();
renderSetlists();
renderLog();
updateCtx();