diff --git a/app/gui/DragonRelayView.qml b/app/gui/DragonRelayView.qml index d278b52..7a5d577 100644 --- a/app/gui/DragonRelayView.qml +++ b/app/gui/DragonRelayView.qml @@ -1,22 +1,17 @@ // 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: +// DragonRelay server connection + host browser. // -// // main.cpp -// engine.rootContext()->setContextProperty("dragonRelay", &g_relayBackend); +// Bound to the C++ DragonRelayBackend exposed via: // -// 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) +// 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 @@ -26,24 +21,34 @@ Page { id: root 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 stateConnecting: 1 - readonly property int stateConnected: 2 - readonly property int stateError: 3 + 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 - // 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 ───────────────────────────────────────────────────────── + property bool connectInFlight: false + + Connections { + target: dragonRelay + function onStatusChanged() { + if (root.currentState !== root.statusConnecting) + root.connectInFlight = false + } + } header: ToolBar { RowLayout { @@ -67,17 +72,26 @@ Page { 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 { 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 + case root.statusConnecting: return "#f59e0b" + case root.statusTunnelUp: return "#10b981" + case root.statusReady: return "#10b981" + case root.statusError: return "#ef4444" + default: return "#6b7280" } } @@ -92,27 +106,23 @@ Page { } } - // ── Body ─────────────────────────────────────────────────────────────── - ColumnLayout { anchors.fill: parent anchors.margins: 24 spacing: 20 - // ── Login form (shown when idle or errored) ──────────────────────── + // 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 + visible: root.isLoginVisible ColumnLayout { anchors.fill: parent spacing: 12 - // Logo Image { Layout.alignment: Qt.AlignHCenter source: "qrc:/app/assets/wilddragon-logo.jpg" @@ -128,12 +138,21 @@ Page { 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 { @@ -152,26 +171,28 @@ Page { Button { id: connectBtn Layout.fillWidth: true - text: root.currentState === root.stateConnecting - ? qsTr("Connecting…") + 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.stateConnecting + && root.currentState !== root.statusConnecting + && !root.connectInFlight onClicked: { - root.currentState = root.stateConnecting - root.statusText = qsTr("Connecting…") - // Real call: dragonRelay.connectRelay(urlField.text, userField.text, passField.text) + root.connectInFlight = true + dragonRelay.connectRelay(urlField.text, + userField.text, + passField.text) } } - // Error message Label { id: errorLabel Layout.fillWidth: true - visible: root.currentState === root.stateError + visible: root.currentState === root.statusError color: "#ef4444" wrapMode: Text.WordWrap text: root.statusText @@ -179,13 +200,13 @@ Page { } } - // ── Host list (shown when connected) ────────────────────────────── + // Host list (shown when connected) GroupBox { title: qsTr("Streaming hosts") Layout.fillWidth: true Layout.fillHeight: true - visible: root.currentState === root.stateConnected + visible: root.isHostListVisible ColumnLayout { anchors.fill: parent @@ -198,12 +219,7 @@ Page { 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" } - } + model: dragonRelay ? dragonRelay.hosts : [] delegate: Rectangle { width: hostList.width @@ -218,13 +234,13 @@ Page { Column { Label { - text: model.hostName + text: modelData.name font.pixelSize: 14 font.bold: true color: "white" } Label { - text: model.hostIp + text: modelData.ip + ":" + modelData.port font.pixelSize: 11 color: "#9ca3af" } @@ -235,14 +251,15 @@ Page { Button { text: qsTr("Stream") onClicked: { - var displays = dragonRelay.displaysForHost(model.hostIp) + if (!dragonRelay) return + var displays = dragonRelay.displaysForHost(modelData.ip) if (displays && displays.length > 1) { - root.pickerHostIP = model.hostIp - root.pickerHostName = model.hostName + root.pickerHostIP = modelData.ip + root.pickerHostName = modelData.name root.pickerDisplays = displays root.showDisplayPicker = true } else { - dragonRelay.streamHost(model.hostIp, "Desktop") + dragonRelay.streamHost(modelData.ip, "Desktop", 0) } } } @@ -253,30 +270,47 @@ Page { anchors.fill: parent hoverEnabled: true 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 - text: qsTr("Disconnect") - flat: true - onClicked: { - root.currentState = root.stateIdle - root.statusText = qsTr("Not connected") - // Real call: dragonRelay.disconnectRelay() + 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.currentState !== root.stateConnected } + Item { Layout.fillHeight: true; visible: !root.isHostListVisible } } - // ── Display Picker Modal ─────────────────────────────────────────────── - DragonDisplayPicker { visible: root.showDisplayPicker anchors.fill: parent @@ -285,7 +319,8 @@ Page { displays: root.pickerDisplays onDisplaySelected: function(idx) { root.showDisplayPicker = false - dragonRelay.streamHostDisplay(root.pickerHostIP, idx) + if (dragonRelay) + dragonRelay.streamHostDisplay(root.pickerHostIP, idx) } onCancelled: { root.showDisplayPicker = false } }