// DragonRelayView.qml // // DragonRelay server connection + host browser. // // Bound to the C++ DragonRelayBackend exposed via: // // engine.rootContext()->setContextProperty("dragonRelay", &relayBackend); // // Status enum (must match DragonRelayBackend::Status): // 0 = Disconnected // 1 = Connecting // 2 = TunnelUp // 3 = Ready // 4 = Error import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 Page { id: root title: qsTr("DragonRelay") readonly property int statusDisconnected: 0 readonly property int statusConnecting: 1 readonly property int statusTunnelUp: 2 readonly property int statusReady: 3 readonly property int statusError: 4 readonly property int currentState: dragonRelay ? dragonRelay.status : statusDisconnected readonly property string statusText: dragonRelay ? dragonRelay.statusText : qsTr("Not connected") readonly property bool isLoginVisible: currentState === statusDisconnected || currentState === statusError readonly property bool isHostListVisible: currentState === statusTunnelUp || currentState === statusReady property bool showDisplayPicker: false property var pickerDisplays: [] property string pickerHostIP: "" property string pickerHostName: "" property bool connectInFlight: false Connections { target: dragonRelay function onStatusChanged() { if (root.currentState !== root.statusConnecting) root.connectInFlight = false } } 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 } Label { visible: root.isHostListVisible && dragonRelay && dragonRelay.tunnelIP.length > 0 text: dragonRelay ? dragonRelay.tunnelIP : "" color: "#9ca3af" font.pixelSize: 11 rightPadding: 8 } Rectangle { width: statusLabel.implicitWidth + 20 height: 24 radius: 12 color: { switch (root.currentState) { case root.statusConnecting: return "#f59e0b" case root.statusTunnelUp: return "#10b981" case root.statusReady: return "#10b981" case root.statusError: return "#ef4444" default: return "#6b7280" } } Label { id: statusLabel anchors.centerIn: parent text: root.statusText color: "white" font.pixelSize: 12 } } } } 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.isLoginVisible ColumnLayout { anchors.fill: parent spacing: 12 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 Component.onCompleted: { if (dragonRelay && dragonRelay.lastURL && dragonRelay.lastURL.length > 0) text = dragonRelay.lastURL } } TextField { id: userField Layout.fillWidth: true placeholderText: qsTr("Username") Component.onCompleted: { if (dragonRelay && dragonRelay.lastUsername && dragonRelay.lastUsername.length > 0) text = dragonRelay.lastUsername } } 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.statusConnecting || root.connectInFlight ? qsTr("Connecting...") : qsTr("Connect") enabled: urlField.text.length > 7 && userField.text.length > 0 && passField.text.length > 0 && root.currentState !== root.statusConnecting && !root.connectInFlight onClicked: { root.connectInFlight = true dragonRelay.connectRelay(urlField.text, userField.text, passField.text) } } Label { id: errorLabel Layout.fillWidth: true visible: root.currentState === root.statusError 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.isHostListVisible ColumnLayout { anchors.fill: parent spacing: 8 ListView { id: hostList Layout.fillWidth: true Layout.fillHeight: true clip: true spacing: 8 model: dragonRelay ? dragonRelay.hosts : [] 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: modelData.name font.pixelSize: 14 font.bold: true color: "white" } Label { text: modelData.ip + ":" + modelData.port font.pixelSize: 11 color: "#9ca3af" } } Item { Layout.fillWidth: true } Button { text: qsTr("Stream") onClicked: { if (!dragonRelay) return var displays = dragonRelay.displaysForHost(modelData.ip) if (displays && displays.length > 1) { root.pickerHostIP = modelData.ip root.pickerHostName = modelData.name root.pickerDisplays = displays root.showDisplayPicker = true } else { dragonRelay.streamHost(modelData.ip, "Desktop", 0) } } } } MouseArea { id: hostMouse anchors.fill: parent hoverEnabled: true onDoubleClicked: { if (dragonRelay) dragonRelay.streamHost(modelData.ip, "Desktop", 0) } } } Label { anchors.centerIn: parent text: qsTr("No hosts visible yet - waiting for Artemis to register...") color: "#9ca3af" font.pixelSize: 12 visible: hostList.count === 0 } } RowLayout { Layout.alignment: Qt.AlignRight spacing: 8 Button { text: qsTr("Refresh") flat: true onClicked: { if (dragonRelay) dragonRelay.refreshHosts() } } Button { text: qsTr("Disconnect") flat: true onClicked: { if (dragonRelay) dragonRelay.disconnectRelay() } } } } } Item { Layout.fillHeight: true; visible: !root.isHostListVisible } } DragonDisplayPicker { visible: root.showDisplayPicker anchors.fill: parent hostIP: root.pickerHostIP hostName: root.pickerHostName displays: root.pickerDisplays onDisplaySelected: function(idx) { root.showDisplayPicker = false if (dragonRelay) dragonRelay.streamHostDisplay(root.pickerHostIP, idx) } onCancelled: { root.showDisplayPicker = false } } }