2026-05-06 19:02:48 -04:00
|
|
|
// DragonRelayView.qml
|
|
|
|
|
//
|
2026-05-07 00:17:47 -04:00
|
|
|
// DragonRelay server connection + host browser.
|
2026-05-06 19:02:48 -04:00
|
|
|
//
|
2026-05-07 00:17:47 -04:00
|
|
|
// Bound to the C++ DragonRelayBackend exposed via:
|
2026-05-06 19:02:48 -04:00
|
|
|
//
|
2026-05-07 00:17:47 -04:00
|
|
|
// engine.rootContext()->setContextProperty("dragonRelay", &relayBackend);
|
|
|
|
|
//
|
|
|
|
|
// Status enum (must match DragonRelayBackend::Status):
|
|
|
|
|
// 0 = Disconnected
|
|
|
|
|
// 1 = Connecting
|
|
|
|
|
// 2 = TunnelUp
|
|
|
|
|
// 3 = Ready
|
|
|
|
|
// 4 = Error
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
import QtQuick 2.15
|
|
|
|
|
import QtQuick.Controls 2.15
|
|
|
|
|
import QtQuick.Layouts 1.15
|
|
|
|
|
|
|
|
|
|
Page {
|
|
|
|
|
id: root
|
|
|
|
|
title: qsTr("DragonRelay")
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
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
|
2026-05-06 19:02:48 -04:00
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
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
|
2026-05-06 19:02:48 -04:00
|
|
|
|
2026-05-06 20:17:51 -04:00
|
|
|
property bool showDisplayPicker: false
|
|
|
|
|
property var pickerDisplays: []
|
|
|
|
|
property string pickerHostIP: ""
|
|
|
|
|
property string pickerHostName: ""
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
property bool connectInFlight: false
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
target: dragonRelay
|
|
|
|
|
function onStatusChanged() {
|
|
|
|
|
if (root.currentState !== root.statusConnecting)
|
|
|
|
|
root.connectInFlight = false
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
header: ToolBar {
|
|
|
|
|
RowLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.leftMargin: 12
|
|
|
|
|
anchors.rightMargin: 12
|
|
|
|
|
|
2026-05-06 22:24:42 -04:00
|
|
|
Image {
|
|
|
|
|
source: "qrc:/app/assets/wilddragon-icon.jpg"
|
|
|
|
|
width: 24
|
|
|
|
|
height: 24
|
|
|
|
|
fillMode: Image.PreserveAspectFit
|
|
|
|
|
smooth: true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 19:02:48 -04:00
|
|
|
Label {
|
2026-05-06 22:24:42 -04:00
|
|
|
text: qsTr("DragonRelay")
|
2026-05-06 19:02:48 -04:00
|
|
|
font.pixelSize: 16
|
|
|
|
|
font.bold: true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
Label {
|
|
|
|
|
visible: root.isHostListVisible && dragonRelay
|
|
|
|
|
&& dragonRelay.tunnelIP.length > 0
|
|
|
|
|
text: dragonRelay ? dragonRelay.tunnelIP : ""
|
|
|
|
|
color: "#9ca3af"
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
rightPadding: 8
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 19:02:48 -04:00
|
|
|
Rectangle {
|
|
|
|
|
width: statusLabel.implicitWidth + 20
|
|
|
|
|
height: 24
|
|
|
|
|
radius: 12
|
|
|
|
|
color: {
|
|
|
|
|
switch (root.currentState) {
|
2026-05-07 00:17:47 -04:00
|
|
|
case root.statusConnecting: return "#f59e0b"
|
|
|
|
|
case root.statusTunnelUp: return "#10b981"
|
|
|
|
|
case root.statusReady: return "#10b981"
|
|
|
|
|
case root.statusError: return "#ef4444"
|
|
|
|
|
default: return "#6b7280"
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
id: statusLabel
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
text: root.statusText
|
|
|
|
|
color: "white"
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.margins: 24
|
|
|
|
|
spacing: 20
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
// Login form (shown when idle or errored)
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
GroupBox {
|
|
|
|
|
id: loginBox
|
|
|
|
|
title: qsTr("Connect to a DragonRelay server")
|
|
|
|
|
Layout.fillWidth: true
|
2026-05-07 00:17:47 -04:00
|
|
|
visible: root.isLoginVisible
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
spacing: 12
|
|
|
|
|
|
2026-05-06 22:24:42 -04:00
|
|
|
Image {
|
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
|
source: "qrc:/app/assets/wilddragon-logo.jpg"
|
|
|
|
|
width: 80
|
|
|
|
|
height: 80
|
|
|
|
|
fillMode: Image.PreserveAspectFit
|
|
|
|
|
smooth: true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 19:02:48 -04:00
|
|
|
TextField {
|
|
|
|
|
id: urlField
|
|
|
|
|
Layout.fillWidth: true
|
2026-05-06 22:24:42 -04:00
|
|
|
placeholderText: qsTr("Relay URL e.g. https://relay.wilddragon.net")
|
|
|
|
|
text: "https://"
|
2026-05-06 19:02:48 -04:00
|
|
|
inputMethodHints: Qt.ImhUrlCharactersOnly
|
2026-05-07 00:17:47 -04:00
|
|
|
Component.onCompleted: {
|
|
|
|
|
if (dragonRelay && dragonRelay.lastURL && dragonRelay.lastURL.length > 0)
|
|
|
|
|
text = dragonRelay.lastURL
|
|
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TextField {
|
|
|
|
|
id: userField
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
placeholderText: qsTr("Username")
|
2026-05-07 00:17:47 -04:00
|
|
|
Component.onCompleted: {
|
|
|
|
|
if (dragonRelay && dragonRelay.lastUsername
|
|
|
|
|
&& dragonRelay.lastUsername.length > 0)
|
|
|
|
|
text = dragonRelay.lastUsername
|
|
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-05-07 00:17:47 -04:00
|
|
|
text: root.currentState === root.statusConnecting
|
|
|
|
|
|| root.connectInFlight
|
|
|
|
|
? qsTr("Connecting...")
|
2026-05-06 19:02:48 -04:00
|
|
|
: qsTr("Connect")
|
|
|
|
|
enabled: urlField.text.length > 7
|
|
|
|
|
&& userField.text.length > 0
|
|
|
|
|
&& passField.text.length > 0
|
2026-05-07 00:17:47 -04:00
|
|
|
&& root.currentState !== root.statusConnecting
|
|
|
|
|
&& !root.connectInFlight
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
onClicked: {
|
2026-05-07 00:17:47 -04:00
|
|
|
root.connectInFlight = true
|
|
|
|
|
dragonRelay.connectRelay(urlField.text,
|
|
|
|
|
userField.text,
|
|
|
|
|
passField.text)
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
id: errorLabel
|
|
|
|
|
Layout.fillWidth: true
|
2026-05-07 00:17:47 -04:00
|
|
|
visible: root.currentState === root.statusError
|
2026-05-06 19:02:48 -04:00
|
|
|
color: "#ef4444"
|
|
|
|
|
wrapMode: Text.WordWrap
|
|
|
|
|
text: root.statusText
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
// Host list (shown when connected)
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
GroupBox {
|
|
|
|
|
title: qsTr("Streaming hosts")
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Layout.fillHeight: true
|
2026-05-07 00:17:47 -04:00
|
|
|
visible: root.isHostListVisible
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
spacing: 8
|
|
|
|
|
|
|
|
|
|
ListView {
|
|
|
|
|
id: hostList
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
clip: true
|
|
|
|
|
spacing: 8
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
model: dragonRelay ? dragonRelay.hosts : []
|
2026-05-06 19:02:48 -04:00
|
|
|
|
|
|
|
|
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 {
|
2026-05-07 00:17:47 -04:00
|
|
|
text: modelData.name
|
2026-05-06 19:02:48 -04:00
|
|
|
font.pixelSize: 14
|
|
|
|
|
font.bold: true
|
|
|
|
|
color: "white"
|
|
|
|
|
}
|
|
|
|
|
Label {
|
2026-05-07 00:17:47 -04:00
|
|
|
text: modelData.ip + ":" + modelData.port
|
2026-05-06 19:02:48 -04:00
|
|
|
font.pixelSize: 11
|
|
|
|
|
color: "#9ca3af"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
text: qsTr("Stream")
|
|
|
|
|
onClicked: {
|
2026-05-07 00:17:47 -04:00
|
|
|
if (!dragonRelay) return
|
|
|
|
|
var displays = dragonRelay.displaysForHost(modelData.ip)
|
2026-05-06 20:17:51 -04:00
|
|
|
if (displays && displays.length > 1) {
|
2026-05-07 00:17:47 -04:00
|
|
|
root.pickerHostIP = modelData.ip
|
|
|
|
|
root.pickerHostName = modelData.name
|
2026-05-06 20:17:51 -04:00
|
|
|
root.pickerDisplays = displays
|
|
|
|
|
root.showDisplayPicker = true
|
|
|
|
|
} else {
|
2026-05-07 00:17:47 -04:00
|
|
|
dragonRelay.streamHost(modelData.ip, "Desktop", 0)
|
2026-05-06 20:17:51 -04:00
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
id: hostMouse
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
onDoubleClicked: {
|
2026-05-07 00:17:47 -04:00
|
|
|
if (dragonRelay)
|
|
|
|
|
dragonRelay.streamHost(modelData.ip, "Desktop", 0)
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 00:17:47 -04:00
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
text: qsTr("No hosts visible yet - waiting for Artemis to register...")
|
|
|
|
|
color: "#9ca3af"
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
visible: hostList.count === 0
|
|
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
RowLayout {
|
2026-05-06 19:02:48 -04:00
|
|
|
Layout.alignment: Qt.AlignRight
|
2026-05-07 00:17:47 -04:00
|
|
|
spacing: 8
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
text: qsTr("Refresh")
|
|
|
|
|
flat: true
|
|
|
|
|
onClicked: {
|
|
|
|
|
if (dragonRelay) dragonRelay.refreshHosts()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
text: qsTr("Disconnect")
|
|
|
|
|
flat: true
|
|
|
|
|
onClicked: {
|
|
|
|
|
if (dragonRelay) dragonRelay.disconnectRelay()
|
|
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 00:17:47 -04:00
|
|
|
Item { Layout.fillHeight: true; visible: !root.isHostListVisible }
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|
2026-05-06 20:17:51 -04:00
|
|
|
|
|
|
|
|
DragonDisplayPicker {
|
|
|
|
|
visible: root.showDisplayPicker
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
hostIP: root.pickerHostIP
|
|
|
|
|
hostName: root.pickerHostName
|
|
|
|
|
displays: root.pickerDisplays
|
|
|
|
|
onDisplaySelected: function(idx) {
|
|
|
|
|
root.showDisplayPicker = false
|
2026-05-07 00:17:47 -04:00
|
|
|
if (dragonRelay)
|
|
|
|
|
dragonRelay.streamHostDisplay(root.pickerHostIP, idx)
|
2026-05-06 20:17:51 -04:00
|
|
|
}
|
|
|
|
|
onCancelled: { root.showDisplayPicker = false }
|
|
|
|
|
}
|
2026-05-06 19:02:48 -04:00
|
|
|
}
|