NAM hero fills screen, pedal-style FX blocks below

This commit is contained in:
2026-06-15 06:03:46 +00:00
parent a69aee357c
commit 0fa8bd387a
+227 -33
View File
@@ -24,20 +24,39 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
/* Main content */
.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:4px 6px;gap:4px}
/* FX Chain */
.fx-strip{display:flex;gap:6px;overflow-x:auto;overflow-y:hidden;flex-shrink:0;padding:4px 0;min-height:72px;scrollbar-width:none;-ms-overflow-style:none}
.fx-strip::-webkit-scrollbar{display:none}
.fx-block{flex-shrink:0;width:100px;border-radius:var(--radius);background:var(--surface);border:1.5px solid var(--border);padding:4px 6px;display:flex;flex-direction:column;cursor:pointer;transition:all .15s;position:relative}
.fx-block:active{transform:scale(.96)}
.fx-block.on{border-color:var(--green);background:rgba(80,208,144,.06)}
.fx-block.off{opacity:.4}
.fx-block-add{flex-shrink:0;width:72px;border-radius:var(--radius);border:1.5px dashed var(--border);background:transparent;display:flex;align-items:center;justify-content:center;font-size:22px;color:var(--text-dim);cursor:pointer;transition:all .15s}
.fx-block-add:active{background:var(--surface);color:var(--amber);border-color:var(--amber)}
.fx-type{font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:1px}
.fx-name{font-size:10px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.fx-indicator{display:flex;gap:2px;margin-top:auto;padding-top:2px}
.fx-dot{width:6px;height:6px;border-radius:50%;background:var(--border)}
.fx-dot.active{background:var(--amber)}
.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:3px 5px;gap:3px}
/* NAM Hero — fills available vertical space */
.nam-hero{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--surface);border-radius:12px;border:2px solid var(--border);position:relative;overflow:hidden;min-height:100px;cursor:pointer;transition:all .15s}
.nam-hero:active{transform:scale(.98)}
.nam-hero .nam-badge{position:absolute;top:4px;left:6px;font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:3px;background:rgba(232,160,48,.15);color:var(--amber)}
.nam-hero .nam-name{font-size:20px;font-weight:700;text-align:center;padding:0 12px;line-height:1.15;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;max-width:90%}
.nam-hero .nam-detail{font-size:10px;color:var(--text2);margin-top:2px;font-variant-numeric:tabular-nums}
.nam-hero .nam-led{position:absolute;top:4px;right:6px;width:10px;height:10px;border-radius:50%;box-shadow:0 0 6px}
.nam-hero .nam-led.on{background:var(--green);box-shadow:0 0 8px var(--green)}
.nam-hero .nam-led.off{background:var(--red);box-shadow:0 0 8px var(--red)}
.nam-hero.empty{border-style:dashed;border-color:var(--border);opacity:.5}
.nam-hero.empty .nam-name{font-size:14px;color:var(--text3)}
/* Pedal row — stompbox style FX blocks */
.pedal-row{display:flex;gap:5px;overflow-x:auto;overflow-y:hidden;flex-shrink:0;padding:2px 0;min-height:80px;scrollbar-width:none;-ms-overflow-style:none}
.pedal-row::-webkit-scrollbar{display:none}
.pedal{flex-shrink:0;width:80px;border-radius:8px;background:var(--card);border:2px solid var(--border);display:flex;flex-direction:column;align-items:center;cursor:pointer;transition:all .12s;position:relative;overflow:hidden;min-height:76px}
.pedal:active{transform:scale(.93)}
.pedal.on{border-color:var(--green)}
.pedal.off{opacity:.55;filter:saturate(.4)}
.pedal .pd-led{width:8px;height:8px;border-radius:50%;margin-top:5px;flex-shrink:0}
.pedal .pd-led.on{background:var(--green);box-shadow:0 0 5px var(--green)}
.pedal .pd-led.off{background:var(--red)}
.pedal .pd-icon{font-size:13px;margin-top:3px;line-height:1}
.pedal .pd-label{font-size:8px;font-weight:600;text-align:center;padding:0 2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%;flex:1;display:flex;align-items:center;justify-content:center}
.pedal .pd-switch{width:28px;height:14px;border-radius:7px;margin-bottom:4px;flex-shrink:0;position:relative;transition:background .15s}
.pedal .pd-switch.on{background:var(--green);box-shadow:0 0 4px var(--green)}
.pedal .pd-switch.off{background:var(--border)}
.pedal .pd-switch::after{content:'';position:absolute;top:2px;left:2px;width:10px;height:10px;border-radius:50%;background:var(--text);transition:transform .15s}
.pedal .pd-switch.on::after{transform:translateX(14px)}
.pedal .pd-add{display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-size:20px;color:var(--text3);border:2px dashed var(--border);border-radius:8px}
.pedal .pd-add:active{color:var(--amber);border-color:var(--amber)}
/* Volume */
.vol-row{display:flex;align-items:center;gap:8px;flex-shrink:0;min-height:36px;padding:0 4px}
@@ -113,11 +132,20 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
.wifi-connect-pw input:focus{border-color:var(--amber)}
.wifi-connect-pw button{min-width:52px;min-height:36px;border-radius:6px;border:1px solid var(--amber);background:var(--amber);color:#000;font-family:var(--font);font-size:11px;font-weight:700;cursor:pointer}
.audio-profile-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.ap-card{border-radius:6px;border:1px solid var(--border);background:var(--surface);padding:6px;cursor:pointer;text-align:center;transition:all .12s;min-height:40px}
.ap-card:active{transform:scale(.97)}
.ap-card.active{border-color:var(--amber);background:rgba(232,160,48,.1)}
.ap-card .ap-key{font-size:10px;font-weight:700;text-transform:capitalize}
.ap-card .ap-desc{font-size:8px;color:var(--text-dim);margin-top:1px}
.ap-card{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:4px 6px;cursor:pointer;transition:all .12s;text-align:center}
.ap-card:active{transform:scale(.95)}
.ap-card.active{border-color:var(--amber);background:rgba(232,160,48,.08)}
.ap-card .ap-key{font-size:9px;font-weight:600;text-transform:capitalize;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ap-card .ap-desc{font-size:8px;color:var(--text2)}
.ap-card.loading{opacity:.5;pointer-events:none}
.ap-card.loading .ap-key::after{content:' ⟳';animation:spin .6s linear infinite;display:inline-block}
/* Loading overlay */
.loading-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:300;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px}
.loading-overlay .spinner{width:28px;height:28px;border:3px solid var(--border);border-top-color:var(--amber);border-radius:50%;animation:spin .6s linear infinite}
.loading-overlay .load-msg{font-size:12px;font-weight:600;color:var(--text2)}
@keyframes spin{to{transform:rotate(360deg)}}
.snap-grid{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px}
.snap-slot{border-radius:6px;border:1px solid var(--border);background:var(--surface);padding:6px 3px;cursor:pointer;text-align:center;transition:all .12s;min-height:40px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.snap-slot:active{transform:scale(.95)}
@@ -146,8 +174,16 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<!-- Main Content -->
<div class="main-content">
<div class="fx-strip" id="fxStrip">
<div class="fx-block-add" id="btnAddBlock">+</div>
<div class="nam-hero empty" id="namHero" onclick="switchTab('FX')">
<div class="nam-badge" id="namBadge">NAM</div>
<div class="nam-name" id="namName">No Model Loaded</div>
<div class="nam-detail" id="namDetail"></div>
<div class="nam-led off" id="namLed"></div>
</div>
<div class="pedal-row" id="pedalRow">
<div class="pedal" id="btnAddPedal" onclick="showAddBlock()">
<div class="pd-add">+</div>
</div>
</div>
<div class="vol-row">
<span class="vol-label">Vol</span>
@@ -224,6 +260,44 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<div class="setting-row"><span class="sr-label">🔄 Channel Mode</span><span class="sr-value" id="channelModeLabel">Dual Mono</span><button class="sr-action" id="channelModeToggle">Toggle</button></div>
<div class="setting-row"><span class="sr-label">🔗 Routing</span><span class="sr-value" id="routingModeLabel">4CM</span><button class="sr-action" id="routingModeToggle">Toggle</button></div>
</div>
<div class="setting-group"><div class="setting-group-title">🎚 Audio Interface</div>
<div class="setting-row"><span class="sr-label">🔌 Input Device</span><select id="audioInputDevice" style="flex:1;min-width:0;font-size:10px;padding:4px 6px;min-height:30px" onchange="setAudioDevice(this.value)"></select></div>
<div class="ch-card" id="ch1Card" style="background:var(--card2);border-radius:8px;padding:6px 8px;margin:4px 0;border:2px solid rgba(232,160,48,.25)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<span style="font-size:11px;font-weight:700;color:var(--amber)">🔴 CH 1 — Input 1/L</span>
<button id="ch1Power" onclick="toggleChPower(0)" style="width:32px;height:32px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;background:rgba(58,184,122,.2);border:1px solid rgba(58,184,122,.4);color:var(--green)"></button>
</div>
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:10px;color:var(--text2);flex-shrink:0">Instrument:</span>
<select id="ch1Instrument" onchange="setChInstrument(0,this.value)" style="flex:1;min-width:0;font-size:10px;padding:3px 6px;min-height:28px">
<option value="guitar">🎸 Guitar</option>
<option value="bass">🎸 Bass</option>
<option value="keys">🎹 Keys</option>
<option value="vocals">🎤 Vocals</option>
<option value="backing_tracks">📻 Backing</option>
</select>
</div>
<div style="font-size:9px;color:var(--text2);margin-top:2px">Output: Main L/R (Scarlett 1+2)</div>
</div>
<div class="ch-card" id="ch2Card" style="background:var(--card2);border-radius:8px;padding:6px 8px;margin:4px 0;border:2px solid rgba(58,184,122,.25)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<span style="font-size:11px;font-weight:700;color:var(--green)">🟢 CH 2 — Input 2/R</span>
<button id="ch2Power" onclick="toggleChPower(1)" style="width:32px;height:32px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;background:rgba(58,184,122,.2);border:1px solid rgba(58,184,122,.4);color:var(--green)"></button>
</div>
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:10px;color:var(--text2);flex-shrink:0">Instrument:</span>
<select id="ch2Instrument" onchange="setChInstrument(1,this.value)" style="flex:1;min-width:0;font-size:10px;padding:3px 6px;min-height:28px">
<option value="guitar">🎸 Guitar</option>
<option value="bass">🎸 Bass</option>
<option value="keys">🎹 Keys</option>
<option value="vocals">🎤 Vocals</option>
<option value="backing_tracks">📻 Backing</option>
</select>
</div>
<div style="font-size:9px;color:var(--text2);margin-top:2px">Output: Main L/R (Scarlett 1+2)</div>
</div>
<div style="font-size:9px;color:var(--text3);padding:2px 0">Dual Mono: each channel has independent presets. Same instrument = shared presets.</div>
</div>
<div class="setting-group"><div class="setting-group-title">Bluetooth</div>
<div class="setting-row"><span class="sr-label">🔵 Status</span><span class="sr-value" id="btStatus"></span></div>
<div class="setting-row"><span class="sr-label">🎛 MIDI</span><span class="sr-value" id="btMidiStatus">Off</span><button class="sr-action" id="btMidiToggle">Enable</button></div>
@@ -332,6 +406,59 @@ function cycleInstrument(){
localStorage.setItem('pedalCurrentChannel',currentChannel);
toast(currentChannel);
}
// Loading overlay
function showLoading(msg){
let el=document.getElementById('loadingOverlay');
if(!el){
el=document.createElement('div');el.id='loadingOverlay';el.className='loading-overlay';
el.innerHTML='<div class=\"spinner\"></div><div class=\"load-msg\"></div>';
document.body.appendChild(el);
}
el.querySelector('.load-msg').textContent=msg||'Loading...';
el.style.display='flex';
}
function hideLoading(){
const el=document.getElementById('loadingOverlay');
if(el)el.style.display='none';
}
// Audio Interface helpers
async function loadAudioDevices(){
const sel=document.getElementById('audioInputDevice');
if(!sel)return;
const devs=await API.listAudioDevices();
sel.innerHTML=(devs||[]).map(d=>'<option value="'+d.id+'">'+d.name+'</option>').join('')||'<option value="">No devices</option>';
}
function setChInstrument(idx,val){
const arr=JSON.parse(localStorage.getItem('chInstruments')||'["guitar","bass"]');
arr[idx]=val;
localStorage.setItem('chInstruments',JSON.stringify(arr));
API.saveSettings({['ch'+(idx+1)+'_instrument']:val}).catch(()=>{});
}
function toggleChPower(idx){
const btn=document.getElementById('ch'+(idx+1)+'Power');
const on=btn.textContent==='⚡';
btn.textContent=on?'⭕':'⚡';
btn.style.background=on?'rgba(160,160,188,.1)':'rgba(58,184,122,.2)';
btn.style.borderColor=on?'rgba(160,160,188,.2)':'rgba(58,184,122,.4)';
btn.style.color=on?'var(--text3)':'var(--green)';
API.bypassToggle(channels[idx]||'guitar').catch(()=>{});
}
async function setAudioDevice(devId){
if(!devId)return;
showLoading('Switching audio device...');
await API.saveSettings({input_device:devId,output_device:devId}).catch(()=>{});
hideLoading();
toast('Device: '+devId);
}
function loadChInstrumentState(){
const arr=JSON.parse(localStorage.getItem('chInstruments')||'["guitar","bass"]');
['ch1Instrument','ch2Instrument'].forEach((id,i)=>{
const el=document.getElementById(id);
if(el)el.value=arr[i]||(i===0?'guitar':'bass');
});
}
function toast(msg,t){const e=$('#toast');e.textContent=msg;e.classList.add('show');clearTimeout(e._h);e._h=setTimeout(()=>e.classList.remove('show'),t||2000);}
// ═══ Overlay Controller ═══
@@ -372,7 +499,7 @@ async function loadState(){
if(s.bypass){fs.className='footswitch off';fs.textContent='MUTE';}
else{fs.className='footswitch on';fs.textContent='BYpass';}
document.getElementById('cpuLabel').textContent=(s.cpu_percent!=null?s.cpu_percent:'—')+'%';
renderFxBlocks();
renderNamHero();
const tb=document.getElementById('tabTuner');
if(s.tuner_enabled){tb.querySelector('.tab-label').textContent='Tuning';tb.classList.add('active');}
else{tb.querySelector('.tab-label').textContent='Tuner';tb.classList.remove('active');}
@@ -381,20 +508,79 @@ async function loadState(){
// ═══ FX Blocks ═══
const BLOCK_COLORS={nam_amp:'#E8A030',ir:'#4090E0',gate:'#50D090',compressor:'#E0E060',eq:'#A080E0',reverb:'#60C0E0',delay:'#80D0A0',distortion:'#E06060'};
function renderFxBlocks(){
const strip=document.getElementById('fxStrip');
const FX_ICONS={nam_amp:'🎛',nam:'🎛',overdrive:'⚡',distortion:'💥',delay:'⏳',reverb:'🌊',chorus:'🌀',compressor:'📈',eq:'📊',cabinet:'📦',ir:'📦',volume:'🔊',gate:'🚪',tremolo:'〰️',phaser:'🌀',flanger:'🌊',boost:'🚀',pitch:'🎵',filter:'🔀'};
const FX_COLORS={nam_amp:'#E0A040',nam:'#E0A040',overdrive:'#E8A030',distortion:'#E04040',delay:'#3090E0',reverb:'#40B0A0',chorus:'#50C080',compressor:'#D09040',eq:'#7080D0',cabinet:'#D07090',ir:'#D07090',volume:'#8888A0',gate:'#80A0C0',tremolo:'#C080D0',phaser:'#40B0D0',flanger:'#60A0E0',boost:'#E8A030',pitch:'#B080D0',filter:'#80C0A0'};
function renderNamHero(){
const hero=document.getElementById('namHero');
const nameEl=document.getElementById('namName');
const detailEl=document.getElementById('namDetail');
const ledEl=document.getElementById('namLed');
const badgeEl=document.getElementById('namBadge');
const blocks=state.blocks||[];
const addBtn=document.getElementById('btnAddBlock');
strip.innerHTML='';
strip.appendChild(addBtn);
blocks.forEach((b,i)=>{
const div=document.createElement('div');
const namBlock=blocks.find(b=>b.fx_type==='nam_amp'||b.fx_type==='nam');
if(namBlock){
hero.className='nam-hero';
hero.style.borderColor=FX_COLORS[namBlock.fx_type]||'var(--border)';
badgeEl.textContent=(namBlock.fx_type||'NAM').toUpperCase();
badgeEl.style.background=(FX_COLORS[namBlock.fx_type]||'#E0A040')+'22';
badgeEl.style.color=FX_COLORS[namBlock.fx_type]||'#E0A040';
nameEl.textContent=namBlock.name||namBlock.nam_model_name||'NAM Model';
const enabled=namBlock.enabled!==false;
ledEl.className='nam-led '+(enabled?'on':'off');
ledEl.style.background=enabled?'var(--green)':'var(--red)';
const detailParts=[];
if(namBlock.nam_model_name)detailParts.push(namBlock.nam_model_name);
detailParts.push(enabled?'ON':'OFF');
if(state.nam_loaded)detailParts.push('loaded');
detailEl.textContent=detailParts.join(' · ')||'—';
}else{
hero.className='nam-hero empty';
hero.style.borderColor='';
nameEl.textContent='No NAM Model';
detailEl.textContent='Tap to add amp block';
ledEl.className='nam-led off';
badgeEl.textContent='NAM';
badgeEl.style='';
}
renderPedals();
}
function renderPedals(){
const row=document.getElementById('pedalRow');
const addBtn=document.getElementById('btnAddPedal');
row.innerHTML='';
row.appendChild(addBtn);
const nonNam=(state.blocks||[]).filter(b=>b.fx_type!=='nam_amp'&&b.fx_type!=='nam');
if(nonNam.length===0){
const empty=document.createElement('div');
empty.style.cssText='flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:9px;color:var(--text3);padding:0 8px;min-height:76px';
empty.textContent='No FX pedals — tap + to add';
row.insertBefore(empty,addBtn);
}
(state.blocks||[]).forEach((b,i)=>{
if(b.fx_type==='nam_amp'||b.fx_type==='nam')return; // NAM shown in hero
const en=b.enabled!==false&&!b.bypass;
div.className='fx-block '+(en?'on':'off');
div.innerHTML='<div class="fx-type">'+(b.fx_type||'fx')+'</div><div class="fx-name">'+(b.name||b.fx_type||'Block')+'</div><div class="fx-indicator">'+[...Array(4)].map((_,j)=>'<div class="fx-dot'+(j<3?' active':'')+'"></div>').join('')+'</div>';
strip.insertBefore(div,addBtn);
const type=b.fx_type||'fx';
const name=b.name||b.fx_type||'Block';
const color=FX_COLORS[type]||'#E8A030';
const icon=FX_ICONS[type]||'🎛';
const pedal=document.createElement('div');
pedal.className='pedal '+(en?'on':'off');
pedal.style.borderColor=en?color:'var(--border)';
pedal.innerHTML='<div class="pd-led '+(en?'on':'off')+'" style="box-shadow:'+(en?'0 0 5px '+color:'none')+'"></div><div class="pd-icon">'+icon+'</div><div class="pd-label">'+name+'</div><div class="pd-switch '+(en?'on':'off')+'"></div>';
pedal.onclick=()=>{toggleBlock(i);};
row.insertBefore(pedal,addBtn);
});
}
async function toggleBlock(idx){
const blocks=state.blocks||[];
const b=blocks[idx];
if(!b)return;
const newEn=!(b.enabled!==false);
await API.toggleBlock(currentChannel,b.block_id||idx,newEn).catch(()=>{});
loadState();
}
function showAddBlock(){openOverlay('Downloads');}
// ═══ Presets ═══
async function loadPresets(){
@@ -481,7 +667,14 @@ async function loadAudioProfiles(){
const d=document.createElement('div');
d.className='ap-card'+(p.key===cp?' active':'');
d.innerHTML='<div class="ap-key">'+p.key.replace(/_/g,' ')+'</div><div class="ap-desc">'+p.period+'fr / '+p.rate+'Hz</div>'+((p.key===cp)?'<div style="font-size:8px;color:var(--amber)">● Active</div>':'');
d.onclick=()=>{API.setAudioProfile(p.key).then(()=>{toast('Profile: '+p.key);loadAudioProfiles();}).catch(e=>toast(e.message));};
d.onclick=async()=>{
d.classList.add('loading');
try{
await API.setAudioProfile(p.key);
toast('Profile: '+p.key);
loadAudioProfiles();
}catch(e){toast(e.message);d.classList.remove('loading');}
};
grid.appendChild(d);
});
document.getElementById('audioProfileLabel').textContent=cp;
@@ -606,6 +799,7 @@ function init(){
openOverlay('Settings');
await loadAudioProfiles();await loadBtStatus();await loadBtMidi();await loadHotspotStatus();
await loadNetworkInfo();await loadSystemInfo();await doWifiScan();
loadAudioDevices();loadChInstrumentState();
};
// Close all overlays