Multi-iPad dest_id routing, activity ring, context menu hide, live label/visibility sync
Desktop: - Replace Remote checkbox with T: dest_spin (0=Off, 1-9=tablet) on all widgets - dest_id persisted in get_state/set_state, broadcast in tablet snapshot - dest_spin.valueChanged triggers save + broadcast to iPad - _on_widget_visibility_from_tablet updates dest_spin from iPad hide event - _broadcast_preset_update shared helper; ws_server.has_clients guard - label editingFinished triggers live label sync to iPad iOS: - VC-Arrange/VC-Logging/Widgets-Reusable-UI file reorganisation - ArrangeObjects/ArrangeView rename, 7-bucket Arrange-Functions split - myTabletId (UserDefaults) + tabletIdField next to port in toolbar - Preset filter: destId == myTabletId instead of visible bool - BaseWidget activity ring: long press gesture (touch) + final update() flash - Context menu long press (arrange mode): hide widget, save layout, send dest_id:0 - Same-UUID preset check skips Preset Switched alert on visibility/label updates - autoSwitchEnabled, toolbarStatusLabel, presetLabel, statusToken Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -155,6 +155,15 @@ class FaderWidget(QWidget):
|
|||||||
self.pickup_checkbox.stateChanged.connect(self.on_pickup_changed)
|
self.pickup_checkbox.stateChanged.connect(self.on_pickup_changed)
|
||||||
inner.addWidget(self.pickup_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
|
inner.addWidget(self.pickup_checkbox, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
|
||||||
|
# Remote checkbox
|
||||||
|
self.dest_spin = QSpinBox()
|
||||||
|
self.dest_spin.setRange(0, 9)
|
||||||
|
self.dest_spin.setSpecialValueText("Off")
|
||||||
|
self.dest_spin.setPrefix("T:")
|
||||||
|
self.dest_spin.setValue(1)
|
||||||
|
self.dest_spin.setFixedWidth(52)
|
||||||
|
inner.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
|
||||||
# Scribble strip
|
# Scribble strip
|
||||||
self.title_edit = QLineEdit(label)
|
self.title_edit = QLineEdit(label)
|
||||||
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||||
@@ -437,7 +446,8 @@ class FaderWidget(QWidget):
|
|||||||
"hw_channel": self.hw_channel,
|
"hw_channel": self.hw_channel,
|
||||||
"zone": self.zone,
|
"zone": self.zone,
|
||||||
"center_index": self.center_index,
|
"center_index": self.center_index,
|
||||||
"zone_index": self.zone_index
|
"zone_index": self.zone_index,
|
||||||
|
"dest_id": self.dest_spin.value(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
@@ -464,6 +474,9 @@ class FaderWidget(QWidget):
|
|||||||
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
|
self.zone_btn.set_state(self.zone == "left", self.zone == "right")
|
||||||
self.hw_cc = state.get("hw_cc", None)
|
self.hw_cc = state.get("hw_cc", None)
|
||||||
self.hw_channel = state.get("hw_channel", None)
|
self.hw_channel = state.get("hw_channel", None)
|
||||||
|
self.dest_spin.blockSignals(True)
|
||||||
|
self.dest_spin.setValue(state.get("dest_id", 1))
|
||||||
|
self.dest_spin.blockSignals(False)
|
||||||
if self.hw_cc is not None:
|
if self.hw_cc is not None:
|
||||||
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
|
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
|
||||||
self.cc_input_lbl.setText(self._cc_input_label())
|
self.cc_input_lbl.setText(self._cc_input_label())
|
||||||
|
|||||||
+55
-5
@@ -135,11 +135,21 @@ containerMain = QWidget()
|
|||||||
#MARK: PRESET ROW
|
#MARK: PRESET ROW
|
||||||
hstack_preset_row = QHBoxLayout()
|
hstack_preset_row = QHBoxLayout()
|
||||||
|
|
||||||
#MARK: PRESENT
|
#MARK: PRESENT
|
||||||
menuPresets = QComboBox()
|
menuPresets = QComboBox()
|
||||||
menuPresets.setMinimumWidth(160)
|
menuPresets.setMinimumWidth(160)
|
||||||
menuPresets.currentTextChanged.connect(lambda name: on_preset_selected(menuPresets, app_window, ready, loaded_presets, on_load=preset_load))
|
menuPresets.currentTextChanged.connect(lambda name: on_preset_selected(menuPresets, app_window, ready, loaded_presets, on_load=preset_load))
|
||||||
|
|
||||||
|
buttonPresetPrev = QPushButton("◀")
|
||||||
|
buttonPresetPrev.setFixedWidth(28)
|
||||||
|
buttonPresetPrev.setToolTip("Previous preset")
|
||||||
|
buttonPresetPrev.clicked.connect(lambda: menuPresets.setCurrentIndex(max(0, menuPresets.currentIndex() - 1)))
|
||||||
|
|
||||||
|
buttonPresetNext = QPushButton("▶")
|
||||||
|
buttonPresetNext.setFixedWidth(28)
|
||||||
|
buttonPresetNext.setToolTip("Next preset")
|
||||||
|
buttonPresetNext.clicked.connect(lambda: menuPresets.setCurrentIndex(min(menuPresets.count() - 1, menuPresets.currentIndex() + 1)))
|
||||||
|
|
||||||
|
|
||||||
#button new blank preset
|
#button new blank preset
|
||||||
buttonNew = QPushButton("New")
|
buttonNew = QPushButton("New")
|
||||||
@@ -781,6 +791,8 @@ for _label, _cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]
|
|||||||
_t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
|
_t.on_select_callback = lambda strip: check_and_update_selected_strip(strip)
|
||||||
_t.on_context_menu_callback = lambda strip, pos: strip_context_menu(strip, pos)
|
_t.on_context_menu_callback = lambda strip, pos: strip_context_menu(strip, pos)
|
||||||
_t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
_t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
||||||
|
_t.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
|
||||||
|
_t.title_edit.editingFinished.connect(lambda: _broadcast_preset_update())
|
||||||
del _t, _label, _cc
|
del _t, _label, _cc
|
||||||
|
|
||||||
# MARK: SELECTED CHANNEL ROW MODULE
|
# MARK: SELECTED CHANNEL ROW MODULE
|
||||||
@@ -964,6 +976,8 @@ class MainWindow(QMainWindow):
|
|||||||
main_layout.addLayout(hstack_preset_row)
|
main_layout.addLayout(hstack_preset_row)
|
||||||
|
|
||||||
hstack_preset_row.addWidget(menuPresets) #dropdown
|
hstack_preset_row.addWidget(menuPresets) #dropdown
|
||||||
|
hstack_preset_row.addWidget(buttonPresetPrev)
|
||||||
|
hstack_preset_row.addWidget(buttonPresetNext)
|
||||||
hstack_preset_row.addWidget(buttonNew)
|
hstack_preset_row.addWidget(buttonNew)
|
||||||
hstack_preset_row.addWidget(buttonSave) #save button
|
hstack_preset_row.addWidget(buttonSave) #save button
|
||||||
hstack_preset_row.addWidget(buttonSaveAs) #save as button
|
hstack_preset_row.addWidget(buttonSaveAs) #save as button
|
||||||
@@ -2181,6 +2195,8 @@ def toggle_add(insert_after=None):
|
|||||||
toggle.on_context_menu_callback = strip_context_menu
|
toggle.on_context_menu_callback = strip_context_menu
|
||||||
toggle.on_zone_change_callback = handle_toggle_zone_change
|
toggle.on_zone_change_callback = handle_toggle_zone_change
|
||||||
toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle))
|
toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle))
|
||||||
|
toggle.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
|
||||||
|
toggle.title_edit.editingFinished.connect(_broadcast_preset_update)
|
||||||
update_window_width()
|
update_window_width()
|
||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
_update_toggle_placeholder()
|
_update_toggle_placeholder()
|
||||||
@@ -2214,6 +2230,8 @@ def fader_add(insert_after=None):
|
|||||||
fader.on_select_callback = check_and_update_selected_strip
|
fader.on_select_callback = check_and_update_selected_strip
|
||||||
fader.on_context_menu_callback = strip_context_menu
|
fader.on_context_menu_callback = strip_context_menu
|
||||||
fader.on_zone_change_callback = handle_fader_zone_change
|
fader.on_zone_change_callback = handle_fader_zone_change
|
||||||
|
fader.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
|
||||||
|
fader.title_edit.editingFinished.connect(_broadcast_preset_update)
|
||||||
update_window_width()
|
update_window_width()
|
||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
_update_fader_placeholder()
|
_update_fader_placeholder()
|
||||||
@@ -2238,6 +2256,8 @@ def transport_toggle_add(label, default_cc, insert_after=None):
|
|||||||
t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip)
|
||||||
t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t))
|
t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t))
|
||||||
t.minus_btn.clicked.connect(lambda: transport_remove(t))
|
t.minus_btn.clicked.connect(lambda: transport_remove(t))
|
||||||
|
t.dest_spin.valueChanged.connect(lambda val: _on_remote_checkbox_changed())
|
||||||
|
t.title_edit.editingFinished.connect(_broadcast_preset_update)
|
||||||
params_visible = not transport_show_hide_config
|
params_visible = not transport_show_hide_config
|
||||||
for attr in ['grp_learn', 'arrow_lbl', 'grp_cc', 'zone_btn', 'minus_btn',
|
for attr in ['grp_learn', 'arrow_lbl', 'grp_cc', 'zone_btn', 'minus_btn',
|
||||||
'plus_btn', 'title_edit', 'id_sep', 'id_label', 'color_swatch_btn']:
|
'plus_btn', 'title_edit', 'id_sep', 'id_label', 'color_swatch_btn']:
|
||||||
@@ -2351,9 +2371,9 @@ def _build_tablet_snapshot():
|
|||||||
"preset_uuid": label_preset_uuid.text().strip(),
|
"preset_uuid": label_preset_uuid.text().strip(),
|
||||||
"preset_name": name,
|
"preset_name": name,
|
||||||
"big_title": big_title_edit.text() if big_title_edit else "",
|
"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],
|
"faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "dest_id": f.dest_spin.value(), "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],
|
"toggles": [{"uid": t.uid, "label": t.title_edit.text(), "state": t.toggle_state, "dest_id": t.dest_spin.value(), "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],
|
"transports": [{"uid": t.uid, "label": t.title_edit.text(), "dest_id": t.dest_spin.value()} for t in transports],
|
||||||
}
|
}
|
||||||
return snapshot, saved_layout
|
return snapshot, saved_layout
|
||||||
|
|
||||||
@@ -2365,6 +2385,35 @@ def _on_tablet_connected():
|
|||||||
log_ws_outgoing(snapshot)
|
log_ws_outgoing(snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_widget_visibility_from_tablet(uid, dest_id):
|
||||||
|
for w in faders + toggles + transports:
|
||||||
|
if w.uid == uid:
|
||||||
|
w.dest_spin.blockSignals(True)
|
||||||
|
w.dest_spin.setValue(dest_id)
|
||||||
|
w.dest_spin.blockSignals(False)
|
||||||
|
name = menuPresets.currentText()
|
||||||
|
if name and name in loaded_presets.get("presets", {}):
|
||||||
|
loaded_presets["presets"][name].update(preset_build_snapshot(name))
|
||||||
|
save_presets_file(loaded_presets)
|
||||||
|
show_save_status()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _broadcast_preset_update():
|
||||||
|
name = menuPresets.currentText()
|
||||||
|
if name and name in loaded_presets.get("presets", {}):
|
||||||
|
loaded_presets["presets"][name].update(preset_build_snapshot(name))
|
||||||
|
save_presets_file(loaded_presets)
|
||||||
|
if ws_server and ws_server.has_clients:
|
||||||
|
snapshot, layout = _build_tablet_snapshot()
|
||||||
|
ws_server.broadcast_preset(snapshot, layout)
|
||||||
|
log_ws_outgoing(snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_remote_checkbox_changed():
|
||||||
|
_broadcast_preset_update()
|
||||||
|
|
||||||
|
|
||||||
def _on_tablet_layout_saved(preset_uuid, layout):
|
def _on_tablet_layout_saved(preset_uuid, layout):
|
||||||
name = menuPresets.currentText()
|
name = menuPresets.currentText()
|
||||||
if name and name in loaded_presets.get("presets", {}):
|
if name and name in loaded_presets.get("presets", {}):
|
||||||
@@ -2498,7 +2547,7 @@ def preset_load(preset):
|
|||||||
|
|
||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
|
|
||||||
if ws_server:
|
if ws_server and ws_server.has_clients:
|
||||||
snapshot, layout = _build_tablet_snapshot()
|
snapshot, layout = _build_tablet_snapshot()
|
||||||
ws_server.broadcast_preset(snapshot, layout)
|
ws_server.broadcast_preset(snapshot, layout)
|
||||||
log_ws_outgoing(snapshot)
|
log_ws_outgoing(snapshot)
|
||||||
@@ -2865,6 +2914,7 @@ def _start_tablet_server():
|
|||||||
ws_server.raw_in_signal.connect(log_ws_incoming)
|
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.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.layout_saved.connect(_on_tablet_layout_saved)
|
||||||
|
ws_server.widget_visibility_received.connect(_on_widget_visibility_from_tablet)
|
||||||
ws_server.client_connected.connect(_on_tablet_connected)
|
ws_server.client_connected.connect(_on_tablet_connected)
|
||||||
try:
|
try:
|
||||||
s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
|
s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
|
||||||
|
|||||||
+14
-1
@@ -2,7 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
|
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy, QCheckBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QEvent, QTimer
|
from PyQt6.QtCore import Qt, QEvent, QTimer
|
||||||
|
|
||||||
@@ -180,6 +180,15 @@ class ToggleWidget(QWidget):
|
|||||||
toggle_btn_row.addWidget(self.plus_btn)
|
toggle_btn_row.addWidget(self.plus_btn)
|
||||||
inner.addLayout(toggle_btn_row)
|
inner.addLayout(toggle_btn_row)
|
||||||
|
|
||||||
|
# Remote checkbox
|
||||||
|
self.dest_spin = QSpinBox()
|
||||||
|
self.dest_spin.setRange(0, 9)
|
||||||
|
self.dest_spin.setSpecialValueText("Off")
|
||||||
|
self.dest_spin.setPrefix("T:")
|
||||||
|
self.dest_spin.setValue(1)
|
||||||
|
self.dest_spin.setFixedWidth(52)
|
||||||
|
inner.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
|
||||||
# Zone button
|
# Zone button
|
||||||
self.zone_btn = ZoneButton()
|
self.zone_btn = ZoneButton()
|
||||||
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
|
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
|
||||||
@@ -370,6 +379,7 @@ class ToggleWidget(QWidget):
|
|||||||
"hw_cc": self.hw_cc,
|
"hw_cc": self.hw_cc,
|
||||||
"hw_channel": self.hw_channel,
|
"hw_channel": self.hw_channel,
|
||||||
"trigger_mode": self.trigger_mode,
|
"trigger_mode": self.trigger_mode,
|
||||||
|
"dest_id": self.dest_spin.value(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
@@ -394,6 +404,9 @@ class ToggleWidget(QWidget):
|
|||||||
self.hw_cc = state.get("hw_cc", None)
|
self.hw_cc = state.get("hw_cc", None)
|
||||||
self.hw_channel = state.get("hw_channel", None)
|
self.hw_channel = state.get("hw_channel", None)
|
||||||
self.set_trigger_mode(state.get("trigger_mode", "toggle"))
|
self.set_trigger_mode(state.get("trigger_mode", "toggle"))
|
||||||
|
self.dest_spin.blockSignals(True)
|
||||||
|
self.dest_spin.setValue(state.get("dest_id", 1))
|
||||||
|
self.dest_spin.blockSignals(False)
|
||||||
if self.hw_cc is not None:
|
if self.hw_cc is not None:
|
||||||
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
|
self.learn_btn.setText(f"HW: CC{self.hw_cc}")
|
||||||
self.cc_input_lbl.setText(self._cc_input_label())
|
self.cc_input_lbl.setText(self._cc_input_label())
|
||||||
|
|||||||
+15
-2
@@ -2,7 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy
|
QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy, QCheckBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QEvent, QTimer
|
from PyQt6.QtCore import Qt, QEvent, QTimer
|
||||||
|
|
||||||
@@ -195,6 +195,15 @@ class TransportWidget(QWidget):
|
|||||||
btn_row.addWidget(self.plus_btn)
|
btn_row.addWidget(self.plus_btn)
|
||||||
inner.addLayout(btn_row)
|
inner.addLayout(btn_row)
|
||||||
|
|
||||||
|
# Remote checkbox
|
||||||
|
self.dest_spin = QSpinBox()
|
||||||
|
self.dest_spin.setRange(0, 9)
|
||||||
|
self.dest_spin.setSpecialValueText("Off")
|
||||||
|
self.dest_spin.setPrefix("T:")
|
||||||
|
self.dest_spin.setValue(1)
|
||||||
|
self.dest_spin.setFixedWidth(52)
|
||||||
|
inner.addWidget(self.dest_spin, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
|
||||||
# Zone button
|
# Zone button
|
||||||
self.zone_btn = ZoneButton()
|
self.zone_btn = ZoneButton()
|
||||||
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
|
self.zone_btn.left_callback = lambda active: self.on_zone_checked("left", active)
|
||||||
@@ -378,6 +387,7 @@ class TransportWidget(QWidget):
|
|||||||
"output_mode": self.output_mode,
|
"output_mode": self.output_mode,
|
||||||
"osc_addr": self.osc_addr_edit.text(),
|
"osc_addr": self.osc_addr_edit.text(),
|
||||||
"trigger_mode": self.trigger_mode,
|
"trigger_mode": self.trigger_mode,
|
||||||
|
"dest_id": self.dest_spin.value(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
@@ -403,4 +413,7 @@ class TransportWidget(QWidget):
|
|||||||
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
|
self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;")
|
||||||
self.osc_addr_edit.setText(state.get("osc_addr", "/play"))
|
self.osc_addr_edit.setText(state.get("osc_addr", "/play"))
|
||||||
self.set_output_mode(state.get("output_mode", "midi"))
|
self.set_output_mode(state.get("output_mode", "midi"))
|
||||||
self.set_trigger_mode(state.get("trigger_mode", "momentary"))
|
self.set_trigger_mode(state.get("trigger_mode", "momentary"))
|
||||||
|
self.dest_spin.blockSignals(True)
|
||||||
|
self.dest_spin.setValue(state.get("dest_id", 1))
|
||||||
|
self.dest_spin.blockSignals(False)
|
||||||
+138
-123
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"__last_used__": "preset3",
|
"__last_used__": "rename whatever",
|
||||||
"presets": {
|
"presets": {
|
||||||
"preset1": {
|
"preset1": {
|
||||||
"faders": [
|
"faders": [
|
||||||
@@ -16,22 +16,24 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095",
|
"uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095",
|
||||||
"label": "Fader 2",
|
"label": "Fader 2",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 83,
|
"value": 96,
|
||||||
"color": "Gray",
|
"color": "Gray",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 83,
|
"last_sent": 96,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": "right",
|
"zone": "right",
|
||||||
"center_index": 1,
|
"center_index": 1,
|
||||||
"zone_index": -1
|
"zone_index": -1,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
|
"uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
|
||||||
@@ -46,7 +48,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": "left",
|
"zone": "left",
|
||||||
"center_index": 2,
|
"center_index": 2,
|
||||||
"zone_index": -1
|
"zone_index": -1,
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"toggles": [
|
"toggles": [
|
||||||
@@ -62,7 +65,8 @@
|
|||||||
"zone_index": -1,
|
"zone_index": -1,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583",
|
"uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583",
|
||||||
@@ -76,7 +80,8 @@
|
|||||||
"zone_index": 0,
|
"zone_index": 0,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "b3cf419a-5183-4272-bd6f-44f858afbec8",
|
"uid": "b3cf419a-5183-4272-bd6f-44f858afbec8",
|
||||||
@@ -90,7 +95,8 @@
|
|||||||
"zone_index": 1,
|
"zone_index": 1,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03",
|
"uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03",
|
||||||
@@ -104,7 +110,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7",
|
"uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7",
|
||||||
@@ -118,7 +125,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a",
|
"uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a",
|
||||||
@@ -132,7 +140,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d",
|
"uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d",
|
||||||
@@ -146,7 +155,8 @@
|
|||||||
"zone_index": -1,
|
"zone_index": -1,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser": {
|
"browser": {
|
||||||
@@ -170,7 +180,7 @@
|
|||||||
"big_title": "STRING THING",
|
"big_title": "STRING THING",
|
||||||
"preset_uuid": "8cbceb6b-ba5d-4b2c-8989-f921e4e3150d",
|
"preset_uuid": "8cbceb6b-ba5d-4b2c-8989-f921e4e3150d",
|
||||||
"show_hide": {
|
"show_hide": {
|
||||||
"fader": true,
|
"fader": false,
|
||||||
"toggle": true,
|
"toggle": true,
|
||||||
"transport": true,
|
"transport": true,
|
||||||
"browser": true
|
"browser": true
|
||||||
@@ -215,7 +225,7 @@
|
|||||||
"faders": [
|
"faders": [
|
||||||
{
|
{
|
||||||
"uid": "c98d2009-d186-489a-8883-ccbd612cb4af",
|
"uid": "c98d2009-d186-489a-8883-ccbd612cb4af",
|
||||||
"label": "Fader 1",
|
"label": "trumpet whatever",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 43,
|
"value": 43,
|
||||||
@@ -226,7 +236,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": "left",
|
"zone": "left",
|
||||||
"center_index": 0,
|
"center_index": 0,
|
||||||
"zone_index": -1
|
"zone_index": -1,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095",
|
"uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095",
|
||||||
@@ -241,7 +252,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
|
"uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
|
||||||
@@ -256,7 +268,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": "right",
|
"zone": "right",
|
||||||
"center_index": 2,
|
"center_index": 2,
|
||||||
"zone_index": -1
|
"zone_index": -1,
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"toggles": [
|
"toggles": [
|
||||||
@@ -272,7 +285,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser": {
|
"browser": {
|
||||||
@@ -307,79 +321,44 @@
|
|||||||
"fader_row": true,
|
"fader_row": true,
|
||||||
"toggle_row": true,
|
"toggle_row": true,
|
||||||
"transport_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": {
|
"whatever": {
|
||||||
"faders": [],
|
"faders": [],
|
||||||
"toggles": [],
|
"toggles": [],
|
||||||
"preset_uuid": "",
|
"browser": {
|
||||||
"browser": {},
|
"back": {
|
||||||
"show_hide": {}
|
"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": "whatever shit",
|
||||||
|
"preset_uuid": "772728ff-6654-46b6-89ce-b7b073b29643",
|
||||||
|
"show_hide": {
|
||||||
|
"fader": false,
|
||||||
|
"toggle": false,
|
||||||
|
"transport": true,
|
||||||
|
"browser": false
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": true,
|
||||||
|
"big_title": true,
|
||||||
|
"fader_row": true,
|
||||||
|
"toggle_row": true,
|
||||||
|
"transport_row": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"preset5": {
|
"preset5": {
|
||||||
"faders": [
|
"faders": [
|
||||||
@@ -388,30 +367,32 @@
|
|||||||
"label": "Fader 1",
|
"label": "Fader 1",
|
||||||
"cc": 90,
|
"cc": 90,
|
||||||
"ch": 15,
|
"ch": 15,
|
||||||
"value": 85,
|
"value": 103,
|
||||||
"color": "Gray",
|
"color": "Gray",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 85,
|
"last_sent": 103,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69",
|
"uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69",
|
||||||
"label": "Fader 2",
|
"label": "Fader 2",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 64,
|
"value": 89,
|
||||||
"color": "Gray",
|
"color": "Gray",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"last_sent": 89,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb",
|
"uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb",
|
||||||
@@ -426,7 +407,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e",
|
"uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e",
|
||||||
@@ -441,7 +423,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "fee53fde-2f59-4800-8873-3d75ca619366",
|
"uid": "fee53fde-2f59-4800-8873-3d75ca619366",
|
||||||
@@ -456,11 +439,12 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421",
|
"uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421",
|
||||||
"label": "Fader 6",
|
"label": "WOOOOW",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 64,
|
"value": 64,
|
||||||
@@ -471,7 +455,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0",
|
"uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0",
|
||||||
@@ -486,7 +471,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"toggles": [
|
"toggles": [
|
||||||
@@ -502,7 +488,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser": {
|
"browser": {
|
||||||
@@ -542,7 +529,6 @@
|
|||||||
"new preset browser shit": {
|
"new preset browser shit": {
|
||||||
"faders": [],
|
"faders": [],
|
||||||
"toggles": [],
|
"toggles": [],
|
||||||
"preset_uuid": "",
|
|
||||||
"browser": {
|
"browser": {
|
||||||
"back": {
|
"back": {
|
||||||
"label": "Back",
|
"label": "Back",
|
||||||
@@ -561,39 +547,55 @@
|
|||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"show_hide": {}
|
"big_title": "",
|
||||||
|
"preset_uuid": "a2cf2b56-b18f-435c-a340-0ff22e9074f4",
|
||||||
|
"show_hide": {
|
||||||
|
"fader": false,
|
||||||
|
"toggle": false,
|
||||||
|
"transport": true,
|
||||||
|
"browser": false
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": true,
|
||||||
|
"big_title": true,
|
||||||
|
"fader_row": true,
|
||||||
|
"toggle_row": true,
|
||||||
|
"transport_row": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rename whatever": {
|
"rename whatever": {
|
||||||
"faders": [
|
"faders": [
|
||||||
{
|
{
|
||||||
"uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e",
|
"uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e",
|
||||||
"label": "Fader 1",
|
"label": "toots",
|
||||||
"cc": 90,
|
"cc": 90,
|
||||||
"ch": 15,
|
"ch": 15,
|
||||||
"value": 85,
|
"value": 59,
|
||||||
"color": "Blue",
|
"color": "Blue",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 85,
|
"last_sent": 59,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69",
|
"uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69",
|
||||||
"label": "Fader 2",
|
"label": "Fader 2",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 79,
|
"value": 66,
|
||||||
"color": "Blue",
|
"color": "Blue",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 79,
|
"last_sent": 66,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb",
|
"uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb",
|
||||||
@@ -608,7 +610,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e",
|
"uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e",
|
||||||
@@ -623,7 +626,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "fee53fde-2f59-4800-8873-3d75ca619366",
|
"uid": "fee53fde-2f59-4800-8873-3d75ca619366",
|
||||||
@@ -638,7 +642,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421",
|
"uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421",
|
||||||
@@ -653,7 +658,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0",
|
"uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0",
|
||||||
@@ -668,7 +674,8 @@
|
|||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"toggles": [
|
"toggles": [
|
||||||
@@ -684,7 +691,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "dbf062b0-6dfe-4a7b-af6a-cf47a943bdec",
|
"uid": "dbf062b0-6dfe-4a7b-af6a-cf47a943bdec",
|
||||||
@@ -698,7 +706,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "93d02fb4-5327-4f0c-8cbd-6f0eb24f7195",
|
"uid": "93d02fb4-5327-4f0c-8cbd-6f0eb24f7195",
|
||||||
@@ -712,7 +721,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "3147496a-7380-401b-9b95-947dc5458886",
|
"uid": "3147496a-7380-401b-9b95-947dc5458886",
|
||||||
@@ -726,7 +736,8 @@
|
|||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null,
|
"hw_channel": null,
|
||||||
"trigger_mode": "toggle"
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser": {
|
"browser": {
|
||||||
@@ -777,7 +788,8 @@
|
|||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"output_mode": "osc",
|
"output_mode": "osc",
|
||||||
"osc_addr": "t/1/play",
|
"osc_addr": "t/1/play",
|
||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary",
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
|
"uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd",
|
||||||
@@ -791,7 +803,8 @@
|
|||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"output_mode": "osc",
|
"output_mode": "osc",
|
||||||
"osc_addr": "t/1/stop",
|
"osc_addr": "t/1/stop",
|
||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary",
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
|
"uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f",
|
||||||
@@ -805,7 +818,8 @@
|
|||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"output_mode": "osc",
|
"output_mode": "osc",
|
||||||
"osc_addr": "t/1/record",
|
"osc_addr": "t/1/record",
|
||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary",
|
||||||
|
"dest_id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
|
"uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46",
|
||||||
@@ -819,7 +833,8 @@
|
|||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"output_mode": "osc",
|
"output_mode": "osc",
|
||||||
"osc_addr": "t/1/recenable/1/@",
|
"osc_addr": "t/1/recenable/1/@",
|
||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary",
|
||||||
|
"dest_id": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"transport_show_hide": true,
|
"transport_show_hide": true,
|
||||||
|
|||||||
@@ -1,283 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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 = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ struct ModelFaderPayload: Codable {
|
|||||||
let uid: String
|
let uid: String
|
||||||
let label: String
|
let label: String
|
||||||
let value: Int
|
let value: Int
|
||||||
let visible: Bool
|
let destId: Int
|
||||||
let color: String
|
let color: String
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ struct ModelTogglePayload: Codable {
|
|||||||
let uid: String
|
let uid: String
|
||||||
let label: String
|
let label: String
|
||||||
let state: Bool
|
let state: Bool
|
||||||
let visible: Bool
|
let destId: Int
|
||||||
let color: String
|
let color: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ModelTransportPayload: Codable {
|
struct ModelTransportPayload: Codable {
|
||||||
let uid: String
|
let uid: String
|
||||||
let label: String
|
let label: String
|
||||||
let visible: Bool
|
let destId: Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - Alerts
|
||||||
|
|
||||||
|
func presentAlert(title: String, message: String) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - Button Actions
|
||||||
|
|
||||||
|
@objc func buttonTappedConnectButton() {
|
||||||
|
if self.connectionState == .connected {
|
||||||
|
//disconnect path
|
||||||
|
self.wsClient?.cancel(with: .goingAway, reason: nil)
|
||||||
|
self.wsClient = nil
|
||||||
|
self.updateUIFromConnectionState(.disconnected)
|
||||||
|
} else {
|
||||||
|
//connect path
|
||||||
|
let ip = self.ipField.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||||
|
let port = self.portField.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
||||||
|
if let idText = self.tabletIdField.text?.trimmingCharacters(in: .whitespaces),
|
||||||
|
let idVal = Int(idText), idVal > 0 {
|
||||||
|
self.myTabletId = idVal
|
||||||
|
}
|
||||||
|
guard checkerIPAddressInput(ip: ip) else { return }
|
||||||
|
guard checkerPortInput(port: port) else { return }
|
||||||
|
guard let url = buildWebSocketURL(ip: ip, port: port) else { return }
|
||||||
|
self.connectWebSocket(url: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonTappedShowLogging() {
|
||||||
|
let visible = (self.logWidget.isHidden == false)
|
||||||
|
self.logWidget.isHidden = visible
|
||||||
|
self.showLoggingButton.setTitle(visible ? "Hide Logging" : "Show Logging", for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonTappedAutoSwitchButton() {
|
||||||
|
autoSwitchEnabled.toggle()
|
||||||
|
let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
|
autoSwitchButton.backgroundColor = autoSwitchEnabled ? green : UIColor(white: 0.15, alpha: 1)
|
||||||
|
autoSwitchButton.setTitleColor(autoSwitchEnabled ? .black : .lightGray, for: .normal)
|
||||||
|
loggingView.log("auto switch \(autoSwitchEnabled ? "enabled" : "disabled")", category: .local)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonTappedArrangeButton() {
|
||||||
|
self.isLocked = false
|
||||||
|
self.updateUIArrangeLockButtons()
|
||||||
|
self.widgets.values.forEach { $0.setArrangeMode(true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonTappedLockButton() {
|
||||||
|
self.isLocked = true
|
||||||
|
self.updateUIArrangeLockButtons()
|
||||||
|
self.saveLayout()
|
||||||
|
self.widgets.values.forEach { $0.setArrangeMode(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - Gestures
|
||||||
|
|
||||||
|
func addDragGesture(to view: UIView) {
|
||||||
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGesturePan(_:)))
|
||||||
|
view.addGestureRecognizer(pan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleGesturePan(_ gesture: UIPanGestureRecognizer) {
|
||||||
|
guard self.isLocked == false, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addContextMenuGesture(to view: UIView) {
|
||||||
|
let press = UILongPressGestureRecognizer(target: self, action: #selector(handleGestureLongPress(_:)))
|
||||||
|
press.minimumPressDuration = 0.5
|
||||||
|
press.cancelsTouchesInView = false
|
||||||
|
view.addGestureRecognizer(press)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||||
|
guard self.isLocked == false else { return }
|
||||||
|
guard gesture.state == .began else { return }
|
||||||
|
guard let widget = gesture.view as? BaseWidget else { return }
|
||||||
|
|
||||||
|
let alert = UIAlertController(title: widget.displayName, message: nil, preferredStyle: .actionSheet)
|
||||||
|
alert.overrideUserInterfaceStyle = .dark
|
||||||
|
|
||||||
|
alert.addAction(UIAlertAction(title: "Hide", style: .destructive) { [weak self] _ in
|
||||||
|
self?.hideWidget(widget)
|
||||||
|
})
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
|
||||||
|
if let popover = alert.popoverPresentationController {
|
||||||
|
popover.sourceView = widget
|
||||||
|
popover.sourceRect = widget.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideWidget(_ widget: BaseWidget) {
|
||||||
|
widget.removeFromSuperview()
|
||||||
|
self.widgets.removeValue(forKey: widget.uid)
|
||||||
|
self.saveLayout()
|
||||||
|
self.wsClientSendJSON(["event": "widget_visibility", "uid": widget.uid, "dest_id": 0])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - Input Validation
|
||||||
|
|
||||||
|
func checkerIPAddressInput(ip: String) -> Bool {
|
||||||
|
|
||||||
|
// Step 1: make sure the field is not empty
|
||||||
|
if ip.isEmpty {
|
||||||
|
presentAlert(title: "Missing IP Address", message: "Please enter an IP address.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: an IPv4 address is four numbers separated by dots e.g. 192.168.1.1
|
||||||
|
// Split the string on "." and check we got exactly 4 parts
|
||||||
|
let parts = ip.split(separator: ".")
|
||||||
|
|
||||||
|
if parts.count != 4 {
|
||||||
|
presentAlert(title: "Invalid IP Address", message: "\"\(ip)\" doesn't look like a valid IP address.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: each part must be a number between 0 and 255
|
||||||
|
for part in parts {
|
||||||
|
if UInt8(part) == nil {
|
||||||
|
presentAlert(title: "Invalid IP Address", message: "\"\(ip)\" doesn't look like a valid IP address.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkerPortInput(port: String) -> Bool {
|
||||||
|
|
||||||
|
// Step 1: make sure the field is not empty
|
||||||
|
if port.isEmpty {
|
||||||
|
presentAlert(title: "Missing Port", message: "Please enter a port number.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: the port must be a whole number
|
||||||
|
guard let number = Int(port) else {
|
||||||
|
presentAlert(title: "Invalid Port", message: "\"\(port)\" is not a valid port number.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: port numbers must be between 1 and 65535
|
||||||
|
if number < 1 || number > 65535 {
|
||||||
|
presentAlert(title: "Invalid Port", message: "\"\(port)\" is not a valid port number (1–65535).")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWebSocketURL(ip: String, port: String) -> URL? {
|
||||||
|
let urlString = "ws://\(ip):\(port)"
|
||||||
|
guard let url = URL(string: urlString) else {
|
||||||
|
presentAlert(title: "Invalid URL", message: "Could not build a valid WebSocket URL from \"\(urlString)\".")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - Preset & Layout
|
||||||
|
|
||||||
|
func buildUIFromPreset(_ preset: ModelPresetLoadToRemoteDevice) {
|
||||||
|
let savedLayout = UserDefaults.standard.dictionary(forKey: "layout_\(preset.presetUuid)") ?? [:]
|
||||||
|
if savedLayout.isEmpty {
|
||||||
|
self.loggingView.log("buildUIFromPreset: no saved layout for key=layout_\(preset.presetUuid) — using defaults", category: .local)
|
||||||
|
} else {
|
||||||
|
self.loggingView.log("buildUIFromPreset: restored \(savedLayout.count) positions from key=layout_\(preset.presetUuid)", category: .local)
|
||||||
|
if let data = try? JSONSerialization.data(withJSONObject: savedLayout),
|
||||||
|
let jsonStr = String(data: data, encoding: .utf8) {
|
||||||
|
self.loggingView.log(jsonStr, category: .local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.addDragGesture(to: titleWidget)
|
||||||
|
self.containerView.addSubview(titleWidget)
|
||||||
|
self.widgets[titleUid] = titleWidget
|
||||||
|
|
||||||
|
var faderIdx = 0
|
||||||
|
var squareIdx = 0
|
||||||
|
|
||||||
|
for f in preset.faders where f.destId == self.myTabletId {
|
||||||
|
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?.wsClientSendJSON(["event": "control", "uid": uid, "value": val]) }
|
||||||
|
self.addDragGesture(to: w)
|
||||||
|
self.addContextMenuGesture(to: w)
|
||||||
|
self.containerView.addSubview(w)
|
||||||
|
self.widgets[f.uid] = w
|
||||||
|
faderIdx += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for t in preset.toggles where t.destId == self.myTabletId {
|
||||||
|
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?.wsClientSendJSON(["event": "control", "uid": uid, "value": val]) }
|
||||||
|
self.addDragGesture(to: w)
|
||||||
|
self.addContextMenuGesture(to: w)
|
||||||
|
self.containerView.addSubview(w)
|
||||||
|
self.widgets[t.uid] = w
|
||||||
|
squareIdx += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for t in preset.transports where t.destId == self.myTabletId {
|
||||||
|
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?.wsClientSendJSON(["event": "control", "uid": uid, "value": val]) }
|
||||||
|
self.addDragGesture(to: w)
|
||||||
|
self.addContextMenuGesture(to: w)
|
||||||
|
self.containerView.addSubview(w)
|
||||||
|
self.widgets[t.uid] = w
|
||||||
|
squareIdx += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLocked = savedLayout.count > 0
|
||||||
|
self.updateUIArrangeLockButtons()
|
||||||
|
self.showStatus("[ \(preset.presetName) \(self.statusTimestamp()) ]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveLayout() {
|
||||||
|
guard self.presetUuid.isEmpty == false else {
|
||||||
|
self.loggingView.log("saveLayout: aborted — presetUuid is empty", category: .local)
|
||||||
|
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.showStatus("[ Saved \(self.statusTimestamp()) ]")
|
||||||
|
self.loggingView.log("✓ saveLayout: \(layoutDict.count) widgets saved key=layout_\(self.presetUuid)", category: .local)
|
||||||
|
if let data = try? JSONSerialization.data(withJSONObject: layoutDict),
|
||||||
|
let jsonStr = String(data: data, encoding: .utf8) {
|
||||||
|
self.loggingView.log(jsonStr, category: .local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - UI Updates
|
||||||
|
|
||||||
|
func updateUIFromConnectionState(_ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIArrangeLockButtons() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIFromDawState(_ 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.count > 0 { self.statusLabel.text = parts.joined(separator: " ") }
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateWidget(uid: String, value: Int) {
|
||||||
|
self.widgets[uid]?.update(value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showStatus(_ message: String) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.statusToken += 1
|
||||||
|
let token = self.statusToken
|
||||||
|
self.toolbarStatusLabel.text = message
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
|
||||||
|
if self.statusToken == token {
|
||||||
|
self.toolbarStatusLabel.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusTimestamp() -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "h:mm:ss a"
|
||||||
|
return "Today \(formatter.string(from: Date()))"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
// MARK: - WebSocket
|
||||||
|
|
||||||
|
func connectWebSocket(url: URL) {
|
||||||
|
self.urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
|
||||||
|
self.wsClient = self.urlSession?.webSocketTask(with: url)
|
||||||
|
self.wsClient?.resume()
|
||||||
|
self.updateUIFromConnectionState(.connected)
|
||||||
|
self.wsClientReceiveMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsClientReceiveMessage() {
|
||||||
|
self.wsClient?.receive { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch result {
|
||||||
|
case .success(let message):
|
||||||
|
switch message {
|
||||||
|
case .string(let text): self.wsClientHandleIncomingJSON(text)
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
self.wsClientReceiveMessage()
|
||||||
|
case .failure:
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateUIFromConnectionState(.disconnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsClientHandleIncomingJSON(_ text: String) {
|
||||||
|
self.loggingView.log(text, category: .incoming)
|
||||||
|
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":
|
||||||
|
guard self.autoSwitchEnabled else {
|
||||||
|
self.loggingView.log("preset event ignored — auto switch is off", category: .local)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let presetData = json["data"] as? [String: Any],
|
||||||
|
let presetBytes = try? JSONSerialization.data(withJSONObject: presetData) else {
|
||||||
|
let msg = "preset: missing or malformed 'data' field in envelope"
|
||||||
|
self.loggingView.log(msg, category: .local)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alert = UIAlertController(title: "Preset Load Error", message: msg, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
let preset = try decoder.decode(ModelPresetLoadToRemoteDevice.self, from: presetBytes)
|
||||||
|
self.loggingView.log(
|
||||||
|
"✓ preset loaded: '\(preset.presetName)' uuid=\(preset.presetUuid) faders=\(preset.faders.count) toggles=\(preset.toggles.count) transports=\(preset.transports.count)",
|
||||||
|
category: .local
|
||||||
|
)
|
||||||
|
if let encoded = try? JSONEncoder().encode(preset),
|
||||||
|
let jsonStr = String(data: encoded, encoding: .utf8) {
|
||||||
|
self.loggingView.log(jsonStr, category: .local)
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let isNewPreset = preset.presetUuid != self.presetUuid
|
||||||
|
if self.isLocked == false && isNewPreset {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Preset Switched",
|
||||||
|
message: "Switching to:\n\"\(preset.presetName)\" — \(preset.displayTitle)\n\nSave your current layout before switching?",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "Save & Switch", style: .default) { _ in
|
||||||
|
self.saveLayout()
|
||||||
|
self.buildUIFromPreset(preset)
|
||||||
|
})
|
||||||
|
alert.addAction(UIAlertAction(title: "Discard & Switch", style: .destructive) { _ in
|
||||||
|
self.buildUIFromPreset(preset)
|
||||||
|
})
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
} else {
|
||||||
|
self.buildUIFromPreset(preset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
let msg = "preset decode failed: \(error.localizedDescription)"
|
||||||
|
self.loggingView.log(msg, category: .local)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alert = UIAlertController(title: "Preset Load Error", message: msg, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.updateUIFromDawState(json) }
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsClientSendJSON(_ dict: [String: Any]) {
|
||||||
|
guard let data = try? JSONSerialization.data(withJSONObject: dict),
|
||||||
|
let text = String(data: data, encoding: .utf8) else { return }
|
||||||
|
self.loggingView.log(text, category: .outgoing)
|
||||||
|
self.wsClient?.send(.string(text)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WebSocket Delegate
|
||||||
|
extension ArrangeObjects: URLSessionWebSocketDelegate {
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
||||||
|
self.updateUIFromConnectionState(.connected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||||
|
self.updateUIFromConnectionState(.disconnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+59
-6
@@ -1,9 +1,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class ObjectsViewController: UIViewController {
|
class ArrangeObjects: UIViewController {
|
||||||
|
|
||||||
// MARK: - WebSocket
|
// MARK: - WebSocket
|
||||||
var wsTask: URLSessionWebSocketTask?
|
var wsClient: URLSessionWebSocketTask?
|
||||||
|
|
||||||
var urlSession: URLSession?
|
var urlSession: URLSession?
|
||||||
|
|
||||||
// MARK: - Connection State
|
// MARK: - Connection State
|
||||||
@@ -16,6 +17,14 @@ class ObjectsViewController: UIViewController {
|
|||||||
var presetName: String = ""
|
var presetName: String = ""
|
||||||
var presetUuid: String = ""
|
var presetUuid: String = ""
|
||||||
var isLocked: Bool = true
|
var isLocked: Bool = true
|
||||||
|
var autoSwitchEnabled: Bool = true
|
||||||
|
var myTabletId: Int {
|
||||||
|
get {
|
||||||
|
let stored = UserDefaults.standard.integer(forKey: "tablet_id")
|
||||||
|
return stored == 0 ? 1 : stored
|
||||||
|
}
|
||||||
|
set { UserDefaults.standard.set(newValue, forKey: "tablet_id") }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Widgets
|
// MARK: - Widgets
|
||||||
var widgets: [String: BaseWidget] = [:]
|
var widgets: [String: BaseWidget] = [:]
|
||||||
@@ -40,7 +49,7 @@ class ObjectsViewController: UIViewController {
|
|||||||
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
btn.layer.cornerRadius = 4
|
btn.layer.cornerRadius = 4
|
||||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
btn.addTarget(self, action: #selector(self.connectTapped), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(self.buttonTappedConnectButton), for: .touchUpInside)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -54,7 +63,19 @@ class ObjectsViewController: UIViewController {
|
|||||||
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
btn.layer.cornerRadius = 4
|
btn.layer.cornerRadius = 4
|
||||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
btn.addTarget(self, action: #selector(self.arrangeTapped), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(self.buttonTappedArrangeButton), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var autoSwitchButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitle("Auto Switch", 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.buttonTappedAutoSwitchButton), for: .touchUpInside)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -66,7 +87,7 @@ class ObjectsViewController: UIViewController {
|
|||||||
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
btn.layer.cornerRadius = 4
|
btn.layer.cornerRadius = 4
|
||||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
btn.addTarget(self, action: #selector(self.lockTapped), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(self.buttonTappedLockButton), for: .touchUpInside)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -90,6 +111,25 @@ class ObjectsViewController: UIViewController {
|
|||||||
return tf
|
return tf
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let tabletIdField: UITextField = {
|
||||||
|
let tf = UITextField()
|
||||||
|
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
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.placeholder = "ID"
|
||||||
|
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 portField: UITextField = {
|
||||||
let tf = UITextField()
|
let tf = UITextField()
|
||||||
tf.translatesAutoresizingMaskIntoConstraints = false
|
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -127,10 +167,23 @@ class ObjectsViewController: UIViewController {
|
|||||||
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
btn.layer.cornerRadius = 4
|
btn.layer.cornerRadius = 4
|
||||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
btn.addTarget(self, action: #selector(self.showLoggingTapped), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(self.buttonTappedShowLogging), for: .touchUpInside)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// MARK: - Toolbar Status
|
||||||
|
var statusToken: Int = 0
|
||||||
|
|
||||||
|
let toolbarStatusLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.text = ""
|
||||||
|
lbl.textColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
|
lbl.font = .italicSystemFont(ofSize: 11)
|
||||||
|
lbl.textAlignment = .right
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
// MARK: - Labels
|
// MARK: - Labels
|
||||||
let statusLabel: UILabel = {
|
let statusLabel: UILabel = {
|
||||||
let lbl = UILabel()
|
let lbl = UILabel()
|
||||||
+21
-5
@@ -1,9 +1,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension ObjectsViewController {
|
extension ArrangeObjects {
|
||||||
|
|
||||||
func setupUI() {
|
func setupUI() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
self.view.backgroundColor = UIColor(white: 0.1, alpha: 1)
|
self.view.backgroundColor = UIColor(white: 0.1, alpha: 1)
|
||||||
|
|
||||||
// containerView
|
// containerView
|
||||||
@@ -20,7 +21,7 @@ extension ObjectsViewController {
|
|||||||
|
|
||||||
// presetLabel
|
// presetLabel
|
||||||
self.view.addSubview(self.presetLabel)
|
self.view.addSubview(self.presetLabel)
|
||||||
self.presetLabel.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -8).isActive = true
|
self.presetLabel.topAnchor.constraint(equalTo: self.statusLabel.bottomAnchor, constant: 2).isActive = true
|
||||||
self.presetLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
self.presetLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
||||||
|
|
||||||
// toolbar buttons (top-right)
|
// toolbar buttons (top-right)
|
||||||
@@ -28,13 +29,24 @@ extension ObjectsViewController {
|
|||||||
self.showLoggingButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
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.showLoggingButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.toolbarStatusLabel)
|
||||||
|
self.toolbarStatusLabel.topAnchor.constraint(equalTo: self.showLoggingButton.bottomAnchor, constant: 4).isActive = true
|
||||||
|
self.toolbarStatusLabel.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
||||||
|
|
||||||
self.view.addSubview(self.connectButton)
|
self.view.addSubview(self.connectButton)
|
||||||
self.connectButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
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.connectButton.trailingAnchor.constraint(equalTo: self.showLoggingButton.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.tabletIdField)
|
||||||
|
self.tabletIdField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.tabletIdField.trailingAnchor.constraint(equalTo: self.connectButton.leadingAnchor, constant: -6).isActive = true
|
||||||
|
self.tabletIdField.widthAnchor.constraint(equalToConstant: 40).isActive = true
|
||||||
|
self.tabletIdField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
||||||
|
self.tabletIdField.text = "\(self.myTabletId)"
|
||||||
|
|
||||||
self.view.addSubview(self.portField)
|
self.view.addSubview(self.portField)
|
||||||
self.portField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
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.trailingAnchor.constraint(equalTo: self.tabletIdField.leadingAnchor, constant: -6).isActive = true
|
||||||
self.portField.widthAnchor.constraint(equalToConstant: 60).isActive = true
|
self.portField.widthAnchor.constraint(equalToConstant: 60).isActive = true
|
||||||
self.portField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
self.portField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
||||||
|
|
||||||
@@ -48,12 +60,16 @@ extension ObjectsViewController {
|
|||||||
self.lockButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
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.lockButton.trailingAnchor.constraint(equalTo: self.ipField.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.autoSwitchButton)
|
||||||
|
self.autoSwitchButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.autoSwitchButton.trailingAnchor.constraint(equalTo: self.lockButton.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
self.view.addSubview(self.arrangeButton)
|
self.view.addSubview(self.arrangeButton)
|
||||||
self.arrangeButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
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.arrangeButton.trailingAnchor.constraint(equalTo: self.autoSwitchButton.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
self.containerView.addSubview(self.logWidget)
|
self.containerView.addSubview(self.logWidget)
|
||||||
self.setupDrag(on: self.logWidget)
|
self.addDragGesture(to: self.logWidget)
|
||||||
self.logWidget.attach(to: self, loggingView: self.loggingView)
|
self.logWidget.attach(to: self, loggingView: self.loggingView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-3
@@ -7,13 +7,11 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class ViewController: ObjectsViewController {
|
class ArrangeView: ArrangeObjects {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
setupUI()
|
setupUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension LoggingObjects {
|
||||||
|
|
||||||
|
func log(_ message: String, category: LogCategory = .local) {
|
||||||
|
var entry = 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) {
|
||||||
|
entry = str
|
||||||
|
}
|
||||||
|
|
||||||
|
switch category {
|
||||||
|
case .incoming: logIncoming.insert(entry, at: 0)
|
||||||
|
case .outgoing: logOutgoing.insert(entry, at: 0)
|
||||||
|
case .local: logLocal.insert(entry, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category == activeCategory {
|
||||||
|
DispatchQueue.main.async { self.rebuildTextView() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func categoryChanged(_ sender: UISegmentedControl) {
|
||||||
|
activeCategory = LogCategory(rawValue: sender.selectedSegmentIndex) ?? .local
|
||||||
|
rebuildTextView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func buttonTappedClearButton() {
|
||||||
|
switch activeCategory {
|
||||||
|
case .incoming: logIncoming.removeAll()
|
||||||
|
case .outgoing: logOutgoing.removeAll()
|
||||||
|
case .local: logLocal.removeAll()
|
||||||
|
}
|
||||||
|
textView.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activeLog() -> [String] {
|
||||||
|
switch activeCategory {
|
||||||
|
case .incoming: return logIncoming
|
||||||
|
case .outgoing: return logOutgoing
|
||||||
|
case .local: return logLocal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rebuildTextView() {
|
||||||
|
textView.text = activeLog().joined(separator: "\n--- ✦ ---\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+20
-1
@@ -1,7 +1,26 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
enum LogCategory: Int {
|
||||||
|
case incoming = 0
|
||||||
|
case outgoing = 1
|
||||||
|
case local = 2
|
||||||
|
}
|
||||||
|
|
||||||
class LoggingObjects: UIViewController {
|
class LoggingObjects: UIViewController {
|
||||||
|
|
||||||
|
var logIncoming: [String] = []
|
||||||
|
var logOutgoing: [String] = []
|
||||||
|
var logLocal: [String] = []
|
||||||
|
var activeCategory: LogCategory = .local
|
||||||
|
|
||||||
|
lazy var segmentedControl: UISegmentedControl = {
|
||||||
|
let sc = UISegmentedControl(items: ["In", "Out", "Local"])
|
||||||
|
sc.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
sc.selectedSegmentIndex = LogCategory.local.rawValue
|
||||||
|
sc.addTarget(self, action: #selector(self.categoryChanged(_:)), for: .valueChanged)
|
||||||
|
return sc
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var textView: UITextView = {
|
lazy var textView: UITextView = {
|
||||||
let tv = UITextView()
|
let tv = UITextView()
|
||||||
tv.translatesAutoresizingMaskIntoConstraints = false
|
tv.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -19,7 +38,7 @@ class LoggingObjects: UIViewController {
|
|||||||
btn.setTitle("Clear", for: .normal)
|
btn.setTitle("Clear", for: .normal)
|
||||||
btn.setTitleColor(UIColor(white: 0.4, alpha: 1), for: .normal)
|
btn.setTitleColor(UIColor(white: 0.4, alpha: 1), for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 10)
|
btn.titleLabel?.font = .systemFont(ofSize: 10)
|
||||||
btn.addTarget(self, action: #selector(self.clearTapped), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(self.buttonTappedClearButton), for: .touchUpInside)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
+6
-2
@@ -7,11 +7,15 @@ extension LoggingObjects {
|
|||||||
self.view.backgroundColor = UIColor(white: 0.08, alpha: 1)
|
self.view.backgroundColor = UIColor(white: 0.08, alpha: 1)
|
||||||
|
|
||||||
self.view.addSubview(self.clearButton)
|
self.view.addSubview(self.clearButton)
|
||||||
self.clearButton.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 6).isActive = true
|
self.view.addSubview(self.segmentedControl)
|
||||||
|
self.segmentedControl.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 6).isActive = true
|
||||||
|
self.segmentedControl.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
||||||
|
self.segmentedControl.trailingAnchor.constraint(equalTo: self.clearButton.leadingAnchor, constant: -8).isActive = true
|
||||||
|
self.clearButton.centerYAnchor.constraint(equalTo: self.segmentedControl.centerYAnchor).isActive = true
|
||||||
self.clearButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
self.clearButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
||||||
|
|
||||||
self.view.addSubview(self.textView)
|
self.view.addSubview(self.textView)
|
||||||
self.textView.topAnchor.constraint(equalTo: self.clearButton.bottomAnchor, constant: 2).isActive = true
|
self.textView.topAnchor.constraint(equalTo: self.segmentedControl.bottomAnchor, constant: 6).isActive = true
|
||||||
self.textView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 4).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.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -4).isActive = true
|
||||||
self.textView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -4).isActive = true
|
self.textView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -4).isActive = true
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class BaseWidget: UIView {
|
||||||
|
|
||||||
|
let uid: String
|
||||||
|
var displayName: String = ""
|
||||||
|
var onSend: ((String, Int) -> Void)?
|
||||||
|
var restingBorderColor: CGColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
private var isTouching = false
|
||||||
|
private let activityGreen = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1).cgColor
|
||||||
|
|
||||||
|
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
|
||||||
|
self.setupActivityRingGesture()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
// Intercepts all incoming value updates so the ring can flash.
|
||||||
|
// Subclasses override applyValue(_:), not this.
|
||||||
|
final func update(value: Int) {
|
||||||
|
applyValue(value)
|
||||||
|
flashActivityRing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyValue(_ value: Int) {}
|
||||||
|
func setArrangeMode(_ enabled: Bool) {}
|
||||||
|
|
||||||
|
// MARK: - Activity Ring
|
||||||
|
|
||||||
|
private func setupActivityRingGesture() {
|
||||||
|
let press = UILongPressGestureRecognizer(target: self, action: #selector(self.handleTouchStateChanged(_:)))
|
||||||
|
press.minimumPressDuration = 0
|
||||||
|
press.allowableMovement = 10000
|
||||||
|
press.cancelsTouchesInView = false
|
||||||
|
press.delegate = self
|
||||||
|
self.addGestureRecognizer(press)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleTouchStateChanged(_ gesture: UILongPressGestureRecognizer) {
|
||||||
|
switch gesture.state {
|
||||||
|
case .began:
|
||||||
|
self.isTouching = true
|
||||||
|
self.layer.borderColor = self.activityGreen
|
||||||
|
self.layer.borderWidth = 3
|
||||||
|
case .ended, .cancelled, .failed:
|
||||||
|
self.isTouching = false
|
||||||
|
self.layer.borderColor = self.restingBorderColor
|
||||||
|
self.layer.borderWidth = 1
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flashActivityRing() {
|
||||||
|
self.layer.borderColor = self.activityGreen
|
||||||
|
self.layer.borderWidth = 3
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
||||||
|
guard let self = self, self.isTouching == false else { return }
|
||||||
|
self.layer.borderColor = self.restingBorderColor
|
||||||
|
self.layer.borderWidth = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BaseWidget: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -37,6 +37,8 @@ class FaderWidget: BaseWidget {
|
|||||||
init(uid: String, label: String, value: Int, color: UIColor, frame: CGRect) {
|
init(uid: String, label: String, value: Int, color: UIColor, frame: CGRect) {
|
||||||
self.color = color
|
self.color = color
|
||||||
super.init(uid: uid, frame: frame)
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.displayName = label
|
||||||
|
self.restingBorderColor = color.cgColor
|
||||||
self.layer.borderColor = color.cgColor
|
self.layer.borderColor = color.cgColor
|
||||||
self.nameLabel.text = label
|
self.nameLabel.text = label
|
||||||
self.slider.value = Float(value)
|
self.slider.value = Float(value)
|
||||||
@@ -69,10 +71,15 @@ class FaderWidget: BaseWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Update
|
// MARK: - Update
|
||||||
override func update(value: Int) {
|
override func applyValue(_ value: Int) {
|
||||||
self.slider.value = Float(value)
|
self.slider.value = Float(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Arrange Mode
|
||||||
|
override func setArrangeMode(_ enabled: Bool) {
|
||||||
|
self.slider.isUserInteractionEnabled = (enabled == false)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Action
|
// MARK: - Action
|
||||||
@objc private func sliderChanged() {
|
@objc private func sliderChanged() {
|
||||||
self.onSend?(self.uid, Int(self.slider.value))
|
self.onSend?(self.uid, Int(self.slider.value))
|
||||||
+9
-3
@@ -31,6 +31,7 @@ class ToggleWidget: BaseWidget {
|
|||||||
self.color = color
|
self.color = color
|
||||||
self.isOn = isOn
|
self.isOn = isOn
|
||||||
super.init(uid: uid, frame: frame)
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.displayName = label
|
||||||
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
self.button.setTitle(label, for: .normal)
|
self.button.setTitle(label, for: .normal)
|
||||||
setupUI()
|
setupUI()
|
||||||
@@ -47,7 +48,7 @@ class ToggleWidget: BaseWidget {
|
|||||||
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
|
|
||||||
self.button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
|
self.button.addTarget(self, action: #selector(self.buttonTappedToggleButton), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - State
|
// MARK: - State
|
||||||
@@ -56,13 +57,18 @@ class ToggleWidget: BaseWidget {
|
|||||||
self.button.setTitleColor(self.isOn ? .black : .lightGray, for: .normal)
|
self.button.setTitleColor(self.isOn ? .black : .lightGray, for: .normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func update(value: Int) {
|
override func applyValue(_ value: Int) {
|
||||||
self.isOn = value > 63
|
self.isOn = value > 63
|
||||||
applyState()
|
applyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Arrange Mode
|
||||||
|
override func setArrangeMode(_ enabled: Bool) {
|
||||||
|
self.button.isUserInteractionEnabled = (enabled == false)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Action
|
// MARK: - Action
|
||||||
@objc private func tapped() {
|
@objc private func buttonTappedToggleButton() {
|
||||||
self.isOn = !self.isOn
|
self.isOn = !self.isOn
|
||||||
applyState()
|
applyState()
|
||||||
self.onSend?(self.uid, self.isOn ? 127 : 0)
|
self.onSend?(self.uid, self.isOn ? 127 : 0)
|
||||||
+8
-2
@@ -27,6 +27,7 @@ class TransportWidget: BaseWidget {
|
|||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
init(uid: String, label: String, frame: CGRect) {
|
init(uid: String, label: String, frame: CGRect) {
|
||||||
super.init(uid: uid, frame: frame)
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.displayName = label
|
||||||
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
self.button.setTitle(label, for: .normal)
|
self.button.setTitle(label, for: .normal)
|
||||||
setupUI()
|
setupUI()
|
||||||
@@ -42,11 +43,16 @@ class TransportWidget: BaseWidget {
|
|||||||
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
|
|
||||||
self.button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
|
self.button.addTarget(self, action: #selector(self.buttonTappedTransportButton), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Arrange Mode
|
||||||
|
override func setArrangeMode(_ enabled: Bool) {
|
||||||
|
self.button.isUserInteractionEnabled = (enabled == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Action
|
// MARK: - Action
|
||||||
@objc private func tapped() {
|
@objc private func buttonTappedTransportButton() {
|
||||||
self.onSend?(self.uid, 127)
|
self.onSend?(self.uid, 127)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
guard let windowScene = (scene as? UIWindowScene) else { return }
|
guard let windowScene = (scene as? UIWindowScene) else { return }
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
window?.rootViewController = ViewController()
|
window?.rootViewController = ArrangeView()
|
||||||
window?.makeKeyAndVisible()
|
window?.makeKeyAndVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from PyQt6.QtNetwork import QHostAddress
|
|||||||
class WSServer(QObject):
|
class WSServer(QObject):
|
||||||
control_received = pyqtSignal(str, int)
|
control_received = pyqtSignal(str, int)
|
||||||
layout_saved = pyqtSignal(str, object)
|
layout_saved = pyqtSignal(str, object)
|
||||||
|
widget_visibility_received = pyqtSignal(str, int)
|
||||||
client_connected = pyqtSignal()
|
client_connected = pyqtSignal()
|
||||||
log_signal = pyqtSignal(str)
|
log_signal = pyqtSignal(str)
|
||||||
raw_in_signal = pyqtSignal(str)
|
raw_in_signal = pyqtSignal(str)
|
||||||
@@ -51,11 +52,20 @@ class WSServer(QObject):
|
|||||||
n = len(data.get("layout", {}))
|
n = len(data.get("layout", {}))
|
||||||
self.log_signal.emit(f"layout saved preset={data['preset_uuid'][:8]}… {n} widgets")
|
self.log_signal.emit(f"layout saved preset={data['preset_uuid'][:8]}… {n} widgets")
|
||||||
self.layout_saved.emit(data["preset_uuid"], data["layout"])
|
self.layout_saved.emit(data["preset_uuid"], data["layout"])
|
||||||
|
elif ev == "widget_visibility":
|
||||||
|
uid = data["uid"]
|
||||||
|
dest_id = int(data.get("dest_id", 0))
|
||||||
|
self.log_signal.emit(f"widget visibility uid={uid[:8]}… dest_id={dest_id}")
|
||||||
|
self.widget_visibility_received.emit(uid, dest_id)
|
||||||
else:
|
else:
|
||||||
self.log_signal.emit(f"unknown event: {ev}")
|
self.log_signal.emit(f"unknown event: {ev}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_signal.emit(f"bad message: {e}")
|
self.log_signal.emit(f"bad message: {e}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_clients(self) -> bool:
|
||||||
|
return len(self._clients) > 0
|
||||||
|
|
||||||
def broadcast(self, data: dict):
|
def broadcast(self, data: dict):
|
||||||
if not self._clients:
|
if not self._clients:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user