dragonmoonlight/app/gui/DragonRelayView.qml

328 lines
11 KiB
QML
Raw Normal View History

// 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
2026-05-06 20:17:51 -04:00
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)
2026-05-06 20:17:51 -04:00
if (displays && displays.length > 1) {
root.pickerHostIP = modelData.ip
root.pickerHostName = modelData.name
2026-05-06 20:17:51 -04:00
root.pickerDisplays = displays
root.showDisplayPicker = true
} else {
dragonRelay.streamHost(modelData.ip, "Desktop", 0)
2026-05-06 20:17:51 -04:00
}
}
}
}
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 }
}
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
if (dragonRelay)
dragonRelay.streamHostDisplay(root.pickerHostIP, idx)
2026-05-06 20:17:51 -04:00
}
onCancelled: { root.showDisplayPicker = false }
}
}