4db7165797
- Restructured project into /app, /server, /client with run.py entry point - WSServer (QWebSocketServer) with log_signal, start/stop, client tracking - HTTP server serves /client on port 8080 - Vanilla JS client: faders, toggles, transport, drag-to-arrange, lock/save layout - Start Tablet button + floating Tablet log window in app UI - Preset broadcast on load/switch, DAW state relay, hardware sync via widget_update - Widget colors synced from app palette, toggle state fixed (sends 0/127 correctly) - Layout saved per preset UUID back to presets.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
237 lines
6.6 KiB
JavaScript
237 lines
6.6 KiB
JavaScript
const WS_PORT = 8765;
|
|
|
|
const state = {
|
|
widgets: {},
|
|
layout: {},
|
|
locked: true,
|
|
presetUuid: null,
|
|
presetName: '',
|
|
};
|
|
|
|
// --- WebSocket ---
|
|
|
|
const ws = new WebSocket(`ws://${window.location.hostname}:${WS_PORT}`);
|
|
|
|
ws.onopen = () => {
|
|
document.getElementById('status').innerText = 'connected';
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
document.getElementById('status').innerText = 'disconnected';
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
document.getElementById('status').innerText = 'error';
|
|
};
|
|
|
|
ws.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.event === 'preset') renderPreset(msg.data, msg.layout || {});
|
|
else if (msg.event === 'widget_update') updateWidget(msg.uid, msg.value);
|
|
else if (msg.event === 'daw_state') updateDaw(msg);
|
|
};
|
|
|
|
function send(data) {
|
|
ws.send(JSON.stringify(data));
|
|
}
|
|
|
|
// --- Rendering ---
|
|
|
|
const canvas = document.getElementById('canvas');
|
|
|
|
function renderPreset(data, savedLayout) {
|
|
state.layout = savedLayout;
|
|
state.presetUuid = data.preset_uuid;
|
|
state.presetName = data.preset_name || '';
|
|
state.widgets = {};
|
|
canvas.innerHTML = '';
|
|
|
|
document.getElementById('preset-name').innerText = state.presetName;
|
|
|
|
let idx = 0;
|
|
(data.faders || []).filter(w => w.visible !== false).forEach(w => makeFader(w, idx++));
|
|
(data.toggles || []).filter(w => w.visible !== false).forEach(w => makeToggle(w, idx++));
|
|
(data.transports|| []).filter(w => w.visible !== false).forEach(w => makeTransport(w, idx++));
|
|
|
|
state.locked = Object.keys(savedLayout).length > 0;
|
|
refreshToolbar();
|
|
}
|
|
|
|
function defaultPos(idx) {
|
|
const cols = 3;
|
|
return { x: 16 + (idx % cols) * 170, y: 48 + Math.floor(idx / cols) * 420, w: 140, h: 380 };
|
|
}
|
|
|
|
function makeContainer(uid, label, idx) {
|
|
const pos = state.layout[uid] || defaultPos(idx);
|
|
|
|
const el = document.createElement('div');
|
|
el.className = 'widget';
|
|
el.style.left = `${pos.x}px`;
|
|
el.style.top = `${pos.y}px`;
|
|
el.style.width = `${pos.w || 70}px`;
|
|
|
|
const lbl = document.createElement('div');
|
|
lbl.className = 'widget-label';
|
|
lbl.innerText = label || ' ';
|
|
el.appendChild(lbl);
|
|
|
|
canvas.appendChild(el);
|
|
state.widgets[uid] = { el, pos: { ...pos } };
|
|
setupDrag(el, lbl, uid);
|
|
return el;
|
|
}
|
|
|
|
function makeFader(data, idx) {
|
|
const { uid, color = '#00e676' } = data;
|
|
const el = makeContainer(uid, data.label || '', idx);
|
|
el.style.borderColor = color;
|
|
|
|
const inp = document.createElement('input');
|
|
inp.type = 'range';
|
|
inp.min = '0'; inp.max = '127';
|
|
inp.value = String(data.value ?? 0);
|
|
inp.style.accentColor = color;
|
|
|
|
const readout = document.createElement('div');
|
|
readout.className = 'val-readout';
|
|
readout.innerText = String(data.value ?? 0);
|
|
readout.id = `ro-${uid}`;
|
|
|
|
inp.oninput = () => {
|
|
const v = parseInt(inp.value);
|
|
readout.innerText = String(v);
|
|
send({ event: 'control', uid, value: v });
|
|
};
|
|
|
|
el.appendChild(inp);
|
|
el.appendChild(readout);
|
|
state.widgets[uid].input = inp;
|
|
state.widgets[uid].color = color;
|
|
}
|
|
|
|
function makeToggle(data, idx) {
|
|
const { uid, color = '#00e676' } = data;
|
|
const el = makeContainer(uid, data.label || '', idx);
|
|
el.style.borderColor = color;
|
|
|
|
const btn = document.createElement('button');
|
|
const on = !!data.state;
|
|
btn.className = 'ctrl-btn' + (on ? ' on' : '');
|
|
btn.innerText = on ? 'ON' : 'OFF';
|
|
if (on) { btn.style.background = color; btn.style.color = '#000'; btn.style.borderColor = color; }
|
|
|
|
btn.onclick = () => {
|
|
const isOn = !!state.widgets[uid].isOn;
|
|
send({ event: 'control', uid, value: isOn ? 0 : 127 });
|
|
};
|
|
|
|
el.appendChild(btn);
|
|
state.widgets[uid].btn = btn;
|
|
state.widgets[uid].kind = 'toggle';
|
|
state.widgets[uid].color = color;
|
|
state.widgets[uid].isOn = on;
|
|
}
|
|
|
|
function makeTransport(data, idx) {
|
|
const { uid } = data;
|
|
const el = makeContainer(uid, '', idx);
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'ctrl-btn';
|
|
btn.innerText = data.label || '';
|
|
|
|
btn.onclick = () => send({ event: 'control', uid, value: 127 });
|
|
|
|
el.appendChild(btn);
|
|
state.widgets[uid].btn = btn;
|
|
state.widgets[uid].kind = 'transport';
|
|
}
|
|
|
|
// --- Widget updates from hardware ---
|
|
|
|
function updateWidget(uid, value) {
|
|
const w = state.widgets[uid];
|
|
if (!w) return;
|
|
|
|
if (w.input) {
|
|
w.input.value = String(value);
|
|
const ro = document.getElementById(`ro-${uid}`);
|
|
if (ro) ro.innerText = String(value);
|
|
}
|
|
|
|
if (w.btn && w.kind === 'toggle') {
|
|
const on = value > 63;
|
|
const color = w.color || '#00e676';
|
|
w.isOn = on;
|
|
w.btn.className = 'ctrl-btn' + (on ? ' on' : '');
|
|
w.btn.innerText = on ? 'ON' : 'OFF';
|
|
if (on) { w.btn.style.background = color; w.btn.style.color = '#000'; w.btn.style.borderColor = color; }
|
|
else { w.btn.style.background = ''; w.btn.style.color = ''; w.btn.style.borderColor = ''; }
|
|
}
|
|
}
|
|
|
|
function updateDaw(msg) {
|
|
const parts = [];
|
|
if (msg.track) parts.push(msg.track);
|
|
if (msg.bar) parts.push(`Bar ${msg.bar}`);
|
|
if (msg.playing !== undefined) parts.push(msg.playing ? '▶' : '■');
|
|
const text = parts.filter(Boolean).join(' ');
|
|
if (text) document.getElementById('status').innerText = text;
|
|
}
|
|
|
|
// --- Drag to arrange ---
|
|
|
|
function setupDrag(el, handle, uid) {
|
|
let active = false, sx = 0, sy = 0, ox = 0, oy = 0;
|
|
|
|
handle.addEventListener('pointerdown', (e) => {
|
|
if (state.locked) return;
|
|
active = true;
|
|
sx = e.clientX; sy = e.clientY;
|
|
ox = parseInt(el.style.left) || 0;
|
|
oy = parseInt(el.style.top) || 0;
|
|
handle.setPointerCapture(e.pointerId);
|
|
e.preventDefault();
|
|
});
|
|
|
|
handle.addEventListener('pointermove', (e) => {
|
|
if (!active) return;
|
|
const x = ox + e.clientX - sx;
|
|
const y = oy + e.clientY - sy;
|
|
el.style.left = `${x}px`;
|
|
el.style.top = `${y}px`;
|
|
if (state.widgets[uid]) {
|
|
state.widgets[uid].pos = { x, y, w: el.offsetWidth, h: el.offsetHeight };
|
|
}
|
|
});
|
|
|
|
handle.addEventListener('pointerup', () => { active = false; });
|
|
}
|
|
|
|
// --- Toolbar ---
|
|
|
|
function saveLayout() {
|
|
if (!state.presetUuid) return;
|
|
const layout = {};
|
|
Object.entries(state.widgets).forEach(([uid, w]) => { layout[uid] = w.pos; });
|
|
send({ event: 'save_layout', preset_uuid: state.presetUuid, layout });
|
|
}
|
|
|
|
function refreshToolbar() {
|
|
document.getElementById('btn-lock').className = 'tbtn' + (state.locked ? ' active' : '');
|
|
document.getElementById('btn-arrange').className = 'tbtn' + (state.locked ? '' : ' active');
|
|
document.getElementById('arrange-hint').style.display = state.locked ? 'none' : 'block';
|
|
}
|
|
|
|
document.getElementById('btn-arrange').onclick = () => {
|
|
state.locked = false;
|
|
refreshToolbar();
|
|
};
|
|
|
|
document.getElementById('btn-lock').onclick = () => {
|
|
state.locked = true;
|
|
refreshToolbar();
|
|
saveLayout();
|
|
};
|