4a6e7900ce
The CPU label in the UI was showing psutil.cpu_percent(interval=None) which returns total system CPU — barely moves and doesn't change when the NAM model is bypassed. Fix: - Added _calc_nam_cpu() that computes NAM engine CPU from the engine's own per-block inference timing (avg_inference_ms) vs the JACK callback interval. When the NAM block is bypassed in the chain, returns 0. - Added 'nam_cpu' field to /api/state response - Status bar shows NAM CPU % next to VU meters (drops to 0 on bypass) - Settings CPU row shows NAM CPU with system CPU in dim text beside it - Changed psutil.cpu_percent(interval=None) → interval=0.2 for a real system CPU reading
1256 lines
74 KiB
HTML
1256 lines
74 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
|
|
<title>Pedal v2</title>
|
|
<style>
|
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
|
:root{--bg:#0A0A0E;--surface:#16161E;--panel:#1E1E2A;--border:#2A2A3E;--text:#EAEAFF;--text-dim:#6A6A8A;--text-sec:#9A9AB0;--amber:#E8A030;--green:#50D090;--red:#E05050;--blue:#4090E0;--radius:10px;--font:'Space Grotesk','Inter',system-ui,-apple-system,sans-serif}
|
|
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--font);overflow:hidden;touch-action:manipulation}
|
|
body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webkit-user-select:none}
|
|
|
|
/* Status Bar */
|
|
.status-bar{display:flex;align-items:center;gap:6px;padding:3px 8px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0;min-height:34px}
|
|
.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
.status-dot.on{background:var(--green);box-shadow:0 0 6px var(--green)}
|
|
.status-dot.off{background:var(--red)}
|
|
.status-channel{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--amber);padding:2px 6px;border-radius:4px;background:rgba(232,160,48,.12);white-space:nowrap}
|
|
.status-preset{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:600;min-width:0}
|
|
.status-vol{font-size:10px;font-weight:700;color:var(--amber);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
.vu-pair{display:flex;align-items:center;gap:2px;margin-left:4px;flex-shrink:0}
|
|
.vu-track{width:28px;height:5px;background:rgba(255,255,255,.08);border-radius:3px;position:relative;overflow:hidden}
|
|
.vu-fill{position:absolute;top:0;left:0;height:100%;border-radius:3px;transition:width .04s linear}
|
|
.vu-fill.in{background:var(--green)}
|
|
.vu-fill.out{background:var(--amber)}
|
|
.vu-lbl{font-size:6px;color:var(--text-dim);letter-spacing:.03em;line-height:1;width:10px;text-align:right}
|
|
.status-btn{width:40px;height:40px;border:none;background:transparent;color:var(--text-dim);font-size:16px;border-radius:6px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:all .15s}
|
|
.status-btn:active{background:var(--border);color:var(--amber)}
|
|
|
|
/* Main content */
|
|
.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:4px 6px;gap:4px}
|
|
|
|
.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}
|
|
.vol-label{font-size:9px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em;width:44px;flex-shrink:0}
|
|
.vol-track{flex:1;height:36px;display:flex;align-items:center;cursor:pointer;touch-action:none;position:relative}
|
|
.vol-track-bg{width:100%;height:6px;border-radius:3px;background:var(--border);overflow:hidden;position:relative}
|
|
.vol-track-fill{height:100%;border-radius:3px;background:var(--amber);transition:width .05s linear}
|
|
.vol-track-thumb{position:absolute;top:50%;transform:translate(-50%,-50%);width:24px;height:24px;border-radius:50%;background:var(--amber);border:2px solid var(--bg);box-shadow:0 1px 6px rgba(0,0,0,.5);pointer-events:none}
|
|
.vol-pct{font-size:12px;font-weight:700;color:var(--amber);min-width:38px;text-align:right;font-variant-numeric:tabular-nums}
|
|
|
|
/* Bottom Tab Bar */
|
|
.tab-bar{display:flex;flex-shrink:0;background:var(--panel);border-top:1px solid var(--border);min-height:44px}
|
|
.tab-btn{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;border:none;background:transparent;color:var(--text-dim);cursor:pointer;padding:4px 2px;transition:all .1s;min-height:44px}
|
|
.tab-btn .tab-icon{font-size:14px;line-height:1}
|
|
.tab-btn .tab-label{font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
|
.tab-btn.active{color:var(--amber);background:rgba(232,160,48,.08)}
|
|
.tab-btn:active{opacity:.7}
|
|
|
|
/* Footswitch */
|
|
.footswitch-row{flex-shrink:0;padding:3px 6px}
|
|
.footswitch{width:100%;min-height:64px;border-radius:12px;border:none;font-family:var(--font);font-size:16px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;transition:all .12s;display:flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 3px 16px rgba(0,0,0,.4)}
|
|
.footswitch:active{transform:scale(.97)}
|
|
.footswitch.on{background:linear-gradient(180deg,#60D898,#30A068);color:#003020}
|
|
.footswitch.off{background:linear-gradient(180deg,#E86868,#B03030);color:#200000}
|
|
|
|
/* Overlays */
|
|
.overlay{position:fixed;inset:0;z-index:100;background:var(--bg);display:none;flex-direction:column;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
|
|
.overlay.open{display:flex}
|
|
.overlay-hdr{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0;min-height:38px}
|
|
.overlay-title{font-size:13px;font-weight:700}
|
|
.overlay-close{width:38px;height:38px;border:none;background:transparent;color:var(--text-dim);font-size:16px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
|
.overlay-close:active{background:var(--border)}
|
|
.overlay-body{flex:1;overflow-y:auto;padding:6px 10px}
|
|
|
|
/* Preset Grid */
|
|
.preset-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:6px 0}
|
|
.preset-card{border-radius:var(--radius);border:1.5px solid var(--border);background:var(--surface);padding:10px 8px;cursor:pointer;transition:all .12s;min-height:52px;display:flex;flex-direction:column;gap:2px}
|
|
.preset-card:active{transform:scale(.97);border-color:var(--amber)}
|
|
.preset-card.active{border-color:var(--amber);background:rgba(232,160,48,.08)}
|
|
.preset-card .pc-num{font-size:9px;font-weight:700;color:var(--text-dim)}
|
|
.preset-card .pc-name{font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
|
|
/* Tuner */
|
|
.tuner-view{position:fixed;inset:0;z-index:90;background:var(--bg);display:none;flex-direction:column;align-items:center;justify-content:center}
|
|
.tuner-view.open{display:flex}
|
|
.tuner-note{font-size:64px;font-weight:800;color:var(--text);line-height:1;margin-bottom:4px}
|
|
.tuner-cents{height:44px;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px}
|
|
.tuner-cents-bar{width:100px;height:6px;border-radius:3px;background:var(--border);overflow:hidden;position:relative}
|
|
.tuner-cents-fill{height:100%;border-radius:3px;position:absolute}
|
|
.tuner-cents-fill.in-tune{background:var(--green);left:calc(50% - 3px);width:6px}
|
|
.tuner-cents-fill.sharp{background:var(--red)}
|
|
.tuner-cents-fill.flat{background:var(--blue)}
|
|
.tuner-freq{font-size:11px;color:var(--text-dim);margin-bottom:12px}
|
|
.tuner-exit{width:60%;max-width:260px;min-height:50px;border-radius:var(--radius);border:1.5px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:14px;font-weight:700;cursor:pointer}
|
|
.tuner-exit:active{background:var(--panel);border-color:var(--amber)}
|
|
|
|
/* Settings */
|
|
.setting-group{margin-bottom:12px}
|
|
.setting-group-title{font-size:10px;font-weight:700;color:var(--text-sec);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;padding:0 4px}
|
|
/* Settings section nav */
|
|
.settings-nav{display:flex;gap:6px;overflow-x:auto;padding:6px 10px 8px;flex-shrink:0;scrollbar-width:none;-ms-overflow-style:none;background:var(--bg);border-bottom:1px solid var(--border)}
|
|
.settings-nav::-webkit-scrollbar{display:none}
|
|
.sn-btn{flex-shrink:0;padding:8px 16px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text-dim);font-family:var(--font);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:all .12s;min-height:38px}
|
|
.sn-btn:active{background:var(--panel);transform:scale(.95)}
|
|
.sn-btn.active{border-color:var(--amber);color:var(--amber);background:rgba(232,160,48,.08)}
|
|
.setting-row{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);margin-bottom:3px;min-height:42px}
|
|
.setting-row .sr-label{font-size:11px;font-weight:600}
|
|
.setting-row .sr-value{font-size:10px;color:var(--text-sec)}
|
|
.setting-row .sr-action{min-width:52px;min-height:32px;border-radius:6px;border:1px solid var(--border);background:var(--panel);color:var(--text);font-family:var(--font);font-size:10px;font-weight:600;cursor:pointer;padding:0 8px;display:flex;align-items:center;justify-content:center}
|
|
.setting-row .sr-action:active{background:var(--border)}
|
|
.setting-row .sr-action.primary{background:var(--amber);color:#000;border-color:var(--amber)}
|
|
.setting-row .sr-action.danger{background:var(--red);color:#fff;border-color:var(--red)}
|
|
.wifi-list{display:flex;flex-direction:column;gap:3px}
|
|
.wifi-item{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);cursor:pointer;min-height:40px}
|
|
.wifi-item:active{background:var(--panel)}
|
|
.wifi-item .wi-ssid{font-size:11px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.wifi-connect-pw{display:flex;gap:4px;padding:6px 0}
|
|
.wifi-connect-pw input{flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:11px;outline:none;min-height:36px}
|
|
.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}
|
|
|
|
/* 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)}
|
|
.snap-slot.used{border-color:var(--amber);background:rgba(232,160,48,.06)}
|
|
.snap-slot.current{border-color:var(--green);background:rgba(80,208,144,.1)}
|
|
.snap-slot .ss-num{font-size:9px;font-weight:700;color:var(--text-dim)}
|
|
.snap-slot .ss-name{font-size:9px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50px}
|
|
.spinner{width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--amber);border-radius:50%;animation:spin .6s linear infinite;display:inline-block;vertical-align:middle}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
.toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:200;background:var(--panel);border:1px solid var(--amber);border-radius:8px;padding:6px 14px;font-size:11px;font-weight:600;color:var(--text);display:none;white-space:nowrap;box-shadow:0 4px 20px rgba(0,0,0,.5)}
|
|
.toast.show{display:block;animation:toastIn .2s ease}
|
|
@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Status Bar -->
|
|
<div class="status-bar">
|
|
<div class="status-dot on" id="statusDot"></div>
|
|
<div class="status-channel" id="statusChannel" onclick="cycleInstrument()" style="cursor:pointer">Guitar</div>
|
|
<div class="status-preset" id="statusPreset">—</div>
|
|
<div class="status-vol" id="statusVolPct">80%</div>
|
|
<div class="vu-pair"><span class="vu-lbl">I</span><div class="vu-track"><div class="vu-fill in" id="vuIn" style="width:0%"></div></div></div>
|
|
<div class="vu-pair"><span class="vu-lbl">O</span><div class="vu-track"><div class="vu-fill out" id="vuOut" style="width:0%"></div></div></div>
|
|
<span id="namCpuLabel" style="font-size:9px;color:var(--text-dim);margin-left:2px;min-width:28px;text-align:right;font-variant-numeric:tabular-nums">0%</span>
|
|
<button class="status-btn" id="btnChannel" onclick="cycleInstrument()">🎛</button>
|
|
<button class="status-btn" id="btnSettings">⚙</button>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-content">
|
|
<div class="nam-hero empty" id="namHero" onclick="document.querySelector('.tab-btn[data-view="downloads"]').click();setTimeout(()=>{var b=document.getElementById('dlTabLocal');if(b)b.click();},100)">
|
|
<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>
|
|
<div class="vol-track" id="volTrack">
|
|
<div class="vol-track-bg"><div class="vol-track-fill" id="volFill" style="width:80%"></div></div>
|
|
<div class="vol-track-thumb" id="volThumb" style="left:80%"></div>
|
|
</div>
|
|
<span class="vol-pct" id="volPct">80%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Bar -->
|
|
<div class="tab-bar" id="tabBar">
|
|
<button class="tab-btn active" data-view="main"><span class="tab-icon">🎛</span><span class="tab-label">FX</span></button>
|
|
<button class="tab-btn" data-view="presets"><span class="tab-icon">🎸</span><span class="tab-label">Presets</span></button>
|
|
<button class="tab-btn" data-view="snapshots"><span class="tab-icon">📸</span><span class="tab-label">Snapshots</span></button>
|
|
<button class="tab-btn" data-view="downloads"><span class="tab-icon">⬇</span><span class="tab-label">Downloads</span></button>
|
|
<button class="tab-btn" id="tabTuner"><span class="tab-icon">🎵</span><span class="tab-label">Tuner</span></button>
|
|
<button class="tab-btn" data-view="settings"><span class="tab-icon">⚙</span><span class="tab-label">Settings</span></button>
|
|
</div>
|
|
|
|
<!-- Footswitch -->
|
|
<div class="footswitch-row">
|
|
<button class="footswitch on" id="footswitch">BYpass</button>
|
|
</div>
|
|
|
|
<!-- Presets Overlay -->
|
|
<div class="overlay" id="overlayPresets">
|
|
<div class="overlay-hdr"><span class="overlay-title">🎸 Presets</span><button class="overlay-close" id="closePresets">✕</button></div>
|
|
<div class="overlay-body"><div class="preset-grid" id="presetGrid"></div></div>
|
|
</div>
|
|
|
|
<!-- Snapshots Overlay -->
|
|
<div class="overlay" id="overlaySnapshots">
|
|
<div class="overlay-hdr"><span class="overlay-title">📸 Snapshots</span><button class="overlay-close" id="closeSnapshots">✕</button></div>
|
|
<div class="overlay-body">
|
|
<div style="display:flex;gap:6px;margin-bottom:6px"><button class="sr-action primary" id="snapSaveCurrent" style="flex:1">💾 Save Current</button></div>
|
|
<div class="snap-grid" id="snapGrid"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Downloads Overlay -->
|
|
<div class="overlay" id="overlayDownloads">
|
|
<div class="overlay-hdr"><span class="overlay-title">⬇ Downloads</span><button class="overlay-close" id="closeDownloads">✕</button></div>
|
|
<!-- Tab + filter nav -->
|
|
<div class="settings-nav">
|
|
<button class="sn-btn active" id="dlTabLocal">📂 Local</button>
|
|
<button class="sn-btn" id="dlTabNam">🎛 NAM</button>
|
|
<button class="sn-btn" id="dlTabIr">📦 IR</button>
|
|
</div>
|
|
<div class="settings-nav" style="border-bottom:1px solid var(--border);padding-top:0">
|
|
<button class="sn-btn active" data-arch="">All</button>
|
|
<button class="sn-btn" data-arch="nano">Nano</button>
|
|
<button class="sn-btn" data-arch="feather">Feather</button>
|
|
<button class="sn-btn" data-arch="standard">Standard</button>
|
|
</div>
|
|
<!-- Type filter row (NAM only) -->
|
|
<div class="settings-nav" id="dlGearRow" style="border-bottom:1px solid var(--border);padding-top:0;padding-bottom:0">
|
|
<button class="sn-btn active" data-gear="">All Types</button>
|
|
<button class="sn-btn" data-gear="amp">Amp</button>
|
|
<button class="sn-btn" data-gear="pedal">Pedal</button>
|
|
<button class="sn-btn" data-gear="full-rig">Full Rig</button>
|
|
<button class="sn-btn" data-gear="outboard">Outboard</button>
|
|
<button class="sn-btn" data-gear="ir">IR</button>
|
|
</div>
|
|
<div class="overlay-body" style="padding-top:4px">
|
|
<div style="display:flex;gap:6px;margin-bottom:4px" id="dlSearchRow">
|
|
<input id="dlSearch" placeholder="Search Tone3000..." style="flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:11px;outline:none;min-height:36px">
|
|
<button class="sr-action primary" id="dlSearchBtn">🔍</button>
|
|
</div>
|
|
<div style="display:flex;gap:6px;margin-bottom:6px;align-items:center" id="dlSortRow">
|
|
<span style="font-size:10px;color:var(--text-dim);flex-shrink:0">Sort:</span>
|
|
<select id="dlSortSelect" style="flex:1;font-size:10px;padding:4px 6px;min-height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
|
<option value="created_at.desc">Newest</option>
|
|
<option value="created_at.asc">Oldest</option>
|
|
<option value="name.asc">Name A-Z</option>
|
|
<option value="name.desc">Name Z-A</option>
|
|
</select>
|
|
</div>
|
|
<div id="dlResults"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model Detail Overlay -->
|
|
<div class="overlay" id="overlayDetail">
|
|
<div class="overlay-hdr"><span class="overlay-title" id="dlDetailTitle">Model</span><button class="overlay-close">✕</button></div>
|
|
<div class="overlay-body">
|
|
<div id="dlDetailContent"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Overlay -->
|
|
<div class="overlay" id="overlaySettings">
|
|
<div class="overlay-hdr"><span class="overlay-title">⚙ Settings</span><button class="overlay-close" id="closeSettings">✕</button></div>
|
|
<!-- Settings section nav — stays fixed below header -->
|
|
<div class="settings-nav">
|
|
<button class="sn-btn active" data-section="network" onclick="scrollToSection('network')">⚙ Network</button>
|
|
<button class="sn-btn" data-section="audio" onclick="scrollToSection('audio')">🎛 Audio</button>
|
|
<button class="sn-btn" data-section="interface" onclick="scrollToSection('interface')">🎚 Interface</button>
|
|
<button class="sn-btn" data-section="bluetooth" onclick="scrollToSection('bluetooth')">🔵 Bluetooth</button>
|
|
<button class="sn-btn" data-section="system" onclick="scrollToSection('system')">⚡ System</button>
|
|
</div>
|
|
<div class="overlay-body">
|
|
<div class="setting-group" id="section-network"><div class="setting-group-title">Network</div>
|
|
<div class="setting-row"><span class="sr-label">🌐 Eth IP</span><span class="sr-value" id="netEthIp">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">🔗 MAC</span><span class="sr-value" id="netEthMac">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">🚪 Gateway</span><span class="sr-value" id="netGateway">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">🌐 DNS</span><span class="sr-value" id="netDns">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">🌍 IP Mode</span><span class="sr-value" id="netDhcpStatus">DHCP</span><button class="sr-action" id="netModeToggle">Static</button></div>
|
|
<div class="setting-row"><span class="sr-label">🔗 Hostname</span><span class="sr-value" id="netHostname">pedal</span></div>
|
|
<div class="setting-row"><span class="sr-label">📶 WiFi</span><span class="sr-value" id="wifiStatus">—</span><button class="sr-action" id="wifiScanBtn">Scan</button></div>
|
|
<div id="wifiNetworks" class="wifi-list"></div>
|
|
<div class="wifi-connect-pw" id="wifiConnectPw" style="display:none"><input type="password" id="wifiPw" placeholder="WiFi password"><button id="wifiConnectBtn">Connect</button></div>
|
|
<div class="setting-row"><span class="sr-label">🔥 Hotspot</span><span class="sr-value" id="hotspotStatus">Off</span><button class="sr-action" id="hotspotToggle">Enable</button></div>
|
|
</div>
|
|
<div class="setting-group" id="section-audio"><div class="setting-group-title">Audio</div>
|
|
<div class="setting-row"><span class="sr-label">🎛 Sample Rate</span>
|
|
<select id="audioSampleRate" onchange="setAudioParams()" style="flex:1;max-width:140px;font-size:11px;padding:4px 8px;min-height:34px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
|
<option value="44100">44.1 kHz</option>
|
|
<option value="48000" selected>48 kHz</option>
|
|
<option value="96000">96 kHz</option>
|
|
<option value="192000">192 kHz</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-row"><span class="sr-label">📦 Buffer Size</span>
|
|
<select id="audioBufferSize" onchange="setAudioParams()" style="flex:1;max-width:140px;font-size:11px;padding:4px 8px;min-height:34px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
|
<option value="64">64</option>
|
|
<option value="128">128</option>
|
|
<option value="256" selected>256</option>
|
|
<option value="512">512</option>
|
|
<option value="1024">1024</option>
|
|
<option value="2048">2048</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-row"><span class="sr-label">🎸 Instrument</span><span class="sr-value" id="instrumentLabel">Guitar</span><button class="sr-action" id="instrumentCycleBtn" style="min-width:44px;padding:4px 10px">Cycle</button></div>
|
|
<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 class="setting-row"><span class="sr-label">🎵 Backing Track</span>
|
|
<select id="backingSource" onchange="setBackingSource(this.value)" style="flex:1;max-width:160px;font-size:10px;padding:4px 6px;min-height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
|
<option value="none">None</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="setting-group" id="section-interface"><div class="setting-group-title">🎚 Audio Interface</div>
|
|
<div class="setting-row"><span class="sr-label">🔌 Input Device</span><select id="audioInputDevice" onchange="setAudioInputDevice(this.value)" style="flex:1;min-width:0;font-size:10px;padding:4px 6px;min-height:30px"><option value="">Loading...</option></select></div>
|
|
<div class="setting-row"><span class="sr-label">🔊 Output Device</span><select id="audioOutputDevice" onchange="setAudioOutputDevice(this.value)" style="flex:1;min-width:0;font-size:10px;padding:4px 6px;min-height:30px"><option value="">Loading...</option></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;display:flex;align-items:center;justify-content:space-between">
|
|
<span>Output:</span>
|
|
<select id="ch1Output" onchange="setChOutput(1,this.value)" style="font-size:9px;padding:2px 4px;min-height:22px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
|
<option value="playback_1">Output 1 (L)</option>
|
|
<option value="playback_2">Output 2 (R)</option>
|
|
<option value="both" selected>Both (L+R)</option>
|
|
</select>
|
|
</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;display:flex;align-items:center;justify-content:space-between">
|
|
<span>Output:</span>
|
|
<select id="ch2Output" onchange="setChOutput(2,this.value)" style="font-size:9px;padding:2px 4px;min-height:22px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
|
|
<option value="playback_1">Output 1 (L)</option>
|
|
<option value="playback_2">Output 2 (R)</option>
|
|
<option value="both" selected>Both (L+R)</option>
|
|
</select>
|
|
</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" id="section-bluetooth"><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>
|
|
<div class="setting-row"><span class="sr-label">🔓 Discoverable</span><span class="sr-value" id="btDiscStatus">On</span><button class="sr-action" id="btDiscToggle">Disable</button></div>
|
|
</div>
|
|
<div class="setting-group" id="section-system"><div class="setting-group-title">System</div>
|
|
<div class="setting-row"><span class="sr-label">🔧 CPU</span><span class="sr-value" id="cpuLabel">0%</span><span class="sr-value" id="sysCpuLabel" style="font-size:10px;color:var(--text-dim);min-width:28px"></span></div>
|
|
<div class="setting-row"><span class="sr-label">🌡 Temp</span><span class="sr-value" id="sysTemp">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">⏱ Uptime</span><span class="sr-value" id="sysUptime">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">💾 Disk</span><span class="sr-value" id="sysDisk">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">🧠 RAM</span><span class="sr-value" id="sysMemory">—</span></div>
|
|
<div class="setting-row"><span class="sr-label">↺ Reset</span><button class="sr-action danger" id="btnResetDefaults">Reset to Defaults</button></div>
|
|
<div class="setting-row"><span class="sr-label">🎛 NAM Engine</span><span class="sr-value" id="namEngineLabel">cpp</span><button class="sr-action" id="btnToggleNamEngine">Switch</button></div>
|
|
<div class="setting-row"><span class="sr-label">🔄 Service</span><button class="sr-action danger" id="btnRestartSvc">Restart</button></div>
|
|
<div class="setting-row"><span class="sr-label">⏻ Reboot</span><button class="sr-action danger" id="btnReboot">Reboot</button></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tuner -->
|
|
<div class="tuner-view" id="tunerView">
|
|
<div class="tuner-note" id="tunerNote">—</div>
|
|
<div class="tuner-cents"><span style="font-size:10px;color:var(--text-dim)">♭</span><div class="tuner-cents-bar"><div class="tuner-cents-fill in-tune" id="tunerCentsFill"></div></div><span style="font-size:10px;color:var(--text-dim)">♯</span></div>
|
|
<div class="tuner-freq" id="tunerFreq">0.0 Hz</div>
|
|
<button class="tuner-exit" id="tunerExit">✕ Close Tuner</button>
|
|
</div>
|
|
|
|
<!-- Toast -->
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
// ═══ API Client ═══
|
|
const API = {
|
|
_base:'',
|
|
async _fetch(path,opts={}){
|
|
const ac=new AbortController();
|
|
const to=setTimeout(()=>ac.abort(),opts.timeout||6000);
|
|
try{
|
|
const r=await fetch(this._base+path,{...opts,signal:ac.signal,headers:{'Content-Type':'application/json',...opts.headers}});
|
|
clearTimeout(to);
|
|
if(!r.ok)throw new Error(r.status+' '+r.statusText);
|
|
return r.status===204?null:r.json();
|
|
}catch(e){clearTimeout(to);if(e.name==='AbortError')throw new Error('timeout');throw e;}
|
|
},
|
|
getState:(ch)=>API._fetch('/api/state?channel='+(ch||'guitar')),
|
|
getTunerPitch:(ch)=>API._fetch('/api/tuner/pitch?channel='+(ch||'guitar'),{timeout:3000}),
|
|
bypassToggle:(ch)=>API._fetch('/api/bypass?channel='+(ch||'guitar'),{method:'POST'}),
|
|
tunerToggle:(ch,on)=>API._fetch('/api/tuner?channel='+(ch||'guitar'),{method:'POST',body:JSON.stringify({on})}),
|
|
setVolume:(ch,v)=>API._fetch('/api/volume?channel='+(ch||'guitar'),{method:'PUT',body:JSON.stringify({volume:v})}),
|
|
toggleBlock:(ch,id,en)=>API._fetch('/api/blocks?channel='+(ch||'guitar'),{method:'PATCH',body:JSON.stringify({block_id:id,enabled:en})}),
|
|
listPresets:(ch)=>API._fetch('/api/presets?channel='+(ch||'guitar')),
|
|
activatePreset:(ch,b,p)=>API._fetch('/api/presets/'+b+'/'+p+'/activate?channel='+(ch||'guitar'),{method:'POST'}),
|
|
listSnapshots:(ch)=>API._fetch('/api/snapshots?channel='+(ch||'guitar')),
|
|
saveSnapshot:(ch,s,data)=>API._fetch('/api/snapshots/'+s+'/save?channel='+(ch||'guitar'),{method:'POST',body:data?JSON.stringify(data):undefined}),
|
|
recallSnapshot:(ch,s)=>API._fetch('/api/snapshots/'+s+'/recall?channel='+(ch||'guitar'),{method:'POST'}),
|
|
wifiScan:()=>API._fetch('/api/wifi/scan',{timeout:15000}),
|
|
wifiStatus:()=>API._fetch('/api/wifi/status'),
|
|
wifiConnect:(s,p)=>API._fetch('/api/wifi/connect',{method:'POST',body:JSON.stringify({ssid:s,password:p}),timeout:20000}),
|
|
hotspotEnable:()=>API._fetch('/api/wifi/hotspot/enable',{method:'POST',timeout:15000}),
|
|
hotspotDisable:()=>API._fetch('/api/wifi/hotspot/disable',{method:'POST'}),
|
|
hotspotStatus:()=>API._fetch('/api/wifi/hotspot'),
|
|
btStatus:()=>API._fetch('/api/bluetooth/status'),
|
|
btPaired:()=>API._fetch('/api/bluetooth/paired'),
|
|
btMidiStatus:()=>API._fetch('/api/bluetooth/midi-status'),
|
|
btMidiEnable:()=>API._fetch('/api/bluetooth/midi-enable',{method:'POST',timeout:15000}),
|
|
btMidiDisable:()=>API._fetch('/api/bluetooth/midi-disable',{method:'POST',timeout:15000}),
|
|
btDiscoverableSet:(on)=>API._fetch('/api/bluetooth/discoverable',{method:'POST',body:JSON.stringify({on}),timeout:15000}),
|
|
listAudioProfiles:()=>API._fetch('/api/audio/profiles'),
|
|
getAudioProfile:()=>API._fetch('/api/audio/profile'),
|
|
setAudioProfile:(p)=>{
|
|
if(typeof p==='string') return API._fetch('/api/audio/profile',{method:'POST',body:JSON.stringify({profile:p})});
|
|
return API._fetch('/api/audio/profile',{method:'POST',body:JSON.stringify(p)});
|
|
},
|
|
getChannelMode:()=>API._fetch('/api/channel-mode'),
|
|
setChannelMode:(m)=>API._fetch('/api/channel-mode',{method:'POST',body:JSON.stringify({channel_mode:m})}),
|
|
getRouting:(ch)=>API._fetch('/api/routing?channel='+(ch||'guitar')),
|
|
setRouting:(ch,data)=>API._fetch('/api/routing?channel='+(ch||'guitar'),{method:'POST',body:JSON.stringify(data)}),
|
|
saveSettings:(d)=>API._fetch('/api/settings',{method:'POST',body:JSON.stringify(d)}),
|
|
getSettings:()=>API._fetch('/api/settings'),
|
|
getSystem:()=>API._fetch('/api/system'),
|
|
getSystemLogs:(n)=>API._fetch('/api/system/logs?lines='+(n||50)),
|
|
setHostname:(h)=>API._fetch('/api/system/hostname',{method:'POST',body:JSON.stringify({hostname:h}),timeout:15000}),
|
|
getNetwork:()=>API._fetch('/api/network'),
|
|
setNetworkStatic:(d)=>API._fetch('/api/network/static',{method:'POST',body:JSON.stringify(d),timeout:25000}),
|
|
setNetworkDhcp:()=>API._fetch('/api/network/dhcp',{method:'POST',timeout:25000}),
|
|
listAudioDevices:()=>API._fetch('/api/audio/devices'),
|
|
getDiagnostics:()=>API._fetch('/api/diagnostics'),
|
|
createBackup:()=>API._fetch('/api/backup',{method:'POST',timeout:35000}),
|
|
listBackups:()=>API._fetch('/api/backup/list'),
|
|
};
|
|
|
|
// ═══ State ═══
|
|
let state={channel:'guitar',connected:false,bypass:false,master_volume:0.8,blocks:[],routing_mode:'4cm',tuner_enabled:false,cpu_percent:0,nam_cpu:0};
|
|
let currentChannel='guitar';
|
|
let channels=['guitar','bass','keys','vocals','backing_tracks'];
|
|
let presets=[];
|
|
let snapshots={};
|
|
let currentBank=0;
|
|
let pollTimer=null;
|
|
let tunerPollTimer=null;
|
|
let volDragging=false;
|
|
|
|
// ═══ Helpers ═══
|
|
const $=(s)=>document.querySelector(s);
|
|
const $$=(s)=>document.querySelectorAll(s);
|
|
function cycleInstrument(){
|
|
const idx=channels.indexOf(currentChannel);
|
|
currentChannel=channels[(idx+1)%channels.length];
|
|
document.getElementById('statusChannel').textContent=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
|
|
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 inSel=document.getElementById('audioInputDevice');
|
|
const outSel=document.getElementById('audioOutputDevice');
|
|
if(!inSel||!outSel)return;
|
|
const devs=await API.listAudioDevices();
|
|
const opts=((devs&&devs.devices)||[]).map(d=>{
|
|
const id='hw:'+d.card+','+d.device;
|
|
return '<option value="'+id+'">'+d.name+' ('+d.desc+')</option>';
|
|
}).join('')||'<option value="">No devices</option>';
|
|
inSel.innerHTML=opts;
|
|
outSel.innerHTML=opts;
|
|
// Load current input/output from audio profile
|
|
try{
|
|
const p=await API.getAudioProfile();
|
|
if(p.input_device){
|
|
// Match by trailing device number (e.g. 'hw:USB,0' → card with device=0)
|
|
const devNum=String(p.input_device).split(',')[1];
|
|
const opts_arr=inSel.options;
|
|
for(let i=0;i<opts_arr.length;i++){if(opts_arr[i].value.endsWith(','+devNum)){inSel.selectedIndex=i;break;}}
|
|
}
|
|
if(p.output_device){
|
|
const devNum=String(p.output_device).split(',')[1];
|
|
const opts_arr=outSel.options;
|
|
for(let i=0;i<opts_arr.length;i++){if(opts_arr[i].value.endsWith(','+devNum)){outSel.selectedIndex=i;break;}}
|
|
}
|
|
}catch(e){}
|
|
}
|
|
async function setAudioInputDevice(id){
|
|
if(!id)return;
|
|
showLoading('Switching input...');
|
|
try{
|
|
await API.saveSettings({input_device:id});
|
|
toast('Input: '+id);
|
|
}catch(e){toast('Input switch failed');}
|
|
hideLoading();
|
|
}
|
|
async function setAudioOutputDevice(id){
|
|
if(!id)return;
|
|
showLoading('Switching output...');
|
|
try{
|
|
await API.saveSettings({output_device:id});
|
|
toast('Output: '+id);
|
|
}catch(e){toast('Output switch failed');}
|
|
}
|
|
async function setChOutput(ch,port){
|
|
try{
|
|
await API._fetch('/api/channel-output',{method:'POST',body:JSON.stringify({ch,port})});
|
|
toast('CH'+ch+': '+port);
|
|
}catch(e){toast('Failed: '+e.message);}
|
|
}
|
|
async function loadChOutput(){
|
|
try{
|
|
const r=await API._fetch('/api/channel-output');
|
|
if(r.ch1){const el=document.getElementById('ch1Output');if(el)el.value=r.ch1;}
|
|
if(r.ch2){const el=document.getElementById('ch2Output');if(el)el.value=r.ch2;}
|
|
}catch(e){}
|
|
}
|
|
async function loadBackingSource(){
|
|
try{
|
|
const r=await API._fetch('/api/backing-source');
|
|
const sel=document.getElementById('backingSource');
|
|
if(!sel)return;
|
|
sel.innerHTML='';
|
|
(r.sources||[]).forEach(s=>{
|
|
const o=document.createElement('option');
|
|
o.value=s.id;o.textContent=s.name;
|
|
if(s.id===r.current)o.selected=true;
|
|
sel.appendChild(o);
|
|
});
|
|
}catch(e){}
|
|
}
|
|
async function setBackingSource(source){
|
|
if(!source)return;
|
|
try{
|
|
await API._fetch('/api/backing-source',{method:'POST',body:JSON.stringify({source})});
|
|
toast('Backing: '+source);
|
|
}catch(e){toast('Failed: '+e.message);}
|
|
}
|
|
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(e=>toast('Instrument failed'));
|
|
}
|
|
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(e=>toast('Channel toggle failed'));
|
|
}
|
|
|
|
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 ═══
|
|
function openOverlay(id){document.getElementById('overlay'+id).classList.add('open');}
|
|
function closeOverlay(id){document.getElementById('overlay'+id).classList.remove('open');}
|
|
document.querySelectorAll('.overlay-close').forEach(b=>{
|
|
b.addEventListener('click',function(){this.closest('.overlay').classList.remove('open');});
|
|
});
|
|
|
|
// ═══ Init Volume Slider ═══
|
|
function initVolSlider(){
|
|
const track=$('#volTrack'),fill=$('#volFill'),thumb=$('#volThumb'),pct=$('#volPct');
|
|
function setV(p){const v=Math.max(0,Math.min(1,p/100));fill.style.width=(v*100)+'%';thumb.style.left=(v*100)+'%';pct.textContent=Math.round(v*100)+'%';}
|
|
function upd(e){const r=track.getBoundingClientRect();const x=(e.touches?e.touches[0].clientX:e.clientX)-r.left;setV((x/r.width)*100);}
|
|
track.addEventListener('mousedown',(e)=>{volDragging=true;upd(e);});
|
|
track.addEventListener('touchstart',(e)=>{volDragging=true;upd(e);},{passive:true});
|
|
document.addEventListener('mousemove',(e)=>{if(volDragging){e.preventDefault();upd(e);}});
|
|
document.addEventListener('touchmove',(e)=>{if(volDragging)upd(e);},{passive:true});
|
|
document.addEventListener('mouseup',()=>{if(!volDragging)return;volDragging=false;API.setVolume(currentChannel,parseFloat($('#volFill').style.width)/100).catch(e=>toast('Volume failed'));});
|
|
document.addEventListener('touchend',()=>{if(!volDragging)return;volDragging=false;API.setVolume(currentChannel,parseFloat($('#volFill').style.width)/100).catch(e=>toast('Volume failed'));});
|
|
}
|
|
|
|
// ═══ State Loading ═══
|
|
async function loadState(){
|
|
try{
|
|
const s=await API.getState(currentChannel);
|
|
if(!s)return;
|
|
state=s;
|
|
document.getElementById('statusChannel').textContent=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
|
|
document.getElementById('statusDot').className='status-dot '+(s.connected?'on':'off');
|
|
document.getElementById('statusPreset').textContent=(s.current_preset&&s.current_preset.name)||'—';
|
|
const vp=Math.round((s.master_volume||0)*100);
|
|
document.getElementById('statusVolPct').textContent=vp+'%';
|
|
document.getElementById('volFill').style.width=vp+'%';
|
|
document.getElementById('volThumb').style.left=vp+'%';
|
|
document.getElementById('volPct').textContent=vp+'%';
|
|
const fs=document.getElementById('footswitch');
|
|
if(s.bypass){fs.className='footswitch off';fs.textContent='MUTE';}
|
|
else{fs.className='footswitch on';fs.textContent='BYpass';}
|
|
document.getElementById('cpuLabel').textContent=(s.nam_cpu!=null?s.nam_cpu:'—')+'%';
|
|
const ncEl=document.getElementById('namCpuLabel');
|
|
if(ncEl)ncEl.textContent=(s.nam_cpu!=null?s.nam_cpu:'—')+'%';
|
|
const scEl=document.getElementById('sysCpuLabel');
|
|
if(scEl)scEl.textContent='sys '+(s.cpu_percent!=null?s.cpu_percent:'—')+'%';
|
|
const vi=Math.min(100,Math.round((s.input_level||0)*300));
|
|
const vo=Math.min(100,Math.round((s.output_level||0)*300));
|
|
document.getElementById('vuIn').style.width=vi+'%';
|
|
document.getElementById('vuOut').style.width=vo+'%';
|
|
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');}
|
|
}catch(e){document.getElementById('statusDot').className='status-dot off';}
|
|
}
|
|
|
|
// ═══ FX Blocks ═══
|
|
const BLOCK_COLORS={nam_amp:'#E8A030',ir:'#4090E0',gate:'#50D090',compressor:'#E0E060',eq:'#A080E0',reverb:'#60C0E0',delay:'#80D0A0',distortion:'#E06060'};
|
|
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 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;
|
|
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(e=>toast('Block failed'));
|
|
loadState();
|
|
}
|
|
function showAddBlock(){document.querySelector('.tab-btn[data-view="downloads"]').click();}
|
|
|
|
// ═══ Presets ═══
|
|
async function loadPresets(){
|
|
try{
|
|
presets=await API.listPresets(currentChannel);
|
|
const grid=document.getElementById('presetGrid');
|
|
grid.innerHTML='';
|
|
const bank=(presets.banks||[])[currentBank];
|
|
const slots=bank?(bank.presets||[]):[];
|
|
for(let i=0;i<4;i++){
|
|
const p=slots[i];
|
|
const div=document.createElement('div');
|
|
const active=state.current_preset&&state.current_preset.bank===currentBank&&state.current_preset.program===i;
|
|
div.className='preset-card'+(active?' active':'');
|
|
if(p){
|
|
div.innerHTML='<div class="pc-num">'+currentBank+'.'+i+'</div><div class="pc-name">'+(p.name||'Preset')+'</div>';
|
|
div.onclick=()=>{API.activatePreset(currentChannel,currentBank,i).then(()=>{closeOverlay('Presets');toast('Loaded: '+(p.name||'Preset'));}).catch(e=>toast(e.message));};
|
|
}else{
|
|
div.innerHTML='<div class="pc-num">'+currentBank+'.'+i+'</div><div class="pc-name" style="color:var(--text-dim);font-weight:400">Empty</div>';
|
|
div.style.opacity='.35';
|
|
}
|
|
grid.appendChild(div);
|
|
}
|
|
if(!bank||slots.length===0)grid.innerHTML='<div style="text-align:center;padding:20px;color:var(--text-dim);font-size:12px">No presets</div>';
|
|
}catch(e){toast('Failed to load presets');}
|
|
}
|
|
// ═══ Snapshots ═══
|
|
async function loadSnapshots(){
|
|
try{
|
|
snapshots=await API.listSnapshots(currentChannel);
|
|
const grid=document.getElementById('snapGrid');
|
|
const slots=snapshots.snapshots||{};
|
|
const cur=snapshots.current_snapshot||0;
|
|
grid.innerHTML='';
|
|
for(let i=1;i<=8;i++){
|
|
const s=slots[i];
|
|
const div=document.createElement('div');
|
|
div.className='snap-slot'+(s?' used':'')+(cur===i?' current':'');
|
|
if(s){div.innerHTML='<div class="ss-num">'+i+'</div><div class="ss-name">'+(s.name||'Slot '+i)+'</div>';
|
|
div.onclick=()=>{API.recallSnapshot(currentChannel,i).then(()=>toast('Recalled: '+(s.name||'Slot '+i))).catch(e=>toast(e.message));};}
|
|
else{div.innerHTML='<div class="ss-num">'+i+'</div><div class="ss-name" style="color:var(--text-dim)">Empty</div>';}
|
|
grid.appendChild(div);
|
|
}
|
|
}catch(e){toast('Failed to load snapshots');}
|
|
}
|
|
|
|
// ═══ Settings Loaders ═══
|
|
async function loadNetworkInfo(){
|
|
try{
|
|
const n=await API.getNetwork();
|
|
if(n.ethernet){
|
|
document.getElementById('netEthIp').textContent=n.ethernet.ip||'—';
|
|
document.getElementById('netEthMac').textContent=n.ethernet.mac||'—';
|
|
document.getElementById('netGateway').textContent=n.ethernet.gateway||'—';
|
|
}
|
|
if(n.dns)document.getElementById('netDns').textContent=n.dns.join(', ');
|
|
}catch(e){toast('Network info failed');}
|
|
try{
|
|
const w=await API.wifiStatus();
|
|
if(w.ssid)document.getElementById('wifiStatus').textContent=w.ssid+(w.signal?' ('+w.signal+'dB)':'');
|
|
else if(w.connected)document.getElementById('wifiStatus').textContent='Connected';
|
|
else document.getElementById('wifiStatus').textContent='—';
|
|
}catch(e){}
|
|
}
|
|
|
|
async function loadSystemInfo(){
|
|
try{
|
|
const s=await API.getSystem();
|
|
if(s.cpu_temp_c)document.getElementById('sysTemp').textContent=s.cpu_temp_c+'°C';
|
|
document.getElementById('sysUptime').textContent=s.uptime||'—';
|
|
document.getElementById('sysDisk').textContent=s.disk?s.disk.pct+' ('+s.disk.avail+' free)':'—';
|
|
document.getElementById('sysMemory').textContent=s.memory_mb?Math.round(s.memory_mb.used/s.memory_mb.total*100)+'% ('+s.memory_mb.used+'/'+s.memory_mb.total+'MB)':'—';
|
|
document.getElementById('netHostname').textContent=s.hostname||'pedal';
|
|
}catch(e){}
|
|
}
|
|
|
|
async function loadAudioParams(){
|
|
try{
|
|
const p=await API.getAudioProfile();
|
|
const rate=p.rate||48000;
|
|
const period=p.period||256;
|
|
document.getElementById('audioSampleRate').value=String(rate);
|
|
document.getElementById('audioBufferSize').value=String(period);
|
|
const cm=await API.getChannelMode();
|
|
if(cm.channel_mode)document.getElementById('channelModeLabel').textContent=cm.channel_mode.replace(/-/g,' ').replace(/\b\w/g,l=>l.toUpperCase());
|
|
const rt=await API.getRouting(currentChannel);
|
|
if(rt.routing_mode)document.getElementById('routingModeLabel').textContent=rt.routing_mode.toUpperCase();
|
|
}catch(e){}
|
|
}
|
|
|
|
async function setAudioParams(){
|
|
const rate=parseInt(document.getElementById('audioSampleRate').value);
|
|
const period=parseInt(document.getElementById('audioBufferSize').value);
|
|
const btn=document.querySelector('.sn-btn[data-section="audio"]');
|
|
if(btn)btn.textContent='⏳ Applying...';
|
|
try{
|
|
await API._fetch('/api/audio/profile',{method:'POST',body:JSON.stringify({period,rate}),timeout:30000});
|
|
toast('Audio: '+rate+'Hz / '+period+'fr');
|
|
loadAudioParams();
|
|
}catch(e){toast('Failed: '+e.message);}
|
|
if(btn)btn.textContent='🎛 Audio';
|
|
}
|
|
|
|
async function loadBtStatus(){
|
|
try{
|
|
const st=await API.btStatus();
|
|
document.getElementById('btStatus').textContent=st.powered?'Powered On':'Powered Off';
|
|
document.getElementById('btDiscStatus').textContent=st.discoverable?'On':'Off';
|
|
document.getElementById('btDiscToggle').textContent=st.discoverable?'Disable':'Enable';
|
|
}catch(e){}
|
|
}
|
|
|
|
async function loadBtMidi(){
|
|
try{
|
|
const st=await API.btMidiStatus();
|
|
const r=st.running||st.enabled||false;
|
|
document.getElementById('btMidiStatus').textContent=r?'Running':'Off';
|
|
document.getElementById('btMidiToggle').textContent=r?'Disable':'Enable';
|
|
}catch(e){}
|
|
}
|
|
|
|
// Settings section nav — scroll to section and highlight active button
|
|
function scrollToSection(id){
|
|
const el=document.getElementById('section-'+id);
|
|
if(el) el.scrollIntoView({behavior:'smooth',block:'start'});
|
|
document.querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active'));
|
|
const btn=document.querySelector('.sn-btn[data-section="'+id+'"]');
|
|
if(btn) btn.classList.add('active');
|
|
}
|
|
|
|
async function loadHotspotStatus(){
|
|
try{
|
|
const hs=await API.hotspotStatus();
|
|
const a=hs.active||false;
|
|
document.getElementById('hotspotStatus').textContent=a?'On':'Off';
|
|
document.getElementById('hotspotToggle').textContent=a?'Disable':'Enable';
|
|
document.getElementById('hotspotToggle').className='sr-action'+(a?' danger':'');
|
|
}catch(e){}
|
|
}
|
|
|
|
async function doWifiScan(){
|
|
const list=document.getElementById('wifiNetworks');
|
|
list.innerHTML='<div style="text-align:center;padding:10px;color:var(--text-dim);font-size:11px">Scanning...</div>';
|
|
try{
|
|
const r=await API.wifiScan();
|
|
const nets=r.networks||[];
|
|
list.innerHTML='';
|
|
if(nets.length===0){list.innerHTML='<div style="text-align:center;padding:10px;color:var(--text-dim);font-size:11px">No networks found</div>';return;}
|
|
nets.forEach(n=>{
|
|
const item=document.createElement('div');
|
|
item.className='wifi-item';
|
|
const sec=n.secured!==false;
|
|
item.innerHTML='<span class="wi-ssid">'+(n.ssid||n.name||'Unknown')+'</span>'+(sec?'<span style="font-size:8px;color:var(--text-dim)">🔒</span>':'<span style="font-size:8px;color:var(--green)">Open</span>')+'<span style="font-size:8px;color:var(--text-dim)">'+(n.signal||'')+'dB</span>';
|
|
item.onclick=()=>{
|
|
if(sec){
|
|
const pwDiv=document.getElementById('wifiConnectPw');
|
|
pwDiv.style.display='flex';
|
|
document.getElementById('wifiPw').value='';
|
|
document.getElementById('wifiPw').focus();
|
|
document.getElementById('wifiConnectBtn').onclick=async()=>{
|
|
const pw=document.getElementById('wifiPw').value;
|
|
if(!pw){toast('Enter password');return;}
|
|
pwDiv.style.display='none';
|
|
try{await API.wifiConnect(n.ssid||n.name,pw);toast('Connected!');setTimeout(doWifiScan,3000);}catch(e){toast(e.message);}
|
|
};
|
|
}else{
|
|
API.wifiConnect(n.ssid||n.name,'').then(()=>toast('Connected!')).catch(e=>toast(e.message));
|
|
}
|
|
};
|
|
list.appendChild(item);
|
|
});
|
|
document.getElementById('wifiStatus').textContent=nets.length+' networks';
|
|
}catch(e){list.innerHTML='<div style="text-align:center;padding:10px;color:var(--text-dim);font-size:11px">Scan failed</div>';}
|
|
}
|
|
|
|
// ═══ Tuner ═══
|
|
function startTunerPolling(){stopTunerPolling();if(state.tuner_enabled)tunerPollTimer=setInterval(pollTuner,150);}
|
|
function stopTunerPolling(){if(tunerPollTimer){clearInterval(tunerPollTimer);tunerPollTimer=null;}}
|
|
async function pollTuner(){
|
|
if(!state.tuner_enabled)return;
|
|
try{
|
|
const d=await API.getTunerPitch(currentChannel);
|
|
if(!d)return;
|
|
document.getElementById('tunerNote').textContent=d.note||'—';
|
|
const fill=document.getElementById('tunerCentsFill');
|
|
const c=d.cents||0;
|
|
if(Math.abs(c)<3){fill.className='tuner-cents-fill in-tune';fill.style.left='calc(50% - 3px)';fill.style.width='6px';}
|
|
else if(c>0){fill.className='tuner-cents-fill sharp';fill.style.left='calc(50% + '+Math.min(c,50)*1.2+'px)';fill.style.width=Math.min(Math.abs(c),50)+'px';}
|
|
else{fill.className='tuner-cents-fill flat';fill.style.left='calc(50% - '+Math.min(Math.abs(c),50)*1.2+'px)';fill.style.width=Math.min(Math.abs(c),50)+'px';}
|
|
document.getElementById('tunerFreq').textContent=(d.frequency||0).toFixed(1)+' Hz';
|
|
}catch(e){}
|
|
}
|
|
|
|
// ═══ Main Poll Loop ═══
|
|
function startPolling(){stopPolling();loadState();pollTimer=setInterval(loadState,2000);}
|
|
function stopPolling(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}}
|
|
|
|
// ═══ Init! ═══
|
|
function init(){
|
|
// Restore saved channel
|
|
const savedChannel=localStorage.getItem('pedalCurrentChannel');
|
|
if(savedChannel && channels.includes(savedChannel)) currentChannel=savedChannel;
|
|
|
|
initVolSlider();
|
|
|
|
// Footswitch
|
|
document.getElementById('footswitch').onclick=()=>API.bypassToggle(currentChannel).catch(e=>toast('Bypass failed'));
|
|
|
|
// Tab bar navigation
|
|
document.querySelectorAll('.tab-btn[data-view]').forEach(btn=>{
|
|
btn.onclick=()=>{
|
|
const view=btn.dataset.view;
|
|
if(view==='main'){document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');}
|
|
else if(view==='settings'){openOverlay('Settings');loadNetworkInfo();loadSystemInfo();loadAudioParams();loadAudioDevices();loadChOutput();loadBackingSource();loadHotspotStatus();loadBtStatus();loadBtMidi();loadNamEngine();return;}
|
|
else if(view==='presets'){openOverlay('Presets');loadPresets();}
|
|
else if(view==='snapshots'){openOverlay('Snapshots');loadSnapshots();}
|
|
else if(view==='downloads'){openOverlay('Downloads');if(dlTab==='local')doDlSearch();}
|
|
};
|
|
});
|
|
|
|
// Status bar: settings → open settings overlay
|
|
document.getElementById('btnSettings').onclick=()=>{
|
|
openOverlay('Settings');
|
|
loadNetworkInfo();
|
|
loadSystemInfo();
|
|
loadAudioParams();
|
|
loadAudioDevices();
|
|
loadChOutput();
|
|
loadBackingSource();
|
|
loadHotspotStatus();
|
|
loadBtStatus();
|
|
loadBtMidi();
|
|
loadNamEngine();
|
|
};
|
|
|
|
// Close all overlays
|
|
document.querySelectorAll('.overlay').forEach(o=>{o.onclick=function(e){if(e.target===this)this.classList.remove('open');};});
|
|
|
|
// Settings: Toggles
|
|
document.getElementById('instrumentCycleBtn').onclick=()=>{
|
|
const idx=channels.indexOf(currentChannel);
|
|
currentChannel=channels[(idx+1)%channels.length];
|
|
const displayName=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
|
|
document.getElementById('instrumentLabel').textContent=displayName;
|
|
document.getElementById('statusChannel').textContent=displayName;
|
|
localStorage.setItem('pedalCurrentChannel',currentChannel);
|
|
toast(currentChannel);
|
|
loadState();
|
|
};
|
|
document.getElementById('channelModeToggle').onclick=async()=>{
|
|
const cur=document.getElementById('channelModeLabel').textContent.toLowerCase();
|
|
const next=cur.includes('dual')?'stereo':'dual-mono';
|
|
try{await API.setChannelMode(next);loadAudioParams();toast('Mode: '+next);}catch(e){toast(e.message);}
|
|
};
|
|
document.getElementById('routingModeToggle').onclick=async()=>{
|
|
const cur=document.getElementById('routingModeLabel').textContent;
|
|
try{await API.setRouting(currentChannel,{routing_mode:cur==='MONO'?'4cm':'mono'});loadAudioParams();}catch(e){toast(e.message);}
|
|
};
|
|
document.getElementById('netModeToggle').onclick=async()=>{
|
|
const lbl=document.getElementById('netDhcpStatus');
|
|
if(lbl.textContent==='DHCP'){
|
|
const ip=prompt('Enter static IP:','192.168.0.100');
|
|
if(!ip)return;
|
|
const gw=prompt('Gateway:','192.168.0.1');
|
|
try{await API.setNetworkStatic({ip,netmask:'24',gateway:gw});lbl.textContent='Static';toast('Static: '+ip);}catch(e){toast(e.message);}
|
|
}else{
|
|
try{await API.setNetworkDhcp();lbl.textContent='DHCP';toast('DHCP enabled');}catch(e){toast(e.message);}
|
|
}
|
|
};
|
|
document.getElementById('wifiScanBtn').onclick=doWifiScan;
|
|
document.getElementById('hotspotToggle').onclick=async()=>{
|
|
const on=document.getElementById('hotspotToggle').textContent==='Disable';
|
|
try{if(on)await API.hotspotDisable();else await API.hotspotEnable();loadHotspotStatus();toast(on?'Hotspot off':'Hotspot on');}catch(e){toast(e.message);}
|
|
};
|
|
document.getElementById('btMidiToggle').onclick=async()=>{
|
|
const on=document.getElementById('btMidiToggle').textContent==='Disable';
|
|
try{if(on)await API.btMidiDisable();else await API.btMidiEnable();loadBtMidi();toast('MIDI '+(on?'off':'on'));}catch(e){toast(e.message);}
|
|
};
|
|
document.getElementById('btDiscToggle').onclick=async()=>{
|
|
const on=document.getElementById('btDiscToggle').textContent==='Disable';
|
|
try{await API.btDiscoverableSet(!on);loadBtStatus();toast(on?'Hidden':'Visible');}catch(e){toast(e.message);}
|
|
};
|
|
|
|
// System
|
|
document.getElementById('btnResetDefaults').onclick=async()=>{
|
|
if(!confirm('Reset all to factory defaults?'))return;
|
|
toast('Resetting...');
|
|
try{
|
|
await API.saveSettings({channel_mode:'dual-mono',brightness:8,tap_tempo:120});
|
|
await API.setRouting(currentChannel,{routing_mode:'mono'}).catch(()=>{});
|
|
toast('Defaults restored');
|
|
setTimeout(()=>{closeOverlay('Settings');loadAudioParams();},1000);
|
|
}catch(e){toast('Failed: '+e.message);}
|
|
};
|
|
document.getElementById('btnRestartSvc').onclick=()=>{toast('Restarting service...');fetch('/api/settings',{method:'POST',body:JSON.stringify({_restart:true})}).catch(()=>{});};
|
|
document.getElementById('btnReboot').onclick=()=>{if(confirm('Reboot pedal?')){toast('Rebooting...');fetch('/api/settings',{method:'POST',body:JSON.stringify({_reboot:true})}).catch(()=>{});}};
|
|
|
|
// NAM engine toggle
|
|
document.getElementById('btnToggleNamEngine').onclick=async()=>{
|
|
const label=document.getElementById('namEngineLabel');
|
|
const cur=label.textContent;
|
|
const next=cur==='cpp'?'pytorch':'cpp';
|
|
label.textContent='...';
|
|
try{
|
|
await API._fetch('/api/nam/engine',{method:'POST',body:JSON.stringify({engine_mode:next}),timeout:30000});
|
|
label.textContent=next;
|
|
toast('NAM engine: '+next);
|
|
}catch(e){label.textContent=cur;toast('Switch failed: '+e.message);}
|
|
};
|
|
// Load current engine on settings open
|
|
async function loadNamEngine(){
|
|
try{
|
|
const r=await API._fetch('/api/nam/engine');
|
|
document.getElementById('namEngineLabel').textContent=r.engine_mode||'cpp';
|
|
}catch(e){}
|
|
}
|
|
|
|
// Tuner
|
|
document.getElementById('tabTuner').onclick=async()=>{
|
|
state.tuner_enabled=!state.tuner_enabled;
|
|
if(state.tuner_enabled){document.getElementById('tunerView').classList.add('open');startTunerPolling();}
|
|
else{document.getElementById('tunerView').classList.remove('open');stopTunerPolling();}
|
|
API.tunerToggle(currentChannel,state.tuner_enabled).catch(e=>toast('Tuner failed'));
|
|
};
|
|
document.getElementById('tunerExit').onclick=()=>{
|
|
state.tuner_enabled=false;
|
|
document.getElementById('tunerView').classList.remove('open');
|
|
stopTunerPolling();
|
|
API.tunerToggle(currentChannel,false).catch(e=>toast('Tuner failed'));
|
|
};
|
|
|
|
// Snapshots save
|
|
document.getElementById('snapSaveCurrent').onclick=async()=>{
|
|
const slots=snapshots.snapshots||{};
|
|
let slot=1;
|
|
for(let i=1;i<=8;i++){if(!slots[i]){slot=i;break;}}
|
|
try{await API.saveSnapshot(currentChannel,slot);toast('Saved to slot '+slot);loadSnapshots();}catch(e){toast(e.message);}
|
|
};
|
|
|
|
// Downloads state & wiring
|
|
// Downloads state & wiring
|
|
let dlTab='local', dlArch='', dlGear='', dlPage=0, dlSort='created_at.desc';
|
|
|
|
function switchDlTab(tab){
|
|
dlTab=tab; dlPage=0;
|
|
document.getElementById('dlTabLocal').className='sn-btn'+(tab==='local'?' active':'');
|
|
document.getElementById('dlTabNam').className='sn-btn'+(tab==='nam'?' active':'');
|
|
document.getElementById('dlTabIr').className='sn-btn'+(tab==='ir'?' active':'');
|
|
// Show/hide filter rows per tab
|
|
const archRow=document.querySelectorAll('#overlayDownloads > .settings-nav')[1];
|
|
const gearRow=document.getElementById('dlGearRow');
|
|
const sortRow=document.getElementById('dlSortRow');
|
|
const searchRow=document.getElementById('dlSearchRow');
|
|
const showNam=tab==='nam';
|
|
if(archRow)archRow.style.display=showNam?'flex':'none';
|
|
if(gearRow)gearRow.style.display=showNam?'flex':'none';
|
|
if(sortRow)sortRow.style.display=showNam?'flex':'none';
|
|
if(searchRow)searchRow.style.display=(tab==='nam'||tab==='ir')?'flex':'none';
|
|
doDlSearch();
|
|
}
|
|
document.getElementById('dlTabLocal').onclick=()=>switchDlTab('local');
|
|
document.getElementById('dlTabNam').onclick=()=>switchDlTab('nam');
|
|
document.getElementById('dlTabIr').onclick=()=>switchDlTab('ir');
|
|
|
|
function setArch(btn,arch){
|
|
document.querySelectorAll('#overlayDownloads > .settings-nav')[1].querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
dlArch=arch; dlPage=0;
|
|
doDlSearch();
|
|
}
|
|
document.querySelectorAll('#overlayDownloads > .settings-nav')[1].querySelectorAll('.sn-btn[data-arch]').forEach(btn=>{
|
|
btn.onclick=()=>setArch(btn,btn.dataset.arch);
|
|
});
|
|
|
|
function setGear(btn,gear){
|
|
document.getElementById('dlGearRow').querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
dlGear=gear; dlPage=0;
|
|
doDlSearch();
|
|
}
|
|
document.getElementById('dlGearRow').querySelectorAll('.sn-btn[data-gear]').forEach(btn=>{
|
|
btn.onclick=()=>setGear(btn,btn.dataset.gear);
|
|
});
|
|
|
|
// Sort dropdown
|
|
document.getElementById('dlSortSelect').onchange=function(){
|
|
dlSort=this.value; dlPage=0; doDlSearch();
|
|
};
|
|
|
|
async function doDlSearch(){
|
|
const res=document.getElementById('dlResults');
|
|
res.innerHTML='<div style="text-align:center;padding:16px"><span class="spinner"></span></div>';
|
|
try{
|
|
// Local tab: list installed models from /api/models
|
|
if(dlTab==='local'){
|
|
const data=await API._fetch('/api/models');
|
|
const items=data.models||[];
|
|
const current=data.current||null;
|
|
res.innerHTML='';
|
|
if(items.length===0){
|
|
res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No models installed — download from the NAM tab</div>';
|
|
return;
|
|
}
|
|
items.forEach(item=>{
|
|
const d=document.createElement('div');
|
|
d.className='preset-card';
|
|
d.style.cursor='pointer';
|
|
const isLoaded=item.path===current;
|
|
if(isLoaded)d.style.borderColor='var(--green)';
|
|
const meta=[item.architecture||'?'];
|
|
if(item.size_mb)meta.push(item.size_mb+'MB');
|
|
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(isLoaded?'✅ Loaded':'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+meta.join(' · ')+'</div>';
|
|
d.onclick=async()=>{
|
|
toast('Loading...');
|
|
try{
|
|
await API._fetch('/api/models/load',{method:'POST',body:JSON.stringify({path:item.path}),timeout:10000});
|
|
toast('Loaded: '+item.name);
|
|
doDlSearch();
|
|
}catch(e){toast('Failed: '+e.message);}
|
|
};
|
|
res.appendChild(d);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// NAM / IR tab: Tone3000 search
|
|
const q=document.getElementById('dlSearch').value.trim();
|
|
const endpoint=dlTab==='nam'?'/api/models/tonedownload/search':'/api/irs/tonedownload/search';
|
|
let params='?q='+encodeURIComponent(q)+'&page='+dlPage;
|
|
if(dlArch)params+='&arch='+dlArch;
|
|
if(dlTab==='nam'&&dlGear)params+='&gear='+dlGear;
|
|
if(dlTab==='nam')params+='&sort='+dlSort;
|
|
const data=await API._fetch(endpoint+params);
|
|
const items=data.results||[];
|
|
res.innerHTML='';
|
|
if(items.length===0){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No results'+(q?'':' — no models found')+'</div>';return;}
|
|
items.forEach(item=>{
|
|
const d=document.createElement('div');
|
|
d.className='preset-card';
|
|
d.style.cursor='pointer';
|
|
const meta=[];
|
|
if(item.size_display)meta.push(item.size_display);
|
|
if(item.architecture)meta.push(item.architecture);
|
|
if(item.tone_gear&&item.tone_gear!=='nam')meta.push(item.tone_gear);
|
|
if(item.downloads>0)meta.push('⬇'+item.downloads);
|
|
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(item.author||'')+(item.pack_name?' · '+item.pack_name:'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+meta.join(' · ')+'</div>';
|
|
d.onclick=()=>showDlDetail(item);
|
|
res.appendChild(d);
|
|
});
|
|
// Pagination bar
|
|
const pbar=document.createElement('div');
|
|
pbar.style.cssText='display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 0;margin-top:4px;border-top:1px solid var(--border)';
|
|
pbar.innerHTML='<button class="sr-action" id="dlPrevBtn"'+(dlPage<1?' disabled':'')+'>← Prev</button>'+
|
|
'<span style="font-size:11px;color:var(--text-dim)">Page '+(dlPage+1)+'</span>'+
|
|
'<button class="sr-action" id="dlNextBtn"'+(items.length<20?' disabled':'')+'>Next →</button>';
|
|
res.appendChild(pbar);
|
|
document.getElementById('dlPrevBtn').onclick=()=>{if(dlPage>0){dlPage--;doDlSearch();}};
|
|
document.getElementById('dlNextBtn').onclick=()=>{if(items.length>=20){dlPage++;doDlSearch();}};
|
|
}catch(e){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim)">Error</div>';}
|
|
}
|
|
function showDlDetail(item){
|
|
const c=document.getElementById('dlDetailContent');
|
|
const isIr=dlTab==='ir';
|
|
let html='<div style="margin-bottom:8px">';
|
|
if(item.thumbnail)html+='<img src="'+item.thumbnail+'" style="width:100%;border-radius:8px;margin-bottom:6px;max-height:160px;object-fit:cover">';
|
|
html+='<div style="font-size:14px;font-weight:700;margin-bottom:4px">'+(item.name||'Unknown')+'</div>';
|
|
html+='<div style="font-size:11px;color:var(--text-sec);margin-bottom:6px">by '+(item.author||'Unknown')+'</div>';
|
|
if(item.pack_name)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Pack: '+item.pack_name+'</div>';
|
|
if(item.architecture)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Arch: '+item.architecture+'</div>';
|
|
if(item.size_display)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Size: '+item.size_display+'</div>';
|
|
if(item.downloads>0)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">⬇ Downloads: '+item.downloads+'</div>';
|
|
if(item.likes>0)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">❤ Likes: '+item.likes+'</div>';
|
|
if(item.tone_gear)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Gear: '+item.tone_gear+'</div>';
|
|
if(item.tone_description)html+='<div style="font-size:10px;color:var(--text2);margin-top:6px;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);line-height:1.4">'+(item.tone_description||'')+'</div>';
|
|
html+='</div>';
|
|
html+='<button class="sr-action primary" id="dlDetailBtn" style="width:100%;min-height:42px;font-size:13px">Download'+(isIr?' IR':'')+'</button>';
|
|
c.innerHTML=html;
|
|
document.getElementById('dlDetailTitle').textContent=(item.name||'Model');
|
|
document.getElementById('overlayDetail').classList.add('open');
|
|
document.getElementById('dlDetailBtn').onclick=async()=>{
|
|
const btn=document.getElementById('dlDetailBtn');
|
|
btn.textContent='...';
|
|
try{
|
|
const endpoint=isIr?'/api/irs/tonedownload/install':'/api/models/tonedownload/install';
|
|
await API._fetch(endpoint,{method:'POST',body:JSON.stringify({id:item.id,download_url:item.download_url,name:item.name,filename:item.filename,size:item.size,author:item.author,author_avatar:item.author_avatar,thumbnail:item.thumbnail,tone_description:item.tone_description,pack_name:item.pack_name,architecture:item.architecture,created_at:item.created_at,downloads:item.downloads,likes:item.likes}),timeout:60000});
|
|
toast('Downloaded!');
|
|
btn.textContent='Downloaded';
|
|
btn.className='sr-action';
|
|
}catch(e){toast(e.message);btn.textContent='Retry';}
|
|
};
|
|
}
|
|
// Wire downloads search
|
|
document.getElementById('dlSearchBtn').onclick=doDlSearch;
|
|
document.getElementById('dlSearch').onkeydown=(e)=>{if(e.key==='Enter')doDlSearch();};
|
|
|
|
// Start polling
|
|
startPolling();
|
|
loadPresets();
|
|
loadSnapshots();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded',init);
|
|
</script>
|
|
</body>
|
|
</html>
|