import json import js js.document.getElementById("loader").className = "hidden" WS_PORT = 8765 _state = { "widgets": {}, "layout": {}, "locked": True, "preset_uuid": None, "preset_name": "", } # --- WebSocket --- _host = js.window.location.hostname _ws = js.WebSocket.new(f"ws://{_host}:{WS_PORT}") def _on_open(e): js.document.getElementById("status").innerText = "connected" def _on_close(e): js.document.getElementById("status").innerText = "disconnected" def _on_error(e): js.document.getElementById("status").innerText = "error" def _on_message(e): msg = json.loads(e.data) ev = msg.get("event") if ev == "preset": _render_preset(msg["data"], msg.get("layout") or {}) elif ev == "widget_update": _update_widget(msg["uid"], msg["value"]) elif ev == "daw_state": _update_daw(msg) _ws.onopen = _on_open _ws.onclose = _on_close _ws.onerror = _on_error _ws.onmessage = _on_message def _send(data): _ws.send(json.dumps(data)) # --- Rendering --- _canvas = js.document.getElementById("canvas") def _render_preset(data, saved_layout): _state["layout"] = saved_layout _state["preset_uuid"] = data.get("preset_uuid") _state["preset_name"] = data.get("preset_name", "") _state["widgets"].clear() _canvas.innerHTML = "" js.document.getElementById("preset-name").innerText = _state["preset_name"] idx = 0 for w in data.get("faders", []): if w.get("visible", True): _make_fader(w, idx) idx += 1 for w in data.get("toggles", []): if w.get("visible", True): _make_toggle(w, idx) idx += 1 for w in data.get("transports", []): if w.get("visible", True): _make_transport(w, idx) idx += 1 _state["locked"] = bool(saved_layout) _refresh_toolbar() def _default_pos(idx): cols = 4 return {"x": 16 + (idx % cols) * 90, "y": 48 + (idx // cols) * 220, "w": 70, "h": 200} def _make_container(uid, label, idx): pos = _state["layout"].get(uid) or _default_pos(idx) el = js.document.createElement("div") el.className = "widget" el.style.left = f"{pos['x']}px" el.style.top = f"{pos['y']}px" el.style.width = f"{pos.get('w', 70)}px" lbl = js.document.createElement("div") lbl.className = "widget-label" lbl.innerText = label or " " el.appendChild(lbl) _canvas.appendChild(el) _state["widgets"][uid] = {"el": el, "pos": dict(pos)} _setup_drag(el, lbl, uid) return el def _make_fader(data, idx): uid = data["uid"] color = data.get("color", "#00e676") el = _make_container(uid, data.get("label", ""), idx) el.style.borderColor = color inp = js.document.createElement("input") inp.type = "range" inp.min = "0" inp.max = "127" inp.value = str(data.get("value", 0)) inp.style.accentColor = color readout = js.document.createElement("div") readout.className = "val-readout" readout.innerText = str(data.get("value", 0)) readout.id = f"ro-{uid}" def on_input(e, _uid=uid, _ro=readout): v = int(e.target.value) _ro.innerText = str(v) _send({"event": "control", "uid": _uid, "value": v}) inp.oninput = on_input el.appendChild(inp) el.appendChild(readout) _state["widgets"][uid]["input"] = inp _state["widgets"][uid]["color"] = color def _make_toggle(data, idx): uid = data["uid"] color = data.get("color", "#00e676") el = _make_container(uid, data.get("label", ""), idx) el.style.borderColor = color btn = js.document.createElement("button") on = bool(data.get("state")) btn.className = "ctrl-btn" + (" on" if on else "") btn.innerText = "ON" if on else "OFF" if on: btn.style.background = color btn.style.color = "#000" btn.style.borderColor = color def on_click(e, _uid=uid): _send({"event": "control", "uid": _uid, "value": 127}) btn.onclick = on_click el.appendChild(btn) _state["widgets"][uid]["btn"] = btn _state["widgets"][uid]["kind"] = "toggle" _state["widgets"][uid]["color"] = color def _make_transport(data, idx): uid = data["uid"] el = _make_container(uid, "", idx) btn = js.document.createElement("button") btn.className = "ctrl-btn" btn.innerText = data.get("label", "") def on_click(e, _uid=uid): _send({"event": "control", "uid": _uid, "value": 127}) btn.onclick = on_click el.appendChild(btn) _state["widgets"][uid]["btn"] = btn _state["widgets"][uid]["kind"] = "transport" # --- Widget updates from hardware --- def _update_widget(uid, value): w = _state["widgets"].get(uid) if not w: return if "input" in w: w["input"].value = str(value) ro = js.document.getElementById(f"ro-{uid}") if ro: ro.innerText = str(value) if "btn" in w and w.get("kind") == "toggle": is_on = value > 63 color = w.get("color", "#00e676") btn = w["btn"] btn.className = "ctrl-btn" + (" on" if is_on else "") btn.innerText = "ON" if is_on else "OFF" if is_on: btn.style.background = color btn.style.color = "#000" btn.style.borderColor = color else: btn.style.background = "" btn.style.color = "" btn.style.borderColor = "" def _update_daw(msg): parts = [] if "track" in msg: parts.append(msg["track"]) if "bar" in msg: parts.append(f"Bar {msg['bar']}") if "playing" in msg: parts.append("▶" if msg["playing"] else "■") text = " ".join(p for p in parts if p) if text: js.document.getElementById("status").innerText = text # --- Drag to arrange --- def _setup_drag(el, handle, uid): drag = {"active": False, "sx": 0, "sy": 0, "ox": 0, "oy": 0} def on_down(e, _d=drag, _el=el, _uid=uid): if _state["locked"]: return _d["active"] = True _d["sx"] = e.clientX _d["sy"] = e.clientY _d["ox"] = int(_el.style.left.replace("px", "") or 0) _d["oy"] = int(_el.style.top.replace("px", "") or 0) handle.setPointerCapture(e.pointerId) e.preventDefault() def on_move(e, _d=drag, _el=el, _uid=uid): if not _d["active"]: return x = _d["ox"] + e.clientX - _d["sx"] y = _d["oy"] + e.clientY - _d["sy"] _el.style.left = f"{x}px" _el.style.top = f"{y}px" if _uid in _state["widgets"]: _state["widgets"][_uid]["pos"] = { "x": x, "y": y, "w": int(_el.offsetWidth), "h": int(_el.offsetHeight), } def on_up(e, _d=drag): _d["active"] = False handle.addEventListener("pointerdown", on_down) handle.addEventListener("pointermove", on_move) handle.addEventListener("pointerup", on_up) # --- Toolbar --- def _save_layout(): uuid = _state["preset_uuid"] if not uuid: return layout = {uid: w["pos"] for uid, w in _state["widgets"].items()} _send({"event": "save_layout", "preset_uuid": uuid, "layout": layout}) def _refresh_toolbar(): locked = _state["locked"] js.document.getElementById("btn-lock").className = "tbtn" + (" active" if locked else "") js.document.getElementById("btn-arrange").className = "tbtn" + (" active" if not locked else "") hint = js.document.getElementById("arrange-hint") hint.style.display = "none" if locked else "block" def _on_arrange(e): _state["locked"] = False _refresh_toolbar() def _on_lock(e): _state["locked"] = True _refresh_toolbar() _save_layout() js.document.getElementById("btn-arrange").onclick = _on_arrange js.document.getElementById("btn-lock").onclick = _on_lock