import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.2 import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" Page { id: root readonly property bool haveHosts: discovery.discoveryModel.count > 0 Component.onCompleted: { print("completed connectPage for tab", connectionTabIndex, "last connected host:", tabSettings.lastConnectedHost) if (tabSettings.lastConnectedHost.length > 0 && engine.connection.connect(tabSettings.lastConnectedHost)) { var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) page.cancel.connect(function() { engine.connection.disconnect(); pageStack.pop(root, StackView.Immediate); pageStack.push(discoveryPage) }) } else { pageStack.push(discoveryPage) } } function connectToHost(url) { var page = pageStack.push(Qt.resolvedUrl("ConnectingPage.qml")) page.cancel.connect(function() { engine.connection.disconnect() pageStack.pop(root, StackView.Immediate); pageStack.push(discoveryPage) }) engine.connection.connect(url) } NymeaDiscovery { id: discovery objectName: "discovery" awsClient: AWSClient discovering: pageStack.currentItem.objectName === "discoveryPage" } Connections { target: engine.connection onVerifyConnectionCertificate: { print("verify cert!") var popup = certDialogComponent.createObject(root, {url: url, issuerInfo: issuerInfo, fingerprint: fingerprint, pem: pem}); popup.open(); } onConnectionError: { var errorMessage; switch (error) { case "ConnectionRefusedError": errorMessage = qsTr("The host has rejected our connection. This probably means that %1 stopped running. Did you unplug your %1 box?").arg(app.systemName); break; case "SslInvalidUserDataError": case "SslHandshakeFailedError": // silently ignore. They'll be handled by the SSL logic return; case "HostNotFoundError": errorMessage = qsTr("The %1 box could not be found on this address. Please make sure you entered the address correctly and that the box is powered on.").arg(app.systemName); break; case "NetworkError": errorMessage = qsTr("It seems you're not connected to the network."); break; case "RemoteHostClosedError": errorMessage = qsTr("The %1 box has closed the connection. This probably means it has been turned off or restarted.").arg(app.systemName); break; default: errorMessage = qsTr("Un unknown error happened. We're very sorry for that. (Error code: %1)").arg(error); } pageStack.pop(root, StackView.Immediate) pageStack.push(discoveryPage) print("opening ErrorDialog with message:", errorMessage, error) var comp = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")) var popup = comp.createObject(app, {text: errorMessage}) popup.open() } onConnectedChanged: { if (!connected) { pageStack.pop(root, StackView.Immediate) pageStack.push(discoveryPage) } } } Component { id: discoveryPage Page { objectName: "discoveryPage" header: FancyHeader { title: qsTr("Connect %1").arg(app.systemName) model: ListModel { ListElement { iconSource: "../images/network-vpn.svg"; text: qsTr("Manual connection"); page: "ManualConnectPage.qml" } ListElement { iconSource: "../images/bluetooth.svg"; text: qsTr("Wireless setup"); page: "wifisetup/BluetoothDiscoveryPage.qml"; } ListElement { iconSource: "../images/private-browsing.svg"; text: qsTr("Demo mode"); page: "" } ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "../appsettings/AppSettingsPage.qml" } } onClicked: { if (index === 2) { root.connectToHost("nymea://nymea.nymea.io:2222") } else { pageStack.push(model.get(index).page, {nymeaDiscovery: discovery}); } } } Timer { id: startupTimer interval: 5000 repeat: false running: true } ColumnLayout { anchors.fill: parent spacing: app.margins ColumnLayout { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins Layout.topMargin: 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 } } ThinDivider { } ListView { Layout.fillWidth: true Layout.fillHeight: true model: discovery.discoveryModel clip: true delegate: MeaListItemDelegate { id: discoveryDeviceDelegate width: parent.width height: app.delegateHeight objectName: "discoveryDelegate" + index property var discoveryDevice: discovery.discoveryModel.get(index) property string defaultConnectionIndex: { var usedConfigIndex = 0; for (var i = 1; i < discoveryDevice.connections.count; i++) { var oldConfig = discoveryDevice.connections.get(usedConfigIndex); var newConfig = discoveryDevice.connections.get(i); // Preference of bearerType var bearerPreference = [Connection.BearerTypeEthernet, Connection.BearerTypeWifi, Connection.BearerTypeBluetooth, Connection.BearerTypeCloud] var oldBearerPriority = bearerPreference.indexOf(oldConfig.bearerType); var newBearerPriority = bearerPreference.indexOf(newConfig.bearerType); if (newBearerPriority < oldBearerPriority) { print(discoveryDevice.name, "switching to preferred index", i, "of bearer type", newConfig.bearerType, "from", oldConfig.bearerType, "new prio:", newBearerPriority, "old:", oldBearerPriority) usedConfigIndex = i; continue; } if (oldBearerPriority < newBearerPriority) { continue; // discard new one the one we have is on a better bearer type } // prefer secure over insecure if (!oldConfig.secure && newConfig.secure) { usedConfigIndex = i; continue; } if (oldConfig.secure && !newConfig.secure) { continue; // discard new one as the one we already have is more secure } // both options are now on the same bearer and either secure or insecure, prefer nymearpc over websocket for less overhead if (oldConfig.url.toString().startsWith("ws") && newConfig.url.toString().startsWith("nymea")) { usedConfigIndex = i; } } return usedConfigIndex } iconName: { switch (discoveryDevice.connections.get(defaultConnectionIndex).bearerType) { case Connection.BearerTypeWifi: return "../images/network-wifi-symbolic.svg"; case Connection.BearerTypeEthernet: return "../images/network-wired-symbolic.svg" case Connection.BearerTypeBluetooth: return "../images/bluetooth.svg"; case Connection.BearerTypeCloud: return "../images/cloud.svg" } return "" } text: model.name subText: discoveryDevice.connections.get(defaultConnectionIndex).url wrapTexts: false prominentSubText: false progressive: false property bool isSecure: discoveryDevice.connections.get(defaultConnectionIndex).secure property bool isTrusted: engine.connection.isTrusted(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url) property bool isOnline: discoveryDevice.connections.get(defaultConnectionIndex).online tertiaryIconName: isSecure ? "../images/network-secure.svg" : "" tertiaryIconColor: isTrusted ? app.accentColor : Material.foreground secondaryIconName: !isOnline ? "../images/cloud-error.svg" : "" secondaryIconColor: "red" swipe.enabled: discoveryDeviceDelegate.discoveryDevice.deviceType === DiscoveryDevice.DeviceTypeNetwork onClicked: { root.connectToHost(discoveryDeviceDelegate.discoveryDevice.connections.get(defaultConnectionIndex).url) } 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: { if (model.deviceType === DiscoveryDevice.DeviceTypeNetwork) { 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 } } } ThinDivider {} Label { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins wrapMode: Text.WordWrap visible: discovery.discoveryModel.count === 0 text: qsTr("Do you have a %1 box but it's not connected to your network yet? Use the wireless setup to connect it!").arg(app.systemName) } Button { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins visible: discovery.discoveryModel.count === 0 text: qsTr("Start wireless setup") onClicked: pageStack.push(Qt.resolvedUrl("wifisetup/BluetoothDiscoveryPage.qml"), {nymeaDiscovery: discovery}) } Button { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins text: qsTr("Cloud login") visible: !AWSClient.isLoggedIn onClicked: pageStack.push(Qt.resolvedUrl("../appsettings/CloudLoginPage.qml")) } Button { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins Layout.bottomMargin: app.margins visible: discovery.discoveryModel.count === 0 text: qsTr("Demo mode (online)") onClicked: { root.connectToHost("nymea://nymea.nymea.io:2222") } } RowLayout { Layout.fillWidth: true Layout.leftMargin: app.margins Layout.rightMargin: app.margins Layout.bottomMargin: 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: 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 property var pem 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.accentColor } Label { id: titleLabel Layout.fillWidth: true wrapMode: Text.WordWrap text: certDialog.hasOldFingerprint ? qsTr("Warning") : qsTr("Hi there!") color: certDialog.hasOldFingerprint ? "red" : app.accentColor 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.pem) root.connectToHost(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.accentColor } Label { id: titleLabel Layout.fillWidth: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere text: dialog.title color: app.accentColor 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 } 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.connections delegate: MeaListItemDelegate { Layout.fillWidth: true wrapTexts: false progressive: false text: model.name subText: model.url prominentSubText: false iconName: { switch (model.bearerType) { case Connection.BearerTypeWifi: return "../images/network-wifi-symbolic.svg"; case Connection.BearerTypeEthernet: return "../images/network-wired-symbolic.svg" case Connection.BearerTypeBluetooth: return "../images/bluetooth.svg"; case Connection.BearerTypeCloud: return "../images/cloud.svg" } return "" } tertiaryIconName: model.secure ? "../images/network-secure.svg" : "" tertiaryIconColor: isTrusted ? app.accentColor : "gray" readonly property bool isTrusted: engine.connection.isTrusted(url) secondaryIconName: !model.online ? "../images/cloud-error.svg" : "" secondaryIconColor: "red" onClicked: { root.connectToHost(dialog.discoveryDevice.connections.get(index).url) dialog.close() } } } } } } } } }