- Complete SPA rewrite: 4 tabs (Mixer, Routing, Plugins, Sessions)
- Touch-optimized faders with animated level meters (dark theme)
- Channel strips: fader, mute, solo, pan, EQ modal with 3-band EQ + compressor + gate
- Routing matrix: drag-and-drop source→destination, connection list with mute toggles
- Plugin control: per-channel plugin cards with bypass and parameter sliders
- Session browser: list/load/save/delete scenes via REST API
- Transport controls: play, stop, record, loop, locate (keyboard shortcuts: space=play)
- Responsive: phone portrait (touch-first), landscape, tablet, desktop
- Zero dependencies: vanilla JS + WebSocket only
- Backend: added DELETE /api/v1/scenes/{name} endpoint, wired delete_scene callback
web/app.js (917 lines), web/style.css (1100 lines), web/index.html (160 lines)
This commit is contained in:
+875
-252
File diff suppressed because it is too large
Load Diff
+130
-26
@@ -2,52 +2,156 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<title>RPi Audio Mixer</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
<!-- ═══ Toolbar ═══ -->
|
||||
<header id="toolbar">
|
||||
<h1>RPi Audio Mixer</h1>
|
||||
<h1>🎚️ RPi Mixer</h1>
|
||||
<div id="connection">
|
||||
<span id="conn-status" class="status disconnected">● Disconnected</span>
|
||||
<span id="conn-status" class="status disconnected">● Offline</span>
|
||||
<input type="text" id="apikey-input" placeholder="API Key" value="mixer-local" size="14">
|
||||
<button id="login-btn" onclick="login()">Login</button>
|
||||
<button id="connect-btn" onclick="connectWS()" disabled>Connect</button>
|
||||
<button id="login-btn" class="btn" onclick="App.login()">Login</button>
|
||||
<button id="connect-btn" class="btn" onclick="App.connectWS()" disabled>Connect</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══ Tab bar ═══ -->
|
||||
<nav id="tabs">
|
||||
<button class="tab active" data-view="mixer" onclick="App.showView('mixer')">
|
||||
<span class="tab-icon">🎛️</span> Mixer
|
||||
</button>
|
||||
<button class="tab" data-view="routing" onclick="App.showView('routing')">
|
||||
<span class="tab-icon">🔀</span> Routing
|
||||
</button>
|
||||
<button class="tab" data-view="plugins" onclick="App.showView('plugins')">
|
||||
<span class="tab-icon">🔌</span> Plugins
|
||||
</button>
|
||||
<button class="tab" data-view="sessions" onclick="App.showView('sessions')">
|
||||
<span class="tab-icon">💾</span> Sessions
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- ═══ Transport bar (global) ═══ -->
|
||||
<div id="transport-bar">
|
||||
<button class="tbtn" id="btn-play" onclick="sendTransport('play')">▶ Play</button>
|
||||
<button class="tbtn" id="btn-stop" onclick="sendTransport('stop')">■ Stop</button>
|
||||
<button class="tbtn" id="btn-record" onclick="sendTransport('record')">● Rec</button>
|
||||
<button class="tbtn" id="btn-loop" onclick="sendTransport('loop')">↻ Loop</button>
|
||||
<span id="tempo-display">120 BPM</span>
|
||||
<span id="time-display">00:00:00</span>
|
||||
<button class="tbtn" id="btn-play" onclick="App.sendTransport('play')" title="Play">▶</button>
|
||||
<button class="tbtn" id="btn-stop" onclick="App.sendTransport('stop')" title="Stop">■</button>
|
||||
<button class="tbtn" id="btn-record" onclick="App.sendTransport('record')" title="Record">●</button>
|
||||
<button class="tbtn" id="btn-loop" onclick="App.sendTransport('loop')" title="Loop">↻</button>
|
||||
<button class="tbtn" id="btn-locate" onclick="App.showLocateDialog()" title="Locate">⏱️</button>
|
||||
<span class="transport-info" id="tempo-display">120.0 BPM</span>
|
||||
<span class="transport-info" id="time-display">00:00:00</span>
|
||||
<span class="transport-info dimmed" id="uptime-display"></span>
|
||||
</div>
|
||||
|
||||
<div id="mixer">
|
||||
<div id="channels"></div>
|
||||
<div id="master-section">
|
||||
<h3>Master</h3>
|
||||
<div class="fader-v" id="master-fader">
|
||||
<input type="range" min="-60" max="12" value="0" step="0.5" orient="vertical"
|
||||
oninput="sendParam('master_volume', this.value, -1)">
|
||||
<span class="fader-label">Master</span>
|
||||
<span class="fader-value" id="master-vol-val">0.0 dB</span>
|
||||
<!-- ═══ Main content ═══ -->
|
||||
<main id="view-container">
|
||||
|
||||
<!-- ── Mixer View ── -->
|
||||
<section id="view-mixer" class="view active">
|
||||
<div id="mixer-scroll">
|
||||
<div id="channels"></div>
|
||||
<div id="master-section">
|
||||
<div class="section-label">Master</div>
|
||||
<div class="fader-container" id="master-fader">
|
||||
<div class="meter-v" id="master-meter"><div class="meter-fill"></div></div>
|
||||
<input type="range" min="-60" max="12" value="0" step="0.5" orient="vertical"
|
||||
oninput="App.sendParam('master_volume', this.value, -1)">
|
||||
<span class="fader-label">Master</span>
|
||||
<span class="fader-db" id="master-vol-val">0.0 dB</span>
|
||||
</div>
|
||||
<div class="master-buttons">
|
||||
<button class="toggle-btn" id="btn-mute-master" onclick="App.toggleParam('master_mute', -1)">M</button>
|
||||
<button class="toggle-btn" id="btn-dim" onclick="App.toggleParam('master_dim', -1)">Dim</button>
|
||||
<button class="toggle-btn" id="btn-mono" onclick="App.toggleParam('master_mono', -1)">Mono</button>
|
||||
</div>
|
||||
<div class="master-buses" id="master-buses-info"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="master-controls">
|
||||
<button id="btn-mute-master" onclick="sendParam('master_mute', 1, -1)">Mute</button>
|
||||
<button id="btn-dim" onclick="sendParam('master_dim', 1, -1)">Dim</button>
|
||||
<button id="btn-mono" onclick="sendParam('master_mono', 1, -1)">Mono</button>
|
||||
</section>
|
||||
|
||||
<!-- ── Routing View ── -->
|
||||
<section id="view-routing" class="view">
|
||||
<div class="routing-layout">
|
||||
<div class="routing-panel sources-panel">
|
||||
<h3>Sources</h3>
|
||||
<div id="routing-sources" class="routing-list"></div>
|
||||
</div>
|
||||
<div class="routing-panel matrix-panel">
|
||||
<h3>Connections</h3>
|
||||
<div id="routing-matrix" class="routing-grid"></div>
|
||||
</div>
|
||||
<div class="routing-panel dests-panel">
|
||||
<h3>Destinations</h3>
|
||||
<div id="routing-dests" class="routing-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Plugins View ── -->
|
||||
<section id="view-plugins" class="view">
|
||||
<div class="plugins-header">
|
||||
<h3>Channel Plugins</h3>
|
||||
<select id="plugin-channel-select" onchange="App.showPluginsForChannel(this.value)">
|
||||
<option value="all">All Channels</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="plugins-list" class="plugins-grid"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── Sessions View ── -->
|
||||
<section id="view-sessions" class="view">
|
||||
<div class="sessions-header">
|
||||
<h3>Saved Sessions</h3>
|
||||
<div class="sessions-actions">
|
||||
<input type="text" id="scene-name-input" placeholder="Scene name" class="text-input">
|
||||
<button class="btn btn-primary" onclick="App.saveScene()">💾 Save Current</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sessions-list" class="sessions-grid"></div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ═══ Channel EQ modal ═══ -->
|
||||
<div id="eq-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="App.closeEQ()"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="eq-modal-title">Channel EQ</h3>
|
||||
<button class="modal-close" onclick="App.closeEQ()">✕</button>
|
||||
</div>
|
||||
<div id="eq-controls">
|
||||
<label class="eq-toggle">
|
||||
<input type="checkbox" id="eq-enable" onchange="App.sendParam('eq_enable', this.checked ? 1 : 0, App.eqChannel)">
|
||||
Enable EQ
|
||||
</label>
|
||||
<div class="eq-bands" id="eq-bands"></div>
|
||||
</div>
|
||||
<div id="dyn-controls">
|
||||
<h4>Compressor</h4>
|
||||
<div class="param-rows" id="comp-params"></div>
|
||||
<h4>Noise Gate</h4>
|
||||
<div class="param-rows" id="gate-params"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Log panel ═══ -->
|
||||
<div id="log-panel">
|
||||
<h3>Activity Log</h3>
|
||||
<div id="log"></div>
|
||||
<div id="log-header">
|
||||
<span id="log-toggle" onclick="App.toggleLog()">📋 Log ▾</span>
|
||||
</div>
|
||||
<div id="log-body" class="collapsed">
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+935
-119
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user