// 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") // Display picker state property bool showDisplayPicker: false property var pickerDisplays: [] property string pickerHostIP: "" property string pickerHostName: "" // ── Header bar ───────────────────────────────────────────────────────── header: ToolBar { RowLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 Image { source: "qrc:/app/assets/wilddragon-icon.jpg" width: 24 height: 24 fillMode: Image.PreserveAspectFit smooth: true } 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 // Logo Image { Layout.alignment: Qt.AlignHCenter source: "qrc:/app/assets/wilddragon-logo.jpg" width: 80 height: 80 fillMode: Image.PreserveAspectFit smooth: true } TextField { id: urlField Layout.fillWidth: true placeholderText: qsTr("Relay URL e.g. https://relay.wilddragon.net") text: "https://" 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: { var displays = dragonRelay.displaysForHost(model.hostIp) if (displays && displays.length > 1) { root.pickerHostIP = model.hostIp root.pickerHostName = model.hostName root.pickerDisplays = displays root.showDisplayPicker = true } else { dragonRelay.streamHost(model.hostIp, "Desktop") } } } } 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 } } // ── Display Picker Modal ─────────────────────────────────────────────── DragonDisplayPicker { visible: root.showDisplayPicker anchors.fill: parent hostIP: root.pickerHostIP hostName: root.pickerHostName displays: root.pickerDisplays onDisplaySelected: function(idx) { root.showDisplayPicker = false dragonRelay.streamHostDisplay(root.pickerHostIP, idx) } onCancelled: { root.showDisplayPicker = false } } }