From 7a74cfa75ef20b3dac13da75861b3a3162f41cd1 Mon Sep 17 00:00:00 2001 From: Paul Lipscomb Date: Wed, 1 Jul 2026 00:02:10 -0400 Subject: [PATCH] iOS remote client: native UIKit surface, WS logging, client-side layout persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add remote-client-ios: full native UIKit app with Objects/View/UILayout/Functions pattern - Widget hierarchy: BaseWidget → Fader, Toggle, Transport, Title, Feedback, LogWidget - LoggingVC: child VC embedded in LogWidget for live WS message inspection - Codable model: ModelPresetLoadToRemoteDevice with snake_case decoder strategy - Layout persistence: UserDefaults keyed by preset_uuid, fully client-side - WS logging: dedicated WS In/Out floating panels on desktop app - Remove HTTP server and JS client (replaced by native iOS app) - Add Xcode noise to .gitignore (xcuserstate, xcuserdata) Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.json | 7 + .gitignore | 9 + app/main.py | 145 ++++- client/app.js | 236 ------- client/app.py | 292 --------- client/index.html | 36 -- presets/presets.json | 367 +++++++++-- .../project.pbxproj | 578 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../remote-client-ios/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../remote-client-ios/Core/Functions.swift | 283 +++++++++ .../Core/LoggingVC/LoggingFunctions.swift | 24 + .../Core/LoggingVC/LoggingObjects.swift | 26 + .../Core/LoggingVC/LoggingUILayout.swift | 21 + .../Core/LoggingVC/LoggingView.swift | 10 + .../Models/PresetLoadToRemoteDevice.swift | 36 ++ .../Core/ObjectsViewController.swift | 157 +++++ .../remote-client-ios/Core/UILayout.swift | 60 ++ .../remote-client-ios/Core/View.swift | 19 + .../Core/ui-reusable/BaseWidget.swift | 35 ++ .../Core/ui-reusable/FaderWidget.swift | 81 +++ .../Core/ui-reusable/FeedbackWidget.swift | 81 +++ .../Core/ui-reusable/LogWidget.swift | 49 ++ .../Core/ui-reusable/TitleWidget.swift | 40 ++ .../Core/ui-reusable/ToggleWidget.swift | 71 +++ .../Core/ui-reusable/TransportWidget.swift | 53 ++ .../remote-client-ios/Info.plist | 25 + .../remote-client-ios/SceneDelegate.swift | 52 ++ .../remote_client_iosTests.swift | 17 + .../remote_client_iosUITests.swift | 41 ++ .../remote_client_iosUITestsLaunchTests.swift | 33 + server/http_server.py | 25 - server/ws_server.py | 5 +- 38 files changed, 2422 insertions(+), 636 deletions(-) create mode 100644 .claude/settings.json delete mode 100644 client/app.js delete mode 100644 client/app.py delete mode 100644 client/index.html create mode 100644 remote-client-ios/remote-client-ios.xcodeproj/project.pbxproj create mode 100644 remote-client-ios/remote-client-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 remote-client-ios/remote-client-ios/AppDelegate.swift create mode 100644 remote-client-ios/remote-client-ios/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 remote-client-ios/remote-client-ios/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 remote-client-ios/remote-client-ios/Assets.xcassets/Contents.json create mode 100644 remote-client-ios/remote-client-ios/Base.lproj/LaunchScreen.storyboard create mode 100644 remote-client-ios/remote-client-ios/Base.lproj/Main.storyboard create mode 100644 remote-client-ios/remote-client-ios/Core/Functions.swift create mode 100644 remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingFunctions.swift create mode 100644 remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingObjects.swift create mode 100644 remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingUILayout.swift create mode 100644 remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingView.swift create mode 100644 remote-client-ios/remote-client-ios/Core/Models/PresetLoadToRemoteDevice.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ObjectsViewController.swift create mode 100644 remote-client-ios/remote-client-ios/Core/UILayout.swift create mode 100644 remote-client-ios/remote-client-ios/Core/View.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/BaseWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/FaderWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/FeedbackWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/LogWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/TitleWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/ToggleWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Core/ui-reusable/TransportWidget.swift create mode 100644 remote-client-ios/remote-client-ios/Info.plist create mode 100644 remote-client-ios/remote-client-ios/SceneDelegate.swift create mode 100644 remote-client-ios/remote-client-iosTests/remote_client_iosTests.swift create mode 100644 remote-client-ios/remote-client-iosUITests/remote_client_iosUITests.swift create mode 100644 remote-client-ios/remote-client-iosUITests/remote_client_iosUITestsLaunchTests.swift delete mode 100644 server/http_server.py diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8c1f328 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(git add *)" + ] + } +} diff --git a/.gitignore b/.gitignore index 1acd030..8d5e8f8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,12 @@ __pycache__/ *.pyc .DS_Store + +# Xcode +*.xcuserstate +xcuserdata/ +*.xccheckout +*.moved-aside +DerivedData/ +*.hmap +*.ipa diff --git a/app/main.py b/app/main.py index 525943d..3a313a6 100644 --- a/app/main.py +++ b/app/main.py @@ -72,7 +72,6 @@ from osc_sender import ( ) from ws_server import WSServer -from http_server import start_http_server, stop_http_server def set_window(w): global _window @@ -83,7 +82,6 @@ _osc_server = None _osc_thread = None ws_server = None -_http_server = None @@ -366,6 +364,11 @@ button_tablet_log = QPushButton("Tablet") button_tablet_log.setFixedWidth(65) button_tablet_log.setCheckable(True) +# Widget: button_ws_log +button_ws_log = QPushButton("WS") +button_ws_log.setFixedWidth(50) +button_ws_log.setCheckable(True) + # Widget: label_tablet_status label_tablet_status = QLabel("") label_tablet_status.setStyleSheet("color: #8e24aa; font-size: 11px; font-family: monospace;") @@ -587,6 +590,62 @@ textedit_midi_out_log.setStyleSheet(""" } """) +# Container: container_ws_in_log +container_ws_in_log = QWidget(None, Qt.WindowType.Window) +container_ws_in_log.setWindowTitle("WS In — Incoming from Tablet") +container_ws_in_log.setStyleSheet("background-color: #1a1a1a;") +container_ws_in_log.setFixedWidth(420) + +vstack_ws_in_log = QVBoxLayout(container_ws_in_log) +vstack_ws_in_log.setContentsMargins(6, 6, 6, 6) +vstack_ws_in_log.setSpacing(4) + +hstack_ws_in_log_header = QHBoxLayout() +label_ws_in_log_title = QLabel("WS In — Incoming from Tablet") +label_ws_in_log_title.setStyleSheet("color: #00acc1; font-weight: bold; font-size: 11px;") +button_ws_in_clear = QPushButton("Clear") +button_ws_in_clear.setFixedSize(45, 18) + +textedit_ws_in_log = QTextEdit() +textedit_ws_in_log.setReadOnly(True) +textedit_ws_in_log.setStyleSheet(""" + QTextEdit { + background-color: #111111; + color: #00e5ff; + font-family: monospace; + font-size: 10px; + border: none; + } +""") + +# Container: container_ws_out_log +container_ws_out_log = QWidget(None, Qt.WindowType.Window) +container_ws_out_log.setWindowTitle("WS Out — Outgoing to Tablet") +container_ws_out_log.setStyleSheet("background-color: #1a1a1a;") +container_ws_out_log.setFixedWidth(420) + +vstack_ws_out_log = QVBoxLayout(container_ws_out_log) +vstack_ws_out_log.setContentsMargins(6, 6, 6, 6) +vstack_ws_out_log.setSpacing(4) + +hstack_ws_out_log_header = QHBoxLayout() +label_ws_out_log_title = QLabel("WS Out — Outgoing to Tablet") +label_ws_out_log_title.setStyleSheet("color: #ff6f00; font-weight: bold; font-size: 11px;") +button_ws_out_clear = QPushButton("Clear") +button_ws_out_clear.setFixedSize(45, 18) + +textedit_ws_out_log = QTextEdit() +textedit_ws_out_log.setReadOnly(True) +textedit_ws_out_log.setStyleSheet(""" + QTextEdit { + background-color: #111111; + color: #ffb300; + font-family: monospace; + font-size: 10px; + border: none; + } +""") + # Container: container_tablet_log container_tablet_log = QWidget(None, Qt.WindowType.Window) container_tablet_log.setWindowTitle("Tablet Server") @@ -1045,6 +1104,7 @@ class MainWindow(QMainWindow): hstack_osc_buttons.addSpacing(16) hstack_osc_buttons.addWidget(button_tablet_server) hstack_osc_buttons.addWidget(button_tablet_log) + hstack_osc_buttons.addWidget(button_ws_log) hstack_osc_buttons.addWidget(label_tablet_status) hstack_osc_buttons.addStretch() @@ -1177,6 +1237,24 @@ class MainWindow(QMainWindow): button_midi_out_clear.clicked.connect(lambda: textedit_midi_out_log.clear()) container_midi_out_log.setVisible(False) + #MARK: WS IN LOG CONTAINER + hstack_ws_in_log_header.addWidget(label_ws_in_log_title) + hstack_ws_in_log_header.addStretch() + hstack_ws_in_log_header.addWidget(button_ws_in_clear) + vstack_ws_in_log.addLayout(hstack_ws_in_log_header) + vstack_ws_in_log.addWidget(textedit_ws_in_log) + button_ws_in_clear.clicked.connect(lambda: textedit_ws_in_log.clear()) + container_ws_in_log.setVisible(False) + + #MARK: WS OUT LOG CONTAINER + hstack_ws_out_log_header.addWidget(label_ws_out_log_title) + hstack_ws_out_log_header.addStretch() + hstack_ws_out_log_header.addWidget(button_ws_out_clear) + vstack_ws_out_log.addLayout(hstack_ws_out_log_header) + vstack_ws_out_log.addWidget(textedit_ws_out_log) + button_ws_out_clear.clicked.connect(lambda: textedit_ws_out_log.clear()) + container_ws_out_log.setVisible(False) + #MARK: TABLET LOG CONTAINER hstack_tablet_log_header.addWidget(label_tablet_log_title) hstack_tablet_log_header.addStretch() @@ -1188,6 +1266,7 @@ class MainWindow(QMainWindow): button_tablet_server.clicked.connect(toggle_tablet_server) button_tablet_log.clicked.connect(tablet_log_show_hide) + button_ws_log.clicked.connect(ws_log_show_hide) #MARK: LAYOUT PANEL container_layout_panel.setVisible(True) @@ -1701,6 +1780,32 @@ def log_osc_outgoing(addr, val): line = f"{ts} {addr} {val}" _append_log(textedit_osc_out_log, line) +def log_ws_incoming(message): + import json + from datetime import datetime + ts = datetime.now().strftime("%H:%M:%S") + try: + obj = json.loads(message) + pretty = json.dumps(obj, indent=2) + except Exception: + pretty = message + header = f"{ts} ◀ WS In" + body = f"
{pretty}
" + textedit_ws_in_log.append(header) + textedit_ws_in_log.append(body) + textedit_ws_in_log.append("───────────────────") + +def log_ws_outgoing(data): + import json + from datetime import datetime + ts = datetime.now().strftime("%H:%M:%S") + pretty = json.dumps(data, indent=2) + header = f"{ts} ▶ WS Out" + body = f"
{pretty}
" + textedit_ws_out_log.append(header) + textedit_ws_out_log.append(body) + textedit_ws_out_log.append("───────────────────") + def update_osc_server_ip(ip): set_osc_target(ip=ip.strip() or "127.0.0.1") save_io_config() @@ -2245,6 +2350,7 @@ def _build_tablet_snapshot(): snapshot = { "preset_uuid": label_preset_uuid.text().strip(), "preset_name": name, + "big_title": big_title_edit.text() if big_title_edit else "", "faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "visible": True, "color": PALETTE_HEX.get(f.color_name, "#00e676")} for f in faders], "toggles": [{"uid": t.uid, "label": t.title_edit.text(), "state": t.toggle_state, "visible": True, "color": PALETTE_HEX.get(t.color_name, "#00e676")} for t in toggles], "transports": [{"uid": t.uid, "label": t.title_edit.text(), "visible": True} for t in transports], @@ -2256,6 +2362,7 @@ def _on_tablet_connected(): if ws_server: snapshot, layout = _build_tablet_snapshot() ws_server.broadcast_preset(snapshot, layout) + log_ws_outgoing(snapshot) def _on_tablet_layout_saved(preset_uuid, layout): @@ -2267,6 +2374,9 @@ def _on_tablet_layout_saved(preset_uuid, layout): def preset_load(preset): + global selected_strips + selected_strips = [] + fader_states = preset.get("faders", []) while len(faders) < len(fader_states): fader_add() @@ -2391,6 +2501,7 @@ def preset_load(preset): if ws_server: snapshot, layout = _build_tablet_snapshot() ws_server.broadcast_preset(snapshot, layout) + log_ws_outgoing(snapshot) def export_preset(): import json as _json @@ -2714,6 +2825,24 @@ def tablet_log_window_calc(): container_tablet_log.move(geo.right() + 732, geo.top()) +def ws_log_show_hide(checked): + container_ws_in_log.setVisible(checked) + container_ws_out_log.setVisible(checked) + if checked: + ws_log_window_calc() + + +def ws_log_window_calc(): + geo = app_window.frameGeometry() + half = geo.height() // 2 - 2 + container_ws_in_log.setFixedWidth(360) + container_ws_in_log.setFixedHeight(half) + container_ws_in_log.move(geo.right() + 4, geo.top()) + container_ws_out_log.setFixedWidth(360) + container_ws_out_log.setFixedHeight(half) + container_ws_out_log.move(geo.right() + 4, geo.top() + half + 4) + + def log_tablet(line): from datetime import datetime ts = datetime.now().strftime("%H:%M:%S") @@ -2729,14 +2858,14 @@ def toggle_tablet_server(): def _start_tablet_server(): - global ws_server, _http_server + global ws_server import socket as _socket ws_server = WSServer(port=8765, parent=app_window) ws_server.log_signal.connect(log_tablet) + ws_server.raw_in_signal.connect(log_ws_incoming) ws_server.control_received.connect(lambda uid, val: midi_receiver.hw_cc_signal.emit(uid, val)) ws_server.layout_saved.connect(_on_tablet_layout_saved) ws_server.client_connected.connect(_on_tablet_connected) - _http_server = start_http_server(port=8080) try: s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) @@ -2745,19 +2874,15 @@ def _start_tablet_server(): except Exception: ip = "localhost" button_tablet_server.setText("Stop Tablet") - label_tablet_status.setText(f"http://{ip}:8080") - log_tablet(f"HTTP http://{ip}:8080") + label_tablet_status.setText(f"ws://{ip}:8765") log_tablet(f"WS ws://{ip}:8765") def _stop_tablet_server(): - global ws_server, _http_server + global ws_server if ws_server: ws_server.stop() ws_server = None - if _http_server: - stop_http_server(_http_server) - _http_server = None button_tablet_server.setText("Start Tablet") label_tablet_status.setText("") log_tablet("server stopped") diff --git a/client/app.js b/client/app.js deleted file mode 100644 index 715eada..0000000 --- a/client/app.js +++ /dev/null @@ -1,236 +0,0 @@ -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(); -}; diff --git a/client/app.py b/client/app.py deleted file mode 100644 index ace1180..0000000 --- a/client/app.py +++ /dev/null @@ -1,292 +0,0 @@ -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 diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 28ed166..0000000 --- a/client/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Virtual Controller - - - -
-
- - -
-
connecting...
-
-
drag to reposition · lock to save
- - - diff --git a/presets/presets.json b/presets/presets.json index a7e97e4..b39a392 100644 --- a/presets/presets.json +++ b/presets/presets.json @@ -1,5 +1,5 @@ { - "__last_used__": "preset5", + "__last_used__": "preset3", "presets": { "preset1": { "faders": [ @@ -8,10 +8,10 @@ "label": "Fader 1", "cc": 1, "ch": 1, - "value": 38, + "value": 69, "color": "Red", "pickup": false, - "last_sent": 38, + "last_sent": 69, "hw_cc": null, "hw_channel": null, "zone": null, @@ -23,10 +23,10 @@ "label": "Fader 2", "cc": 1, "ch": 1, - "value": 64, + "value": 83, "color": "Gray", "pickup": false, - "last_sent": 64, + "last_sent": 83, "hw_cc": null, "hw_channel": null, "zone": "right", @@ -38,10 +38,10 @@ "label": "Fader 3", "cc": 1, "ch": 1, - "value": 64, + "value": 56, "color": "Orange", "pickup": false, - "last_sent": 64, + "last_sent": 56, "hw_cc": null, "hw_channel": null, "zone": "left", @@ -61,20 +61,22 @@ "center_index": 0, "zone_index": -1, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" }, { "uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583", "label": "Toggle 2", "cc": 20, "ch": 1, - "state": false, + "state": true, "color": "Blue", "zone": "left", "center_index": 1, "zone_index": 0, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" }, { "uid": "b3cf419a-5183-4272-bd6f-44f858afbec8", @@ -87,7 +89,8 @@ "center_index": 2, "zone_index": 1, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "momentary" }, { "uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03", @@ -100,20 +103,22 @@ "center_index": null, "zone_index": null, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" }, { "uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7", "label": "Toggle 6", "cc": 20, "ch": 1, - "state": false, + "state": true, "color": "Gray", "zone": null, "center_index": null, "zone_index": null, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" }, { "uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a", @@ -126,7 +131,8 @@ "center_index": null, "zone_index": null, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" }, { "uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d", @@ -139,7 +145,8 @@ "center_index": 4, "zone_index": -1, "hw_cc": null, - "hw_channel": null + "hw_channel": null, + "trigger_mode": "toggle" } ], "browser": { @@ -160,12 +167,20 @@ "trigger_mode": "momentary" } }, + "big_title": "STRING THING", "preset_uuid": "8cbceb6b-ba5d-4b2c-8989-f921e4e3150d", "show_hide": { - "fader": false, - "toggle": false, - "transport": false, - "browser": false + "fader": true, + "toggle": true, + "transport": true, + "browser": true + }, + "sections": { + "preset_browser": true, + "big_title": true, + "fader_row": true, + "toggle_row": true, + "transport_row": true } }, "preset2": { @@ -203,10 +218,10 @@ "label": "Fader 1", "cc": 1, "ch": 1, - "value": 64, - "color": "Gray", + "value": 43, + "color": "Orange", "pickup": false, - "last_sent": 64, + "last_sent": 43, "hw_cc": null, "hw_channel": null, "zone": "left", @@ -218,10 +233,10 @@ "label": "Fader 2", "cc": 1, "ch": 1, - "value": 64, - "color": "Gray", + "value": 48, + "color": "Pink", "pickup": false, - "last_sent": 64, + "last_sent": 48, "hw_cc": null, "hw_channel": null, "zone": null, @@ -233,10 +248,10 @@ "label": "Fader 3", "cc": 1, "ch": 1, - "value": 64, - "color": "Gray", + "value": 93, + "color": "Yellow", "pickup": false, - "last_sent": 64, + "last_sent": 93, "hw_cc": null, "hw_channel": null, "zone": "right", @@ -247,7 +262,7 @@ "toggles": [ { "uid": "e291394c-cc09-4d3c-9ac3-ed5fe2803a46", - "label": "Toggle 1", + "label": "whatever", "cc": 20, "ch": 1, "state": false, @@ -278,13 +293,86 @@ "trigger_mode": "momentary" } }, + "big_title": "", "preset_uuid": "7483fb5b-f47f-49e4-bccb-e8d20195078c", "show_hide": { "fader": false, "toggle": false, - "transport": false, + "transport": true, "browser": true - } + }, + "sections": { + "preset_browser": true, + "big_title": true, + "fader_row": true, + "toggle_row": true, + "transport_row": true + }, + "tablet_layout": [ + { + "x": 550.3499755859375, + "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", + "w": 140, + "h": 119, + "y": 896.0333251953125 + }, + { + "h": 119, + "y": 885.0333251953125, + "x": 368.2833251953125, + "w": 140, + "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46" + }, + { + "y": 16, + "uid": "title-big", + "x": 16, + "w": 300, + "h": 44 + }, + { + "uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790", + "h": 329, + "x": 542.9166259765625, + "y": 359.79998779296875, + "w": 140 + }, + { + "h": 329, + "uid": "c98d2009-d186-489a-8883-ccbd612cb4af", + "x": 39.433349609375, + "y": 345.5, + "w": 140 + }, + { + "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", + "x": 714.5833740234375, + "y": 892.8333740234375, + "h": 119, + "w": 140 + }, + { + "w": 140, + "h": 329, + "y": 322.2833251953125, + "x": 299.04998779296875, + "uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095" + }, + { + "x": 1138.566650390625, + "y": 915.4666748046875, + "h": 140, + "uid": "e291394c-cc09-4d3c-9ac3-ed5fe2803a46", + "w": 140 + }, + { + "x": 158.23332977294922, + "w": 140, + "h": 119, + "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", + "y": 885.6500244140625 + } + ] }, "whatever": { "faders": [], @@ -304,8 +392,8 @@ "color": "Gray", "pickup": false, "last_sent": 85, - "hw_cc": 67, - "hw_channel": 1, + "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null @@ -439,9 +527,16 @@ "preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959", "show_hide": { "fader": false, - "toggle": false, - "transport": false, + "toggle": true, + "transport": true, "browser": true + }, + "sections": { + "preset_browser": true, + "big_title": true, + "fader_row": true, + "toggle_row": true, + "transport_row": true } }, "new preset browser shit": { @@ -467,6 +562,206 @@ } }, "show_hide": {} + }, + "rename whatever": { + "faders": [ + { + "uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e", + "label": "Fader 1", + "cc": 90, + "ch": 15, + "value": 85, + "color": "Blue", + "pickup": false, + "last_sent": 85, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69", + "label": "Fader 2", + "cc": 1, + "ch": 1, + "value": 79, + "color": "Blue", + "pickup": false, + "last_sent": 79, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb", + "label": "Fader 3", + "cc": 1, + "ch": 1, + "value": 64, + "color": "Blue", + "pickup": false, + "last_sent": 64, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e", + "label": "Fader 4", + "cc": 1, + "ch": 1, + "value": 64, + "color": "Green", + "pickup": false, + "last_sent": 64, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "fee53fde-2f59-4800-8873-3d75ca619366", + "label": "Fader 5", + "cc": 1, + "ch": 1, + "value": 64, + "color": "Green", + "pickup": false, + "last_sent": 64, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421", + "label": "Fader 6", + "cc": 1, + "ch": 1, + "value": 64, + "color": "Green", + "pickup": false, + "last_sent": 64, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0", + "label": "Fader 7", + "cc": 1, + "ch": 1, + "value": 127, + "color": "Green", + "pickup": false, + "last_sent": 127, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + } + ], + "toggles": [ + { + "uid": "46f17f40-1bb2-4527-b6c5-fd26e535beed", + "label": "Toggle 1", + "cc": 12, + "ch": 1, + "state": false, + "color": "Green", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "toggle" + }, + { + "uid": "dbf062b0-6dfe-4a7b-af6a-cf47a943bdec", + "label": "Toggle 2", + "cc": 13, + "ch": 1, + "state": false, + "color": "Teal", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "toggle" + }, + { + "uid": "93d02fb4-5327-4f0c-8cbd-6f0eb24f7195", + "label": "Toggle 3", + "cc": 14, + "ch": 1, + "state": false, + "color": "Purple", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "toggle" + }, + { + "uid": "3147496a-7380-401b-9b95-947dc5458886", + "label": "Toggle 4", + "cc": 15, + "ch": 1, + "state": false, + "color": "Pink", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "toggle" + } + ], + "browser": { + "back": { + "label": "Back", + "cc": 22, + "ch": 1, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "momentary" + }, + "fwd": { + "label": "Fwd", + "cc": 23, + "ch": 1, + "hw_cc": null, + "hw_channel": null, + "trigger_mode": "momentary" + } + }, + "big_title": "STRING SHORTS", + "preset_uuid": "cf0155c7-81ea-4291-8680-6f058b80007f", + "show_hide": { + "fader": false, + "toggle": false, + "transport": true, + "browser": true + }, + "sections": { + "preset_browser": true, + "big_title": true, + "fader_row": true, + "toggle_row": true, + "transport_row": true + } } }, "transport_preset": [ diff --git a/remote-client-ios/remote-client-ios.xcodeproj/project.pbxproj b/remote-client-ios/remote-client-ios.xcodeproj/project.pbxproj new file mode 100644 index 0000000..18694ec --- /dev/null +++ b/remote-client-ios/remote-client-ios.xcodeproj/project.pbxproj @@ -0,0 +1,578 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 979A62DE2FF478D6002D3E46 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 979A62BF2FF478D4002D3E46 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 979A62C62FF478D4002D3E46; + remoteInfo = "remote-client-ios"; + }; + 979A62E82FF478D6002D3E46 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 979A62BF2FF478D4002D3E46 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 979A62C62FF478D4002D3E46; + remoteInfo = "remote-client-ios"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 979A62C72FF478D4002D3E46 /* remote-client-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "remote-client-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "remote-client-iosTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "remote-client-iosUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 979A62EF2FF478D6002D3E46 /* Exceptions for "remote-client-ios" folder in "remote-client-ios" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 979A62C62FF478D4002D3E46 /* remote-client-ios */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 979A62C92FF478D4002D3E46 /* remote-client-ios */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 979A62EF2FF478D6002D3E46 /* Exceptions for "remote-client-ios" folder in "remote-client-ios" target */, + ); + path = "remote-client-ios"; + sourceTree = ""; + }; + 979A62E02FF478D6002D3E46 /* remote-client-iosTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "remote-client-iosTests"; + sourceTree = ""; + }; + 979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "remote-client-iosUITests"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 979A62C42FF478D4002D3E46 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 979A62DA2FF478D6002D3E46 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 979A62E42FF478D6002D3E46 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 979A62BE2FF478D4002D3E46 = { + isa = PBXGroup; + children = ( + 979A62C92FF478D4002D3E46 /* remote-client-ios */, + 979A62E02FF478D6002D3E46 /* remote-client-iosTests */, + 979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */, + 979A62C82FF478D4002D3E46 /* Products */, + ); + sourceTree = ""; + }; + 979A62C82FF478D4002D3E46 /* Products */ = { + isa = PBXGroup; + children = ( + 979A62C72FF478D4002D3E46 /* remote-client-ios.app */, + 979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */, + 979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 979A62C62FF478D4002D3E46 /* remote-client-ios */ = { + isa = PBXNativeTarget; + buildConfigurationList = 979A62F02FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-ios" */; + buildPhases = ( + 979A62C32FF478D4002D3E46 /* Sources */, + 979A62C42FF478D4002D3E46 /* Frameworks */, + 979A62C52FF478D4002D3E46 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 979A62C92FF478D4002D3E46 /* remote-client-ios */, + ); + name = "remote-client-ios"; + packageProductDependencies = ( + ); + productName = "remote-client-ios"; + productReference = 979A62C72FF478D4002D3E46 /* remote-client-ios.app */; + productType = "com.apple.product-type.application"; + }; + 979A62DC2FF478D6002D3E46 /* remote-client-iosTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 979A62F52FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosTests" */; + buildPhases = ( + 979A62D92FF478D6002D3E46 /* Sources */, + 979A62DA2FF478D6002D3E46 /* Frameworks */, + 979A62DB2FF478D6002D3E46 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 979A62DF2FF478D6002D3E46 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 979A62E02FF478D6002D3E46 /* remote-client-iosTests */, + ); + name = "remote-client-iosTests"; + packageProductDependencies = ( + ); + productName = "remote-client-iosTests"; + productReference = 979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 979A62E62FF478D6002D3E46 /* remote-client-iosUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 979A62F82FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosUITests" */; + buildPhases = ( + 979A62E32FF478D6002D3E46 /* Sources */, + 979A62E42FF478D6002D3E46 /* Frameworks */, + 979A62E52FF478D6002D3E46 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 979A62E92FF478D6002D3E46 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */, + ); + name = "remote-client-iosUITests"; + packageProductDependencies = ( + ); + productName = "remote-client-iosUITests"; + productReference = 979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 979A62BF2FF478D4002D3E46 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 979A62C62FF478D4002D3E46 = { + CreatedOnToolsVersion = 16.4; + }; + 979A62DC2FF478D6002D3E46 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 979A62C62FF478D4002D3E46; + }; + 979A62E62FF478D6002D3E46 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 979A62C62FF478D4002D3E46; + }; + }; + }; + buildConfigurationList = 979A62C22FF478D4002D3E46 /* Build configuration list for PBXProject "remote-client-ios" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 979A62BE2FF478D4002D3E46; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 979A62C82FF478D4002D3E46 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 979A62C62FF478D4002D3E46 /* remote-client-ios */, + 979A62DC2FF478D6002D3E46 /* remote-client-iosTests */, + 979A62E62FF478D6002D3E46 /* remote-client-iosUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 979A62C52FF478D4002D3E46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 979A62DB2FF478D6002D3E46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 979A62E52FF478D6002D3E46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 979A62C32FF478D4002D3E46 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 979A62D92FF478D6002D3E46 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 979A62E32FF478D6002D3E46 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 979A62DF2FF478D6002D3E46 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 979A62C62FF478D4002D3E46 /* remote-client-ios */; + targetProxy = 979A62DE2FF478D6002D3E46 /* PBXContainerItemProxy */; + }; + 979A62E92FF478D6002D3E46 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 979A62C62FF478D4002D3E46 /* remote-client-ios */; + targetProxy = 979A62E82FF478D6002D3E46 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 979A62F12FF478D6002D3E46 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "remote-client-ios/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 979A62F22FF478D6002D3E46 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "remote-client-ios/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 979A62F32FF478D6002D3E46 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 979A62F42FF478D6002D3E46 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 979A62F62FF478D6002D3E46 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/remote-client-ios.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/remote-client-ios"; + }; + name = Debug; + }; + 979A62F72FF478D6002D3E46 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/remote-client-ios.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/remote-client-ios"; + }; + name = Release; + }; + 979A62F92FF478D6002D3E46 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "remote-client-ios"; + }; + name = Debug; + }; + 979A62FA2FF478D6002D3E46 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = LTQ6N8Q7YG; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "remote-client-ios"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 979A62C22FF478D4002D3E46 /* Build configuration list for PBXProject "remote-client-ios" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 979A62F32FF478D6002D3E46 /* Debug */, + 979A62F42FF478D6002D3E46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 979A62F02FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-ios" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 979A62F12FF478D6002D3E46 /* Debug */, + 979A62F22FF478D6002D3E46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 979A62F52FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 979A62F62FF478D6002D3E46 /* Debug */, + 979A62F72FF478D6002D3E46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 979A62F82FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 979A62F92FF478D6002D3E46 /* Debug */, + 979A62FA2FF478D6002D3E46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 979A62BF2FF478D4002D3E46 /* Project object */; +} diff --git a/remote-client-ios/remote-client-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/remote-client-ios/remote-client-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/remote-client-ios/remote-client-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/remote-client-ios/remote-client-ios/AppDelegate.swift b/remote-client-ios/remote-client-ios/AppDelegate.swift new file mode 100644 index 0000000..9a2f760 --- /dev/null +++ b/remote-client-ios/remote-client-ios/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// remote-client-ios +// +// Created by p4piwabl0 on 6/30/26. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/remote-client-ios/remote-client-ios/Assets.xcassets/AccentColor.colorset/Contents.json b/remote-client-ios/remote-client-ios/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/remote-client-ios/remote-client-ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/remote-client-ios/remote-client-ios/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/remote-client-ios/remote-client-ios/Assets.xcassets/Contents.json b/remote-client-ios/remote-client-ios/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/remote-client-ios/remote-client-ios/Base.lproj/LaunchScreen.storyboard b/remote-client-ios/remote-client-ios/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/remote-client-ios/remote-client-ios/Base.lproj/Main.storyboard b/remote-client-ios/remote-client-ios/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/remote-client-ios/remote-client-ios/Core/Functions.swift b/remote-client-ios/remote-client-ios/Core/Functions.swift new file mode 100644 index 0000000..b60264a --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/Functions.swift @@ -0,0 +1,283 @@ +import UIKit + +extension ObjectsViewController { + + // MARK: - Connection State UI + func applyConnectionState(_ state: ConnectionState) { + self.connectionState = state + let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) + let red = UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1) + let gray = UIColor(white: 0.3, alpha: 1) + + switch state { + case .notStarted: + self.statusLabel.text = "not started" + self.connectButton.setTitle("Connect", for: .normal) + self.connectButton.backgroundColor = green + self.connectButton.setTitleColor(.black, for: .normal) + self.connectButton.isEnabled = true + case .connecting: + self.statusLabel.text = "connecting..." + self.connectButton.setTitle("Connecting...", for: .normal) + self.connectButton.backgroundColor = gray + self.connectButton.setTitleColor(.lightGray, for: .normal) + self.connectButton.isEnabled = false + case .connected: + self.statusLabel.text = "connected" + self.connectButton.setTitle("Disconnect", for: .normal) + self.connectButton.backgroundColor = red + self.connectButton.setTitleColor(.white, for: .normal) + self.connectButton.isEnabled = true + case .disconnected: + self.statusLabel.text = "disconnected" + self.connectButton.setTitle("Reconnect", for: .normal) + self.connectButton.backgroundColor = green + self.connectButton.setTitleColor(.black, for: .normal) + self.connectButton.isEnabled = true + } + } + + // MARK: - WebSocket Connect / Disconnect + @objc func connectTapped() { + if self.connectionState == .connected { + self.wsTask?.cancel(with: .goingAway, reason: nil) + self.wsTask = nil + self.applyConnectionState(.disconnected) + } else { + self.connectWebSocket() + } + } + + func connectWebSocket() { + let ip = self.ipField.text?.trimmingCharacters(in: .whitespaces) ?? "127.0.0.1" + let port = self.portField.text?.trimmingCharacters(in: .whitespaces) ?? "8765" + guard let url = URL(string: "ws://\(ip):\(port)") else { return } + self.urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main) + self.wsTask = self.urlSession?.webSocketTask(with: url) + self.wsTask?.resume() + self.applyConnectionState(.connecting) + self.listen() + } + + // MARK: - Listen + func listen() { + self.wsTask?.receive { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let message): + switch message { + case .string(let text): self.handleMessage(text) + default: break + } + self.listen() + case .failure: + DispatchQueue.main.async { self.applyConnectionState(.disconnected) } + } + } + } + + // MARK: - Show Logging Toggle + @objc func showLoggingTapped() { + let nowVisible = self.logWidget.isHidden + self.logWidget.isHidden = !nowVisible + self.showLoggingButton.setTitle(nowVisible ? "Hide Logging" : "Show Logging", for: .normal) + } + + // MARK: - Handle Incoming Message + func handleMessage(_ text: String) { + self.loggingView.log(text) + guard let data = text.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let event = json["event"] as? String else { return } + + switch event { + case "preset": + if let presetData = json["data"] as? [String: Any], + let presetBytes = try? JSONSerialization.data(withJSONObject: presetData) { + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let preset = try decoder.decode(ModelPresetLoadToRemoteDevice.self, from: presetBytes) + DispatchQueue.main.async { self.renderPreset(preset) } + } catch { + self.loggingView.log("decode error: \(error)") + } + } + case "widget_update": + if let uid = json["uid"] as? String, let value = json["value"] as? Int { + DispatchQueue.main.async { self.updateWidget(uid: uid, value: value) } + } + case "daw_state": + DispatchQueue.main.async { self.updateDawState(json) } + default: + break + } + } + + // MARK: - Send + func send(_ dict: [String: Any]) { + guard let data = try? JSONSerialization.data(withJSONObject: dict), + let text = String(data: data, encoding: .utf8) else { return } + self.wsTask?.send(.string(text)) { _ in } + } + + // MARK: - Render Preset + func renderPreset(_ preset: ModelPresetLoadToRemoteDevice) { + let savedLayout = UserDefaults.standard.dictionary(forKey: "layout_\(preset.presetUuid)") ?? [:] + self.presetName = preset.presetName + self.presetUuid = preset.presetUuid + self.widgets = [:] + self.feedbackWidgets = [] + self.containerView.subviews.forEach { if $0 !== self.logWidget { $0.removeFromSuperview() } } + self.presetLabel.text = preset.presetName + + let titleUid = "title-big" + let titleFrame = savedFrame(uid: titleUid, in: savedLayout) ?? CGRect(x: 16, y: 16, width: 300, height: 44) + let titleWidget = TitleWidget(uid: titleUid, text: preset.displayTitle, frame: titleFrame) + self.setupDrag(on: titleWidget) + self.containerView.addSubview(titleWidget) + self.widgets[titleUid] = titleWidget + + var faderIdx = 0 + var squareIdx = 0 + + for f in preset.faders where f.visible { + let frame = savedFrame(uid: f.uid, in: savedLayout) ?? FaderWidget.defaultFrame(idx: faderIdx) + let w = FaderWidget(uid: f.uid, + label: f.label, + value: f.value, + color: UIColor.fromHex(f.color), + frame: frame) + w.onSend = { [weak self] uid, val in self?.send(["event": "control", "uid": uid, "value": val]) } + self.setupDrag(on: w) + self.containerView.addSubview(w) + self.widgets[f.uid] = w + faderIdx += 1 + } + + for t in preset.toggles where t.visible { + let frame = savedFrame(uid: t.uid, in: savedLayout) ?? ToggleWidget.defaultFrame(idx: squareIdx) + let w = ToggleWidget(uid: t.uid, + label: t.label, + isOn: t.state, + color: UIColor.fromHex(t.color), + frame: frame) + w.onSend = { [weak self] uid, val in self?.send(["event": "control", "uid": uid, "value": val]) } + self.setupDrag(on: w) + self.containerView.addSubview(w) + self.widgets[t.uid] = w + squareIdx += 1 + } + + for t in preset.transports where t.visible { + let frame = savedFrame(uid: t.uid, in: savedLayout) ?? TransportWidget.defaultFrame(idx: squareIdx) + let w = TransportWidget(uid: t.uid, + label: t.label, + frame: frame) + w.onSend = { [weak self] uid, val in self?.send(["event": "control", "uid": uid, "value": val]) } + self.setupDrag(on: w) + self.containerView.addSubview(w) + self.widgets[t.uid] = w + squareIdx += 1 + } + + self.isLocked = !savedLayout.isEmpty + self.refreshToolbar() + } + + // MARK: - Update Widget + func updateWidget(uid: String, value: Int) { + self.widgets[uid]?.update(value: value) + } + + // MARK: - DAW State + func updateDawState(_ json: [String: Any]) { + let track = json["track"] as? String + let bar = json["bar"] as? Int + let playing = json["playing"] as? Bool + + self.feedbackWidgets.forEach { $0.updateDaw(track: track, bar: bar, playing: playing) } + + var parts: [String] = [] + if let track = track { parts.append(track) } + if let bar = bar { parts.append("Bar \(bar)") } + if let playing = playing { parts.append(playing ? "▶" : "■") } + if !parts.isEmpty { self.statusLabel.text = parts.joined(separator: " ") } + } + + // MARK: - Toolbar + @objc func arrangeTapped() { + self.isLocked = false + self.refreshToolbar() + } + + @objc func lockTapped() { + self.isLocked = true + self.refreshToolbar() + self.saveLayout() + } + + func refreshToolbar() { + let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) + self.lockButton.backgroundColor = self.isLocked ? green : UIColor(white: 0.15, alpha: 1) + self.lockButton.setTitleColor(self.isLocked ? .black : .lightGray, for: .normal) + self.arrangeButton.backgroundColor = self.isLocked ? UIColor(white: 0.15, alpha: 1) : green + self.arrangeButton.setTitleColor(self.isLocked ? .lightGray : .black, for: .normal) + } + + // MARK: - Save Layout + func saveLayout() { + guard !self.presetUuid.isEmpty else { + self.loggingView.log("saveLayout: aborted — presetUuid is empty") + return + } + var layoutDict: [String: Any] = [:] + for (uid, w) in self.widgets { + let f = w.frame + layoutDict[uid] = [ + "x": Double(f.origin.x), + "y": Double(f.origin.y), + "w": Double(f.width), + "h": Double(f.height) + ] + } + UserDefaults.standard.set(layoutDict, forKey: "layout_\(self.presetUuid)") + self.loggingView.log("saveLayout: \(layoutDict.count) widgets saved locally for \(self.presetUuid)") + } + + // MARK: - Drag + func setupDrag(on view: UIView) { + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) + view.addGestureRecognizer(pan) + } + + @objc func handlePan(_ gesture: UIPanGestureRecognizer) { + guard !self.isLocked, let view = gesture.view else { return } + let translation = gesture.translation(in: self.containerView) + view.center = CGPoint(x: view.center.x + translation.x, y: view.center.y + translation.y) + gesture.setTranslation(.zero, in: self.containerView) + } + + // MARK: - Helpers + func savedFrame(uid: String, in savedLayout: [String: Any]) -> CGRect? { + guard let saved = savedLayout[uid] as? [String: Any], + let x = saved["x"] as? CGFloat, let y = saved["y"] as? CGFloat, + let w = saved["w"] as? CGFloat, let h = saved["h"] as? CGFloat + else { return nil } + return CGRect(x: x, y: y, width: w, height: h) + } + +} + +// MARK: - WebSocket Delegate +extension ObjectsViewController: URLSessionWebSocketDelegate { + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + self.applyConnectionState(.connected) + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + self.applyConnectionState(.disconnected) + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingFunctions.swift b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingFunctions.swift new file mode 100644 index 0000000..48ff07b --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingFunctions.swift @@ -0,0 +1,24 @@ +import UIKit + +extension LoggingObjects { + + func log(_ message: String) { + DispatchQueue.main.async { + var display = message + if let data = message.data(using: .utf8), + let obj = try? JSONSerialization.jsonObject(with: data), + let pretty = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted), + let str = String(data: pretty, encoding: .utf8) { + display = str + } + let current = self.textView.text ?? "" + let separator = current.isEmpty ? "" : "\n--- ✦ ---\n" + self.textView.text = display + separator + current + } + } + + @objc func clearTapped() { + self.textView.text = "" + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingObjects.swift b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingObjects.swift new file mode 100644 index 0000000..efe7408 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingObjects.swift @@ -0,0 +1,26 @@ +import UIKit + +class LoggingObjects: UIViewController { + + lazy var textView: UITextView = { + let tv = UITextView() + tv.translatesAutoresizingMaskIntoConstraints = false + tv.backgroundColor = .clear + tv.textColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) + tv.font = .monospacedSystemFont(ofSize: 11, weight: .regular) + tv.isEditable = false + tv.isScrollEnabled = true + return tv + }() + + lazy var clearButton: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.setTitle("Clear", for: .normal) + btn.setTitleColor(UIColor(white: 0.4, alpha: 1), for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 10) + btn.addTarget(self, action: #selector(self.clearTapped), for: .touchUpInside) + return btn + }() + +} diff --git a/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingUILayout.swift b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingUILayout.swift new file mode 100644 index 0000000..b4e0662 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingUILayout.swift @@ -0,0 +1,21 @@ +import UIKit + +extension LoggingObjects { + + func setupUI() { + DispatchQueue.main.async { + self.view.backgroundColor = UIColor(white: 0.08, alpha: 1) + + self.view.addSubview(self.clearButton) + self.clearButton.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 6).isActive = true + self.clearButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true + + self.view.addSubview(self.textView) + self.textView.topAnchor.constraint(equalTo: self.clearButton.bottomAnchor, constant: 2).isActive = true + self.textView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 4).isActive = true + self.textView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -4).isActive = true + self.textView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -4).isActive = true + } + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingView.swift b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingView.swift new file mode 100644 index 0000000..b354193 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/LoggingVC/LoggingView.swift @@ -0,0 +1,10 @@ +import UIKit + +class LoggingView: LoggingObjects { + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/Models/PresetLoadToRemoteDevice.swift b/remote-client-ios/remote-client-ios/Core/Models/PresetLoadToRemoteDevice.swift new file mode 100644 index 0000000..a9940d7 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/Models/PresetLoadToRemoteDevice.swift @@ -0,0 +1,36 @@ +import Foundation + +struct ModelPresetLoadToRemoteDevice: Codable { + let presetUuid: String + let presetName: String + let bigTitle: String + let faders: [ModelFaderPayload] + let toggles: [ModelTogglePayload] + let transports: [ModelTransportPayload] + + var displayTitle: String { + bigTitle.isEmpty ? "No Title" : bigTitle + } +} + +struct ModelFaderPayload: Codable { + let uid: String + let label: String + let value: Int + let visible: Bool + let color: String +} + +struct ModelTogglePayload: Codable { + let uid: String + let label: String + let state: Bool + let visible: Bool + let color: String +} + +struct ModelTransportPayload: Codable { + let uid: String + let label: String + let visible: Bool +} diff --git a/remote-client-ios/remote-client-ios/Core/ObjectsViewController.swift b/remote-client-ios/remote-client-ios/Core/ObjectsViewController.swift new file mode 100644 index 0000000..d1eede7 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ObjectsViewController.swift @@ -0,0 +1,157 @@ +import UIKit + +class ObjectsViewController: UIViewController { + + // MARK: - WebSocket + var wsTask: URLSessionWebSocketTask? + var urlSession: URLSession? + + // MARK: - Connection State + enum ConnectionState { + case notStarted, connecting, connected, disconnected + } + var connectionState: ConnectionState = .notStarted + + // MARK: - State + var presetName: String = "" + var presetUuid: String = "" + var isLocked: Bool = true + + // MARK: - Widgets + var widgets: [String: BaseWidget] = [:] + var feedbackWidgets: [FeedbackWidget] = [] + + // MARK: - Layout + var layout: [String: CGRect] = [:] + + let containerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(white: 0.1, alpha: 1) + return view + }() + + // MARK: - Toolbar + lazy var connectButton: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.setTitle("Connect", for: .normal) + btn.setTitleColor(.black, for: .normal) + btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) + btn.layer.cornerRadius = 4 + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14) + btn.addTarget(self, action: #selector(self.connectTapped), for: .touchUpInside) + return btn + }() + + lazy var arrangeButton: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.setTitle("Arrange", for: .normal) + btn.setTitleColor(.lightGray, for: .normal) + btn.backgroundColor = UIColor(white: 0.15, alpha: 1) + btn.layer.borderWidth = 1 + btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + btn.layer.cornerRadius = 4 + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14) + btn.addTarget(self, action: #selector(self.arrangeTapped), for: .touchUpInside) + return btn + }() + + lazy var lockButton: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.setTitle("Lock / Save", for: .normal) + btn.setTitleColor(.black, for: .normal) + btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) + btn.layer.cornerRadius = 4 + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14) + btn.addTarget(self, action: #selector(self.lockTapped), for: .touchUpInside) + return btn + }() + + // MARK: - Connection Fields + let ipField: UITextField = { + let tf = UITextField() + tf.translatesAutoresizingMaskIntoConstraints = false + tf.text = "127.0.0.1" + tf.textColor = .lightGray + tf.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + tf.backgroundColor = UIColor(white: 0.12, alpha: 1) + tf.layer.borderWidth = 1 + tf.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + tf.layer.cornerRadius = 4 + tf.keyboardType = .decimalPad + tf.textAlignment = .center + tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0)) + tf.leftViewMode = .always + tf.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0)) + tf.rightViewMode = .always + return tf + }() + + let portField: UITextField = { + let tf = UITextField() + tf.translatesAutoresizingMaskIntoConstraints = false + tf.text = "8765" + tf.textColor = .lightGray + tf.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + tf.backgroundColor = UIColor(white: 0.12, alpha: 1) + tf.layer.borderWidth = 1 + tf.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + tf.layer.cornerRadius = 4 + tf.keyboardType = .numberPad + tf.textAlignment = .center + tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0)) + tf.leftViewMode = .always + tf.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0)) + tf.rightViewMode = .always + return tf + }() + + // MARK: - Logging + lazy var loggingView: LoggingView = LoggingView() + + lazy var logWidget: LogWidget = { + let w = LogWidget(uid: "log-widget", frame: LogWidget.defaultFrame()) + return w + }() + + lazy var showLoggingButton: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.setTitle("Show Logging", for: .normal) + btn.setTitleColor(.lightGray, for: .normal) + btn.backgroundColor = UIColor(white: 0.15, alpha: 1) + btn.layer.borderWidth = 1 + btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + btn.layer.cornerRadius = 4 + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14) + btn.addTarget(self, action: #selector(self.showLoggingTapped), for: .touchUpInside) + return btn + }() + + // MARK: - Labels + let statusLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.text = "not started" + lbl.textColor = UIColor(white: 0.33, alpha: 1) + lbl.font = .systemFont(ofSize: 11) + return lbl + }() + + let presetLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.text = "" + lbl.textColor = UIColor(white: 0.33, alpha: 1) + lbl.font = .systemFont(ofSize: 11) + return lbl + }() + + override func viewDidLoad() { + super.viewDidLoad() + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/UILayout.swift b/remote-client-ios/remote-client-ios/Core/UILayout.swift new file mode 100644 index 0000000..ef60028 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/UILayout.swift @@ -0,0 +1,60 @@ +import UIKit + +extension ObjectsViewController { + + func setupUI() { + DispatchQueue.main.async { + self.view.backgroundColor = UIColor(white: 0.1, alpha: 1) + + // containerView + self.view.addSubview(self.containerView) + self.containerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true + self.containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true + self.containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true + self.containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true + + // statusLabel + self.view.addSubview(self.statusLabel) + self.statusLabel.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.statusLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true + + // presetLabel + self.view.addSubview(self.presetLabel) + self.presetLabel.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -8).isActive = true + self.presetLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true + + // toolbar buttons (top-right) + self.view.addSubview(self.showLoggingButton) + self.showLoggingButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.showLoggingButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true + + self.view.addSubview(self.connectButton) + self.connectButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.connectButton.trailingAnchor.constraint(equalTo: self.showLoggingButton.leadingAnchor, constant: -6).isActive = true + + self.view.addSubview(self.portField) + self.portField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.portField.trailingAnchor.constraint(equalTo: self.connectButton.leadingAnchor, constant: -6).isActive = true + self.portField.widthAnchor.constraint(equalToConstant: 60).isActive = true + self.portField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true + + self.view.addSubview(self.ipField) + self.ipField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.ipField.trailingAnchor.constraint(equalTo: self.portField.leadingAnchor, constant: -6).isActive = true + self.ipField.widthAnchor.constraint(equalToConstant: 120).isActive = true + self.ipField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true + + self.view.addSubview(self.lockButton) + self.lockButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.lockButton.trailingAnchor.constraint(equalTo: self.ipField.leadingAnchor, constant: -6).isActive = true + + self.view.addSubview(self.arrangeButton) + self.arrangeButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + self.arrangeButton.trailingAnchor.constraint(equalTo: self.lockButton.leadingAnchor, constant: -6).isActive = true + + self.containerView.addSubview(self.logWidget) + self.setupDrag(on: self.logWidget) + self.logWidget.attach(to: self, loggingView: self.loggingView) + } + } +} diff --git a/remote-client-ios/remote-client-ios/Core/View.swift b/remote-client-ios/remote-client-ios/Core/View.swift new file mode 100644 index 0000000..e9289b3 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/View.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// remote-client-ios +// +// Created by p4piwabl0 on 6/30/26. +// + +import UIKit + +class ViewController: ObjectsViewController { + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + +} + diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/BaseWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/BaseWidget.swift new file mode 100644 index 0000000..3e56f94 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/BaseWidget.swift @@ -0,0 +1,35 @@ +import UIKit + +class BaseWidget: UIView { + + let uid: String + var onSend: ((String, Int) -> Void)? + + init(uid: String, frame: CGRect) { + self.uid = uid + super.init(frame: frame) + self.backgroundColor = UIColor(white: 0.13, alpha: 1) + self.layer.borderWidth = 1 + self.layer.cornerRadius = 6 + } + + required init?(coder: NSCoder) { fatalError() } + + func update(value: Int) {} + +} + +extension UIColor { + static func fromHex(_ hex: String) -> UIColor { + var h = hex.trimmingCharacters(in: .alphanumerics.inverted) + if h.count == 6 { h = "FF" + h } + var int: UInt64 = 0 + Scanner(string: h).scanHexInt64(&int) + return UIColor( + red: CGFloat((int >> 16) & 0xFF) / 255, + green: CGFloat((int >> 8) & 0xFF) / 255, + blue: CGFloat(int & 0xFF) / 255, + alpha: CGFloat((int >> 24) & 0xFF) / 255 + ) + } +} diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/FaderWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/FaderWidget.swift new file mode 100644 index 0000000..2532c23 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/FaderWidget.swift @@ -0,0 +1,81 @@ +import UIKit + +class FaderWidget: BaseWidget { + + static let size = CGSize(width: 140, height: 360) + + static func defaultFrame(idx: Int) -> CGRect { + let col = idx % 3 + let row = idx / 3 + return CGRect(x: 16 + CGFloat(col) * 170, y: 48 + CGFloat(row) * 380, width: size.width, height: size.height) + } + + // MARK: - Properties + let color: UIColor + private var sliderWidthConstraint: NSLayoutConstraint? + + // MARK: - Subviews + lazy var nameLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.textColor = UIColor(white: 0.53, alpha: 1) + lbl.font = .systemFont(ofSize: 14) + lbl.textAlignment = .center + return lbl + }() + + lazy var slider: UISlider = { + let s = UISlider() + s.translatesAutoresizingMaskIntoConstraints = false + s.minimumValue = 0 + s.maximumValue = 127 + s.transform = CGAffineTransform(rotationAngle: -.pi / 2) + return s + }() + + // MARK: - Init + init(uid: String, label: String, value: Int, color: UIColor, frame: CGRect) { + self.color = color + super.init(uid: uid, frame: frame) + self.layer.borderColor = color.cgColor + self.nameLabel.text = label + self.slider.value = Float(value) + self.slider.minimumTrackTintColor = color + setupUI() + } + + required init?(coder: NSCoder) { fatalError() } + + // MARK: - Layout + func setupUI() { + self.addSubview(self.nameLabel) + self.nameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true + self.nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true + self.nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true + + self.addSubview(self.slider) + self.slider.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + self.slider.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 10).isActive = true + let wc = self.slider.widthAnchor.constraint(equalToConstant: 216) + wc.isActive = true + self.sliderWidthConstraint = wc + + self.slider.addTarget(self, action: #selector(self.sliderChanged), for: .valueChanged) + } + + override func layoutSubviews() { + super.layoutSubviews() + self.sliderWidthConstraint?.constant = bounds.height * 0.6 + } + + // MARK: - Update + override func update(value: Int) { + self.slider.value = Float(value) + } + + // MARK: - Action + @objc private func sliderChanged() { + self.onSend?(self.uid, Int(self.slider.value)) + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/FeedbackWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/FeedbackWidget.swift new file mode 100644 index 0000000..00ac7b4 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/FeedbackWidget.swift @@ -0,0 +1,81 @@ +import UIKit + +class FeedbackWidget: BaseWidget { + + static let defaultSize = CGSize(width: 220, height: 100) + + static func defaultFrame(idx: Int) -> CGRect { + let col = idx % 3 + let row = idx / 3 + return CGRect(x: 16 + CGFloat(col) * 250, y: 48 + CGFloat(row) * 120, + width: defaultSize.width, height: defaultSize.height) + } + + // MARK: - Subviews + lazy var trackLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.textColor = UIColor(white: 0.9, alpha: 1) + lbl.font = .systemFont(ofSize: 16, weight: .semibold) + lbl.textAlignment = .left + lbl.text = "—" + return lbl + }() + + lazy var barLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.textColor = UIColor(white: 0.6, alpha: 1) + lbl.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + lbl.textAlignment = .left + lbl.text = "Bar —" + return lbl + }() + + lazy var stateLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.textColor = UIColor(white: 0.5, alpha: 1) + lbl.font = .systemFont(ofSize: 20, weight: .medium) + lbl.textAlignment = .right + lbl.text = "■" + return lbl + }() + + // MARK: - Init + override init(uid: String, frame: CGRect) { + super.init(uid: uid, frame: frame) + self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + setupUI() + } + + required init?(coder: NSCoder) { fatalError() } + + // MARK: - Layout + func setupUI() { + self.addSubview(self.trackLabel) + self.trackLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true + self.trackLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12).isActive = true + self.trackLabel.trailingAnchor.constraint(equalTo: self.stateLabel.leadingAnchor, constant: -8).isActive = true + + self.addSubview(self.stateLabel) + self.stateLabel.centerYAnchor.constraint(equalTo: self.trackLabel.centerYAnchor).isActive = true + self.stateLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true + + self.addSubview(self.barLabel) + self.barLabel.topAnchor.constraint(equalTo: self.trackLabel.bottomAnchor, constant: 8).isActive = true + self.barLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12).isActive = true + self.barLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true + } + + // MARK: - Update + func updateDaw(track: String?, bar: Int?, playing: Bool?) { + if let track = track { self.trackLabel.text = track } + if let bar = bar { self.barLabel.text = "Bar \(bar)" } + if let playing = playing { + self.stateLabel.text = playing ? "▶" : "■" + self.stateLabel.textColor = playing ? UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) : UIColor(white: 0.5, alpha: 1) + } + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/LogWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/LogWidget.swift new file mode 100644 index 0000000..202eb23 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/LogWidget.swift @@ -0,0 +1,49 @@ +import UIKit + +class LogWidget: BaseWidget { + + static let defaultSize = CGSize(width: 360, height: 660) + + static func defaultFrame() -> CGRect { + CGRect(x: 16, y: 80, width: defaultSize.width, height: defaultSize.height) + } + + private weak var hostedView: UIView? + + override init(uid: String, frame: CGRect) { + super.init(uid: uid, frame: frame) + self.backgroundColor = UIColor(white: 0.08, alpha: 0.95) + self.layer.borderColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 0.3).cgColor + self.isHidden = true + setupPinch() + } + + required init?(coder: NSCoder) { fatalError() } + + func attach(to parentVC: UIViewController, loggingView: LoggingView) { + parentVC.addChild(loggingView) + loggingView.view.frame = self.bounds + self.addSubview(loggingView.view) + loggingView.didMove(toParent: parentVC) + self.hostedView = loggingView.view + } + + override func layoutSubviews() { + super.layoutSubviews() + hostedView?.frame = self.bounds + } + + private func setupPinch() { + let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:))) + self.addGestureRecognizer(pinch) + } + + @objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) { + let scale = gesture.scale + let newWidth = max(200, min(800, self.bounds.width * scale)) + let newHeight = max(100, min(600, self.bounds.height * scale)) + self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: newWidth, height: newHeight)) + gesture.scale = 1 + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/TitleWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/TitleWidget.swift new file mode 100644 index 0000000..f5cb7ab --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/TitleWidget.swift @@ -0,0 +1,40 @@ +import UIKit + +class TitleWidget: BaseWidget { + + static func defaultFrame(idx: Int) -> CGRect { + let col = idx % 3 + let row = idx / 3 + return CGRect(x: 16 + CGFloat(col) * 220, y: 16 + CGFloat(row) * 60, width: 200, height: 44) + } + + // MARK: - Subviews + lazy var titleLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.textColor = UIColor(white: 0.85, alpha: 1) + lbl.font = .systemFont(ofSize: 22, weight: .semibold) + lbl.textAlignment = .left + return lbl + }() + + // MARK: - Init + init(uid: String, text: String, frame: CGRect) { + super.init(uid: uid, frame: frame) + self.backgroundColor = .clear + self.layer.borderColor = UIColor.clear.cgColor + self.titleLabel.text = text + setupUI() + } + + required init?(coder: NSCoder) { fatalError() } + + // MARK: - Layout + func setupUI() { + self.addSubview(self.titleLabel) + self.titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true + self.titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true + self.titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/ToggleWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/ToggleWidget.swift new file mode 100644 index 0000000..56973ef --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/ToggleWidget.swift @@ -0,0 +1,71 @@ +import UIKit + +class ToggleWidget: BaseWidget { + + static let size = CGSize(width: 75, height: 75) + + static func defaultFrame(idx: Int) -> CGRect { + let gap: CGFloat = 12 + let col = idx % 6 + let row = idx / 6 + return CGRect(x: 16 + CGFloat(col) * (size.width + gap), + y: 420 + CGFloat(row) * (size.height + gap), + width: size.width, height: size.height) + } + + // MARK: - Properties + let color: UIColor + var isOn: Bool + + // MARK: - Subviews + lazy var button: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.layer.cornerRadius = 4 + btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold) + return btn + }() + + // MARK: - Init + init(uid: String, label: String, isOn: Bool, color: UIColor, frame: CGRect) { + self.color = color + self.isOn = isOn + super.init(uid: uid, frame: frame) + self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + self.button.setTitle(label, for: .normal) + setupUI() + applyState() + } + + required init?(coder: NSCoder) { fatalError() } + + // MARK: - Layout + func setupUI() { + self.addSubview(self.button) + self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true + self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true + + self.button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside) + } + + // MARK: - State + func applyState() { + self.button.backgroundColor = self.isOn ? self.color : UIColor(white: 0.15, alpha: 1) + self.button.setTitleColor(self.isOn ? .black : .lightGray, for: .normal) + } + + override func update(value: Int) { + self.isOn = value > 63 + applyState() + } + + // MARK: - Action + @objc private func tapped() { + self.isOn = !self.isOn + applyState() + self.onSend?(self.uid, self.isOn ? 127 : 0) + } + +} diff --git a/remote-client-ios/remote-client-ios/Core/ui-reusable/TransportWidget.swift b/remote-client-ios/remote-client-ios/Core/ui-reusable/TransportWidget.swift new file mode 100644 index 0000000..15bfa22 --- /dev/null +++ b/remote-client-ios/remote-client-ios/Core/ui-reusable/TransportWidget.swift @@ -0,0 +1,53 @@ +import UIKit + +class TransportWidget: BaseWidget { + + static let size = CGSize(width: 75, height: 75) + + static func defaultFrame(idx: Int) -> CGRect { + let gap: CGFloat = 12 + let col = idx % 6 + let row = idx / 6 + return CGRect(x: 16 + CGFloat(col) * (size.width + gap), + y: 420 + CGFloat(row) * (size.height + gap), + width: size.width, height: size.height) + } + + // MARK: - Subviews + lazy var button: UIButton = { + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.setTitleColor(.lightGray, for: .normal) + btn.backgroundColor = UIColor(white: 0.15, alpha: 1) + btn.layer.cornerRadius = 4 + btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold) + return btn + }() + + // MARK: - Init + init(uid: String, label: String, frame: CGRect) { + super.init(uid: uid, frame: frame) + self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor + self.button.setTitle(label, for: .normal) + setupUI() + } + + required init?(coder: NSCoder) { fatalError() } + + // MARK: - Layout + func setupUI() { + self.addSubview(self.button) + self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true + self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true + + self.button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside) + } + + // MARK: - Action + @objc private func tapped() { + self.onSend?(self.uid, 127) + } + +} diff --git a/remote-client-ios/remote-client-ios/Info.plist b/remote-client-ios/remote-client-ios/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/remote-client-ios/remote-client-ios/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/remote-client-ios/remote-client-ios/SceneDelegate.swift b/remote-client-ios/remote-client-ios/SceneDelegate.swift new file mode 100644 index 0000000..519dba9 --- /dev/null +++ b/remote-client-ios/remote-client-ios/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// remote-client-ios +// +// Created by p4piwabl0 on 6/30/26. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + window?.rootViewController = ViewController() + window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/remote-client-ios/remote-client-iosTests/remote_client_iosTests.swift b/remote-client-ios/remote-client-iosTests/remote_client_iosTests.swift new file mode 100644 index 0000000..e1cd90a --- /dev/null +++ b/remote-client-ios/remote-client-iosTests/remote_client_iosTests.swift @@ -0,0 +1,17 @@ +// +// remote_client_iosTests.swift +// remote-client-iosTests +// +// Created by p4piwabl0 on 6/30/26. +// + +import Testing +@testable import remote_client_ios + +struct remote_client_iosTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/remote-client-ios/remote-client-iosUITests/remote_client_iosUITests.swift b/remote-client-ios/remote-client-iosUITests/remote_client_iosUITests.swift new file mode 100644 index 0000000..1d64ac1 --- /dev/null +++ b/remote-client-ios/remote-client-iosUITests/remote_client_iosUITests.swift @@ -0,0 +1,41 @@ +// +// remote_client_iosUITests.swift +// remote-client-iosUITests +// +// Created by p4piwabl0 on 6/30/26. +// + +import XCTest + +final class remote_client_iosUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/remote-client-ios/remote-client-iosUITests/remote_client_iosUITestsLaunchTests.swift b/remote-client-ios/remote-client-iosUITests/remote_client_iosUITestsLaunchTests.swift new file mode 100644 index 0000000..8c330b0 --- /dev/null +++ b/remote-client-ios/remote-client-iosUITests/remote_client_iosUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// remote_client_iosUITestsLaunchTests.swift +// remote-client-iosUITests +// +// Created by p4piwabl0 on 6/30/26. +// + +import XCTest + +final class remote_client_iosUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/server/http_server.py b/server/http_server.py deleted file mode 100644 index 5ec8071..0000000 --- a/server/http_server.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import threading -from http.server import SimpleHTTPRequestHandler, HTTPServer - -_CLIENT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "client") - - -class _Handler(SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=_CLIENT_DIR, **kwargs) - - def log_message(self, fmt, *args): - pass - - -def start_http_server(port=8080): - server = HTTPServer(("0.0.0.0", port), _Handler) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - return server - - -def stop_http_server(server): - if server: - threading.Thread(target=server.shutdown, daemon=True).start() diff --git a/server/ws_server.py b/server/ws_server.py index 959e9e3..61b7293 100644 --- a/server/ws_server.py +++ b/server/ws_server.py @@ -9,6 +9,7 @@ class WSServer(QObject): layout_saved = pyqtSignal(str, object) client_connected = pyqtSignal() log_signal = pyqtSignal(str) + raw_in_signal = pyqtSignal(str) def __init__(self, port=8765, parent=None): super().__init__(parent) @@ -37,6 +38,7 @@ class WSServer(QObject): self.log_signal.emit(f"client disconnected {peer} ({len(self._clients)} remaining)") def _on_message(self, message): + self.raw_in_signal.emit(message) try: data = json.loads(message) ev = data.get("event") @@ -64,7 +66,8 @@ class WSServer(QObject): def broadcast_preset(self, snapshot: dict, layout: dict): name = snapshot.get("preset_name", "") self.log_signal.emit(f"broadcast preset '{name}' → {len(self._clients)} client(s)") - self.broadcast({"event": "preset", "data": snapshot, "layout": layout}) + # layout omitted — owned and persisted client-side, keyed by preset_uuid + self.broadcast({"event": "preset", "data": snapshot}) def broadcast_widget_update(self, uid: str, value: int): self.broadcast({"event": "widget_update", "uid": uid, "value": value})