From 33edc6affe78b99b528cb1496ee01e11101a4b0a Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Wed, 6 May 2026 19:02:48 -0400 Subject: [PATCH] =?UTF-8?q?gui:=20add=20DragonRelayView=20=E2=80=94=20add/?= =?UTF-8?q?connect=20to=20relay=20server=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gui/DragonRelayView.qml | 246 ++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 app/gui/DragonRelayView.qml diff --git a/app/gui/DragonRelayView.qml b/app/gui/DragonRelayView.qml new file mode 100644 index 0000000..f9c6992 --- /dev/null +++ b/app/gui/DragonRelayView.qml @@ -0,0 +1,246 @@ +// DragonRelayView.qml +// +// Add a DragonRelay server and connect to its VPN. +// Wire this view up to a DragonRelayBackend C++ object exposed to QML via +// qmlRegisterType or setContextProperty: +// +// // main.cpp +// engine.rootContext()->setContextProperty("dragonRelay", &g_relayBackend); +// +// DragonRelayBackend (to be written as a thin QObject wrapper around +// RelayClient + TunnelManager) should expose: +// Q_PROPERTY(int status ...) // 0=Idle, 1=Connecting, 2=Connected, 3=Error +// Q_PROPERTY(QString statusText ...) // human-readable status +// Q_PROPERTY(var hosts ...) // list model of {name, ip, port} +// Q_INVOKABLE void connectRelay(url, username, password) +// Q_INVOKABLE void disconnectRelay() +// Q_INVOKABLE void streamHost(ip, app) +// signal: hostsChanged() +// signal: errorOccurred(message) + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Page { + id: root + title: qsTr("DragonRelay") + + // ── State ────────────────────────────────────────────────────────────── + + readonly property int stateIdle: 0 + readonly property int stateConnecting: 1 + readonly property int stateConnected: 2 + readonly property int stateError: 3 + + // Bound to dragonRelay.status in real integration + property int currentState: stateIdle + property string statusText: qsTr("Not connected") + + // ── Header bar ───────────────────────────────────────────────────────── + + header: ToolBar { + RowLayout { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.rightMargin: 12 + + Label { + text: qsTr("🐉 DragonRelay") + font.pixelSize: 16 + font.bold: true + } + + Item { Layout.fillWidth: true } + + // Connection status pill + Rectangle { + width: statusLabel.implicitWidth + 20 + height: 24 + radius: 12 + color: { + switch (root.currentState) { + case root.stateConnecting: return "#f59e0b" // amber + case root.stateConnected: return "#10b981" // green + case root.stateError: return "#ef4444" // red + default: return "#6b7280" // grey + } + } + + Label { + id: statusLabel + anchors.centerIn: parent + text: root.statusText + color: "white" + font.pixelSize: 12 + } + } + } + } + + // ── Body ─────────────────────────────────────────────────────────────── + + ColumnLayout { + anchors.fill: parent + anchors.margins: 24 + spacing: 20 + + // ── Login form (shown when idle or errored) ──────────────────────── + + GroupBox { + id: loginBox + title: qsTr("Connect to a DragonRelay server") + Layout.fillWidth: true + visible: root.currentState === root.stateIdle + || root.currentState === root.stateError + + ColumnLayout { + anchors.fill: parent + spacing: 12 + + TextField { + id: urlField + Layout.fillWidth: true + placeholderText: qsTr("Relay URL e.g. http://10.0.0.5:8080") + text: "http://" + inputMethodHints: Qt.ImhUrlCharactersOnly + } + + TextField { + id: userField + Layout.fillWidth: true + placeholderText: qsTr("Username") + } + + TextField { + id: passField + Layout.fillWidth: true + placeholderText: qsTr("Password") + echoMode: TextInput.Password + } + + CheckBox { + id: saveCheck + text: qsTr("Remember this server") + checked: true + } + + Button { + id: connectBtn + Layout.fillWidth: true + text: root.currentState === root.stateConnecting + ? qsTr("Connecting…") + : qsTr("Connect") + enabled: urlField.text.length > 7 + && userField.text.length > 0 + && passField.text.length > 0 + && root.currentState !== root.stateConnecting + + onClicked: { + root.currentState = root.stateConnecting + root.statusText = qsTr("Connecting…") + // Real call: dragonRelay.connectRelay(urlField.text, userField.text, passField.text) + } + } + + // Error message + Label { + id: errorLabel + Layout.fillWidth: true + visible: root.currentState === root.stateError + color: "#ef4444" + wrapMode: Text.WordWrap + text: root.statusText + } + } + } + + // ── Host list (shown when connected) ────────────────────────────── + + GroupBox { + title: qsTr("Streaming hosts") + Layout.fillWidth: true + Layout.fillHeight: true + visible: root.currentState === root.stateConnected + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + ListView { + id: hostList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 8 + + // In real integration: model: dragonRelay.hosts + model: ListModel { + // Placeholder for design-time preview + ListElement { hostName: "Gaming PC"; hostIp: "10.99.0.10" } + ListElement { hostName: "Living Room"; hostIp: "10.99.0.11" } + } + + delegate: Rectangle { + width: hostList.width + height: 56 + radius: 8 + color: hostMouse.containsMouse ? "#374151" : "#1f2937" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Column { + Label { + text: model.hostName + font.pixelSize: 14 + font.bold: true + color: "white" + } + Label { + text: model.hostIp + font.pixelSize: 11 + color: "#9ca3af" + } + } + + Item { Layout.fillWidth: true } + + Button { + text: qsTr("Stream") + onClicked: { + // Real call: dragonRelay.streamHost(model.hostIp, "Desktop") + console.log("stream", model.hostIp) + } + } + } + + MouseArea { + id: hostMouse + anchors.fill: parent + hoverEnabled: true + onDoubleClicked: { + // Real call: dragonRelay.streamHost(model.hostIp, "Desktop") + } + } + } + } + + Button { + Layout.alignment: Qt.AlignRight + text: qsTr("Disconnect") + flat: true + onClicked: { + root.currentState = root.stateIdle + root.statusText = qsTr("Not connected") + // Real call: dragonRelay.disconnectRelay() + } + } + } + } + + Item { Layout.fillHeight: true; visible: root.currentState !== root.stateConnected } + } +}