UX: select-to-load set-list items; now-playing info block; drop preset dropdown
- Set-list rows are now click-to-load (the transport is the only play/stop); the
per-item ▶/⏹ and ↑/↓ buttons are gone. Reorder via Alt+↑/↓ (tooltip + help).
Selected row is highlighted and reveals compact 💾 (save-back) / ✕ (remove).
- Replaced the main-screen preset dropdown with a 'Now loaded' info block showing
the item name, config summary, and the set list's title + description.
- Presets consolidated into set-list items (removed preset functions/UI). N loads
the next item; shortcuts help + demo description updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7207ffe1c7
commit
3f04383e2b
1 changed files with 84 additions and 78 deletions
162
index.html
162
index.html
|
|
@ -97,9 +97,18 @@
|
||||||
.x:hover { color:#ff9a8a; border-color:#c0392b; }
|
.x:hover { color:#ff9a8a; border-color:#c0392b; }
|
||||||
.hint { font-size:11px; color:var(--muted); margin-top:8px; }
|
.hint { font-size:11px; color:var(--muted); margin-top:8px; }
|
||||||
code { background:#0d1014; padding:1px 5px; border-radius:4px; color:#cfe3ff; }
|
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 .nm { flex:1; }
|
||||||
.ex-item .meta { color:var(--muted); font-family:"Courier New",monospace; font-size:11px; }
|
.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; }
|
.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-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; }
|
.log-head { font-weight:600; font-size:13px; margin-bottom:3px; }
|
||||||
|
|
@ -180,12 +189,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="flex:1; min-width:200px">
|
<div style="flex:1; min-width:200px">
|
||||||
<div class="knob" style="margin-bottom:10px">
|
<div class="nowplaying">
|
||||||
<label>Preset</label>
|
<div class="np-label">Now loaded</div>
|
||||||
<div style="display:flex; gap:6px">
|
<div class="np-name" id="npName">Free play</div>
|
||||||
<select class="cmp" id="presetSelect" style="flex:1"></select>
|
<div class="np-sub" id="npSub"></div>
|
||||||
<button class="x" id="delPresetBtn" title="delete selected preset" style="margin-left:0">✕</button>
|
<div class="np-desc" id="npDesc"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="knob"><label>Tempo (BPM) <b id="bpmVal">120</b></label><input type="range" id="bpm" min="30" max="300" value="120"></div>
|
<div class="knob"><label>Tempo (BPM) <b id="bpmVal">120</b></label><input type="range" id="bpm" min="30" max="300" value="120"></div>
|
||||||
<div class="knob" style="margin-bottom:0"><label>Master Volume <b id="volVal">70%</b></label><input type="range" id="vol" min="0" max="100" value="70"></div>
|
<div class="knob" style="margin-bottom:0"><label>Master Volume <b id="volVal">70%</b></label><input type="range" id="vol" min="0" max="100" value="70"></div>
|
||||||
|
|
@ -271,7 +279,8 @@
|
||||||
<tr><td><kbd>⇧↑</kbd> <kbd>⇧↓</kbd></td><td>Tempo ±10 BPM</td></tr>
|
<tr><td><kbd>⇧↑</kbd> <kbd>⇧↓</kbd></td><td>Tempo ±10 BPM</td></tr>
|
||||||
<tr><td><kbd>A</kbd></td><td>Add meter lane</td></tr>
|
<tr><td><kbd>A</kbd></td><td>Add meter lane</td></tr>
|
||||||
<tr><td><kbd>R</kbd></td><td>Set lists tray</td></tr>
|
<tr><td><kbd>R</kbd></td><td>Set lists tray</td></tr>
|
||||||
<tr><td><kbd>N</kbd></td><td>Next set-list item</td></tr>
|
<tr><td><kbd>N</kbd></td><td>Load next set-list item</td></tr>
|
||||||
|
<tr><td><kbd>⌥↑</kbd> <kbd>⌥↓</kbd></td><td>Reorder selected item</td></tr>
|
||||||
<tr><td><kbd>1</kbd>–<kbd>9</kbd></td><td>Mute lane 1–9</td></tr>
|
<tr><td><kbd>1</kbd>–<kbd>9</kbd></td><td>Mute lane 1–9</td></tr>
|
||||||
<tr><td><kbd>?</kbd></td><td>This help</td></tr>
|
<tr><td><kbd>?</kbd></td><td>This help</td></tr>
|
||||||
<tr><td><kbd>Esc</kbd></td><td>Close tray / help</td></tr>
|
<tr><td><kbd>Esc</kbd></td><td>Close tray / help</td></tr>
|
||||||
|
|
@ -621,24 +630,7 @@ function applyLanes(lanes) {
|
||||||
const cb = m.el.querySelector(`#m${m.id}_mute`); if (cb) cb.checked = m.mute;
|
const cb = m.el.querySelector(`#m${m.id}_mute`); if (cb) cb.checked = m.mute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function savePreset(name) {
|
// (Presets removed — set-list items are now the single "saved setup" mechanism.)
|
||||||
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 = '<option value="">— preset —</option><option value="__save__">+ Save current as…</option>';
|
|
||||||
Object.keys(all).sort().forEach((n) => { const o = document.createElement("option"); o.value = n; o.textContent = n; list.appendChild(o); });
|
|
||||||
list.value = sel || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
SET LISTS + PRACTICE LOG
|
SET LISTS + PRACTICE LOG
|
||||||
|
|
@ -647,10 +639,10 @@ function refreshPresetList(sel) {
|
||||||
Each played item is logged (timestamp, name, duration, BPM, conditions).
|
Each played item is logged (timestamp, name, duration, BPM, conditions).
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
let setlists = lsGet(LS.setlists, []);
|
let setlists = lsGet(LS.setlists, []);
|
||||||
let activeSL = 0; // index of the selected set list
|
let activeSL = 0; // selected set list
|
||||||
let playingItem = -1; // index of the item currently playing in the active set list
|
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; // 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 currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp } }; }
|
||||||
function applySetup(s) {
|
function applySetup(s) {
|
||||||
|
|
@ -670,44 +662,66 @@ function saveSetlists() { lsSet(LS.setlists, setlists); }
|
||||||
// --- set list CRUD ---
|
// --- set list CRUD ---
|
||||||
function newSetlist() {
|
function newSetlist() {
|
||||||
setlists.push({ title: "Set list " + (setlists.length + 1), description: "", items: [] });
|
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() {
|
function deleteSetlist() {
|
||||||
if (!setlists.length || !confirm("Delete this set list?")) return;
|
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) {
|
function addItem(name) {
|
||||||
const sl = getSL(); if (!sl) return;
|
const sl = getSL(); if (!sl) return;
|
||||||
sl.items.push({ name: name || ("Item " + (sl.items.length + 1)), ...currentSetup() });
|
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();
|
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(); }
|
||||||
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 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 ---
|
// --- select / advance: clicking an item LOADS it; the transport is the only play/stop ---
|
||||||
function playItem(i) {
|
function loadItem(i) {
|
||||||
const sl = getSL(); if (!sl || !sl.items[i]) return;
|
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]);
|
applySetup(sl.items[i]);
|
||||||
if (state.running) stop(); // clean restart so ramp start-BPM + bar counter reset
|
activeItem = i; historyName = sl.items[i].name;
|
||||||
start();
|
if (wasRunning) { stop(); start(); nowPlaying = { at: Date.now(), name: sl.items[i].name }; } // keep playing the new item
|
||||||
playingItem = i;
|
|
||||||
historyName = sl.items[i].name;
|
|
||||||
nowPlaying = { at: Date.now(), name: sl.items[i].name };
|
|
||||||
renderItems(); renderLog();
|
renderItems(); renderLog();
|
||||||
}
|
}
|
||||||
function nextItem() { const sl = getSL(); if (sl && playingItem + 1 < sl.items.length) playItem(playingItem + 1); }
|
function nextItem() { const sl = getSL(); if (sl && activeItem + 1 < sl.items.length) loadItem(activeItem + 1); }
|
||||||
function updateItem(i) { // load → adjust → save back to the item
|
function updateItem(i) { // overwrite item with current settings (keeps its name)
|
||||||
const sl = getSL(); if (!sl || !sl.items[i]) return;
|
const sl = getSL(); if (!sl || !sl.items[i]) return;
|
||||||
const nm = sl.items[i].name;
|
sl.items[i] = { name: sl.items[i].name, ...currentSetup() };
|
||||||
sl.items[i] = { name: nm, ...currentSetup() };
|
|
||||||
saveSetlists(); renderItems();
|
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() {
|
function toggleTransport() {
|
||||||
if (state.running) { logFinalize(); playingItem = -1; renderItems(); stop(); }
|
if (state.running) { logFinalize(); stop(); }
|
||||||
else start();
|
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 ---
|
// --- render ---
|
||||||
|
|
@ -724,26 +738,24 @@ function renderSetlists() {
|
||||||
}
|
}
|
||||||
function renderItems() {
|
function renderItems() {
|
||||||
const box = $("itemList"); box.innerHTML = ""; const sl = getSL();
|
const box = $("itemList"); box.innerHTML = ""; const sl = getSL();
|
||||||
if (!sl) { box.innerHTML = '<div class="hint">Create a set list, then add the current settings as items.</div>'; return; }
|
if (!sl) { box.innerHTML = '<div class="hint">Create a set list, then “Add current settings” to capture items.</div>'; renderNowPlaying(); return; }
|
||||||
if (!sl.items.length) { box.innerHTML = '<div class="hint">No items yet — set up the metronome and “Add current settings”.</div>'; return; }
|
if (!sl.items.length) { box.innerHTML = '<div class="hint">No items yet — set up the metronome and “Add current settings”.</div>'; renderNowPlaying(); return; }
|
||||||
sl.items.forEach((it, i) => {
|
sl.items.forEach((it, i) => {
|
||||||
const row = document.createElement("div"); row.className = "ex-item";
|
const row = document.createElement("div");
|
||||||
const playing = (i === playingItem && state.running);
|
row.className = "ex-item" + (i === activeItem ? " active" : "");
|
||||||
if (i === playingItem) row.style.borderColor = "#2e7d32";
|
row.title = "Click to load into the player · Alt+↑/↓ to reorder";
|
||||||
row.innerHTML = `<button class="${playing ? "stop" : "play"} iconbtn" data-act="play" title="${playing ? "stop" : "load & start"}">${playing ? "⏹" : "▶"}</button>
|
row.innerHTML = `<span class="nm">${i + 1}. ${it.name}</span>
|
||||||
<span class="nm">${i + 1}. ${it.name}</span>
|
<span class="meta">${it.bpm} · ${it.lanes.map((l) => l.groupsStr).join("/")}</span>
|
||||||
<span class="meta">${it.bpm}bpm · ${it.lanes.map((l) => l.groupsStr).join("/")}</span>
|
<span class="row-actions">
|
||||||
<button class="iconbtn" data-act="save" title="save current settings to this item">💾</button>
|
<button class="iconbtn" data-act="save" title="overwrite this item with the current settings">💾</button>
|
||||||
<button class="iconbtn" data-act="up" title="up">↑</button>
|
<button class="x iconbtn" data-act="del" title="remove this item">✕</button>
|
||||||
<button class="iconbtn" data-act="down" title="down">↓</button>
|
</span>`;
|
||||||
<button class="x iconbtn" data-act="del" title="remove">✕</button>`;
|
row.onclick = () => loadItem(i);
|
||||||
row.querySelector('[data-act=play]').onclick = () => (i === playingItem && state.running) ? toggleTransport() : playItem(i);
|
row.querySelector('[data-act=save]').onclick = (e) => { e.stopPropagation(); updateItem(i); };
|
||||||
row.querySelector('[data-act=save]').onclick = () => updateItem(i);
|
row.querySelector('[data-act=del]').onclick = (e) => { e.stopPropagation(); removeItem(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);
|
|
||||||
box.appendChild(row);
|
box.appendChild(row);
|
||||||
});
|
});
|
||||||
|
renderNowPlaying();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- practice log (flat entries, one per played item) ---
|
// --- practice log (flat entries, one per played item) ---
|
||||||
|
|
@ -786,9 +798,9 @@ function importAll(file) {
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(reader.result);
|
const d = JSON.parse(reader.result);
|
||||||
if (d.presets) lsSet(LS.presets, d.presets);
|
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);
|
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.");
|
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); }
|
} catch (e) { alert("Import failed: " + e.message); }
|
||||||
};
|
};
|
||||||
|
|
@ -889,7 +901,7 @@ function applyHashShare() {
|
||||||
if (h.startsWith("#sl=")) {
|
if (h.startsWith("#sl=")) {
|
||||||
const sl = codeToSetlist(decodeURIComponent(h.slice(4)));
|
const sl = codeToSetlist(decodeURIComponent(h.slice(4)));
|
||||||
setlists.push(sl); activeSL = setlists.length - 1; saveSetlists(); renderSetlists();
|
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);
|
history.replaceState(null, "", location.pathname);
|
||||||
alert("Imported set list: " + sl.title + " (" + sl.items.length + " items)");
|
alert("Imported set list: " + sl.title + " (" + sl.items.length + " items)");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -990,19 +1002,13 @@ $("rampStart").addEventListener("input", (e) => ramp.startBpm = +e.target.value)
|
||||||
$("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value);
|
$("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value);
|
||||||
$("rampEvery").addEventListener("input", (e) => ramp.everyBars = +e.target.value);
|
$("rampEvery").addEventListener("input", (e) => ramp.everyBars = +e.target.value);
|
||||||
$("addMeterBtn").addEventListener("click", () => addMeter("4", 1, "claves"));
|
$("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"));
|
$("routineToggle").addEventListener("click", () => $("routineTray").classList.toggle("open"));
|
||||||
$("routineClose").addEventListener("click", () => $("routineTray").classList.remove("open"));
|
$("routineClose").addEventListener("click", () => $("routineTray").classList.remove("open"));
|
||||||
$("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);
|
||||||
$("delSetlistBtn").addEventListener("click", deleteSetlist);
|
$("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)); } });
|
$("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(); } });
|
$("slDesc").addEventListener("input", (e) => { const sl = getSL(); if (sl) { sl.description = e.target.value; saveSetlists(); } });
|
||||||
$("addItemBtn").addEventListener("click", () => { addItem($("itemName").value.trim()); $("itemName").value = ""; });
|
$("addItemBtn").addEventListener("click", () => { addItem($("itemName").value.trim()); $("itemName").value = ""; });
|
||||||
|
|
@ -1023,8 +1029,9 @@ $("shareOpen").addEventListener("click", () => window.open($("shareUrl").value,
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
if (t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
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;
|
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(); }
|
if (e.code === "Space") { e.preventDefault(); toggleTransport(); }
|
||||||
else if (k === "t" || k === "T") { tapTempo(); }
|
else if (k === "t" || k === "T") { tapTempo(); }
|
||||||
else if (k === "ArrowUp") { e.preventDefault(); setBpm(state.bpm + (e.shiftKey ? 10 : 1)); }
|
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)
|
// seed the demo set list once (first run only; not re-added if the user deletes it)
|
||||||
if (!lsGet(LS.seeded, false)) {
|
if (!lsGet(LS.seeded, false)) {
|
||||||
if (!setlists.length) {
|
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();
|
activeSL = 0; saveSetlists();
|
||||||
}
|
}
|
||||||
lsSet(LS.seeded, true);
|
lsSet(LS.seeded, true);
|
||||||
|
|
@ -1054,7 +1061,6 @@ if (!applyHashShare()) {
|
||||||
addMeter("4", 1, "kick"); // reference bar
|
addMeter("4", 1, "kick"); // reference bar
|
||||||
addMeter("5", 1, "claves", null, true); // 5 fit into the bar → true 5:4 polyrhythm
|
addMeter("5", 1, "claves", null, true); // 5 fit into the bar → true 5:4 polyrhythm
|
||||||
}
|
}
|
||||||
refreshPresetList();
|
|
||||||
renderSetlists();
|
renderSetlists();
|
||||||
renderLog();
|
renderLog();
|
||||||
updateCtx();
|
updateCtx();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue