Single Save button by Tap (disabled when nothing loaded); per-entry + Clear-all history delete; bigger display text
- Move per-item 💾 out of set-list rows into one 💾 Save next to Tap; it overwrites the loaded item and is disabled when no item is loaded. - History list: red ✕ on hover deletes one session; Clear all wipes history for the current item only. - Bump dark-display text again (BPM 80px, timers 26px, status 19px); widen the display column to fit. - README: play key Space -> P. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1be6920827
commit
17492fdfb0
2 changed files with 54 additions and 15 deletions
|
|
@ -101,7 +101,7 @@ In the set‑list panel's **⋯** menu:
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `Space` | start / stop |
|
| `P` | play / stop |
|
||||||
| `T` | tap tempo |
|
| `T` | tap tempo |
|
||||||
| `↑` / `↓` | tempo ±1 (`Shift` = ±10) |
|
| `↑` / `↓` | tempo ±1 (`Shift` = ±10) |
|
||||||
| `A` | add meter lane |
|
| `A` | add meter lane |
|
||||||
|
|
|
||||||
67
index.html
67
index.html
|
|
@ -54,10 +54,10 @@
|
||||||
.card { background:var(--panel-2); border:1px solid var(--edge); border-radius:12px; padding:13px; flex:1; min-width:240px; }
|
.card { background:var(--panel-2); border:1px solid var(--edge); border-radius:12px; padding:13px; flex:1; min-width:240px; }
|
||||||
.card h2 { font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0 0 14px; }
|
.card h2 { font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0 0 14px; }
|
||||||
.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:58px; color:#ffd166; letter-spacing:2px; line-height:1.05; text-shadow:0 0 12px rgba(255,209,102,.5); }
|
.display .big { font-family:"Courier New",monospace; font-weight:700; font-size:80px; color:#ffd166; letter-spacing:2px; line-height:1.0; text-shadow:0 0 12px rgba(255,209,102,.5); }
|
||||||
.display .dtimers { font-family:"Courier New",monospace; font-size:18px; color:#4dd0e1; margin:4px 0; display:flex; gap:16px; justify-content:center; flex-wrap:wrap; }
|
.display .dtimers { font-family:"Courier New",monospace; font-size:26px; color:#4dd0e1; margin:6px 0; display:flex; gap:18px; justify-content:center; flex-wrap:wrap; }
|
||||||
.display .dtimers[hidden] { display:none; }
|
.display .dtimers[hidden] { display:none; }
|
||||||
.display .ctx { font-family:"Courier New",monospace; font-size:15px; color:#4dd0e1; min-height:18px; line-height:1.3; }
|
.display .ctx { font-family:"Courier New",monospace; font-size:19px; color:#4dd0e1; min-height:22px; line-height:1.25; }
|
||||||
.display .ctx.muted-cue { color:#ffb454; }
|
.display .ctx.muted-cue { color:#ffb454; }
|
||||||
.knob { margin-bottom:10px; }
|
.knob { margin-bottom:10px; }
|
||||||
.knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; }
|
.knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; }
|
||||||
|
|
@ -114,8 +114,13 @@
|
||||||
.np-desc { font-size:12px; color:var(--muted); margin-top:4px; }
|
.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:5px; display:flex; align-items:center; gap:8px; }
|
||||||
.log-seg { font-size:12px; color:var(--muted); margin:2px 0 0 12px; font-family:"Courier New",monospace; }
|
.log-head-nm { flex:1; }
|
||||||
|
.hist-row { display:flex; align-items:center; gap:6px; margin:2px 0 0 12px; }
|
||||||
|
.hist-txt { flex:1; font-size:12px; color:var(--muted); font-family:"Courier New",monospace; }
|
||||||
|
.hist-del { display:none; background:transparent; border:none; color:#ff6b5e; cursor:pointer; font-size:12px; line-height:1; padding:2px 4px; border-radius:4px; }
|
||||||
|
.hist-del:hover { background:rgba(255,107,94,.15); }
|
||||||
|
.hist-row:hover .hist-del, .hist-row:focus-within .hist-del { display:inline; }
|
||||||
.practice { border-top:1px solid var(--edge); margin-top:16px; padding-top:4px; }
|
.practice { border-top:1px solid var(--edge); margin-top:16px; padding-top:4px; }
|
||||||
/* set-list panel: always shown — sticky beside the metronome on desktop,
|
/* set-list panel: always shown — sticky beside the metronome on desktop,
|
||||||
stacks below it on narrow screens */
|
stacks below it on narrow screens */
|
||||||
|
|
@ -190,7 +195,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="card" style="flex:1">
|
<div class="card" style="flex:1">
|
||||||
<div class="row" style="gap:22px; align-items:flex-start">
|
<div class="row" style="gap:22px; align-items:flex-start">
|
||||||
<div style="flex:0 0 212px; min-width:190px">
|
<div style="flex:0 0 260px; min-width:230px">
|
||||||
<div class="display">
|
<div class="display">
|
||||||
<div class="big" id="bpmDisplay">120</div>
|
<div class="big" id="bpmDisplay">120</div>
|
||||||
<div class="dtimers" id="dtimers">
|
<div class="dtimers" id="dtimers">
|
||||||
|
|
@ -199,7 +204,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="ctx" id="ctxDisplay"> </div>
|
<div class="ctx" id="ctxDisplay"> </div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button></div>
|
<div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button><button id="saveItemBtn" disabled title="overwrite the loaded set-list item with the current settings">💾 Save</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="flex:1; min-width:200px">
|
<div style="flex:1; min-width:200px">
|
||||||
|
|
@ -756,6 +761,7 @@ function toggleTransport() {
|
||||||
// --- now-playing info on the main screen (replaces the old preset dropdown) ---
|
// --- now-playing info on the main screen (replaces the old preset dropdown) ---
|
||||||
function renderNowPlaying() {
|
function renderNowPlaying() {
|
||||||
const sl = getSL(); const it = (sl && activeItem >= 0) ? sl.items[activeItem] : null;
|
const sl = getSL(); const it = (sl && activeItem >= 0) ? sl.items[activeItem] : null;
|
||||||
|
$("saveItemBtn").disabled = !it; // single save button targets the loaded item
|
||||||
if (!it) {
|
if (!it) {
|
||||||
$("npName").textContent = "Free play";
|
$("npName").textContent = "Free play";
|
||||||
$("npSub").textContent = "No set-list item loaded — edit the lanes freely.";
|
$("npSub").textContent = "No set-list item loaded — edit the lanes freely.";
|
||||||
|
|
@ -790,11 +796,9 @@ function renderItems() {
|
||||||
row.innerHTML = `<span class="nm">${i + 1}. ${it.name}</span>
|
row.innerHTML = `<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} · ${it.lanes.map((l) => l.groupsStr).join("/")}</span>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button class="iconbtn" data-act="save" title="overwrite this item with the current settings">💾</button>
|
|
||||||
<button class="x iconbtn" data-act="del" title="remove this item">✕</button>
|
<button class="x iconbtn" data-act="del" title="remove this item">✕</button>
|
||||||
</span>`;
|
</span>`;
|
||||||
row.onclick = () => loadItem(i);
|
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); };
|
row.querySelector('[data-act=del]').onclick = (e) => { e.stopPropagation(); removeItem(i); };
|
||||||
box.appendChild(row);
|
box.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
@ -813,11 +817,41 @@ function logFinalize() {
|
||||||
function renderLog() {
|
function renderLog() {
|
||||||
const box = $("logView"); box.innerHTML = "";
|
const box = $("logView"); box.innerHTML = "";
|
||||||
if (!historyName) { box.innerHTML = '<div class="hint">Play a set-list item to see its history — compare BPM & duration across days.</div>'; return; }
|
if (!historyName) { box.innerHTML = '<div class="hint">Play a set-list item to see its history — compare BPM & duration across days.</div>'; return; }
|
||||||
const logs = lsGet(LS.logs, []).filter((e) => e.name === historyName);
|
const entries = lsGet(LS.logs, []).filter((e) => e.name === historyName);
|
||||||
let html = `<div class="log-head" style="margin-bottom:5px">History — ${historyName}</div>`;
|
|
||||||
if (!logs.length) html += '<div class="hint">No past sessions for this item yet.</div>';
|
const head = document.createElement("div"); head.className = "log-head";
|
||||||
else html += logs.map((e) => `<div class="log-seg">${new Date(e.at).toLocaleString()} · ${fmtDur(e.durationSec)} @ ${e.bpm}bpm</div>`).join("");
|
head.innerHTML = `<span class="log-head-nm">History — ${historyName}</span>`;
|
||||||
box.innerHTML = html;
|
if (entries.length) {
|
||||||
|
const clr = document.createElement("button"); clr.className = "iconbtn"; clr.textContent = "Clear all";
|
||||||
|
clr.title = "delete all history for this item";
|
||||||
|
clr.onclick = () => clearItemHistory();
|
||||||
|
head.appendChild(clr);
|
||||||
|
}
|
||||||
|
box.appendChild(head);
|
||||||
|
|
||||||
|
if (!entries.length) {
|
||||||
|
const h = document.createElement("div"); h.className = "hint"; h.textContent = "No past sessions for this item yet.";
|
||||||
|
box.appendChild(h); return;
|
||||||
|
}
|
||||||
|
entries.forEach((e) => {
|
||||||
|
const row = document.createElement("div"); row.className = "hist-row";
|
||||||
|
const txt = document.createElement("span"); txt.className = "hist-txt";
|
||||||
|
txt.textContent = `${new Date(e.at).toLocaleString()} · ${fmtDur(e.durationSec)} @ ${e.bpm}bpm`;
|
||||||
|
const del = document.createElement("button"); del.className = "hist-del"; del.textContent = "✕";
|
||||||
|
del.title = "delete this entry";
|
||||||
|
del.onclick = () => deleteHistoryEntry(e.at);
|
||||||
|
row.appendChild(txt); row.appendChild(del); box.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function deleteHistoryEntry(at) { // remove one session by its timestamp
|
||||||
|
const logs = lsGet(LS.logs, []).filter((e) => !(e.at === at && e.name === historyName));
|
||||||
|
lsSet(LS.logs, logs); renderLog();
|
||||||
|
}
|
||||||
|
function clearItemHistory() { // clear every session for the current item
|
||||||
|
if (!historyName) return;
|
||||||
|
if (!confirm("Clear all history for “" + historyName + "”? (other items, set lists & presets are kept)")) return;
|
||||||
|
const logs = lsGet(LS.logs, []).filter((e) => e.name !== historyName);
|
||||||
|
lsSet(LS.logs, logs); renderLog();
|
||||||
}
|
}
|
||||||
function clearLog() { if (confirm("Clear the practice log? (set lists & presets are kept)")) { lsSet(LS.logs, []); renderLog(); } }
|
function clearLog() { if (confirm("Clear the practice log? (set lists & presets are kept)")) { lsSet(LS.logs, []); renderLog(); } }
|
||||||
function resetAll() {
|
function resetAll() {
|
||||||
|
|
@ -1078,6 +1112,11 @@ function tapTempo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$("tapBtn").addEventListener("click", tapTempo);
|
$("tapBtn").addEventListener("click", tapTempo);
|
||||||
|
$("saveItemBtn").addEventListener("click", () => {
|
||||||
|
if (activeItem < 0) return;
|
||||||
|
updateItem(activeItem);
|
||||||
|
const b = $("saveItemBtn"), t = b.textContent; b.textContent = "✓ Saved"; setTimeout(() => { b.textContent = t; }, 900);
|
||||||
|
});
|
||||||
$("bpm").addEventListener("input", (e) => setBpm(+e.target.value));
|
$("bpm").addEventListener("input", (e) => setBpm(+e.target.value));
|
||||||
$("vol").addEventListener("input", (e) => {
|
$("vol").addEventListener("input", (e) => {
|
||||||
state.volume = +e.target.value / 100; volVal.textContent = e.target.value + "%";
|
state.volume = +e.target.value / 100; volVal.textContent = e.target.value + "%";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue