Wire DragonRelayView to the dragonRelay backend (real model + status binding)

This commit is contained in:
Zac Gaetano 2026-05-07 00:17:47 -04:00
parent 41b431a11c
commit 967539de5d

View file

@ -1,22 +1,17 @@
// DragonRelayView.qml // DragonRelayView.qml
// //
// Add a DragonRelay server and connect to its VPN. // DragonRelay server connection + host browser.
// Wire this view up to a DragonRelayBackend C++ object exposed to QML via
// qmlRegisterType or setContextProperty:
// //
// // main.cpp // Bound to the C++ DragonRelayBackend exposed via:
// engine.rootContext()->setContextProperty("dragonRelay", &g_relayBackend);
// //
// DragonRelayBackend (to be written as a thin QObject wrapper around // engine.rootContext()->setContextProperty("dragonRelay", &relayBackend);
// RelayClient + TunnelManager) should expose: //
// Q_PROPERTY(int status ...) // 0=Idle, 1=Connecting, 2=Connected, 3=Error // Status enum (must match DragonRelayBackend::Status):
// Q_PROPERTY(QString statusText ...) // human-readable status // 0 = Disconnected
// Q_PROPERTY(var hosts ...) // list model of {name, ip, port} // 1 = Connecting
// Q_INVOKABLE void connectRelay(url, username, password) // 2 = TunnelUp
// Q_INVOKABLE void disconnectRelay() // 3 = Ready
// Q_INVOKABLE void streamHost(ip, app) // 4 = Error
// signal: hostsChanged()
// signal: errorOccurred(message)
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
@ -26,24 +21,34 @@ Page {
id: root id: root
title: qsTr("DragonRelay") title: qsTr("DragonRelay")
// State 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 stateIdle: 0 readonly property int currentState: dragonRelay ? dragonRelay.status : statusDisconnected
readonly property int stateConnecting: 1 readonly property string statusText: dragonRelay ? dragonRelay.statusText
readonly property int stateConnected: 2 : qsTr("Not connected")
readonly property int stateError: 3 readonly property bool isLoginVisible:
currentState === statusDisconnected || currentState === statusError
readonly property bool isHostListVisible:
currentState === statusTunnelUp || currentState === statusReady
// 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 bool showDisplayPicker: false
property var pickerDisplays: [] property var pickerDisplays: []
property string pickerHostIP: "" property string pickerHostIP: ""
property string pickerHostName: "" property string pickerHostName: ""
// Header bar property bool connectInFlight: false
Connections {
target: dragonRelay
function onStatusChanged() {
if (root.currentState !== root.statusConnecting)
root.connectInFlight = false
}
}
header: ToolBar { header: ToolBar {
RowLayout { RowLayout {
@ -67,17 +72,26 @@ Page {
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
// Connection status pill Label {
visible: root.isHostListVisible && dragonRelay
&& dragonRelay.tunnelIP.length > 0
text: dragonRelay ? dragonRelay.tunnelIP : ""
color: "#9ca3af"
font.pixelSize: 11
rightPadding: 8
}
Rectangle { Rectangle {
width: statusLabel.implicitWidth + 20 width: statusLabel.implicitWidth + 20
height: 24 height: 24
radius: 12 radius: 12
color: { color: {
switch (root.currentState) { switch (root.currentState) {
case root.stateConnecting: return "#f59e0b" // amber case root.statusConnecting: return "#f59e0b"
case root.stateConnected: return "#10b981" // green case root.statusTunnelUp: return "#10b981"
case root.stateError: return "#ef4444" // red case root.statusReady: return "#10b981"
default: return "#6b7280" // grey case root.statusError: return "#ef4444"
default: return "#6b7280"
} }
} }
@ -92,27 +106,23 @@ Page {
} }
} }
// Body
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 24 anchors.margins: 24
spacing: 20 spacing: 20
// Login form (shown when idle or errored) // Login form (shown when idle or errored)
GroupBox { GroupBox {
id: loginBox id: loginBox
title: qsTr("Connect to a DragonRelay server") title: qsTr("Connect to a DragonRelay server")
Layout.fillWidth: true Layout.fillWidth: true
visible: root.currentState === root.stateIdle visible: root.isLoginVisible
|| root.currentState === root.stateError
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 12 spacing: 12
// Logo
Image { Image {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
source: "qrc:/app/assets/wilddragon-logo.jpg" source: "qrc:/app/assets/wilddragon-logo.jpg"
@ -128,12 +138,21 @@ Page {
placeholderText: qsTr("Relay URL e.g. https://relay.wilddragon.net") placeholderText: qsTr("Relay URL e.g. https://relay.wilddragon.net")
text: "https://" text: "https://"
inputMethodHints: Qt.ImhUrlCharactersOnly inputMethodHints: Qt.ImhUrlCharactersOnly
Component.onCompleted: {
if (dragonRelay && dragonRelay.lastURL && dragonRelay.lastURL.length > 0)
text = dragonRelay.lastURL
}
} }
TextField { TextField {
id: userField id: userField
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: qsTr("Username") placeholderText: qsTr("Username")
Component.onCompleted: {
if (dragonRelay && dragonRelay.lastUsername
&& dragonRelay.lastUsername.length > 0)
text = dragonRelay.lastUsername
}
} }
TextField { TextField {
@ -152,26 +171,28 @@ Page {
Button { Button {
id: connectBtn id: connectBtn
Layout.fillWidth: true Layout.fillWidth: true
text: root.currentState === root.stateConnecting text: root.currentState === root.statusConnecting
? qsTr("Connecting…") || root.connectInFlight
? qsTr("Connecting...")
: qsTr("Connect") : qsTr("Connect")
enabled: urlField.text.length > 7 enabled: urlField.text.length > 7
&& userField.text.length > 0 && userField.text.length > 0
&& passField.text.length > 0 && passField.text.length > 0
&& root.currentState !== root.stateConnecting && root.currentState !== root.statusConnecting
&& !root.connectInFlight
onClicked: { onClicked: {
root.currentState = root.stateConnecting root.connectInFlight = true
root.statusText = qsTr("Connecting…") dragonRelay.connectRelay(urlField.text,
// Real call: dragonRelay.connectRelay(urlField.text, userField.text, passField.text) userField.text,
passField.text)
} }
} }
// Error message
Label { Label {
id: errorLabel id: errorLabel
Layout.fillWidth: true Layout.fillWidth: true
visible: root.currentState === root.stateError visible: root.currentState === root.statusError
color: "#ef4444" color: "#ef4444"
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: root.statusText text: root.statusText
@ -179,13 +200,13 @@ Page {
} }
} }
// Host list (shown when connected) // Host list (shown when connected)
GroupBox { GroupBox {
title: qsTr("Streaming hosts") title: qsTr("Streaming hosts")
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
visible: root.currentState === root.stateConnected visible: root.isHostListVisible
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@ -198,12 +219,7 @@ Page {
clip: true clip: true
spacing: 8 spacing: 8
// In real integration: model: dragonRelay.hosts model: dragonRelay ? 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 { delegate: Rectangle {
width: hostList.width width: hostList.width
@ -218,13 +234,13 @@ Page {
Column { Column {
Label { Label {
text: model.hostName text: modelData.name
font.pixelSize: 14 font.pixelSize: 14
font.bold: true font.bold: true
color: "white" color: "white"
} }
Label { Label {
text: model.hostIp text: modelData.ip + ":" + modelData.port
font.pixelSize: 11 font.pixelSize: 11
color: "#9ca3af" color: "#9ca3af"
} }
@ -235,14 +251,15 @@ Page {
Button { Button {
text: qsTr("Stream") text: qsTr("Stream")
onClicked: { onClicked: {
var displays = dragonRelay.displaysForHost(model.hostIp) if (!dragonRelay) return
var displays = dragonRelay.displaysForHost(modelData.ip)
if (displays && displays.length > 1) { if (displays && displays.length > 1) {
root.pickerHostIP = model.hostIp root.pickerHostIP = modelData.ip
root.pickerHostName = model.hostName root.pickerHostName = modelData.name
root.pickerDisplays = displays root.pickerDisplays = displays
root.showDisplayPicker = true root.showDisplayPicker = true
} else { } else {
dragonRelay.streamHost(model.hostIp, "Desktop") dragonRelay.streamHost(modelData.ip, "Desktop", 0)
} }
} }
} }
@ -253,30 +270,47 @@ Page {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onDoubleClicked: { onDoubleClicked: {
// Real call: dragonRelay.streamHost(model.hostIp, "Desktop") 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
}
} }
Button { RowLayout {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
text: qsTr("Disconnect") spacing: 8
flat: true
onClicked: { Button {
root.currentState = root.stateIdle text: qsTr("Refresh")
root.statusText = qsTr("Not connected") flat: true
// Real call: dragonRelay.disconnectRelay() onClicked: {
if (dragonRelay) dragonRelay.refreshHosts()
}
}
Button {
text: qsTr("Disconnect")
flat: true
onClicked: {
if (dragonRelay) dragonRelay.disconnectRelay()
}
} }
} }
} }
} }
Item { Layout.fillHeight: true; visible: root.currentState !== root.stateConnected } Item { Layout.fillHeight: true; visible: !root.isHostListVisible }
} }
// Display Picker Modal
DragonDisplayPicker { DragonDisplayPicker {
visible: root.showDisplayPicker visible: root.showDisplayPicker
anchors.fill: parent anchors.fill: parent
@ -285,7 +319,8 @@ Page {
displays: root.pickerDisplays displays: root.pickerDisplays
onDisplaySelected: function(idx) { onDisplaySelected: function(idx) {
root.showDisplayPicker = false root.showDisplayPicker = false
dragonRelay.streamHostDisplay(root.pickerHostIP, idx) if (dragonRelay)
dragonRelay.streamHostDisplay(root.pickerHostIP, idx)
} }
onCancelled: { root.showDisplayPicker = false } onCancelled: { root.showDisplayPicker = false }
} }