import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.2 import QtQuick.Layouts 1.3 import Mea 1.0 import "components" Page { id: root readonly property bool haveHosts: discovery.discoveryModel.count > 0 Component.onCompleted: { print("completed connectPage. last connected host:", settings.lastConnectedHost) if (settings.lastConnectedHost.length > 0) { pageStack.push(connectingPage) Engine.connection.connect(settings.lastConnectedHost) } else { pageStack.push(discoveryPage) } } Connections { target: Engine.connection onVerifyConnectionCertificate: { print("verify cert!") var popup = certDialogComponent.createObject(app, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint}); popup.open(); } onConnectionError: { pageStack.pop(root) pageStack.push(discoveryPage) } } Component { id: discoveryPage Page { objectName: "discoveryPage" header: GuhHeader { text: qsTr("Connect %1").arg(app.systemName) backButtonVisible: false menuButtonVisible: true onMenuPressed: connectionMenu.open() } Timer { id: startupTimer interval: 5000 repeat: false running: true } Menu { id: connectionMenu objectName: "connectionMenu" width: implicitWidth + app.margins IconMenuItem { objectName: "manualConnectMenuItem" iconSource: "../images/network-vpn.svg" text: qsTr("Manual connection") onTriggered: pageStack.push(manualConnectPage) } IconMenuItem { iconSource: "../images/bluetooth.svg" text: qsTr("Wireless setup") onTriggered: pageStack.push(Qt.resolvedUrl("BluetoothDiscoveryPage.qml")) } MenuSeparator {} IconMenuItem { iconSource: "../images/stock_application.svg" text: qsTr("App settings") onTriggered: pageStack.push(Qt.resolvedUrl("AppSettingsPage.qml")) } } ColumnLayout { anchors.fill: parent spacing: app.margins ColumnLayout { Layout.fillWidth: true Layout.margins: app.margins spacing: app.margins Label { Layout.fillWidth: true text: root.haveHosts ? qsTr("Oh, look!") : startupTimer.running ? qsTr("Just a moment...") : qsTr("Uh oh") //color: "black" font.pixelSize: app.largeFont } Label { Layout.fillWidth: true text: root.haveHosts ? qsTr("There are %1 %2 boxes in your network! Which one would you like to use?").arg(discovery.discoveryModel.count).arg(app.systemName) : startupTimer.running ? qsTr("We haven't found any %1 boxes in your network yet.").arg(app.systemName) : qsTr("There doesn't seem to be a %1 box installed in your network. Please make sure your %1 box is correctly set up and connected.").arg(app.systemName) wrapMode: Text.WordWrap } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 color: Material.accent } ListView { Layout.fillWidth: true Layout.fillHeight: true model: discovery.discoveryModel clip: true delegate: SwipeDelegate { id: discoveryDeviceDelegate width: parent.width height: app.delegateHeight objectName: "discoveryDelegate" + index property var discoveryDevice: discovery.discoveryModel.get(index) property string defaultPortConfigIndex: { var usedConfigIndex = 0; for (var i = 1; i < discoveryDevice.portConfigs.count; i++) { var oldConfig = discoveryDevice.portConfigs.get(usedConfigIndex); var newConfig = discoveryDevice.portConfigs.get(i); // prefer secure over insecure if (!oldConfig.sslEnabled && newConfig.sslEnabled) { usedConfigIndex = i; continue; } if (oldConfig.sslEnabled && !newConfig.sslEnabled) { continue; // discard new one as the one we already have is more secure } // both options are new either secure or insecure, prefer nymearpc over websocket for less overhead if (oldConfig.protocol === PortConfig.ProtocolWebSocket && newConfig.protocol === PortConfig.ProtocolNymeaRpc) { usedConfigIndex = i; } } return usedConfigIndex } contentItem: RowLayout { ColumnLayout { Layout.fillWidth: true Label { text: model.name Layout.fillWidth: true elide: Text.ElideRight } Label { text: model.hostAddress font.pixelSize: app.smallFont } } ColorIcon { Layout.fillHeight: true Layout.preferredWidth: height property bool hasSecurePort: discoveryDeviceDelegate.discoveryDevice.portConfigs.get(discoveryDeviceDelegate.defaultPortConfigIndex).sslEnabled property bool isTrusted: Engine.connection.isTrusted(discoveryDeviceDelegate.discoveryDevice.toUrl(discoveryDeviceDelegate.defaultPortConfigIndex)) visible: hasSecurePort name: "../images/network-secure.svg" color: isTrusted ? app.guhAccent : keyColor } } onClicked: { Engine.connection.connect(discoveryDevice.toUrl(defaultPortConfigIndex)) pageStack.push(connectingPage) } swipe.right: MouseArea { height: parent.height width: height anchors.right: parent.right ColorIcon { anchors.fill: parent anchors.margins: app.margins name: "../images/info.svg" } onClicked: { swipe.close() var popup = infoDialog.createObject(app,{discoveryDevice: discovery.discoveryModel.get(index)}) popup.open() } } } Column { anchors.centerIn: parent spacing: app.margins visible: !root.haveHosts Label { text: qsTr("Searching for %1 boxes...").arg(app.systemName) } BusyIndicator { running: visible anchors.horizontalCenter: parent.horizontalCenter } } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 color: Material.accent } RowLayout { Layout.fillWidth: true Layout.margins: app.margins visible: root.haveHosts Label { Layout.fillWidth: true text: qsTr("Not the ones you're looking for? We're looking for more!") wrapMode: Text.WordWrap } BusyIndicator { } } } } } Component { id: manualConnectPage Page { objectName: "manualConnectPage" header: GuhHeader { text: qsTr("Manual connection") onBackPressed: pageStack.pop() } ColumnLayout { anchors { left: parent.left; top: parent.top; right: parent.right } anchors.margins: app.margins spacing: app.margins GridLayout { columns: 2 Label { text: qsTr("Protocol") } ComboBox { id: connectionTypeComboBox Layout.fillWidth: true model: [ qsTr("TCP"), qsTr("Websocket") ] } Label { text: qsTr("Address:") } TextField { id: addressTextInput objectName: "addressTextInput" Layout.fillWidth: true placeholderText: "127.0.0.1" validator: RegExpValidator { regExp: /^((?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.){0,3}(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/ } } Label { text: qsTr("Port:") } TextField { id: portTextInput Layout.fillWidth: true placeholderText: connectionTypeComboBox.currentIndex === 0 ? "2222" : "4444" validator: IntValidator{bottom: 1; top: 65535;} } Label { Layout.fillWidth: true text: qsTr("Encrypted connection:") } CheckBox { id: secureCheckBox checked: true } } Button { text: qsTr("Connect") objectName: "connectButton" Layout.fillWidth: true onClicked: { var rpcUrl var hostAddress var port // Set default to placeholder if (addressTextInput.text === "") { hostAddress = addressTextInput.placeholderText } else { hostAddress = addressTextInput.text } if (portTextInput.text === "") { port = portTextInput.placeholderText } else { port = portTextInput.text } if (connectionTypeComboBox.currentIndex == 0) { if (secureCheckBox.checked) { rpcUrl = "nymeas://" + hostAddress + ":" + port } else { rpcUrl = "nymea://" + hostAddress + ":" + port } } else if (connectionTypeComboBox.currentIndex == 1) { if (secureCheckBox.checked) { rpcUrl = "wss://" + hostAddress + ":" + port } else { rpcUrl = "ws://" + hostAddress + ":" + port } } print("Try to connect ", rpcUrl) Engine.connection.connect(rpcUrl) pageStack.push(connectingPage) } } } } } Component { id: connectingPage Page { ColumnLayout { id: columnLayout anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter; margins: app.margins } spacing: app.margins BusyIndicator { anchors.horizontalCenter: parent.horizontalCenter running: parent.visible } Label { text: qsTr("Trying to connect...") font.pixelSize: app.largeFont Layout.fillWidth: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter } } Button { text: qsTr("Cancel") anchors { left: parent.left; top: columnLayout.bottom; right: parent.right } anchors.margins: app.margins onClicked: { Engine.connection.disconnect() pageStack.pop(root); pageStack.push(discoveryPage); } } } } Component { id: certDialogComponent Dialog { id: certDialog width: Math.min(parent.width * .9, 400) x: (parent.width - width) / 2 y: (parent.height - height) / 2 standardButtons: Dialog.Yes | Dialog.No property string url property var fingerprint property var issuerInfo readonly property bool hasOldFingerprint: Engine.connection.isTrusted(url) ColumnLayout { id: certLayout anchors.fill: parent // spacing: app.margins RowLayout { Layout.fillWidth: true spacing: app.margins ColorIcon { Layout.preferredHeight: app.iconSize * 2 Layout.preferredWidth: height name: certDialog.hasOldFingerprint ? "../images/lock-broken.svg" : "../images/info.svg" color: certDialog.hasOldFingerprint ? "red" : app.guhAccent } Label { id: titleLabel Layout.fillWidth: true wrapMode: Text.WordWrap text: certDialog.hasOldFingerprint ? qsTr("Warning") : qsTr("Hi there!") color: certDialog.hasOldFingerprint ? "red" : app.guhAccent font.pixelSize: app.largeFont } } Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: certDialog.hasOldFingerprint ? qsTr("The certificate of this %1 box has changed!").arg(app.systemName) : qsTr("It seems this is the first time you connect to this %1 box.").arg(app.systemName) } Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: certDialog.hasOldFingerprint ? qsTr("Did you change the box's configuration? Verify if this information is correct.") : qsTr("This is the box's certificate. Once you trust it, an encrypted connection will be established.") } ThinDivider {} Item { Layout.fillWidth: true Layout.fillHeight: true implicitHeight: certGridLayout.implicitHeight Flickable { anchors.fill: parent contentHeight: certGridLayout.implicitHeight clip: true ScrollBar.vertical: ScrollBar { policy: contentHeight > height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded } GridLayout { id: certGridLayout columns: 2 width: parent.width Repeater { model: certDialog.issuerInfo Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: modelData } } Label { Layout.fillWidth: true Layout.columnSpan: 2 wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: qsTr("Fingerprint: ") + certDialog.fingerprint } } } } ThinDivider {} Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: certDialog.hasOldFingerprint ? qsTr("Do you want to connect nevertheless?") : qsTr("Do you want to trust this device?") font.bold: true } } onAccepted: { Engine.connection.acceptCertificate(certDialog.url, certDialog.fingerprint) Engine.connection.connect(certDialog.url) } } } Component { id: infoDialog Dialog { id: dialog width: Math.min(parent.width, contentGrid.implicitWidth) x: (parent.width - width) / 2 y: (parent.height - height) / 2 modal: true title: qsTr("Box information") standardButtons: Dialog.Ok property var discoveryDevice: null header: Item { implicitHeight: headerRow.height + app.margins * 2 implicitWidth: parent.width RowLayout { id: headerRow anchors { left: parent.left; right: parent.right; top: parent.top; margins: app.margins } spacing: app.margins ColorIcon { Layout.preferredHeight: app.iconSize * 2 Layout.preferredWidth: height name: "../images/info.svg" color: app.guhAccent } Label { id: titleLabel Layout.fillWidth: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: dialog.title color: app.guhAccent font.pixelSize: app.largeFont } } } GridLayout { id: contentGrid anchors.fill: parent rowSpacing: app.margins columns: 2 Label { text: "Name:" } Label { text: dialog.discoveryDevice.name Layout.fillWidth: true elide: Text.ElideRight } Label { text: "UUID:" } Label { text: dialog.discoveryDevice.uuid Layout.fillWidth: true elide: Text.ElideRight } Label { text: "Version:" } Label { text: dialog.discoveryDevice.version Layout.fillWidth: true elide: Text.ElideRight } Label { text: "IP Address:" } Label { text: dialog.discoveryDevice.hostAddress Layout.fillWidth: true elide: Text.ElideRight } ThinDivider { Layout.columnSpan: 2 } Label { Layout.columnSpan: 2 text: qsTr("Available connections") } Flickable { Layout.columnSpan: 2 Layout.fillWidth: true Layout.preferredHeight: 200 contentHeight: contentColumn.implicitHeight clip: true ColumnLayout { id: contentColumn width: parent.width Repeater { model: dialog.discoveryDevice.portConfigs ItemDelegate { Layout.fillWidth: true contentItem: RowLayout { ColumnLayout { Layout.fillWidth: true Label { text: qsTr("Port: %1").arg(model.port) } Label { text: model.protocol === PortConfig.ProtocolNymeaRpc ? "nymea-rpc" : "websocket" Layout.fillWidth: true font.pixelSize: app.smallFont } } ColorIcon { Layout.preferredHeight: app.iconSize Layout.preferredWidth: height visible: model.sslEnabled name: "../images/network-secure.svg" property bool isTrusted: Engine.connection.isTrusted(dialog.discoveryDevice.toUrl(index)) color: isTrusted ? app.guhAccent : keyColor } } onClicked: { Engine.connection.connect(dialog.discoveryDevice.toUrl(index)) dialog.close() } } } } } } } } }