Compare commits
3 Commits
22e4fa770e
...
d6201c59b4
| Author | SHA1 | Date | |
|---|---|---|---|
| d6201c59b4 | |||
| 7a74cfa75e | |||
| 4db7165797 |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,3 +5,12 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Xcode
|
||||||
|
*.xcuserstate
|
||||||
|
xcuserdata/
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
DerivedData/
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -71,6 +71,8 @@ from osc_sender import (
|
|||||||
set_osc_target,
|
set_osc_target,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ws_server import WSServer
|
||||||
|
|
||||||
def set_window(w):
|
def set_window(w):
|
||||||
global _window
|
global _window
|
||||||
_window = w
|
_window = w
|
||||||
@@ -79,6 +81,8 @@ _osc_server = None
|
|||||||
|
|
||||||
_osc_thread = None
|
_osc_thread = None
|
||||||
|
|
||||||
|
ws_server = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -136,6 +140,16 @@ 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")
|
||||||
@@ -351,6 +365,24 @@ spinner_osc_send_port.setFixedWidth(65)
|
|||||||
button_osc_toggle = QPushButton("Start OSC")
|
button_osc_toggle = QPushButton("Start OSC")
|
||||||
button_osc_toggle.setFixedWidth(90)
|
button_osc_toggle.setFixedWidth(90)
|
||||||
|
|
||||||
|
# Widget: button_tablet_server
|
||||||
|
button_tablet_server = QPushButton("Start Tablet")
|
||||||
|
button_tablet_server.setFixedWidth(90)
|
||||||
|
|
||||||
|
# Widget: button_tablet_log
|
||||||
|
button_tablet_log = QPushButton("Tablet")
|
||||||
|
button_tablet_log.setFixedWidth(65)
|
||||||
|
button_tablet_log.setCheckable(True)
|
||||||
|
|
||||||
|
# Widget: button_ws_log
|
||||||
|
button_ws_log = QPushButton("WS")
|
||||||
|
button_ws_log.setFixedWidth(50)
|
||||||
|
button_ws_log.setCheckable(True)
|
||||||
|
|
||||||
|
# Widget: label_tablet_status
|
||||||
|
label_tablet_status = QLabel("")
|
||||||
|
label_tablet_status.setStyleSheet("color: #8e24aa; font-size: 11px; font-family: monospace;")
|
||||||
|
|
||||||
# Widget: button_osc_messages_log
|
# Widget: button_osc_messages_log
|
||||||
button_osc_messages_log = QPushButton("Messages")
|
button_osc_messages_log = QPushButton("Messages")
|
||||||
button_osc_messages_log.setFixedWidth(80)
|
button_osc_messages_log.setFixedWidth(80)
|
||||||
@@ -568,6 +600,90 @@ textedit_midi_out_log.setStyleSheet("""
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Container: container_ws_in_log
|
||||||
|
container_ws_in_log = QWidget(None, Qt.WindowType.Window)
|
||||||
|
container_ws_in_log.setWindowTitle("WS In — Incoming from Tablet")
|
||||||
|
container_ws_in_log.setStyleSheet("background-color: #1a1a1a;")
|
||||||
|
container_ws_in_log.setFixedWidth(420)
|
||||||
|
|
||||||
|
vstack_ws_in_log = QVBoxLayout(container_ws_in_log)
|
||||||
|
vstack_ws_in_log.setContentsMargins(6, 6, 6, 6)
|
||||||
|
vstack_ws_in_log.setSpacing(4)
|
||||||
|
|
||||||
|
hstack_ws_in_log_header = QHBoxLayout()
|
||||||
|
label_ws_in_log_title = QLabel("WS In — Incoming from Tablet")
|
||||||
|
label_ws_in_log_title.setStyleSheet("color: #00acc1; font-weight: bold; font-size: 11px;")
|
||||||
|
button_ws_in_clear = QPushButton("Clear")
|
||||||
|
button_ws_in_clear.setFixedSize(45, 18)
|
||||||
|
|
||||||
|
textedit_ws_in_log = QTextEdit()
|
||||||
|
textedit_ws_in_log.setReadOnly(True)
|
||||||
|
textedit_ws_in_log.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
background-color: #111111;
|
||||||
|
color: #00e5ff;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Container: container_ws_out_log
|
||||||
|
container_ws_out_log = QWidget(None, Qt.WindowType.Window)
|
||||||
|
container_ws_out_log.setWindowTitle("WS Out — Outgoing to Tablet")
|
||||||
|
container_ws_out_log.setStyleSheet("background-color: #1a1a1a;")
|
||||||
|
container_ws_out_log.setFixedWidth(420)
|
||||||
|
|
||||||
|
vstack_ws_out_log = QVBoxLayout(container_ws_out_log)
|
||||||
|
vstack_ws_out_log.setContentsMargins(6, 6, 6, 6)
|
||||||
|
vstack_ws_out_log.setSpacing(4)
|
||||||
|
|
||||||
|
hstack_ws_out_log_header = QHBoxLayout()
|
||||||
|
label_ws_out_log_title = QLabel("WS Out — Outgoing to Tablet")
|
||||||
|
label_ws_out_log_title.setStyleSheet("color: #ff6f00; font-weight: bold; font-size: 11px;")
|
||||||
|
button_ws_out_clear = QPushButton("Clear")
|
||||||
|
button_ws_out_clear.setFixedSize(45, 18)
|
||||||
|
|
||||||
|
textedit_ws_out_log = QTextEdit()
|
||||||
|
textedit_ws_out_log.setReadOnly(True)
|
||||||
|
textedit_ws_out_log.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
background-color: #111111;
|
||||||
|
color: #ffb300;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Container: container_tablet_log
|
||||||
|
container_tablet_log = QWidget(None, Qt.WindowType.Window)
|
||||||
|
container_tablet_log.setWindowTitle("Tablet Server")
|
||||||
|
container_tablet_log.setStyleSheet("background-color: #1a1a1a;")
|
||||||
|
container_tablet_log.setFixedWidth(360)
|
||||||
|
|
||||||
|
vstack_tablet_log = QVBoxLayout(container_tablet_log)
|
||||||
|
vstack_tablet_log.setContentsMargins(6, 6, 6, 6)
|
||||||
|
vstack_tablet_log.setSpacing(4)
|
||||||
|
|
||||||
|
hstack_tablet_log_header = QHBoxLayout()
|
||||||
|
label_tablet_log_title = QLabel("Tablet Server")
|
||||||
|
label_tablet_log_title.setStyleSheet("color: #8e24aa; font-weight: bold; font-size: 11px;")
|
||||||
|
button_tablet_log_clear = QPushButton("Clear")
|
||||||
|
button_tablet_log_clear.setFixedSize(45, 18)
|
||||||
|
|
||||||
|
textedit_tablet_log = QTextEdit()
|
||||||
|
textedit_tablet_log.setReadOnly(True)
|
||||||
|
textedit_tablet_log.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
background-color: #111111;
|
||||||
|
color: #8e24aa;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
# MARK: LAYOUT PANEL
|
# MARK: LAYOUT PANEL
|
||||||
container_layout_panel = QWidget(None, Qt.WindowType.Window)
|
container_layout_panel = QWidget(None, Qt.WindowType.Window)
|
||||||
container_layout_panel.setWindowTitle("Layout")
|
container_layout_panel.setWindowTitle("Layout")
|
||||||
@@ -675,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
|
||||||
@@ -858,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
|
||||||
@@ -995,6 +1115,11 @@ class MainWindow(QMainWindow):
|
|||||||
hstack_osc_buttons.addWidget(button_osc_toggle)
|
hstack_osc_buttons.addWidget(button_osc_toggle)
|
||||||
hstack_osc_buttons.addWidget(button_osc_messages_log)
|
hstack_osc_buttons.addWidget(button_osc_messages_log)
|
||||||
hstack_osc_buttons.addWidget(button_layout_panel)
|
hstack_osc_buttons.addWidget(button_layout_panel)
|
||||||
|
hstack_osc_buttons.addSpacing(16)
|
||||||
|
hstack_osc_buttons.addWidget(button_tablet_server)
|
||||||
|
hstack_osc_buttons.addWidget(button_tablet_log)
|
||||||
|
hstack_osc_buttons.addWidget(button_ws_log)
|
||||||
|
hstack_osc_buttons.addWidget(label_tablet_status)
|
||||||
hstack_osc_buttons.addStretch()
|
hstack_osc_buttons.addStretch()
|
||||||
|
|
||||||
# add all rows to vstack
|
# add all rows to vstack
|
||||||
@@ -1126,6 +1251,37 @@ class MainWindow(QMainWindow):
|
|||||||
button_midi_out_clear.clicked.connect(lambda: textedit_midi_out_log.clear())
|
button_midi_out_clear.clicked.connect(lambda: textedit_midi_out_log.clear())
|
||||||
container_midi_out_log.setVisible(False)
|
container_midi_out_log.setVisible(False)
|
||||||
|
|
||||||
|
#MARK: WS IN LOG CONTAINER
|
||||||
|
hstack_ws_in_log_header.addWidget(label_ws_in_log_title)
|
||||||
|
hstack_ws_in_log_header.addStretch()
|
||||||
|
hstack_ws_in_log_header.addWidget(button_ws_in_clear)
|
||||||
|
vstack_ws_in_log.addLayout(hstack_ws_in_log_header)
|
||||||
|
vstack_ws_in_log.addWidget(textedit_ws_in_log)
|
||||||
|
button_ws_in_clear.clicked.connect(lambda: textedit_ws_in_log.clear())
|
||||||
|
container_ws_in_log.setVisible(False)
|
||||||
|
|
||||||
|
#MARK: WS OUT LOG CONTAINER
|
||||||
|
hstack_ws_out_log_header.addWidget(label_ws_out_log_title)
|
||||||
|
hstack_ws_out_log_header.addStretch()
|
||||||
|
hstack_ws_out_log_header.addWidget(button_ws_out_clear)
|
||||||
|
vstack_ws_out_log.addLayout(hstack_ws_out_log_header)
|
||||||
|
vstack_ws_out_log.addWidget(textedit_ws_out_log)
|
||||||
|
button_ws_out_clear.clicked.connect(lambda: textedit_ws_out_log.clear())
|
||||||
|
container_ws_out_log.setVisible(False)
|
||||||
|
|
||||||
|
#MARK: TABLET LOG CONTAINER
|
||||||
|
hstack_tablet_log_header.addWidget(label_tablet_log_title)
|
||||||
|
hstack_tablet_log_header.addStretch()
|
||||||
|
hstack_tablet_log_header.addWidget(button_tablet_log_clear)
|
||||||
|
vstack_tablet_log.addLayout(hstack_tablet_log_header)
|
||||||
|
vstack_tablet_log.addWidget(textedit_tablet_log)
|
||||||
|
button_tablet_log_clear.clicked.connect(lambda: textedit_tablet_log.clear())
|
||||||
|
container_tablet_log.setVisible(False)
|
||||||
|
|
||||||
|
button_tablet_server.clicked.connect(toggle_tablet_server)
|
||||||
|
button_tablet_log.clicked.connect(tablet_log_show_hide)
|
||||||
|
button_ws_log.clicked.connect(ws_log_show_hide)
|
||||||
|
|
||||||
#MARK: LAYOUT PANEL
|
#MARK: LAYOUT PANEL
|
||||||
container_layout_panel.setVisible(True)
|
container_layout_panel.setVisible(True)
|
||||||
button_layout_panel.setChecked(True)
|
button_layout_panel.setChecked(True)
|
||||||
@@ -1396,6 +1552,8 @@ class MainWindow(QMainWindow):
|
|||||||
log_ui_window_calc()
|
log_ui_window_calc()
|
||||||
if container_layout_panel.isVisible():
|
if container_layout_panel.isVisible():
|
||||||
layout_panel_window_calc()
|
layout_panel_window_calc()
|
||||||
|
if container_tablet_log.isVisible():
|
||||||
|
tablet_log_window_calc()
|
||||||
|
|
||||||
def moveEvent(self, event):
|
def moveEvent(self, event):
|
||||||
super().moveEvent(event)
|
super().moveEvent(event)
|
||||||
@@ -1403,6 +1561,8 @@ class MainWindow(QMainWindow):
|
|||||||
log_ui_window_calc()
|
log_ui_window_calc()
|
||||||
if container_layout_panel.isVisible():
|
if container_layout_panel.isVisible():
|
||||||
layout_panel_window_calc()
|
layout_panel_window_calc()
|
||||||
|
if container_tablet_log.isVisible():
|
||||||
|
tablet_log_window_calc()
|
||||||
|
|
||||||
def make_separator():
|
def make_separator():
|
||||||
sep = QWidget()
|
sep = QWidget()
|
||||||
@@ -1634,6 +1794,32 @@ def log_osc_outgoing(addr, val):
|
|||||||
line = f"<span style='color:#555'>{ts}</span> <span style='color:#1e88e5'>{addr}</span> <span style='color:#d4d4d4'>{val}</span>"
|
line = f"<span style='color:#555'>{ts}</span> <span style='color:#1e88e5'>{addr}</span> <span style='color:#d4d4d4'>{val}</span>"
|
||||||
_append_log(textedit_osc_out_log, line)
|
_append_log(textedit_osc_out_log, line)
|
||||||
|
|
||||||
|
def log_ws_incoming(message):
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
try:
|
||||||
|
obj = json.loads(message)
|
||||||
|
pretty = json.dumps(obj, indent=2)
|
||||||
|
except Exception:
|
||||||
|
pretty = message
|
||||||
|
header = f"<span style='color:#555'>{ts}</span> <span style='color:#00acc1'>◀ WS In</span>"
|
||||||
|
body = f"<pre style='color:#00e5ff; margin:0; font-size:10px'>{pretty}</pre>"
|
||||||
|
textedit_ws_in_log.append(header)
|
||||||
|
textedit_ws_in_log.append(body)
|
||||||
|
textedit_ws_in_log.append("<span style='color:#333'>───────────────────</span>")
|
||||||
|
|
||||||
|
def log_ws_outgoing(data):
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
pretty = json.dumps(data, indent=2)
|
||||||
|
header = f"<span style='color:#555'>{ts}</span> <span style='color:#ff6f00'>▶ WS Out</span>"
|
||||||
|
body = f"<pre style='color:#ffb300; margin:0; font-size:10px'>{pretty}</pre>"
|
||||||
|
textedit_ws_out_log.append(header)
|
||||||
|
textedit_ws_out_log.append(body)
|
||||||
|
textedit_ws_out_log.append("<span style='color:#333'>───────────────────</span>")
|
||||||
|
|
||||||
def update_osc_server_ip(ip):
|
def update_osc_server_ip(ip):
|
||||||
set_osc_target(ip=ip.strip() or "127.0.0.1")
|
set_osc_target(ip=ip.strip() or "127.0.0.1")
|
||||||
save_io_config()
|
save_io_config()
|
||||||
@@ -1693,6 +1879,8 @@ def update_ui_on_osc_track_received(name):
|
|||||||
label_osc_status.setText("")
|
label_osc_status.setText("")
|
||||||
_track_uuid_check_pending = True
|
_track_uuid_check_pending = True
|
||||||
QTimer.singleShot(400, _check_track_uuid_arrived)
|
QTimer.singleShot(400, _check_track_uuid_arrived)
|
||||||
|
if ws_server:
|
||||||
|
ws_server.broadcast_daw_state(track=name)
|
||||||
|
|
||||||
def update_ui_on_osc_fx_info_received(name):
|
def update_ui_on_osc_fx_info_received(name):
|
||||||
label_osc_preset.setText(name)
|
label_osc_preset.setText(name)
|
||||||
@@ -1700,6 +1888,8 @@ def update_ui_on_osc_fx_info_received(name):
|
|||||||
|
|
||||||
def update_ui_on_osc_bar_received(bar):
|
def update_ui_on_osc_bar_received(bar):
|
||||||
label_osc_bar.setText(f"Bar: {bar}")
|
label_osc_bar.setText(f"Bar: {bar}")
|
||||||
|
if ws_server:
|
||||||
|
ws_server.broadcast_daw_state(bar=bar)
|
||||||
|
|
||||||
def update_ui_on_osc_playhead_received(pos):
|
def update_ui_on_osc_playhead_received(pos):
|
||||||
label_osc_position.setText(f"Time: {pos}")
|
label_osc_position.setText(f"Time: {pos}")
|
||||||
@@ -1711,6 +1901,8 @@ def update_ui_on_osc_play_transport_received(state):
|
|||||||
else:
|
else:
|
||||||
label_osc_play_state.setText("■ STOPPED")
|
label_osc_play_state.setText("■ STOPPED")
|
||||||
label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
|
label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
|
||||||
|
if ws_server:
|
||||||
|
ws_server.broadcast_daw_state(playing=bool(state))
|
||||||
|
|
||||||
def update_ui_on_osc_record_transport_received(state):
|
def update_ui_on_osc_record_transport_received(state):
|
||||||
if state:
|
if state:
|
||||||
@@ -1719,6 +1911,8 @@ def update_ui_on_osc_record_transport_received(state):
|
|||||||
else:
|
else:
|
||||||
label_osc_record_state.setText("● REC")
|
label_osc_record_state.setText("● REC")
|
||||||
label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
|
label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;")
|
||||||
|
if ws_server:
|
||||||
|
ws_server.broadcast_daw_state(recording=bool(state))
|
||||||
|
|
||||||
def check_and_start_midi_learn(strip):
|
def check_and_start_midi_learn(strip):
|
||||||
global learning_strip
|
global learning_strip
|
||||||
@@ -2001,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()
|
||||||
@@ -2034,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()
|
||||||
@@ -2058,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']:
|
||||||
@@ -2164,7 +2364,68 @@ def preset_build_snapshot(name):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tablet_snapshot():
|
||||||
|
name = menuPresets.currentText()
|
||||||
|
saved_layout = loaded_presets.get("presets", {}).get(name, {}).get("tablet_layout", {})
|
||||||
|
snapshot = {
|
||||||
|
"preset_uuid": label_preset_uuid.text().strip(),
|
||||||
|
"preset_name": name,
|
||||||
|
"big_title": big_title_edit.text() if big_title_edit else "",
|
||||||
|
"faders": [{"uid": f.uid, "label": f.title_edit.text(), "value": f.fader.value(), "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, "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(), "dest_id": t.dest_spin.value()} for t in transports],
|
||||||
|
}
|
||||||
|
return snapshot, saved_layout
|
||||||
|
|
||||||
|
|
||||||
|
def _on_tablet_connected():
|
||||||
|
if ws_server:
|
||||||
|
snapshot, layout = _build_tablet_snapshot()
|
||||||
|
ws_server.broadcast_preset(snapshot, layout)
|
||||||
|
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):
|
||||||
|
name = menuPresets.currentText()
|
||||||
|
if name and name in loaded_presets.get("presets", {}):
|
||||||
|
if loaded_presets["presets"][name].get("preset_uuid") == preset_uuid:
|
||||||
|
loaded_presets["presets"][name]["tablet_layout"] = layout
|
||||||
|
save_presets_file(loaded_presets)
|
||||||
|
|
||||||
|
|
||||||
def preset_load(preset):
|
def preset_load(preset):
|
||||||
|
global selected_strips
|
||||||
|
selected_strips = []
|
||||||
|
|
||||||
fader_states = preset.get("faders", [])
|
fader_states = preset.get("faders", [])
|
||||||
while len(faders) < len(fader_states):
|
while len(faders) < len(fader_states):
|
||||||
fader_add()
|
fader_add()
|
||||||
@@ -2286,6 +2547,11 @@ def preset_load(preset):
|
|||||||
|
|
||||||
check_and_index_strip_labels()
|
check_and_index_strip_labels()
|
||||||
|
|
||||||
|
if ws_server and ws_server.has_clients:
|
||||||
|
snapshot, layout = _build_tablet_snapshot()
|
||||||
|
ws_server.broadcast_preset(snapshot, layout)
|
||||||
|
log_ws_outgoing(snapshot)
|
||||||
|
|
||||||
def export_preset():
|
def export_preset():
|
||||||
import json as _json
|
import json as _json
|
||||||
from PyQt6.QtWidgets import QFileDialog, QMessageBox
|
from PyQt6.QtWidgets import QFileDialog, QMessageBox
|
||||||
@@ -2503,6 +2769,8 @@ def midi_message_route_hw_to_output_cc(fader_uid, value):
|
|||||||
fader.last_sent = value
|
fader.last_sent = value
|
||||||
fader.prev_position = value
|
fader.prev_position = value
|
||||||
send_cc(out_cc, value, channel=out_ch)
|
send_cc(out_cc, value, channel=out_ch)
|
||||||
|
if ws_server:
|
||||||
|
ws_server.broadcast_widget_update(fader_uid, value)
|
||||||
return
|
return
|
||||||
for toggle in toggles:
|
for toggle in toggles:
|
||||||
if toggle.uid == fader_uid:
|
if toggle.uid == fader_uid:
|
||||||
@@ -2523,6 +2791,8 @@ def midi_message_route_hw_to_output_cc(fader_uid, value):
|
|||||||
send_cc(out_cc, out_val, channel=out_ch)
|
send_cc(out_cc, out_val, channel=out_ch)
|
||||||
toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val))
|
toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val))
|
||||||
toggle._update_btn_style()
|
toggle._update_btn_style()
|
||||||
|
if ws_server:
|
||||||
|
ws_server.broadcast_widget_update(fader_uid, out_val)
|
||||||
return
|
return
|
||||||
for t in transports:
|
for t in transports:
|
||||||
if t.uid == fader_uid:
|
if t.uid == fader_uid:
|
||||||
@@ -2591,6 +2861,83 @@ def layout_panel_window_calc():
|
|||||||
container_layout_panel.move(geo.left() - 224, geo.top())
|
container_layout_panel.move(geo.left() - 224, geo.top())
|
||||||
|
|
||||||
|
|
||||||
|
def tablet_log_show_hide(checked):
|
||||||
|
container_tablet_log.setVisible(checked)
|
||||||
|
if checked:
|
||||||
|
tablet_log_window_calc()
|
||||||
|
|
||||||
|
|
||||||
|
def tablet_log_window_calc():
|
||||||
|
geo = app_window.frameGeometry()
|
||||||
|
container_tablet_log.setFixedWidth(360)
|
||||||
|
container_tablet_log.setFixedHeight(geo.height())
|
||||||
|
container_tablet_log.move(geo.right() + 732, geo.top())
|
||||||
|
|
||||||
|
|
||||||
|
def ws_log_show_hide(checked):
|
||||||
|
container_ws_in_log.setVisible(checked)
|
||||||
|
container_ws_out_log.setVisible(checked)
|
||||||
|
if checked:
|
||||||
|
ws_log_window_calc()
|
||||||
|
|
||||||
|
|
||||||
|
def ws_log_window_calc():
|
||||||
|
geo = app_window.frameGeometry()
|
||||||
|
half = geo.height() // 2 - 2
|
||||||
|
container_ws_in_log.setFixedWidth(360)
|
||||||
|
container_ws_in_log.setFixedHeight(half)
|
||||||
|
container_ws_in_log.move(geo.right() + 4, geo.top())
|
||||||
|
container_ws_out_log.setFixedWidth(360)
|
||||||
|
container_ws_out_log.setFixedHeight(half)
|
||||||
|
container_ws_out_log.move(geo.right() + 4, geo.top() + half + 4)
|
||||||
|
|
||||||
|
|
||||||
|
def log_tablet(line):
|
||||||
|
from datetime import datetime
|
||||||
|
ts = datetime.now().strftime("%H:%M:%S")
|
||||||
|
full = f"<span style='color:#555'>{ts}</span> <span style='color:#8e24aa'>{line}</span>"
|
||||||
|
_append_log(textedit_tablet_log, full)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_tablet_server():
|
||||||
|
if ws_server is not None:
|
||||||
|
_stop_tablet_server()
|
||||||
|
else:
|
||||||
|
_start_tablet_server()
|
||||||
|
|
||||||
|
|
||||||
|
def _start_tablet_server():
|
||||||
|
global ws_server
|
||||||
|
import socket as _socket
|
||||||
|
ws_server = WSServer(port=8765, parent=app_window)
|
||||||
|
ws_server.log_signal.connect(log_tablet)
|
||||||
|
ws_server.raw_in_signal.connect(log_ws_incoming)
|
||||||
|
ws_server.control_received.connect(lambda uid, val: midi_receiver.hw_cc_signal.emit(uid, val))
|
||||||
|
ws_server.layout_saved.connect(_on_tablet_layout_saved)
|
||||||
|
ws_server.widget_visibility_received.connect(_on_widget_visibility_from_tablet)
|
||||||
|
ws_server.client_connected.connect(_on_tablet_connected)
|
||||||
|
try:
|
||||||
|
s = _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
except Exception:
|
||||||
|
ip = "localhost"
|
||||||
|
button_tablet_server.setText("Stop Tablet")
|
||||||
|
label_tablet_status.setText(f"ws://{ip}:8765")
|
||||||
|
log_tablet(f"WS ws://{ip}:8765")
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_tablet_server():
|
||||||
|
global ws_server
|
||||||
|
if ws_server:
|
||||||
|
ws_server.stop()
|
||||||
|
ws_server = None
|
||||||
|
button_tablet_server.setText("Start Tablet")
|
||||||
|
label_tablet_status.setText("")
|
||||||
|
log_tablet("server stopped")
|
||||||
|
|
||||||
|
|
||||||
def start_osc_server(port=9000):
|
def start_osc_server(port=9000):
|
||||||
global _osc_server, _osc_thread
|
global _osc_server, _osc_thread
|
||||||
stop_osc_server()
|
stop_osc_server()
|
||||||
@@ -9,7 +9,7 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
PRESETS_DIR = os.path.join(BASE_DIR, "presets")
|
PRESETS_DIR = os.path.join(BASE_DIR, "presets")
|
||||||
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
||||||
os.makedirs(PRESETS_DIR, exist_ok=True)
|
os.makedirs(PRESETS_DIR, exist_ok=True)
|
||||||
@@ -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())
|
||||||
@@ -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):
|
||||||
@@ -404,3 +414,6 @@ class TransportWidget(QWidget):
|
|||||||
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)
|
||||||
+374
-64
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"__last_used__": "preset5",
|
"__last_used__": "rename whatever",
|
||||||
"presets": {
|
"presets": {
|
||||||
"preset1": {
|
"preset1": {
|
||||||
"faders": [
|
"faders": [
|
||||||
@@ -8,45 +8,48 @@
|
|||||||
"label": "Fader 1",
|
"label": "Fader 1",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 38,
|
"value": 69,
|
||||||
"color": "Red",
|
"color": "Red",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 38,
|
"last_sent": 69,
|
||||||
"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": "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": 64,
|
"value": 96,
|
||||||
"color": "Gray",
|
"color": "Gray",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"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",
|
||||||
"label": "Fader 3",
|
"label": "Fader 3",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 64,
|
"value": 56,
|
||||||
"color": "Orange",
|
"color": "Orange",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"last_sent": 56,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"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": [
|
||||||
@@ -61,20 +64,24 @@
|
|||||||
"center_index": 0,
|
"center_index": 0,
|
||||||
"zone_index": -1,
|
"zone_index": -1,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583",
|
"uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583",
|
||||||
"label": "Toggle 2",
|
"label": "Toggle 2",
|
||||||
"cc": 20,
|
"cc": 20,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"state": false,
|
"state": true,
|
||||||
"color": "Blue",
|
"color": "Blue",
|
||||||
"zone": "left",
|
"zone": "left",
|
||||||
"center_index": 1,
|
"center_index": 1,
|
||||||
"zone_index": 0,
|
"zone_index": 0,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "b3cf419a-5183-4272-bd6f-44f858afbec8",
|
"uid": "b3cf419a-5183-4272-bd6f-44f858afbec8",
|
||||||
@@ -87,7 +94,9 @@
|
|||||||
"center_index": 2,
|
"center_index": 2,
|
||||||
"zone_index": 1,
|
"zone_index": 1,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "momentary",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03",
|
"uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03",
|
||||||
@@ -100,20 +109,24 @@
|
|||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7",
|
"uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7",
|
||||||
"label": "Toggle 6",
|
"label": "Toggle 6",
|
||||||
"cc": 20,
|
"cc": 20,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"state": false,
|
"state": true,
|
||||||
"color": "Gray",
|
"color": "Gray",
|
||||||
"zone": null,
|
"zone": null,
|
||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a",
|
"uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a",
|
||||||
@@ -126,7 +139,9 @@
|
|||||||
"center_index": null,
|
"center_index": null,
|
||||||
"zone_index": null,
|
"zone_index": null,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d",
|
"uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d",
|
||||||
@@ -139,7 +154,9 @@
|
|||||||
"center_index": 4,
|
"center_index": 4,
|
||||||
"zone_index": -1,
|
"zone_index": -1,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"hw_channel": null
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"remote": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser": {
|
"browser": {
|
||||||
@@ -160,12 +177,20 @@
|
|||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"big_title": "STRING THING",
|
||||||
"preset_uuid": "8cbceb6b-ba5d-4b2c-8989-f921e4e3150d",
|
"preset_uuid": "8cbceb6b-ba5d-4b2c-8989-f921e4e3150d",
|
||||||
"show_hide": {
|
"show_hide": {
|
||||||
"fader": false,
|
"fader": false,
|
||||||
"toggle": false,
|
"toggle": true,
|
||||||
"transport": false,
|
"transport": true,
|
||||||
"browser": false
|
"browser": true
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": true,
|
||||||
|
"big_title": true,
|
||||||
|
"fader_row": true,
|
||||||
|
"toggle_row": true,
|
||||||
|
"transport_row": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preset2": {
|
"preset2": {
|
||||||
@@ -200,54 +225,57 @@
|
|||||||
"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": 64,
|
"value": 43,
|
||||||
"color": "Gray",
|
"color": "Orange",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"last_sent": 43,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"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",
|
||||||
"label": "Fader 2",
|
"label": "Fader 2",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 64,
|
"value": 48,
|
||||||
"color": "Gray",
|
"color": "Pink",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"last_sent": 48,
|
||||||
"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": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
|
"uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790",
|
||||||
"label": "Fader 3",
|
"label": "Fader 3",
|
||||||
"cc": 1,
|
"cc": 1,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"value": 64,
|
"value": 93,
|
||||||
"color": "Gray",
|
"color": "Yellow",
|
||||||
"pickup": false,
|
"pickup": false,
|
||||||
"last_sent": 64,
|
"last_sent": 93,
|
||||||
"hw_cc": null,
|
"hw_cc": null,
|
||||||
"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": [
|
||||||
{
|
{
|
||||||
"uid": "e291394c-cc09-4d3c-9ac3-ed5fe2803a46",
|
"uid": "e291394c-cc09-4d3c-9ac3-ed5fe2803a46",
|
||||||
"label": "Toggle 1",
|
"label": "whatever",
|
||||||
"cc": 20,
|
"cc": 20,
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"state": false,
|
"state": false,
|
||||||
@@ -257,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": {
|
||||||
@@ -278,20 +307,58 @@
|
|||||||
"trigger_mode": "momentary"
|
"trigger_mode": "momentary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"big_title": "",
|
||||||
"preset_uuid": "7483fb5b-f47f-49e4-bccb-e8d20195078c",
|
"preset_uuid": "7483fb5b-f47f-49e4-bccb-e8d20195078c",
|
||||||
"show_hide": {
|
"show_hide": {
|
||||||
"fader": false,
|
"fader": false,
|
||||||
"toggle": false,
|
"toggle": false,
|
||||||
"transport": false,
|
"transport": true,
|
||||||
"browser": true
|
"browser": true
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": true,
|
||||||
|
"big_title": true,
|
||||||
|
"fader_row": true,
|
||||||
|
"toggle_row": true,
|
||||||
|
"transport_row": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": [
|
||||||
@@ -300,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": 67,
|
"hw_cc": null,
|
||||||
"hw_channel": 1,
|
"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",
|
||||||
@@ -338,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",
|
||||||
@@ -353,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",
|
||||||
@@ -368,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,
|
||||||
@@ -383,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",
|
||||||
@@ -398,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": [
|
||||||
@@ -414,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": {
|
||||||
@@ -439,15 +514,21 @@
|
|||||||
"preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959",
|
"preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959",
|
||||||
"show_hide": {
|
"show_hide": {
|
||||||
"fader": false,
|
"fader": false,
|
||||||
"toggle": false,
|
"toggle": true,
|
||||||
"transport": false,
|
"transport": true,
|
||||||
"browser": true
|
"browser": true
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": true,
|
||||||
|
"big_title": true,
|
||||||
|
"fader_row": true,
|
||||||
|
"toggle_row": true,
|
||||||
|
"transport_row": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"new preset browser shit": {
|
"new preset browser shit": {
|
||||||
"faders": [],
|
"faders": [],
|
||||||
"toggles": [],
|
"toggles": [],
|
||||||
"preset_uuid": "",
|
|
||||||
"browser": {
|
"browser": {
|
||||||
"back": {
|
"back": {
|
||||||
"label": "Back",
|
"label": "Back",
|
||||||
@@ -466,7 +547,232 @@
|
|||||||
"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": {
|
||||||
|
"faders": [
|
||||||
|
{
|
||||||
|
"uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e",
|
||||||
|
"label": "toots",
|
||||||
|
"cc": 90,
|
||||||
|
"ch": 15,
|
||||||
|
"value": 59,
|
||||||
|
"color": "Blue",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 59,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69",
|
||||||
|
"label": "Fader 2",
|
||||||
|
"cc": 1,
|
||||||
|
"ch": 1,
|
||||||
|
"value": 66,
|
||||||
|
"color": "Blue",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 66,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb",
|
||||||
|
"label": "Fader 3",
|
||||||
|
"cc": 1,
|
||||||
|
"ch": 1,
|
||||||
|
"value": 64,
|
||||||
|
"color": "Blue",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 64,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e",
|
||||||
|
"label": "Fader 4",
|
||||||
|
"cc": 1,
|
||||||
|
"ch": 1,
|
||||||
|
"value": 64,
|
||||||
|
"color": "Green",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 64,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "fee53fde-2f59-4800-8873-3d75ca619366",
|
||||||
|
"label": "Fader 5",
|
||||||
|
"cc": 1,
|
||||||
|
"ch": 1,
|
||||||
|
"value": 64,
|
||||||
|
"color": "Green",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 64,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421",
|
||||||
|
"label": "Fader 6",
|
||||||
|
"cc": 1,
|
||||||
|
"ch": 1,
|
||||||
|
"value": 64,
|
||||||
|
"color": "Green",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 64,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0",
|
||||||
|
"label": "Fader 7",
|
||||||
|
"cc": 1,
|
||||||
|
"ch": 1,
|
||||||
|
"value": 127,
|
||||||
|
"color": "Green",
|
||||||
|
"pickup": false,
|
||||||
|
"last_sent": 127,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"dest_id": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"toggles": [
|
||||||
|
{
|
||||||
|
"uid": "46f17f40-1bb2-4527-b6c5-fd26e535beed",
|
||||||
|
"label": "Toggle 1",
|
||||||
|
"cc": 12,
|
||||||
|
"ch": 1,
|
||||||
|
"state": false,
|
||||||
|
"color": "Green",
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "dbf062b0-6dfe-4a7b-af6a-cf47a943bdec",
|
||||||
|
"label": "Toggle 2",
|
||||||
|
"cc": 13,
|
||||||
|
"ch": 1,
|
||||||
|
"state": false,
|
||||||
|
"color": "Teal",
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "93d02fb4-5327-4f0c-8cbd-6f0eb24f7195",
|
||||||
|
"label": "Toggle 3",
|
||||||
|
"cc": 14,
|
||||||
|
"ch": 1,
|
||||||
|
"state": false,
|
||||||
|
"color": "Purple",
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "3147496a-7380-401b-9b95-947dc5458886",
|
||||||
|
"label": "Toggle 4",
|
||||||
|
"cc": 15,
|
||||||
|
"ch": 1,
|
||||||
|
"state": false,
|
||||||
|
"color": "Pink",
|
||||||
|
"zone": null,
|
||||||
|
"center_index": null,
|
||||||
|
"zone_index": null,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "toggle",
|
||||||
|
"dest_id": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"browser": {
|
||||||
|
"back": {
|
||||||
|
"label": "Back",
|
||||||
|
"cc": 22,
|
||||||
|
"ch": 1,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "momentary"
|
||||||
|
},
|
||||||
|
"fwd": {
|
||||||
|
"label": "Fwd",
|
||||||
|
"cc": 23,
|
||||||
|
"ch": 1,
|
||||||
|
"hw_cc": null,
|
||||||
|
"hw_channel": null,
|
||||||
|
"trigger_mode": "momentary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"big_title": "STRING SHORTS",
|
||||||
|
"preset_uuid": "cf0155c7-81ea-4291-8680-6f058b80007f",
|
||||||
|
"show_hide": {
|
||||||
|
"fader": false,
|
||||||
|
"toggle": false,
|
||||||
|
"transport": true,
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"preset_browser": true,
|
||||||
|
"big_title": true,
|
||||||
|
"fader_row": true,
|
||||||
|
"toggle_row": true,
|
||||||
|
"transport_row": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"transport_preset": [
|
"transport_preset": [
|
||||||
@@ -482,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",
|
||||||
@@ -496,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",
|
||||||
@@ -510,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",
|
||||||
@@ -524,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,
|
||||||
|
|||||||
@@ -0,0 +1,578 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 77;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
979A62DE2FF478D6002D3E46 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 979A62BF2FF478D4002D3E46 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 979A62C62FF478D4002D3E46;
|
||||||
|
remoteInfo = "remote-client-ios";
|
||||||
|
};
|
||||||
|
979A62E82FF478D6002D3E46 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 979A62BF2FF478D4002D3E46 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 979A62C62FF478D4002D3E46;
|
||||||
|
remoteInfo = "remote-client-ios";
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
979A62C72FF478D4002D3E46 /* remote-client-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "remote-client-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "remote-client-iosTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "remote-client-iosUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
979A62EF2FF478D6002D3E46 /* Exceptions for "remote-client-ios" folder in "remote-client-ios" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = 979A62C62FF478D4002D3E46 /* remote-client-ios */;
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
979A62C92FF478D4002D3E46 /* remote-client-ios */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
979A62EF2FF478D6002D3E46 /* Exceptions for "remote-client-ios" folder in "remote-client-ios" target */,
|
||||||
|
);
|
||||||
|
path = "remote-client-ios";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
979A62E02FF478D6002D3E46 /* remote-client-iosTests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = "remote-client-iosTests";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = "remote-client-iosUITests";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
979A62C42FF478D4002D3E46 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
979A62DA2FF478D6002D3E46 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
979A62E42FF478D6002D3E46 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
979A62BE2FF478D4002D3E46 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
979A62C92FF478D4002D3E46 /* remote-client-ios */,
|
||||||
|
979A62E02FF478D6002D3E46 /* remote-client-iosTests */,
|
||||||
|
979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */,
|
||||||
|
979A62C82FF478D4002D3E46 /* Products */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
979A62C82FF478D4002D3E46 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
979A62C72FF478D4002D3E46 /* remote-client-ios.app */,
|
||||||
|
979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */,
|
||||||
|
979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
979A62C62FF478D4002D3E46 /* remote-client-ios */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 979A62F02FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-ios" */;
|
||||||
|
buildPhases = (
|
||||||
|
979A62C32FF478D4002D3E46 /* Sources */,
|
||||||
|
979A62C42FF478D4002D3E46 /* Frameworks */,
|
||||||
|
979A62C52FF478D4002D3E46 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
979A62C92FF478D4002D3E46 /* remote-client-ios */,
|
||||||
|
);
|
||||||
|
name = "remote-client-ios";
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = "remote-client-ios";
|
||||||
|
productReference = 979A62C72FF478D4002D3E46 /* remote-client-ios.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
979A62DC2FF478D6002D3E46 /* remote-client-iosTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 979A62F52FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
979A62D92FF478D6002D3E46 /* Sources */,
|
||||||
|
979A62DA2FF478D6002D3E46 /* Frameworks */,
|
||||||
|
979A62DB2FF478D6002D3E46 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
979A62DF2FF478D6002D3E46 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
979A62E02FF478D6002D3E46 /* remote-client-iosTests */,
|
||||||
|
);
|
||||||
|
name = "remote-client-iosTests";
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = "remote-client-iosTests";
|
||||||
|
productReference = 979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
979A62E62FF478D6002D3E46 /* remote-client-iosUITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 979A62F82FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosUITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
979A62E32FF478D6002D3E46 /* Sources */,
|
||||||
|
979A62E42FF478D6002D3E46 /* Frameworks */,
|
||||||
|
979A62E52FF478D6002D3E46 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
979A62E92FF478D6002D3E46 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */,
|
||||||
|
);
|
||||||
|
name = "remote-client-iosUITests";
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = "remote-client-iosUITests";
|
||||||
|
productReference = 979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
979A62BF2FF478D4002D3E46 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = 1;
|
||||||
|
LastSwiftUpdateCheck = 1640;
|
||||||
|
LastUpgradeCheck = 1640;
|
||||||
|
TargetAttributes = {
|
||||||
|
979A62C62FF478D4002D3E46 = {
|
||||||
|
CreatedOnToolsVersion = 16.4;
|
||||||
|
};
|
||||||
|
979A62DC2FF478D6002D3E46 = {
|
||||||
|
CreatedOnToolsVersion = 16.4;
|
||||||
|
TestTargetID = 979A62C62FF478D4002D3E46;
|
||||||
|
};
|
||||||
|
979A62E62FF478D6002D3E46 = {
|
||||||
|
CreatedOnToolsVersion = 16.4;
|
||||||
|
TestTargetID = 979A62C62FF478D4002D3E46;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 979A62C22FF478D4002D3E46 /* Build configuration list for PBXProject "remote-client-ios" */;
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 979A62BE2FF478D4002D3E46;
|
||||||
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
preferredProjectObjectVersion = 77;
|
||||||
|
productRefGroup = 979A62C82FF478D4002D3E46 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
979A62C62FF478D4002D3E46 /* remote-client-ios */,
|
||||||
|
979A62DC2FF478D6002D3E46 /* remote-client-iosTests */,
|
||||||
|
979A62E62FF478D6002D3E46 /* remote-client-iosUITests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
979A62C52FF478D4002D3E46 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
979A62DB2FF478D6002D3E46 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
979A62E52FF478D6002D3E46 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
979A62C32FF478D4002D3E46 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
979A62D92FF478D6002D3E46 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
979A62E32FF478D6002D3E46 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
979A62DF2FF478D6002D3E46 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 979A62C62FF478D4002D3E46 /* remote-client-ios */;
|
||||||
|
targetProxy = 979A62DE2FF478D6002D3E46 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
979A62E92FF478D6002D3E46 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 979A62C62FF478D4002D3E46 /* remote-client-ios */;
|
||||||
|
targetProxy = 979A62E82FF478D6002D3E46 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
979A62F12FF478D6002D3E46 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "remote-client-ios/Info.plist";
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-ios";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
979A62F22FF478D6002D3E46 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "remote-client-ios/Info.plist";
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-ios";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
979A62F32FF478D6002D3E46 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
979A62F42FF478D6002D3E46 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
979A62F62FF478D6002D3E46 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosTests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/remote-client-ios.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/remote-client-ios";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
979A62F72FF478D6002D3E46 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosTests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/remote-client-ios.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/remote-client-ios";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
979A62F92FF478D6002D3E46 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosUITests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = "remote-client-ios";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
979A62FA2FF478D6002D3E46 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosUITests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = "remote-client-ios";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
979A62C22FF478D4002D3E46 /* Build configuration list for PBXProject "remote-client-ios" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
979A62F32FF478D6002D3E46 /* Debug */,
|
||||||
|
979A62F42FF478D6002D3E46 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
979A62F02FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-ios" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
979A62F12FF478D6002D3E46 /* Debug */,
|
||||||
|
979A62F22FF478D6002D3E46 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
979A62F52FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
979A62F62FF478D6002D3E46 /* Debug */,
|
||||||
|
979A62F72FF478D6002D3E46 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
979A62F82FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
979A62F92FF478D6002D3E46 /* Debug */,
|
||||||
|
979A62FA2FF478D6002D3E46 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 979A62BF2FF478D4002D3E46 /* Project object */;
|
||||||
|
}
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// remote-client-ios
|
||||||
|
//
|
||||||
|
// Created by p4piwabl0 on 6/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
// Override point for customization after application launch.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
// Called when a new scene session is being created.
|
||||||
|
// Use this method to select a configuration to create the new scene with.
|
||||||
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||||
|
// Called when the user discards a scene session.
|
||||||
|
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||||
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ModelPresetLoadToRemoteDevice: Codable {
|
||||||
|
let presetUuid: String
|
||||||
|
let presetName: String
|
||||||
|
let bigTitle: String
|
||||||
|
let faders: [ModelFaderPayload]
|
||||||
|
let toggles: [ModelTogglePayload]
|
||||||
|
let transports: [ModelTransportPayload]
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
bigTitle.isEmpty ? "No Title" : bigTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModelFaderPayload: Codable {
|
||||||
|
let uid: String
|
||||||
|
let label: String
|
||||||
|
let value: Int
|
||||||
|
let destId: Int
|
||||||
|
let color: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModelTogglePayload: Codable {
|
||||||
|
let uid: String
|
||||||
|
let label: String
|
||||||
|
let state: Bool
|
||||||
|
let destId: Int
|
||||||
|
let color: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModelTransportPayload: Codable {
|
||||||
|
let uid: String
|
||||||
|
let label: String
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ArrangeObjects: UIViewController {
|
||||||
|
|
||||||
|
// MARK: - WebSocket
|
||||||
|
var wsClient: URLSessionWebSocketTask?
|
||||||
|
|
||||||
|
var urlSession: URLSession?
|
||||||
|
|
||||||
|
// MARK: - Connection State
|
||||||
|
enum ConnectionState {
|
||||||
|
case notStarted, connecting, connected, disconnected
|
||||||
|
}
|
||||||
|
var connectionState: ConnectionState = .notStarted
|
||||||
|
|
||||||
|
// MARK: - State
|
||||||
|
var presetName: String = ""
|
||||||
|
var presetUuid: String = ""
|
||||||
|
var isLocked: Bool = true
|
||||||
|
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
|
||||||
|
var widgets: [String: BaseWidget] = [:]
|
||||||
|
var feedbackWidgets: [FeedbackWidget] = []
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
var layout: [String: CGRect] = [:]
|
||||||
|
|
||||||
|
let containerView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.backgroundColor = UIColor(white: 0.1, alpha: 1)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Toolbar
|
||||||
|
lazy var connectButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitle("Connect", for: .normal)
|
||||||
|
btn.setTitleColor(.black, for: .normal)
|
||||||
|
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
|
btn.layer.cornerRadius = 4
|
||||||
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
|
btn.addTarget(self, action: #selector(self.buttonTappedConnectButton), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var arrangeButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitle("Arrange", for: .normal)
|
||||||
|
btn.setTitleColor(.lightGray, for: .normal)
|
||||||
|
btn.backgroundColor = UIColor(white: 0.15, alpha: 1)
|
||||||
|
btn.layer.borderWidth = 1
|
||||||
|
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
btn.layer.cornerRadius = 4
|
||||||
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
|
btn.addTarget(self, action: #selector(self.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
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var lockButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitle("Lock / Save", for: .normal)
|
||||||
|
btn.setTitleColor(.black, for: .normal)
|
||||||
|
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
|
btn.layer.cornerRadius = 4
|
||||||
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
|
btn.addTarget(self, action: #selector(self.buttonTappedLockButton), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Connection Fields
|
||||||
|
let ipField: UITextField = {
|
||||||
|
let tf = UITextField()
|
||||||
|
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tf.text = "127.0.0.1"
|
||||||
|
tf.textColor = .lightGray
|
||||||
|
tf.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||||
|
tf.backgroundColor = UIColor(white: 0.12, alpha: 1)
|
||||||
|
tf.layer.borderWidth = 1
|
||||||
|
tf.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
tf.layer.cornerRadius = 4
|
||||||
|
tf.keyboardType = .decimalPad
|
||||||
|
tf.textAlignment = .center
|
||||||
|
tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||||
|
tf.leftViewMode = .always
|
||||||
|
tf.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||||
|
tf.rightViewMode = .always
|
||||||
|
return tf
|
||||||
|
}()
|
||||||
|
|
||||||
|
let 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 tf = UITextField()
|
||||||
|
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tf.text = "8765"
|
||||||
|
tf.textColor = .lightGray
|
||||||
|
tf.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||||
|
tf.backgroundColor = UIColor(white: 0.12, alpha: 1)
|
||||||
|
tf.layer.borderWidth = 1
|
||||||
|
tf.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
tf.layer.cornerRadius = 4
|
||||||
|
tf.keyboardType = .numberPad
|
||||||
|
tf.textAlignment = .center
|
||||||
|
tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||||
|
tf.leftViewMode = .always
|
||||||
|
tf.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||||
|
tf.rightViewMode = .always
|
||||||
|
return tf
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Logging
|
||||||
|
lazy var loggingView: LoggingView = LoggingView()
|
||||||
|
|
||||||
|
lazy var logWidget: LogWidget = {
|
||||||
|
let w = LogWidget(uid: "log-widget", frame: LogWidget.defaultFrame())
|
||||||
|
return w
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var showLoggingButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitle("Show Logging", for: .normal)
|
||||||
|
btn.setTitleColor(.lightGray, for: .normal)
|
||||||
|
btn.backgroundColor = UIColor(white: 0.15, alpha: 1)
|
||||||
|
btn.layer.borderWidth = 1
|
||||||
|
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
btn.layer.cornerRadius = 4
|
||||||
|
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||||
|
btn.addTarget(self, action: #selector(self.buttonTappedShowLogging), for: .touchUpInside)
|
||||||
|
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
|
||||||
|
let statusLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.text = "not started"
|
||||||
|
lbl.textColor = UIColor(white: 0.33, alpha: 1)
|
||||||
|
lbl.font = .systemFont(ofSize: 11)
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
let presetLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.text = ""
|
||||||
|
lbl.textColor = UIColor(white: 0.33, alpha: 1)
|
||||||
|
lbl.font = .systemFont(ofSize: 11)
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ArrangeObjects {
|
||||||
|
|
||||||
|
func setupUI() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
self.view.backgroundColor = UIColor(white: 0.1, alpha: 1)
|
||||||
|
|
||||||
|
// containerView
|
||||||
|
self.view.addSubview(self.containerView)
|
||||||
|
self.containerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
|
||||||
|
self.containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
|
||||||
|
self.containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
|
||||||
|
self.containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
|
||||||
|
|
||||||
|
// statusLabel
|
||||||
|
self.view.addSubview(self.statusLabel)
|
||||||
|
self.statusLabel.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.statusLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
||||||
|
|
||||||
|
// presetLabel
|
||||||
|
self.view.addSubview(self.presetLabel)
|
||||||
|
self.presetLabel.topAnchor.constraint(equalTo: self.statusLabel.bottomAnchor, constant: 2).isActive = true
|
||||||
|
self.presetLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
||||||
|
|
||||||
|
// toolbar buttons (top-right)
|
||||||
|
self.view.addSubview(self.showLoggingButton)
|
||||||
|
self.showLoggingButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.showLoggingButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.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.connectButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.connectButton.trailingAnchor.constraint(equalTo: self.showLoggingButton.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.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.portField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.portField.trailingAnchor.constraint(equalTo: self.tabletIdField.leadingAnchor, constant: -6).isActive = true
|
||||||
|
self.portField.widthAnchor.constraint(equalToConstant: 60).isActive = true
|
||||||
|
self.portField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.ipField)
|
||||||
|
self.ipField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.ipField.trailingAnchor.constraint(equalTo: self.portField.leadingAnchor, constant: -6).isActive = true
|
||||||
|
self.ipField.widthAnchor.constraint(equalToConstant: 120).isActive = true
|
||||||
|
self.ipField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.lockButton)
|
||||||
|
self.lockButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.lockButton.trailingAnchor.constraint(equalTo: self.ipField.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
|
self.view.addSubview(self.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.arrangeButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||||
|
self.arrangeButton.trailingAnchor.constraint(equalTo: self.autoSwitchButton.leadingAnchor, constant: -6).isActive = true
|
||||||
|
|
||||||
|
self.containerView.addSubview(self.logWidget)
|
||||||
|
self.addDragGesture(to: self.logWidget)
|
||||||
|
self.logWidget.attach(to: self, loggingView: self.loggingView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// ViewController.swift
|
||||||
|
// remote-client-ios
|
||||||
|
//
|
||||||
|
// Created by p4piwabl0 on 6/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ArrangeView: ArrangeObjects {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum LogCategory: Int {
|
||||||
|
case incoming = 0
|
||||||
|
case outgoing = 1
|
||||||
|
case local = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
let tv = UITextView()
|
||||||
|
tv.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tv.backgroundColor = .clear
|
||||||
|
tv.textColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||||
|
tv.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
|
||||||
|
tv.isEditable = false
|
||||||
|
tv.isScrollEnabled = true
|
||||||
|
return tv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var clearButton: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitle("Clear", for: .normal)
|
||||||
|
btn.setTitleColor(UIColor(white: 0.4, alpha: 1), for: .normal)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 10)
|
||||||
|
btn.addTarget(self, action: #selector(self.buttonTappedClearButton), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension LoggingObjects {
|
||||||
|
|
||||||
|
func setupUI() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.view.backgroundColor = UIColor(white: 0.08, alpha: 1)
|
||||||
|
|
||||||
|
self.view.addSubview(self.clearButton)
|
||||||
|
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.view.addSubview(self.textView)
|
||||||
|
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.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -4).isActive = true
|
||||||
|
self.textView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -4).isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class LoggingView: LoggingObjects {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FaderWidget: BaseWidget {
|
||||||
|
|
||||||
|
static let size = CGSize(width: 140, height: 360)
|
||||||
|
|
||||||
|
static func defaultFrame(idx: Int) -> CGRect {
|
||||||
|
let col = idx % 3
|
||||||
|
let row = idx / 3
|
||||||
|
return CGRect(x: 16 + CGFloat(col) * 170, y: 48 + CGFloat(row) * 380, width: size.width, height: size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
let color: UIColor
|
||||||
|
private var sliderWidthConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
lazy var nameLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.textColor = UIColor(white: 0.53, alpha: 1)
|
||||||
|
lbl.font = .systemFont(ofSize: 14)
|
||||||
|
lbl.textAlignment = .center
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var slider: UISlider = {
|
||||||
|
let s = UISlider()
|
||||||
|
s.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
s.minimumValue = 0
|
||||||
|
s.maximumValue = 127
|
||||||
|
s.transform = CGAffineTransform(rotationAngle: -.pi / 2)
|
||||||
|
return s
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
init(uid: String, label: String, value: Int, color: UIColor, frame: CGRect) {
|
||||||
|
self.color = color
|
||||||
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.displayName = label
|
||||||
|
self.restingBorderColor = color.cgColor
|
||||||
|
self.layer.borderColor = color.cgColor
|
||||||
|
self.nameLabel.text = label
|
||||||
|
self.slider.value = Float(value)
|
||||||
|
self.slider.minimumTrackTintColor = color
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
func setupUI() {
|
||||||
|
self.addSubview(self.nameLabel)
|
||||||
|
self.nameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
|
||||||
|
self.nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true
|
||||||
|
self.nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true
|
||||||
|
|
||||||
|
self.addSubview(self.slider)
|
||||||
|
self.slider.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
||||||
|
self.slider.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 10).isActive = true
|
||||||
|
let wc = self.slider.widthAnchor.constraint(equalToConstant: 216)
|
||||||
|
wc.isActive = true
|
||||||
|
self.sliderWidthConstraint = wc
|
||||||
|
|
||||||
|
self.slider.addTarget(self, action: #selector(self.sliderChanged), for: .valueChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
self.sliderWidthConstraint?.constant = bounds.height * 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Update
|
||||||
|
override func applyValue(_ value: Int) {
|
||||||
|
self.slider.value = Float(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Arrange Mode
|
||||||
|
override func setArrangeMode(_ enabled: Bool) {
|
||||||
|
self.slider.isUserInteractionEnabled = (enabled == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Action
|
||||||
|
@objc private func sliderChanged() {
|
||||||
|
self.onSend?(self.uid, Int(self.slider.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FeedbackWidget: BaseWidget {
|
||||||
|
|
||||||
|
static let defaultSize = CGSize(width: 220, height: 100)
|
||||||
|
|
||||||
|
static func defaultFrame(idx: Int) -> CGRect {
|
||||||
|
let col = idx % 3
|
||||||
|
let row = idx / 3
|
||||||
|
return CGRect(x: 16 + CGFloat(col) * 250, y: 48 + CGFloat(row) * 120,
|
||||||
|
width: defaultSize.width, height: defaultSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
lazy var trackLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.textColor = UIColor(white: 0.9, alpha: 1)
|
||||||
|
lbl.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
|
lbl.textAlignment = .left
|
||||||
|
lbl.text = "—"
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var barLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.textColor = UIColor(white: 0.6, alpha: 1)
|
||||||
|
lbl.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||||
|
lbl.textAlignment = .left
|
||||||
|
lbl.text = "Bar —"
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var stateLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.textColor = UIColor(white: 0.5, alpha: 1)
|
||||||
|
lbl.font = .systemFont(ofSize: 20, weight: .medium)
|
||||||
|
lbl.textAlignment = .right
|
||||||
|
lbl.text = "■"
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
override init(uid: String, frame: CGRect) {
|
||||||
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
func setupUI() {
|
||||||
|
self.addSubview(self.trackLabel)
|
||||||
|
self.trackLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true
|
||||||
|
self.trackLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12).isActive = true
|
||||||
|
self.trackLabel.trailingAnchor.constraint(equalTo: self.stateLabel.leadingAnchor, constant: -8).isActive = true
|
||||||
|
|
||||||
|
self.addSubview(self.stateLabel)
|
||||||
|
self.stateLabel.centerYAnchor.constraint(equalTo: self.trackLabel.centerYAnchor).isActive = true
|
||||||
|
self.stateLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true
|
||||||
|
|
||||||
|
self.addSubview(self.barLabel)
|
||||||
|
self.barLabel.topAnchor.constraint(equalTo: self.trackLabel.bottomAnchor, constant: 8).isActive = true
|
||||||
|
self.barLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12).isActive = true
|
||||||
|
self.barLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Update
|
||||||
|
func updateDaw(track: String?, bar: Int?, playing: Bool?) {
|
||||||
|
if let track = track { self.trackLabel.text = track }
|
||||||
|
if let bar = bar { self.barLabel.text = "Bar \(bar)" }
|
||||||
|
if let playing = playing {
|
||||||
|
self.stateLabel.text = playing ? "▶" : "■"
|
||||||
|
self.stateLabel.textColor = playing ? UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) : UIColor(white: 0.5, alpha: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class LogWidget: BaseWidget {
|
||||||
|
|
||||||
|
static let defaultSize = CGSize(width: 360, height: 660)
|
||||||
|
|
||||||
|
static func defaultFrame() -> CGRect {
|
||||||
|
CGRect(x: 16, y: 80, width: defaultSize.width, height: defaultSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private weak var hostedView: UIView?
|
||||||
|
|
||||||
|
override init(uid: String, frame: CGRect) {
|
||||||
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.backgroundColor = UIColor(white: 0.08, alpha: 0.95)
|
||||||
|
self.layer.borderColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 0.3).cgColor
|
||||||
|
self.isHidden = true
|
||||||
|
setupPinch()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
func attach(to parentVC: UIViewController, loggingView: LoggingView) {
|
||||||
|
parentVC.addChild(loggingView)
|
||||||
|
loggingView.view.frame = self.bounds
|
||||||
|
self.addSubview(loggingView.view)
|
||||||
|
loggingView.didMove(toParent: parentVC)
|
||||||
|
self.hostedView = loggingView.view
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
hostedView?.frame = self.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupPinch() {
|
||||||
|
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
|
||||||
|
self.addGestureRecognizer(pinch)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
|
||||||
|
let scale = gesture.scale
|
||||||
|
let newWidth = max(200, min(800, self.bounds.width * scale))
|
||||||
|
let newHeight = max(100, min(600, self.bounds.height * scale))
|
||||||
|
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: newWidth, height: newHeight))
|
||||||
|
gesture.scale = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TitleWidget: BaseWidget {
|
||||||
|
|
||||||
|
static func defaultFrame(idx: Int) -> CGRect {
|
||||||
|
let col = idx % 3
|
||||||
|
let row = idx / 3
|
||||||
|
return CGRect(x: 16 + CGFloat(col) * 220, y: 16 + CGFloat(row) * 60, width: 200, height: 44)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
lazy var titleLabel: UILabel = {
|
||||||
|
let lbl = UILabel()
|
||||||
|
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
lbl.textColor = UIColor(white: 0.85, alpha: 1)
|
||||||
|
lbl.font = .systemFont(ofSize: 22, weight: .semibold)
|
||||||
|
lbl.textAlignment = .left
|
||||||
|
return lbl
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
init(uid: String, text: String, frame: CGRect) {
|
||||||
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
self.layer.borderColor = UIColor.clear.cgColor
|
||||||
|
self.titleLabel.text = text
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
func setupUI() {
|
||||||
|
self.addSubview(self.titleLabel)
|
||||||
|
self.titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true
|
||||||
|
self.titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true
|
||||||
|
self.titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ToggleWidget: BaseWidget {
|
||||||
|
|
||||||
|
static let size = CGSize(width: 75, height: 75)
|
||||||
|
|
||||||
|
static func defaultFrame(idx: Int) -> CGRect {
|
||||||
|
let gap: CGFloat = 12
|
||||||
|
let col = idx % 6
|
||||||
|
let row = idx / 6
|
||||||
|
return CGRect(x: 16 + CGFloat(col) * (size.width + gap),
|
||||||
|
y: 420 + CGFloat(row) * (size.height + gap),
|
||||||
|
width: size.width, height: size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
let color: UIColor
|
||||||
|
var isOn: Bool
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
lazy var button: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.layer.cornerRadius = 4
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
init(uid: String, label: String, isOn: Bool, color: UIColor, frame: CGRect) {
|
||||||
|
self.color = color
|
||||||
|
self.isOn = isOn
|
||||||
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.displayName = label
|
||||||
|
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
self.button.setTitle(label, for: .normal)
|
||||||
|
setupUI()
|
||||||
|
applyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
func setupUI() {
|
||||||
|
self.addSubview(self.button)
|
||||||
|
self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
||||||
|
self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
||||||
|
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
|
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
|
|
||||||
|
self.button.addTarget(self, action: #selector(self.buttonTappedToggleButton), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - State
|
||||||
|
func applyState() {
|
||||||
|
self.button.backgroundColor = self.isOn ? self.color : UIColor(white: 0.15, alpha: 1)
|
||||||
|
self.button.setTitleColor(self.isOn ? .black : .lightGray, for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applyValue(_ value: Int) {
|
||||||
|
self.isOn = value > 63
|
||||||
|
applyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Arrange Mode
|
||||||
|
override func setArrangeMode(_ enabled: Bool) {
|
||||||
|
self.button.isUserInteractionEnabled = (enabled == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Action
|
||||||
|
@objc private func buttonTappedToggleButton() {
|
||||||
|
self.isOn = !self.isOn
|
||||||
|
applyState()
|
||||||
|
self.onSend?(self.uid, self.isOn ? 127 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TransportWidget: BaseWidget {
|
||||||
|
|
||||||
|
static let size = CGSize(width: 75, height: 75)
|
||||||
|
|
||||||
|
static func defaultFrame(idx: Int) -> CGRect {
|
||||||
|
let gap: CGFloat = 12
|
||||||
|
let col = idx % 6
|
||||||
|
let row = idx / 6
|
||||||
|
return CGRect(x: 16 + CGFloat(col) * (size.width + gap),
|
||||||
|
y: 420 + CGFloat(row) * (size.height + gap),
|
||||||
|
width: size.width, height: size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subviews
|
||||||
|
lazy var button: UIButton = {
|
||||||
|
let btn = UIButton(type: .system)
|
||||||
|
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
btn.setTitleColor(.lightGray, for: .normal)
|
||||||
|
btn.backgroundColor = UIColor(white: 0.15, alpha: 1)
|
||||||
|
btn.layer.cornerRadius = 4
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
init(uid: String, label: String, frame: CGRect) {
|
||||||
|
super.init(uid: uid, frame: frame)
|
||||||
|
self.displayName = label
|
||||||
|
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||||
|
self.button.setTitle(label, for: .normal)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
func setupUI() {
|
||||||
|
self.addSubview(self.button)
|
||||||
|
self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
||||||
|
self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
||||||
|
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
|
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
||||||
|
|
||||||
|
self.button.addTarget(self, action: #selector(self.buttonTappedTransportButton), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Arrange Mode
|
||||||
|
override func setArrangeMode(_ enabled: Bool) {
|
||||||
|
self.button.isUserInteractionEnabled = (enabled == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Action
|
||||||
|
@objc private func buttonTappedTransportButton() {
|
||||||
|
self.onSend?(self.uid, 127)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>Default Configuration</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// SceneDelegate.swift
|
||||||
|
// remote-client-ios
|
||||||
|
//
|
||||||
|
// Created by p4piwabl0 on 6/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
|
||||||
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
|
guard let windowScene = (scene as? UIWindowScene) else { return }
|
||||||
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
window?.rootViewController = ArrangeView()
|
||||||
|
window?.makeKeyAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
|
// Called as the scene is being released by the system.
|
||||||
|
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||||
|
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||||
|
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene has moved from an inactive state to an active state.
|
||||||
|
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene will move from an active state to an inactive state.
|
||||||
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the background to the foreground.
|
||||||
|
// Use this method to undo the changes made on entering the background.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the foreground to the background.
|
||||||
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
|
// to restore the scene back to its current state.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// remote_client_iosTests.swift
|
||||||
|
// remote-client-iosTests
|
||||||
|
//
|
||||||
|
// Created by p4piwabl0 on 6/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
@testable import remote_client_ios
|
||||||
|
|
||||||
|
struct remote_client_iosTests {
|
||||||
|
|
||||||
|
@Test func example() async throws {
|
||||||
|
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// remote_client_iosUITests.swift
|
||||||
|
// remote-client-iosUITests
|
||||||
|
//
|
||||||
|
// Created by p4piwabl0 on 6/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class remote_client_iosUITests: XCTestCase {
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
|
||||||
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
|
continueAfterFailure = false
|
||||||
|
|
||||||
|
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testExample() throws {
|
||||||
|
// UI tests must launch the application that they test.
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testLaunchPerformance() throws {
|
||||||
|
// This measures how long it takes to launch your application.
|
||||||
|
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
|
XCUIApplication().launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// remote_client_iosUITestsLaunchTests.swift
|
||||||
|
// remote-client-iosUITests
|
||||||
|
//
|
||||||
|
// Created by p4piwabl0 on 6/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class remote_client_iosUITestsLaunchTests: XCTestCase {
|
||||||
|
|
||||||
|
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testLaunch() throws {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||||
|
// such as logging into a test account or navigating somewhere in the app
|
||||||
|
|
||||||
|
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||||
|
attachment.name = "Launch Screen"
|
||||||
|
attachment.lifetime = .keepAlways
|
||||||
|
add(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
_root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, os.path.join(_root, "app"))
|
||||||
|
sys.path.insert(0, os.path.join(_root, "server"))
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(os.path.join(_root, "app", "main.py"), run_name="__main__")
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import json
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
from PyQt6.QtWebSockets import QWebSocketServer
|
||||||
|
from PyQt6.QtNetwork import QHostAddress
|
||||||
|
|
||||||
|
|
||||||
|
class WSServer(QObject):
|
||||||
|
control_received = pyqtSignal(str, int)
|
||||||
|
layout_saved = pyqtSignal(str, object)
|
||||||
|
widget_visibility_received = pyqtSignal(str, int)
|
||||||
|
client_connected = pyqtSignal()
|
||||||
|
log_signal = pyqtSignal(str)
|
||||||
|
raw_in_signal = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, port=8765, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._clients = []
|
||||||
|
self._port = port
|
||||||
|
self._server = QWebSocketServer("VC", QWebSocketServer.SslMode.NonSecureMode, self)
|
||||||
|
if self._server.listen(QHostAddress.SpecialAddress.AnyIPv4, port):
|
||||||
|
self.log_signal.emit(f"WS listening on :{port}")
|
||||||
|
else:
|
||||||
|
self.log_signal.emit(f"WS failed to bind :{port}")
|
||||||
|
self._server.newConnection.connect(self._on_new_connection)
|
||||||
|
|
||||||
|
def _on_new_connection(self):
|
||||||
|
client = self._server.nextPendingConnection()
|
||||||
|
peer = client.peerAddress().toString()
|
||||||
|
self._clients.append(client)
|
||||||
|
client.textMessageReceived.connect(self._on_message)
|
||||||
|
client.disconnected.connect(lambda c=client, p=peer: self._on_disconnect(c, p))
|
||||||
|
self.log_signal.emit(f"client connected {peer} ({len(self._clients)} total)")
|
||||||
|
self.client_connected.emit()
|
||||||
|
|
||||||
|
def _on_disconnect(self, client, peer):
|
||||||
|
if client in self._clients:
|
||||||
|
self._clients.remove(client)
|
||||||
|
client.deleteLater()
|
||||||
|
self.log_signal.emit(f"client disconnected {peer} ({len(self._clients)} remaining)")
|
||||||
|
|
||||||
|
def _on_message(self, message):
|
||||||
|
self.raw_in_signal.emit(message)
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
ev = data.get("event")
|
||||||
|
if ev == "control":
|
||||||
|
uid = data["uid"]
|
||||||
|
val = int(data["value"])
|
||||||
|
self.log_signal.emit(f"control uid={uid[:8]}… val={val}")
|
||||||
|
self.control_received.emit(uid, val)
|
||||||
|
elif ev == "save_layout":
|
||||||
|
n = len(data.get("layout", {}))
|
||||||
|
self.log_signal.emit(f"layout saved preset={data['preset_uuid'][:8]}… {n} widgets")
|
||||||
|
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:
|
||||||
|
self.log_signal.emit(f"unknown event: {ev}")
|
||||||
|
except Exception as 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):
|
||||||
|
if not self._clients:
|
||||||
|
return
|
||||||
|
msg = json.dumps(data)
|
||||||
|
for client in list(self._clients):
|
||||||
|
client.sendTextMessage(msg)
|
||||||
|
|
||||||
|
def broadcast_preset(self, snapshot: dict, layout: dict):
|
||||||
|
name = snapshot.get("preset_name", "")
|
||||||
|
self.log_signal.emit(f"broadcast preset '{name}' → {len(self._clients)} client(s)")
|
||||||
|
# layout omitted — owned and persisted client-side, keyed by preset_uuid
|
||||||
|
self.broadcast({"event": "preset", "data": snapshot})
|
||||||
|
|
||||||
|
def broadcast_widget_update(self, uid: str, value: int):
|
||||||
|
self.broadcast({"event": "widget_update", "uid": uid, "value": value})
|
||||||
|
|
||||||
|
def broadcast_daw_state(self, **kwargs):
|
||||||
|
self.broadcast({"event": "daw_state", **kwargs})
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._server.close()
|
||||||
|
for client in list(self._clients):
|
||||||
|
client.close()
|
||||||
|
self._clients.clear()
|
||||||
|
self.log_signal.emit("WS server stopped")
|
||||||
Reference in New Issue
Block a user