diff --git a/index.html b/index.html index 8e484ef..4d97f7d 100644 --- a/index.html +++ b/index.html @@ -97,9 +97,18 @@ .x:hover { color:#ff9a8a; border-color:#c0392b; } .hint { font-size:11px; color:var(--muted); margin-top:8px; } code { background:#0d1014; padding:1px 5px; border-radius:4px; color:#cfe3ff; } - .ex-item { display:flex; gap:8px; align-items:center; padding:7px 9px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; font-size:13px; background:var(--panel); } + .ex-item { display:flex; gap:8px; align-items:center; padding:7px 9px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; font-size:13px; background:var(--panel); cursor:pointer; } + .ex-item:hover { border-color:var(--muted); } + .ex-item.active { border-color:#2e7d32; box-shadow:inset 3px 0 0 #2e7d32; } .ex-item .nm { flex:1; } .ex-item .meta { color:var(--muted); font-family:"Courier New",monospace; font-size:11px; } + .ex-item .row-actions { display:none; gap:4px; } + .ex-item.active .row-actions, .ex-item:hover .row-actions { display:inline-flex; } + .nowplaying { background:var(--panel); border:1px solid var(--edge); border-radius:10px; padding:10px 12px; margin-bottom:12px; } + .np-label { font-size:10px; letter-spacing:1.4px; color:var(--muted); text-transform:uppercase; } + .np-name { font-size:16px; font-weight:600; margin:2px 0; } + .np-sub { font-size:12px; color:var(--muted); font-family:"Courier New",monospace; word-break:break-word; } + .np-desc { font-size:12px; color:var(--muted); margin-top:4px; } .iconbtn { padding:3px 8px; font-size:12px; } .log-item { padding:8px 10px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; background:var(--panel); } .log-head { font-weight:600; font-size:13px; margin-bottom:3px; } @@ -180,12 +189,11 @@
-
- -
- - -
+
+
Now loaded
+
Free play
+
+
@@ -271,7 +279,8 @@ ⇧↑ ⇧↓Tempo ±10 BPM AAdd meter lane RSet lists tray - NNext set-list item + NLoad next set-list item + ⌥↑ ⌥↓Reorder selected item 19Mute lane 1–9 ?This help EscClose 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();