610 lines
23 KiB
QML
610 lines
23 KiB
QML
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|