Improve setup wizard for things

This commit is contained in:
Michael Zanetti 2020-07-07 11:25:22 +02:00
parent b22e1f9110
commit 14057b7ec9
5 changed files with 250 additions and 285 deletions

View File

@ -37,13 +37,25 @@ Rectangle {
visible: shown visible: shown
property bool shown: false property bool shown: false
property alias text: textLabel.text
// Event eater // Event eater
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
} }
BusyIndicator { Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - app.margins * 2
spacing: app.margins * 2
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: textLabel
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
} }
} }

View File

@ -42,8 +42,9 @@ Page {
onBackPressed: pageStack.pop() onBackPressed: pageStack.pop()
} }
property alias busy: busyOverlay.shown
default property alias content: contentColumn.data default property alias content: contentColumn.data
property alias busy: busyOverlay.shown
property alias busyText: busyOverlay.text
Flickable { Flickable {
anchors.fill: parent anchors.fill: parent

View File

@ -86,6 +86,7 @@ SettingsPageBase {
function reconfigureThing() { function reconfigureThing() {
var configPage = pageStack.push(Qt.resolvedUrl("SetupWizard.qml"), {thing: root.thing}) var configPage = pageStack.push(Qt.resolvedUrl("SetupWizard.qml"), {thing: root.thing})
configPage.done.connect(function() {pageStack.pop(root)}) configPage.done.connect(function() {pageStack.pop(root)})
configPage.aborted.connect(function() {pageStack.pop(root)})
} }
Component { Component {

View File

@ -174,6 +174,9 @@ Page {
pageStack.pop(root, StackView.Immediate); pageStack.pop(root, StackView.Immediate);
pageStack.pop(); pageStack.pop();
}) })
page.aborted.connect(function() {
pageStack.pop();
})
} }
} }
} }

View File

@ -45,24 +45,9 @@ Page {
// Optional: If set, it will be reconfigred, otherwise a new one will be created // Optional: If set, it will be reconfigred, otherwise a new one will be created
property Thing thing: null property Thing thing: null
signal aborted();
signal done(); signal done();
header: NymeaHeader {
text: root.thing ? qsTr("Reconfigure %1").arg(root.thing.name) : qsTr("Set up %1").arg(root.thingClass.displayName)
onBackPressed: {
if (internalPageStack.depth > 1) {
internalPageStack.pop();
} else {
pageStack.pop();
}
}
HeaderButton {
imageSource: "../images/close.svg"
onClicked: root.done();
}
}
QtObject { QtObject {
id: d id: d
property var vendorId: null property var vendorId: null
@ -72,6 +57,54 @@ Page {
property int pairRequestId: 0 property int pairRequestId: 0
property var pairingTransactionId: null property var pairingTransactionId: null
property int addRequestId: 0 property int addRequestId: 0
property var name: ""
property var params: []
function pairThing() {
print("setupMethod", root.thingClass.setupMethod)
switch (root.thingClass.setupMethod) {
case 0:
if (root.thing) {
if (d.thingDescriptor) {
engine.thingManager.reconfigureDiscoveredThing(root.thing.id, d.thingDescriptor.id, params);
} else {
engine.thingManager.reconfigureThing(root.thing.id, params);
}
} else {
if (d.thingDescriptor) {
engine.thingManager.addDiscoveredThing(root.thingClass.id, d.thingDescriptor.id, d.name, params);
} else {
engine.thingManager.addThing(root.thingClass.id, d.name, params);
}
}
break;
case 1:
case 2:
case 3:
case 4:
case 5:
if (root.thing) {
if (d.thingDescriptor) {
engine.thingManager.pairDiscoveredThing(d.thingDescriptor.id, params, d.name);
} else {
engine.thingManager.rePairThing(root.thing.id, params, d.name);
}
return;
} else {
if (d.thingDescriptor) {
engine.thingManager.pairDiscoveredThing(d.thingDescriptor.id, params, d.name);
} else {
engine.thingManager.pairThing(root.thingClass.id, params, d.name);
}
}
break;
}
busyOverlay.shown = true;
}
} }
Component.onCompleted: { Component.onCompleted: {
@ -176,64 +209,50 @@ Page {
id: internalPageStack id: internalPageStack
anchors.fill: parent anchors.fill: parent
} }
property QtObject pageStack: QtObject {
function pop(item) {
if (internalPageStack.depth > 1) {
internalPageStack.pop(item)
} else {
root.aborted()
}
}
}
Component { Component {
id: discoveryParamsPage id: discoveryParamsPage
Page { SettingsPageBase {
id: discoveryParamsView id: discoveryParamsView
title: qsTr("Discover %1").arg(root.thingClass.displayName)
ColumnLayout { SettingsPageSectionHeader {
anchors.fill: parent text: qsTr("Discovery options")
anchors.margins: 10 }
Flickable { Repeater {
Layout.fillHeight: true id: paramRepeater
model: root.thingClass ? root.thingClass.discoveryParamTypes : null
delegate: ParamDelegate {
Layout.fillWidth: true Layout.fillWidth: true
ColumnLayout { paramType: root.thingClass.discoveryParamTypes.get(index)
width: parent.width
Repeater {
id: paramRepeater
model: root.thingClass ? root.thingClass["discoveryParamTypes"] : null
Loader {
Layout.fillWidth: true
sourceComponent: searchStringEntryComponent
property var discoveryParams: model
property var value: item ? item.value : null
}
}
Button {
Layout.fillWidth: true
text: "Next"
onClicked: {
var paramTypes = root.thingClass["discoveryParamTypes"];
d.discoveryParams = [];
for (var i = 0; i < paramTypes.count; i++) {
var param = {};
param["paramTypeId"] = paramTypes.get(i).id;
param["value"] = paramRepeater.itemAt(i).value
d.discoveryParams.push(param);
}
discovery.discoverThings(root.thingClass.id, d.discoveryParams)
internalPageStack.push(discoveryPage, {thingClass: root.thingClass})
}
}
}
} }
}
Component { Button {
id: searchStringEntryComponent Layout.fillWidth: true
ColumnLayout { Layout.margins: app.margins
property alias value: searchTextField.text text: "Next"
Label { onClicked: {
text: discoveryParams.displayName var paramTypes = root.thingClass.discoveryParamTypes;
Layout.fillWidth: true d.discoveryParams = [];
} for (var i = 0; i < paramTypes.count; i++) {
TextField { var param = {};
id: searchTextField param["paramTypeId"] = paramTypes.get(i).id;
Layout.fillWidth: true param["value"] = paramRepeater.itemAt(i).value
} d.discoveryParams.push(param);
} }
discovery.discoverThings(root.thingClass.id, d.discoveryParams)
internalPageStack.push(discoveryPage, {thingClass: root.thingClass})
} }
} }
} }
@ -242,102 +261,90 @@ Page {
Component { Component {
id: discoveryPage id: discoveryPage
Page { SettingsPageBase {
id: discoveryView id: discoveryView
property ThingClass thingClass: null header: NymeaHeader {
text: qsTr("Discover %1").arg(root.thingClass.displayName)
backButtonVisible: true
onBackPressed: pageStack.pop()
ColumnLayout { HeaderButton {
anchors.fill: parent imageSource: "../images/configure.svg"
anchors.bottomMargin: app.margins
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: ThingDiscoveryProxy {
id: discoveryProxy
thingDiscovery: discovery
showAlreadyAdded: root.thing !== null
showNew: root.thing === null
filterThingId: root.thing ? root.thing.id : ""
}
delegate: NymeaSwipeDelegate {
width: parent.width
height: app.delegateHeight
text: model.name
subText: model.description
iconName: app.interfacesToIcon(discoveryView.thingClass.interfaces)
onClicked: {
d.thingDescriptor = discoveryProxy.get(index);
d.thingName = model.name;
internalPageStack.push(paramsPage)
}
}
}
Button {
id: retryButton
Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
text: qsTr("Search again")
onClicked: discovery.discoverThings(root.thingClass.id, d.discoveryParams)
visible: !discovery.busy
}
Button {
id: manualAddButton
Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins;
visible: root.thingClass.createMethods.indexOf("CreateMethodUser") >= 0 visible: root.thingClass.createMethods.indexOf("CreateMethodUser") >= 0
text: qsTr("Add thing manually") text: qsTr("Add thing manually")
onClicked: internalPageStack.push(paramsPage) onClicked: internalPageStack.push(paramsPage)
} }
} }
property ThingClass thingClass: null
ColumnLayout { SettingsPageSectionHeader {
anchors.centerIn: parent text: qsTr("Nymea found the following things")
width: parent.width - app.margins * 2 visible: !discovery.busy && discoveryProxy.count > 0
visible: discovery.busy }
spacing: app.margins * 2
Label { Repeater {
text: qsTr("Searching for things...") model: ThingDiscoveryProxy {
Layout.fillWidth: true id: discoveryProxy
font.pixelSize: app.largeFont thingDiscovery: discovery
horizontalAlignment: Text.AlignHCenter showAlreadyAdded: root.thing !== null
showNew: root.thing === null
filterThingId: root.thing ? root.thing.id : ""
} }
BusyIndicator { delegate: NymeaItemDelegate {
running: visible Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter text: model.name
subText: model.description
iconName: app.interfacesToIcon(discoveryView.thingClass.interfaces)
onClicked: {
d.thingDescriptor = discoveryProxy.get(index);
d.thingName = model.name;
internalPageStack.push(paramsPage)
}
} }
} }
busy: discovery.busy
busyText: qsTr("Searching for things...")
ColumnLayout { ColumnLayout {
anchors.centerIn: parent
width: parent.width - app.margins * 2
visible: !discovery.busy && discoveryProxy.count === 0 visible: !discovery.busy && discoveryProxy.count === 0
spacing: app.margins * 2 spacing: app.margins
Layout.preferredHeight: discoveryView.height - discoveryView.header.height - retryButton.height - app.margins * 3
Label { Label {
text: qsTr("Too bad...") text: qsTr("Too bad...")
font.pixelSize: app.largeFont font.pixelSize: app.largeFont
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
Label { Label {
text: qsTr("No things of this kind could be found...") text: qsTr("No things of this kind could be found...")
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: discovery.displayMessage.length === 0 ? text: discovery.displayMessage.length === 0 ?
qsTr("Make sure your things are set up and connected, try searching again or go back and pick a different kind of thing.") qsTr("Make sure your things are set up and connected, try searching again or go back and pick a different kind of thing.")
: discovery.displayMessage : discovery.displayMessage
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
}
Button {
id: retryButton
Layout.fillWidth: true
Layout.margins: app.margins
text: qsTr("Search again")
onClicked: discovery.discoverThings(root.thingClass.id, d.discoveryParams)
visible: !discovery.busy
} }
} }
} }
@ -345,138 +352,63 @@ Page {
Component { Component {
id: paramsPage id: paramsPage
Page { SettingsPageBase {
id: paramsView id: paramsView
Flickable { title: root.thing ? qsTr("Reconfigure %1").arg(root.thing.name) : qsTr("Set up %1").arg(root.thingClass.displayName)
anchors.fill: parent
contentHeight: paramsColumn.implicitHeight
ColumnLayout { SettingsPageSectionHeader {
id: paramsColumn text: qsTr("Name the thing:")
width: parent.width }
ColumnLayout { TextField {
// visible: root.thing === null id: nameTextField
Label { text: d.thingName ? d.thingName : root.thingClass.displayName
Layout.leftMargin: app.margins Layout.fillWidth: true
Layout.rightMargin: app.margins Layout.leftMargin: app.margins
Layout.topMargin: app.margins Layout.rightMargin: app.margins
Layout.fillWidth: true }
text: qsTr("Name the thing:")
}
TextField {
id: nameTextField
text: (d.thingName ? d.thingName : root.thingClass.displayName)
+ (root.thingClass.id.toString().match(/\{?f0dd4c03-0aca-42cc-8f34-9902457b05de\}?/) ? " (" + PlatformHelper.machineHostname + ")" : "")
Layout.fillWidth: true
Layout.leftMargin: app.margins
Layout.rightMargin: app.margins
}
ThinDivider { SettingsPageSectionHeader {
visible: paramRepeater.count > 0 text: qsTr("Thing parameters")
} visible: paramRepeater.count > 0
} }
Repeater { Repeater {
id: paramRepeater id: paramRepeater
model: engine.jsonRpcClient.ensureServerVersion("1.12") || d.thingDescriptor == null ? root.thingClass.paramTypes : null model: engine.jsonRpcClient.ensureServerVersion("1.12") || d.thingDescriptor == null ? root.thingClass.paramTypes : null
delegate: ParamDelegate { delegate: ParamDelegate {
// Layout.preferredHeight: 60 // Layout.preferredHeight: 60
Layout.fillWidth: true Layout.fillWidth: true
enabled: !model.readOnly enabled: !model.readOnly
paramType: root.thingClass.paramTypes.get(index) paramType: root.thingClass.paramTypes.get(index)
//visible: root.thingClass.id.toString().match(/\{?f0dd4c03-0aca-42cc-8f34-9902457b05de\}?/) === null value: d.thingDescriptor && d.thingDescriptor.params.getParam(paramType.id) ?
value: { d.thingDescriptor.params.getParam(paramType.id).value :
// Discovery, use params from discovered descriptor root.thingClass.paramTypes.get(index).defaultValue
if (d.thingDescriptor && d.thingDescriptor.params.getParam(paramType.id)) { }
return d.thingDescriptor.params.getParam(paramType.id).value }
}
// Special hook for push notifications as we need to provide the token etc implicitly Button {
print("Setting up params for thing class:", root.thingClass.id, root.thingClass.name) Layout.fillWidth: true
if (root.thingClass.id.toString().match(/\{?f0dd4c03-0aca-42cc-8f34-9902457b05de\}?/)) { Layout.leftMargin: app.margins
if (paramType.id.toString().match(/\{?3cb8e30e-2ec5-4b4b-8c8c-03eaf7876839\}?/)) { Layout.rightMargin: app.margins
return PushNotifications.service;
}
if (paramType.id.toString().match(/\{?12ec06b2-44e7-486a-9169-31c684b91c8f\}?/)) {
return PushNotifications.coreToken;
}
if (paramType.id.toString().match(/\{?d76da367-64e3-4b7d-aa84-c96b3acfb65e\}?/)) {
return PushNotifications.clientId;
}
}
// Manual setup, use default value from thing class text: "OK"
return root.thingClass.paramTypes.get(index).defaultValue onClicked: {
} var params = []
for (var i = 0; i < paramRepeater.count; i++) {
var param = {}
var paramType = paramRepeater.itemAt(i).paramType
if (!paramType.readOnly) {
param.paramTypeId = paramType.id
param.value = paramRepeater.itemAt(i).value
print("adding param", param.paramTypeId, param.value)
params.push(param)
} }
} }
Button { d.params = params
Layout.fillWidth: true d.name = nameTextField.text
Layout.leftMargin: app.margins d.pairThing();
Layout.rightMargin: app.margins
text: "OK"
onClicked: {
print("setupMethod", root.thingClass.setupMethod)
var params = []
for (var i = 0; i < paramRepeater.count; i++) {
var param = {}
var paramType = paramRepeater.itemAt(i).paramType
if (!paramType.readOnly) {
param.paramTypeId = paramType.id
param.value = paramRepeater.itemAt(i).value
print("adding param", param.paramTypeId, param.value)
params.push(param)
}
}
switch (root.thingClass.setupMethod) {
case 0:
if (root.thing) {
if (d.thingDescriptor) {
engine.thingManager.reconfigureDiscoveredThing(d.thingDescriptor.id, params);
} else {
engine.thingManager.reconfigureThing(root.thing.id, params);
}
} else {
if (d.thingDescriptor) {
engine.thingManager.addDiscoveredThing(root.thingClass.id, d.thingDescriptor.id, nameTextField.text, params);
} else {
engine.thingManager.addThing(root.thingClass.id, nameTextField.text, params);
}
}
break;
case 1:
case 2:
case 3:
case 4:
case 5:
if (root.thing) {
if (d.thingDescriptor) {
engine.thingManager.pairDiscoveredThing(d.thingDescriptor.id, params, nameTextField.text);
} else {
engine.thingManager.rePairThing(root.thing.id, params, nameTextField.text);
}
return;
} else {
if (d.thingDescriptor) {
engine.thingManager.pairDiscoveredThing(d.thingDescriptor.id, params, nameTextField.text);
} else {
engine.thingManager.pairThing(root.thingClass.id, params, nameTextField.text);
}
}
break;
}
busyOverlay.shown = true;
}
}
} }
} }
} }
@ -484,54 +416,48 @@ Page {
Component { Component {
id: pairingPageComponent id: pairingPageComponent
Page { SettingsPageBase {
id: pairingPage id: pairingPage
title: root.thing ? qsTr("Reconfigure %1").arg(root.thing.name) : qsTr("Set up %1").arg(root.thingClass.displayName)
property alias text: textLabel.text property alias text: textLabel.text
property string setupMethod property string setupMethod
ColumnLayout { SettingsPageSectionHeader {
anchors.centerIn: parent text: qsTr("Login required")
width: parent.width - app.margins * 2 }
spacing: app.margins * 2
Label { Label {
Layout.fillWidth: true id: textLabel
wrapMode: Text.WordWrap Layout.fillWidth: true
font.pixelSize: app.largeFont Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
text: qsTr("Pairing...") wrapMode: Text.WordWrap
color: Style.accentColor }
horizontalAlignment: Text.AlignHCenter
}
Label { TextField {
id: textLabel id: usernameTextField
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
horizontalAlignment: Text.AlignHCenter placeholderText: qsTr("Username")
} visible: pairingPage.setupMethod === "SetupMethodUserAndPassword"
}
TextField { PasswordTextField {
id: usernameTextField id: pinTextField
Layout.fillWidth: true Layout.fillWidth: true
visible: pairingPage.setupMethod === "SetupMethodUserAndPassword" Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
} visible: pairingPage.setupMethod === "SetupMethodDisplayPin" || pairingPage.setupMethod === "SetupMethodUserAndPassword"
signup: false
PasswordTextField { }
id: pinTextField
Layout.fillWidth: true
visible: pairingPage.setupMethod === "SetupMethodDisplayPin" || pairingPage.setupMethod === "SetupMethodUserAndPassword"
signup: false
}
Button { Button {
Layout.fillWidth: true Layout.fillWidth: true
text: "OK" Layout.margins: app.margins
onClicked: { text: "OK"
engine.thingManager.confirmPairing(d.pairingTransactionId, pinTextField.password, usernameTextField.displayText); onClicked: {
busyOverlay.shown = true; engine.thingManager.confirmPairing(d.pairingTransactionId, pinTextField.password, usernameTextField.displayText);
} busyOverlay.shown = true;
} }
} }
} }
@ -542,6 +468,10 @@ Page {
Page { Page {
id: oAuthPage id: oAuthPage
property string oAuthUrl property string oAuthUrl
header: NymeaHeader {
text: root.thing ? qsTr("Reconfigure %1").arg(root.thing.name) : qsTr("Set up %1").arg(root.thingClass.displayName)
onBackPressed: pageStack.pop()
}
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
@ -617,6 +547,10 @@ Page {
Page { Page {
id: resultsView id: resultsView
header: NymeaHeader {
text: root.thing ? qsTr("Reconfigure %1").arg(root.thing.name) : qsTr("Set up %1").arg(root.thingClass.displayName)
onBackPressed: pageStack.pop()
}
property string thingId property string thingId
property int thingError property int thingError
@ -627,7 +561,7 @@ Page {
readonly property Thing thing: root.thing ? root.thing : engine.thingManager.things.getThing(thingId) readonly property Thing thing: root.thing ? root.thing : engine.thingManager.things.getThing(thingId)
ColumnLayout { ColumnLayout {
width: parent.width - app.margins * 2 width: Math.min(500, parent.width - app.margins * 2)
anchors.centerIn: parent anchors.centerIn: parent
spacing: app.margins * 2 spacing: app.margins * 2
Label { Label {
@ -652,8 +586,22 @@ Page {
text: resultsView.message text: resultsView.message
} }
Button { Button {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
visible: !resultsView.success
text: "Retry"
onClicked: {
internalPageStack.pop({immediate: true});
internalPageStack.pop({immediate: true});
d.pairThing();
}
}
Button {
Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
text: qsTr("Ok") text: qsTr("Ok")
onClicked: { onClicked: {
root.done(); root.done();