From 582118abf960138524a31e1127d2c961d4090e7d Mon Sep 17 00:00:00 2001 From: Me Here Date: Sun, 7 Jun 2026 10:35:57 -0500 Subject: [PATCH] pm-mobile: per-lane gain + Save & Library (create/save-as/reorder tracks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Per-lane gain: a volume slider (-18..+6 dB) in the lane dialog β†’ m.gainDb, encoded in the lane token (e.g. kick:4@-6) and saved with the track. - Save & Library sheet (πŸ’Ύ): writes to the SAME store/format as the editor (metronome.setlists), so tracks made on the phone show up in the editor too. * Save current track: name + target set list (or "+ New set list"), "Save as new track" (always) and "Update " for your own tracks β€” Update confirms the overwrite (the iterate-and-resave path is one tap, but a named confirm prevents accidentally clobbering the original when you meant to save a new track). * Manage library: reorder (up/down), rename and delete your set lists and their tracks; built-ins stay read-only (Save-as copies edits out of them). - Built-in/transient set lists can't be overwritten β€” saving promotes the live working copy into one of your own set lists. Engine untouched; conformance suite unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile.html | 135 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 12 deletions(-) diff --git a/mobile.html b/mobile.html index cd20f97..c24a78b 100644 --- a/mobile.html +++ b/mobile.html @@ -145,18 +145,18 @@ /* ---- bottom sheet (lane editor) ---- */ #scrim{ position:fixed; inset:0; background:rgba(0,0,0,.55); opacity:0; pointer-events:none; transition:opacity .2s; z-index:40; } #scrim.open{ opacity:1; pointer-events:auto; } - #laneSheet, #trackSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto; + #laneSheet, #trackSheet, #saveSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto; background:var(--panel-bg); border-top:1px solid var(--panel-bd); border-radius:18px 18px 0 0; transform:translateY(110%); transition:transform .26s cubic-bezier(.2,.8,.2,1); padding:12px max(16px,env(safe-area-inset-right)) max(18px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left)); } - #laneSheet.open, #trackSheet.open{ transform:none; } - #laneSheet .grab, #trackSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; } - #laneSheet h2, #trackSheet h2{ margin:0 0 10px; font-size:16px; } - #laneSheet label, #trackSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; } - #laneSheet select, #trackSheet select, - #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number]{ + #laneSheet.open, #trackSheet.open, #saveSheet.open{ transform:none; } + #laneSheet .grab, #trackSheet .grab, #saveSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; } + #laneSheet h2, #trackSheet h2, #saveSheet h2{ margin:0 0 10px; font-size:16px; } + #laneSheet label, #trackSheet label, #saveSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; } + #laneSheet select, #trackSheet select, #saveSheet select, + #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number], #saveSheet input[type=text]{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:10px; padding:11px; font-size:15px; } - #laneSheet .lrow, #trackSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; } + #laneSheet .lrow, #trackSheet .lrow, #saveSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; align-items:center; } #laneSheet .half, #trackSheet .half{ display:block; flex:1 1 120px; margin:0; } #laneSheet .chk, #trackSheet .chk{ display:flex; align-items:center; gap:8px; font-size:14px; color:var(--txt); margin-top:16px; } #laneSheet .chk input, #trackSheet .chk input{ width:20px; height:20px; accent-color:var(--cyan); flex:0 0 auto; } @@ -164,6 +164,15 @@ .lfoot{ display:flex; justify-content:space-between; align-items:center; margin-top:18px; } .lbtn{ cursor:pointer; color:var(--txt); background:linear-gradient(180deg,var(--btn1),var(--btn2)); border:1px solid var(--btn-bd); border-radius:10px; padding:10px 16px; font-size:14px; } .lbtn.danger{ background:transparent; color:#ff7a7a; border-color:#ff7a7a; } + .seclbl{ font-size:11px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin:4px 0 8px; } + .savemsg{ font-size:12px; color:#5fd08a; align-self:center; } + .liblbl{ font-size:12px; color:var(--muted); margin:14px 0 4px; } + .libhint{ font-size:12px; color:var(--muted); padding:6px 2px; line-height:1.4; } + .librow{ display:flex; align-items:center; gap:4px; padding:4px 0; border-bottom:1px solid var(--panel-bd); } + .librow.active .libname{ color:var(--cyan); font-weight:600; } + .libname{ flex:1 1 auto; min-width:0; text-align:left; background:transparent; border:none; color:var(--txt); font-size:14px; padding:7px 2px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .ibtn{ flex:0 0 auto; width:32px; height:32px; border-radius:7px; background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--txt); font-size:13px; cursor:pointer; } + .ibtn:disabled{ opacity:.3; } /* ---- help tour (coachmarks) ---- */ #tour{ position:fixed; inset:0; z-index:200; display:none; } @@ -186,6 +195,7 @@
πŸ”ˆπŸ”Š
+
πŸ’Ύ
?
◐
β›Ά
@@ -239,6 +249,8 @@
+ +
@@ -271,6 +283,26 @@
+ +
+
+

Save & library

+
Save current track
+ + + + + +
+ + + +
+
Manage library
+
+
+
+
@@ -448,17 +480,20 @@ function addLane(){ meters.push(buildMeters([{groupsStr:"4",stepsPerBeat:1,sound /* lane settings sheet */ (function(){ const sel=$("lsSound"); VOICES.forEach(([k,lab])=>{ const o=document.createElement("option"); o.value=k; o.textContent=lab; sel.appendChild(o); }); })(); +function gainLabel(db){ return (db>0?"+":"")+db+" dB"; } function openLaneSheet(i){ editLaneIdx=i; const m=meters[i]; if(!m) return; $("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsSub").value=String(m.stepsPerBeat); $("lsSwing").checked=!!m.swing; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled; - $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); } -function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("trackSheet").classList.remove("open"); } + $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0); + $("trackSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); } +function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("trackSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); } const closeLaneSheet=closeSheets; function applyLane(){ const m=meters[editLaneIdx]; if(!m) return; let grp=($("lsGroup").value||"").trim()||"4"; rebuildLane(editLaneIdx,{groupsStr:grp,stepsPerBeat:Math.max(1,Math.min(4,parseInt($("lsSub").value,10)||1)),swing:$("lsSwing").checked, - poly:$("lsPoly").checked,enabled:!$("lsMute").checked,sound:$("lsSound").value,gainDb:m.gainDb||0,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()}); + poly:$("lsPoly").checked,enabled:!$("lsMute").checked,sound:$("lsSound").value,gainDb:parseInt($("lsGain").value,10)||0,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()}); laneSig=null; renderAll(); saveState(); } ["lsSound","lsSub","lsSwing","lsPoly","lsMute"].forEach(id=>$(id).addEventListener("change",applyLane)); +$("lsGain").addEventListener("input",()=>{ const m=meters[editLaneIdx]; if(!m) return; m.gainDb=parseInt($("lsGain").value,10)||0; $("lsGainVal").textContent=gainLabel(m.gainDb); saveState(); }); // live, no rebuild $("lsGroup").addEventListener("change",applyLane); $("lsDone").onclick=closeLaneSheet; $("lsDel").onclick=()=>{ if(meters.length<=1){ closeLaneSheet(); return; } meters.splice(editLaneIdx,1); laneSig=null; renderAll(); saveState(); closeLaneSheet(); }; @@ -473,7 +508,7 @@ function openTrackSheet(){ $("tsGap").checked=trainer.on; $("tsGapPlay").value=trainer.playBars||2; $("tsGapMute").value=trainer.muteBars||2; $("tsRampRow").classList.toggle("off",!ramp.on); $("tsGapRow").classList.toggle("off",!trainer.on); $("tsStr").value=currentPatch(); $("tsMsg").textContent=""; - $("laneSheet").classList.remove("open"); $("scrim").classList.add("open"); $("trackSheet").classList.add("open"); + $("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("scrim").classList.add("open"); $("trackSheet").classList.add("open"); } function applyTrackSettings(){ segBars=Math.max(0,parseInt($("tsBars").value,10)||0); @@ -493,6 +528,82 @@ $("tsApply").onclick=()=>{ const txt=($("tsStr").value||"").trim().replace(/^[#? $("tsDone").onclick=closeSheets; $("trackBtn").onclick=openTrackSheet; +/* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */ +function userSetlists(){ return lsGet(LS_SETLISTS,[]); } +function saveUserSetlists(a){ lsSet(LS_SETLISTS,a); savedLists=a; } +function curSetupObj(){ return { bpm:state.bpm, lanes:snapshotLanes(), trainer:{...trainer}, ramp:{...ramp}, countMs:0, bars:segBars, rep:curRep, end:curEnd }; } +function flashSave(msg){ $("saveMsg").textContent=msg; setTimeout(()=>{ if($("saveMsg").textContent===msg) $("saveMsg").textContent=""; },1800); } +function el(tag,cls,txt){ const e=document.createElement(tag); if(cls) e.className=cls; if(txt!=null) e.textContent=txt; return e; } +function ibtn(label,fn,dis){ const b=el("button","ibtn",label); b.disabled=!!dis; b.onclick=(e)=>{ e.stopPropagation(); fn(); }; return b; } + +function selectUserList(i){ const arr=userSetlists(); if(!arr[i]) return; slKey="s"+i; transientTitle=null; + loadSetlistObj({title:arr[i].title,items:(arr[i].items||[]).map(it=>({...it}))}); renderLibrary(); } +function selectUserTrack(i,j){ slKey="s"+i; transientTitle=null; savedLists=userSetlists(); + setlist={title:savedLists[i].title,items:savedLists[i].items.map(it=>({...it}))}; idx=Math.max(0,Math.min(j,setlist.items.length-1)); + loadSetup(setlist.items[idx]); buildSetlistOptions(); buildTrackOptions(); renderAll(); saveState(); } + +function doSaveAsNew(){ + const name=($("saveName").value||"My track").trim(); const arr=userSetlists(); let i; + if($("saveTo").value==="__new"){ const t=($("saveNewName").value||"My set list").trim(); arr.push({title:t,description:"",items:[]}); i=arr.length-1; } + else { i=+$("saveTo").value.slice(1); if(!arr[i]) return; } + arr[i].items.push({name, ...curSetupObj()}); saveUserSetlists(arr); + selectUserTrack(i, arr[i].items.length-1); $("saveNewName").value=""; buildSaveTo(); renderLibrary(); flashSave("Saved βœ“"); +} +function doUpdate(){ + if(slKey[0]!=="s") return; const arr=userSetlists(), i=+slKey.slice(1); if(!arr[i]||!arr[i].items[idx]) return; + const oldName=arr[i].items[idx].name||"this track"; const nm=($("saveName").value||oldName).trim(); + if(!confirm('Overwrite "'+oldName+'" with the current settings?')) return; + arr[i].items[idx]={name:nm, ...curSetupObj()}; saveUserSetlists(arr); + setlist.items[idx]={name:nm, ...curSetupObj()}; lastCur=null; buildTrackOptions(); renderInfo(); renderLibrary(); flashSave("Updated βœ“"); +} +function moveList(i,dir){ const arr=userSetlists(), j=i+dir; if(j<0||j>=arr.length) return; const t=arr[i]; arr[i]=arr[j]; arr[j]=t; saveUserSetlists(arr); + if(slKey==="s"+i) slKey="s"+j; else if(slKey==="s"+j) slKey="s"+i; buildSetlistOptions(); renderLibrary(); saveState(); } +function moveTrack(i,j,dir){ const arr=userSetlists(), sl=arr[i], k=j+dir; if(!sl||k<0||k>=sl.items.length) return; const t=sl.items[j]; sl.items[j]=sl.items[k]; sl.items[k]=t; saveUserSetlists(arr); + if(slKey==="s"+i){ if(idx===j) idx=k; else if(idx===k) idx=j; setlist.items=sl.items.map(it=>({...it})); buildTrackOptions(); renderInfo(); } renderLibrary(); saveState(); } +function renameList(i){ const arr=userSetlists(); if(!arr[i]) return; const n=prompt("Rename set list:",arr[i].title||""); if(n==null) return; arr[i].title=n.trim()||arr[i].title; saveUserSetlists(arr); + if(slKey==="s"+i&&setlist) setlist.title=arr[i].title; buildSetlistOptions(); renderLibrary(); saveState(); } +function renameTrack(i,j){ const arr=userSetlists(); if(!arr[i]||!arr[i].items[j]) return; const n=prompt("Rename track:",arr[i].items[j].name||""); if(n==null) return; arr[i].items[j].name=n.trim()||arr[i].items[j].name; saveUserSetlists(arr); + if(slKey==="s"+i){ setlist.items[j].name=arr[i].items[j].name; if(idx===j){ lastCur=null; } buildTrackOptions(); renderInfo(); } renderLibrary(); saveState(); } +function deleteTrack(i,j){ const arr=userSetlists(), sl=arr[i]; if(!sl||!sl.items[j]) return; if(!confirm('Delete track "'+(sl.items[j].name||"")+'"?')) return; sl.items.splice(j,1); saveUserSetlists(arr); + if(slKey==="s"+i){ if(!sl.items.length){ deleteListResolved(i); return; } if(idx>=sl.items.length) idx=sl.items.length-1; else if(idx>j) idx--; selectUserTrack(i,idx); } renderLibrary(); } +function deleteList(i){ const arr=userSetlists(); if(!arr[i]) return; if(!confirm('Delete set list "'+(arr[i].title||"")+'" and all its tracks?')) return; deleteListResolved(i); } +function deleteListResolved(i){ const arr=userSetlists(); arr.splice(i,1); saveUserSetlists(arr); + if(slKey==="s"+i){ slKey="b0"; setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); buildSetlistOptions(); buildTrackOptions(); renderAll(); saveState(); } + else if(slKey[0]==="s"){ const k=+slKey.slice(1); if(k>i) slKey="s"+(k-1); buildSetlistOptions(); } + buildSaveTo(); renderLibrary(); } +function newList(){ const n=prompt("New set list name:","My set list"); if(n==null) return; const arr=userSetlists(); arr.push({title:n.trim()||"My set list",description:"",items:[]}); saveUserSetlists(arr); buildSaveTo(); renderLibrary(); } + +function buildSaveTo(){ savedLists=userSetlists(); const sel=$("saveTo"); sel.innerHTML=""; + savedLists.forEach((sl,i)=>sel.appendChild(opt("s"+i,(sl.title||("Set list "+(i+1)))+" ("+(sl.items?sl.items.length:0)+")"))); + sel.appendChild(opt("__new","βž• New set list…")); + sel.value = slKey[0]==="s" ? slKey : (savedLists.length?"s0":"__new"); + $("saveNewName").style.display = sel.value==="__new" ? "block":"none"; } +$("saveTo").onchange=()=>{ $("saveNewName").style.display = $("saveTo").value==="__new" ? "block":"none"; }; + +function renderLibrary(){ savedLists=userSetlists(); const box=$("libBody"); box.innerHTML=""; + box.appendChild(el("div","liblbl","Your set lists")); + if(!savedLists.length) box.appendChild(el("div","libhint","None yet β€” β€œSave as new track” creates one.")); + savedLists.forEach((sl,i)=>{ const row=el("div","librow"+(slKey==="s"+i?" active":"")); + const nm=el("button","libname",(sl.title||"set list")+" ("+(sl.items?sl.items.length:0)+")"); nm.onclick=()=>selectUserList(i); row.appendChild(nm); + row.appendChild(ibtn("↑",()=>moveList(i,-1),i===0)); row.appendChild(ibtn("↓",()=>moveList(i,1),i===savedLists.length-1)); + row.appendChild(ibtn("✎",()=>renameList(i))); row.appendChild(ibtn("πŸ—‘",()=>deleteList(i))); box.appendChild(row); }); + const addL=el("button","addlane","βž• New set list"); addL.onclick=newList; box.appendChild(addL); + if(slKey[0]==="s"){ const i=+slKey.slice(1), sl=savedLists[i]; if(sl){ + box.appendChild(el("div","liblbl","Tracks in β€œ"+(sl.title||"set list")+"”")); + sl.items.forEach((it,j)=>{ const row=el("div","librow"+(idx===j?" active":"")); + const nm=el("button","libname",(j+1)+". "+(it.name||"track")); nm.onclick=()=>{ gotoItem(j,state.running); renderLibrary(); }; row.appendChild(nm); + row.appendChild(ibtn("↑",()=>moveTrack(i,j,-1),j===0)); row.appendChild(ibtn("↓",()=>moveTrack(i,j,1),j===sl.items.length-1)); + row.appendChild(ibtn("✎",()=>renameTrack(i,j))); row.appendChild(ibtn("πŸ—‘",()=>deleteTrack(i,j))); box.appendChild(row); }); + const addT=el("button","addlane","βž• Add current track here"); addT.onclick=()=>{ $("saveTo").value="s"+i; doSaveAsNew(); }; box.appendChild(addT); + }} else { box.appendChild(el("div","libhint","This set list is built-in (read-only). β€œSave as new track” copies your edits into one of your own set lists.")); } +} +function openSaveSheet(){ + $("saveName").value=currentName()||"My track"; buildSaveTo(); + const upd=$("saveUpd"); if(slKey[0]==="s"){ upd.style.display=""; upd.textContent='Update β€œ'+(currentName()||"track")+'”'; } else upd.style.display="none"; + $("saveMsg").textContent=""; renderLibrary(); + $("laneSheet").classList.remove("open"); $("trackSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); } +$("saveNew").onclick=doSaveAsNew; $("saveUpd").onclick=doUpdate; $("saveDone").onclick=closeSheets; $("saveBtn").onclick=openSaveSheet; + /* ========================= SET-LIST / TRACK DROPDOWNS ======================== */ function opt(v,t){ const o=document.createElement("option"); o.value=v; o.textContent=t; return o; } function og(label){ const g=document.createElement("optgroup"); g.label=label; return g; }