Refine mockup: set lists, per-beat patterns, theming, shortcuts
- Tempo ramp gains a start BPM and signed (up/down) steps.
- Presets and set-list items now capture practice settings (trainer + ramp).
- Set-list items: per-item ▶/⏹ play, 💾 save-back, per-item history view.
- Log & backup moved into a ⋯ menu; "+ Add meter" moved below the lanes.
- Light/dark theming (OS-aware + toggle, persisted) and mobile/desktop responsive layout.
- Keyboard shortcuts (Space/T/↑↓/A/R/N/1-9/?) + help overlay; fixed dead Tap button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
49c8584c8c
commit
38d860b4dd
1 changed files with 115 additions and 42 deletions
157
index.html
157
index.html
|
|
@ -4,6 +4,17 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stackable Metronome — Mockup</title>
|
||||
<script>
|
||||
// Set theme before first paint (avoids a flash). Stored choice wins; else
|
||||
// follow the OS preference.
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem("metronome.theme");
|
||||
if (t !== "light" && t !== "dark") t = matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
||||
document.documentElement.dataset.theme = t;
|
||||
} catch (e) { document.documentElement.dataset.theme = "dark"; }
|
||||
})();
|
||||
</script>
|
||||
<!--
|
||||
Browser mockup / simulator for the Pi Pico metronome.
|
||||
|
||||
|
|
@ -18,26 +29,30 @@
|
|||
-->
|
||||
<style>
|
||||
:root {
|
||||
--bg:#14171c; --panel:#1d222b; --panel-2:#242b36; --edge:#333d4b;
|
||||
--bg:#14171c; --bg2:#1b212b; --panel:#1d222b; --panel-2:#242b36; --edge:#333d4b;
|
||||
--txt:#c7d0db; --muted:#7f8b9a; --hot:#ffd166; --led-off:#2b323d;
|
||||
}
|
||||
:root[data-theme="light"] {
|
||||
--bg:#e9edf2; --bg2:#f6f8fb; --panel:#ffffff; --panel-2:#eef2f7; --edge:#d2dae4;
|
||||
--txt:#1e2630; --muted:#5c6776; --hot:#a9760a; --led-off:#dbe2ea;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin:0; padding:24px;
|
||||
background: radial-gradient(circle at 50% -10%, #1b212b, var(--bg));
|
||||
margin:0; padding:24px; min-height:100vh;
|
||||
background: radial-gradient(circle at 50% -10%, var(--bg2), var(--bg));
|
||||
color: var(--txt); font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
h1 { font-size:18px; font-weight:600; letter-spacing:.5px; margin:0 0 2px; }
|
||||
.sub { color:var(--muted); font-size:12px; margin-bottom:18px; }
|
||||
.kbd-legend { color:var(--muted); font-size:11px; font-family:"Courier New",monospace; text-align:right; }
|
||||
.device { max-width:1000px; margin:0 auto; background:linear-gradient(180deg,var(--panel),#171c24);
|
||||
.device { max-width:1000px; margin:0 auto; background:linear-gradient(180deg, var(--panel), var(--bg));
|
||||
border:1px solid var(--edge); border-radius:16px; padding:18px; box-shadow:0 18px 50px rgba(0,0,0,.5); }
|
||||
.row { display:flex; gap:18px; flex-wrap:wrap; }
|
||||
.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; }
|
||||
.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:40px; color:var(--hot); letter-spacing:3px; text-shadow:0 0 12px rgba(255,209,102,.5); }
|
||||
.display .big { font-family:"Courier New",monospace; font-weight:700; font-size:40px; color:#ffd166; letter-spacing:3px; text-shadow:0 0 12px rgba(255,209,102,.5); }
|
||||
.display .ctx { font-family:"Courier New",monospace; font-size:12px; color:#4dd0e1; height:15px; }
|
||||
.knob { margin-bottom:10px; }
|
||||
.knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; }
|
||||
|
|
@ -89,9 +104,24 @@
|
|||
.log-seg { font-size:12px; color:var(--muted); margin:2px 0 0 12px; font-family:"Courier New",monospace; }
|
||||
.practice { border-top:1px solid var(--edge); margin-top:16px; padding-top:4px; }
|
||||
#routineToggle { position:fixed; top:16px; right:16px; z-index:40; background:#2c3a4d; border-color:#3b5168; color:#cfe3ff; font-weight:600; }
|
||||
#routineTray { position:fixed; top:0; right:0; height:100%; width:380px; max-width:92vw; background:linear-gradient(180deg,var(--panel),#171c24); border-left:1px solid var(--edge); box-shadow:-12px 0 40px rgba(0,0,0,.55); transform:translateX(102%); transition:transform .25s ease; z-index:60; padding:18px; overflow:auto; }
|
||||
#routineTray { position:fixed; top:0; right:0; height:100%; width:380px; max-width:92vw; background:linear-gradient(180deg, var(--panel), var(--bg)); border-left:1px solid var(--edge); box-shadow:-12px 0 40px rgba(0,0,0,.45); transform:translateX(102%); transition:transform .25s ease; z-index:60; padding:18px; overflow:auto; }
|
||||
#routineTray.open { transform:translateX(0); }
|
||||
.tray-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; }
|
||||
.practice-col { border-left:1px solid var(--edge); padding-left:18px; }
|
||||
#themeBtn, #helpBtn { padding:4px 11px; }
|
||||
/* --- responsive --- */
|
||||
@media (max-width: 760px) {
|
||||
body { padding: 12px; }
|
||||
.device { padding: 13px; border-radius:12px; }
|
||||
.row { gap: 14px; }
|
||||
/* when the practice column wraps under the others, swap its side rule for a top one */
|
||||
.practice-col { border-left:none; padding-left:0; border-top:1px solid var(--edge); padding-top:12px; margin-top:4px; }
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.kbd-legend { display:none; } /* the ? overlay covers discovery on small screens */
|
||||
#routineTray { width:100%; }
|
||||
.meter-card .led { width:24px; height:24px; }
|
||||
}
|
||||
.num { width:54px; background:var(--panel); color:var(--txt); border:1px solid var(--edge); border-radius:8px; padding:6px 6px; font-size:13px; text-align:center; }
|
||||
#helpBtn { padding:4px 11px; }
|
||||
.overlay { position:fixed; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:80; }
|
||||
|
|
@ -101,9 +131,13 @@
|
|||
.kbd-table td { padding:6px 4px; border-bottom:1px solid var(--edge); }
|
||||
.kbd-table tr:last-child td { border-bottom:none; }
|
||||
.kbd-table td:first-child { width:100px; white-space:nowrap; }
|
||||
kbd { background:var(--panel); border:1px solid var(--edge); border-bottom-width:2px; border-radius:5px; padding:2px 7px; font-family:"Courier New",monospace; font-size:12px; color:#fff; }
|
||||
kbd { background:var(--panel); border:1px solid var(--edge); border-bottom-width:2px; border-radius:5px; padding:2px 7px; font-family:"Courier New",monospace; font-size:12px; color:var(--txt); }
|
||||
.setlist-fields textarea { width:100%; background:var(--panel); color:var(--txt); border:1px solid var(--edge); border-radius:8px; padding:8px; font-size:12px; resize:vertical; min-height:40px; }
|
||||
.play { background:#2e7d32; border-color:#2e7d32; color:#fff; padding:3px 10px; }
|
||||
.stop { background:#c0392b; border-color:#c0392b; color:#fff; padding:3px 10px; }
|
||||
.menu { position:absolute; top:36px; right:0; background:var(--panel-2); border:1px solid var(--edge); border-radius:10px; padding:6px; display:flex; flex-direction:column; gap:4px; box-shadow:0 12px 30px rgba(0,0,0,.5); z-index:70; min-width:150px; }
|
||||
.menu[hidden] { display:none; }
|
||||
.menu button { text-align:left; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -112,6 +146,7 @@
|
|||
<h1 style="margin:0">Stackable Metronome <span class="lane-meta">mockup</span></h1>
|
||||
<div style="display:flex; align-items:center; gap:10px">
|
||||
<span class="kbd-legend">Space play · T tap · ↑↓ tempo (⇧×10) · A add · R set lists · N next · ? help</span>
|
||||
<button id="themeBtn" title="toggle light / dark theme">☀</button>
|
||||
<button id="helpBtn" title="keyboard shortcuts (?)">?</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,15 +175,16 @@
|
|||
<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>
|
||||
|
||||
<div style="flex:1; min-width:215px; border-left:1px solid var(--edge); padding-left:18px">
|
||||
<div class="practice-col" style="flex:1; min-width:215px">
|
||||
<div class="checkrow" style="margin-bottom:8px"><input type="checkbox" id="trainerOn"><label for="trainerOn">Gap / mute trainer</label></div>
|
||||
<div class="row" style="gap:14px; align-items:center">
|
||||
<label style="font-size:12px">Play <input type="number" class="num" id="playBars" min="1" max="16" value="2"></label>
|
||||
<label style="font-size:12px">Mute <input type="number" class="num" id="muteBars" min="0" max="16" value="2"> bars</label>
|
||||
</div>
|
||||
<div class="checkrow" style="margin:10px 0 8px"><input type="checkbox" id="rampOn"><label for="rampOn">Tempo ramp</label></div>
|
||||
<div class="row" style="gap:14px; align-items:center">
|
||||
<label style="font-size:12px"><input type="number" class="num" id="rampAmt" min="-10" max="10" value="2"> BPM</label>
|
||||
<div class="row" style="gap:12px 14px; align-items:center; flex-wrap:wrap">
|
||||
<label style="font-size:12px">from <input type="number" class="num" id="rampStart" min="30" max="300" value="80"> BPM</label>
|
||||
<label style="font-size:12px" title="negative ramps down, positive ramps up"><input type="number" class="num" id="rampAmt" min="-30" max="30" value="5"> BPM</label>
|
||||
<label style="font-size:12px">every <input type="number" class="num" id="rampEvery" min="1" max="16" value="4"> bars</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,9 +195,9 @@
|
|||
<div class="row" style="align-items:center; gap:12px; margin:14px 0 8px">
|
||||
<h2 style="font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0">Meter lanes</h2>
|
||||
<span class="hint" style="margin:0; flex:1">Click a beat pad to toggle it (rest) — e.g. snare on 2 & 4</span>
|
||||
<button class="add" id="addMeterBtn">+ Add meter</button>
|
||||
</div>
|
||||
<div id="meters"></div>
|
||||
<div style="margin-top:10px"><button class="add" id="addMeterBtn">+ Add meter</button></div>
|
||||
|
||||
<!-- (presets moved into the Transport card; set lists live in the slide-out tray) -->
|
||||
|
||||
|
|
@ -172,7 +208,19 @@
|
|||
<!-- Routine slide-out tray (from the right) -->
|
||||
<button id="routineToggle">☰ Routine & Log</button>
|
||||
<aside id="routineTray">
|
||||
<div class="tray-head"><h2 style="margin:0">Set Lists</h2><button class="x" id="routineClose" style="margin-left:0">✕</button></div>
|
||||
<div class="tray-head">
|
||||
<h2 style="margin:0">Set Lists</h2>
|
||||
<div style="display:flex; gap:6px; position:relative">
|
||||
<button class="x" id="trayMenuBtn" title="log & backup" style="margin-left:0">⋯</button>
|
||||
<button class="x" id="routineClose" title="close" style="margin-left:0">✕</button>
|
||||
<div id="trayMenu" class="menu" hidden>
|
||||
<button id="exportBtn">⭳ Export all</button>
|
||||
<button id="importBtn">⭱ Import…</button>
|
||||
<input type="file" id="importFile" accept="application/json" style="display:none">
|
||||
<button id="clearLogBtn">🗑 Clear log</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lane-row" style="margin-bottom:8px">
|
||||
<select class="cmp" id="setlistSelect" style="flex:1"></select>
|
||||
|
|
@ -189,17 +237,9 @@
|
|||
<button id="addItemBtn">+ Add current settings</button>
|
||||
</div>
|
||||
<div id="itemList"></div>
|
||||
<div class="hint" style="margin-top:6px">▶ loads & starts an item (one click) · press <kbd>N</kbd> to advance to the next.</div>
|
||||
<div class="hint" style="margin-top:6px">▶ loads & starts an item · <kbd>N</kbd> advances · 💾 saves current settings back to an item.</div>
|
||||
|
||||
<div class="tray-head" style="margin-top:22px"><h2 style="margin:0">Log & Backup</h2></div>
|
||||
<div class="lane-row" style="margin-bottom:8px; flex-wrap:wrap">
|
||||
<button id="exportBtn">⭳ Export all</button>
|
||||
<button id="importBtn">⭱ Import…</button>
|
||||
<input type="file" id="importFile" accept="application/json" style="display:none">
|
||||
<button id="clearLogBtn">Clear log</button>
|
||||
</div>
|
||||
<div class="hint" style="margin:0 0 8px">Export/Import covers presets, set lists & logs.</div>
|
||||
<div id="logView"></div>
|
||||
<div id="logView" style="margin-top:18px"></div>
|
||||
</aside>
|
||||
|
||||
<div id="shortcutsOverlay" class="overlay" hidden>
|
||||
|
|
@ -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 = '<div class="hint">No items yet — set up the metronome and “Add current settings”.</div>'; 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 = `<button class="play iconbtn" data-act="play" title="load & start">▶</button>
|
||||
row.innerHTML = `<button class="${playing ? "stop" : "play"} iconbtn" data-act="play" title="${playing ? "stop" : "load & start"}">${playing ? "⏹" : "▶"}</button>
|
||||
<span class="nm">${i + 1}. ${it.name}</span>
|
||||
<span class="meta">${it.bpm}bpm · ${it.lanes.map((l) => l.groupsStr).join("/")}</span>
|
||||
<button class="iconbtn" data-act="save" title="save current settings to this item">💾</button>
|
||||
<button class="iconbtn" data-act="up" title="up">↑</button>
|
||||
<button class="iconbtn" data-act="down" title="down">↓</button>
|
||||
<button class="x iconbtn" data-act="del" title="remove">✕</button>`;
|
||||
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 = '<div class="hint">No practice logged yet — play set-list items to record what you do.</div>'; return; }
|
||||
logs.forEach((e) => {
|
||||
const div = document.createElement("div"); div.className = "log-item";
|
||||
div.innerHTML = `<div class="log-head">${new Date(e.at).toLocaleString()} — ${e.name}</div>
|
||||
<div class="log-seg">${fmtDur(e.durationSec)} @ ${e.bpm}bpm · ${(e.lanes || []).map((l) => l.groupsStr + "/" + l.sound).join(", ")}</div>`;
|
||||
box.appendChild(div);
|
||||
});
|
||||
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; }
|
||||
const logs = 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>';
|
||||
else html += logs.map((e) => `<div class="log-seg">${new Date(e.at).toLocaleString()} · ${fmtDur(e.durationSec)} @ ${e.bpm}bpm</div>`).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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue