From 0d41958b5b13c45f90cfd9d41eeb2cc602cea71c Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 30 Jan 2020 22:22:32 +0100 Subject: [PATCH 1/4] A first stab on managing users --- libnymea-app-core/engine.cpp | 9 +- libnymea-app-core/engine.h | 4 + libnymea-app-core/libnymea-app-core.pro | 3 + libnymea-app-core/usersmanager.cpp | 20 ++ libnymea-app-core/usersmanager.h | 23 +++ nymea-app/resources.qrc | 1 + nymea-app/ui/SettingsPage.qml | 15 ++ nymea-app/ui/system/UsersSettingsPage.qml | 237 ++++++++++++++++++++++ 8 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 libnymea-app-core/usersmanager.cpp create mode 100644 libnymea-app-core/usersmanager.h create mode 100644 nymea-app/ui/system/UsersSettingsPage.qml diff --git a/libnymea-app-core/engine.cpp b/libnymea-app-core/engine.cpp index f3d07012..68dcdd24 100644 --- a/libnymea-app-core/engine.cpp +++ b/libnymea-app-core/engine.cpp @@ -38,6 +38,7 @@ #include "connection/awsclient.h" #include "system/systemcontroller.h" #include "configuration/networkmanager.h" +#include "usersmanager.h" #include "connection/tcpsockettransport.h" #include "connection/websockettransport.h" @@ -54,7 +55,8 @@ Engine::Engine(QObject *parent) : m_logManager(new LogManager(m_jsonRpcClient, this)), m_tagsManager(new TagsManager(m_jsonRpcClient, this)), m_nymeaConfiguration(new NymeaConfiguration(m_jsonRpcClient, this)), - m_systemController(new SystemController(m_jsonRpcClient, this)) + m_systemController(new SystemController(m_jsonRpcClient, this)), + m_usersManager(new UsersManager(m_jsonRpcClient, this)) { m_connection->registerTransport(new TcpSocketTransportFactory()); m_connection->registerTransport(new WebsocketTransportFactory()); @@ -122,6 +124,11 @@ SystemController *Engine::systemController() const return m_systemController; } +UsersManager *Engine::usersManager() const +{ + return m_usersManager; +} + void Engine::deployCertificate() { if (!m_jsonRpcClient->connected()) { diff --git a/libnymea-app-core/engine.h b/libnymea-app-core/engine.h index 70e87dd0..b077678e 100644 --- a/libnymea-app-core/engine.h +++ b/libnymea-app-core/engine.h @@ -45,6 +45,7 @@ class TagsManager; class NymeaConfiguration; class SystemController; class NetworkManager; +class UsersManager; class Engine : public QObject { @@ -57,6 +58,7 @@ class Engine : public QObject Q_PROPERTY(JsonRpcClient* jsonRpcClient READ jsonRpcClient CONSTANT) Q_PROPERTY(NymeaConfiguration* nymeaConfiguration READ nymeaConfiguration CONSTANT) Q_PROPERTY(SystemController* systemController READ systemController CONSTANT) + Q_PROPERTY(UsersManager* usersManager READ usersManager CONSTANT) public: explicit Engine(QObject *parent = nullptr); @@ -73,6 +75,7 @@ public: LogManager *logManager() const; NymeaConfiguration *nymeaConfiguration() const; SystemController *systemController() const; + UsersManager *usersManager() const; Q_INVOKABLE void deployCertificate(); @@ -86,6 +89,7 @@ private: TagsManager *m_tagsManager; NymeaConfiguration *m_nymeaConfiguration; SystemController *m_systemController; + UsersManager *m_usersManager; private slots: void onConnectedChanged(); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index c1fc4788..ebf067f5 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -90,6 +90,8 @@ SOURCES += \ configuration/mqttpolicies.cpp \ models/devicemodel.cpp \ system/systemcontroller.cpp \ + usersmanager.cpp \ + HEADERS += \ configuration/networkmanager.h \ @@ -157,6 +159,7 @@ HEADERS += \ configuration/mqttpolicies.h \ models/devicemodel.h \ system/systemcontroller.h \ + usersmanager.h \ ubports: { DEFINES += UBPORTS diff --git a/libnymea-app-core/usersmanager.cpp b/libnymea-app-core/usersmanager.cpp new file mode 100644 index 00000000..da931238 --- /dev/null +++ b/libnymea-app-core/usersmanager.cpp @@ -0,0 +1,20 @@ +#include "usersmanager.h" + +#include + +UsersManager::UsersManager(JsonRpcClient *client, QObject *parent): + JsonHandler(parent), + m_jsonRpcClient(client) +{ + m_jsonRpcClient->registerNotificationHandler(this, "notificationReceived"); +} + +QString UsersManager::nameSpace() const +{ + return "Users"; +} + +void UsersManager::notificationReceived(const QVariantMap &data) +{ + qDebug() << "Users notification" << data; +} diff --git a/libnymea-app-core/usersmanager.h b/libnymea-app-core/usersmanager.h new file mode 100644 index 00000000..5b3684cc --- /dev/null +++ b/libnymea-app-core/usersmanager.h @@ -0,0 +1,23 @@ +#ifndef USERSMANAGER_H +#define USERSMANAGER_H + +#include + +#include "jsonrpc/jsonrpcclient.h" + +class UsersManager: public JsonHandler +{ + Q_OBJECT +public: + explicit UsersManager(JsonRpcClient *client, QObject *parent = nullptr); + + QString nameSpace() const override; + +private slots: + void notificationReceived(const QVariantMap &data); + +private: + JsonRpcClient *m_jsonRpcClient = nullptr; +}; + +#endif // USERSMANAGER_H diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 966f82c6..32b0a312 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -208,5 +208,6 @@ ui/magic/scripting/LineNumbers.qml ui/magic/scripting/CompletionBox.qml ui/magic/scripting/EditorPane.qml + ui/system/UsersSettingsPage.qml diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 6a68d943..59a31f73 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -70,6 +70,21 @@ Page { } } + Pane { + Layout.fillWidth: true + Material.elevation: layout.isGrid ? 1 : 0 + padding: 0 + NymeaListItemDelegate { + width: parent.width + iconName: "../images/lock-closed.svg" + text: qsTr("Authentication") + subText: qsTr("Configure who can log in") + prominentSubText: false + wrapTexts: false + onClicked: pageStack.push(Qt.resolvedUrl("system/UsersSettingsPage.qml")) + } + } + Pane { Layout.fillWidth: true Material.elevation: layout.isGrid ? 1 : 0 diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml new file mode 100644 index 00000000..97aecc29 --- /dev/null +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -0,0 +1,237 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +Page { + id: root + header: NymeaHeader { + text: qsTr("Authentication") + backButtonVisible: true + onBackPressed: pageStack.pop() + } + + ColumnLayout { + id: settingsGrid + anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; margins: app.margins } + width: Math.min(500, parent.width - app.margins * 2) + + RowLayout { + Layout.fillWidth: true + spacing: app.margins + Label { + text: qsTr("Name") + } + TextField { + id: nameTextField + Layout.fillWidth: true + text: engine.nymeaConfiguration.serverName + } + Button { + text: qsTr("OK") + visible: nameTextField.displayText !== engine.nymeaConfiguration.serverName + onClicked: engine.nymeaConfiguration.serverName = nameTextField.displayText + } + } + + RowLayout { + Layout.fillWidth: true + visible: engine.jsonRpcClient.ensureServerVersion("4.1") && engine.systemController.automaticTimeAvailable + Label { + text: qsTr("Set date and time automatically") + Layout.fillWidth: true + } + CheckBox { + checked: engine.systemController.automaticTime + onClicked: { + engine.systemController.automaticTime = checked + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: app.margins + Layout.preferredHeight: dateButton.implicitHeight + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + Label { + text: qsTr("Date") + Layout.fillWidth: true + } + Label { + text: engine.systemController.serverTime.toLocaleDateString() + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + Button { + id: dateButton + visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable + contentItem: Item { + ColorIcon { + name: "../images/edit.svg" + color: app.foregroundColor + anchors.centerIn: parent + height: parent.height + width: height + } + } + + onClicked: { + var popup = datePickerComponent.createObject(root, {dateTime: engine.systemController.serverTime}) + popup.accepted.connect(function() { + print("setting new date", popup.dateTime) + engine.systemController.serverTime = popup.dateTime + }) + popup.open(); + + } + } + } + RowLayout { + Layout.fillWidth: true + spacing: app.margins + Layout.preferredHeight: timeButton.implicitHeight + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + Label { + text: qsTr("Time") + Layout.fillWidth: true + } + Label { + text: engine.systemController.serverTime.toLocaleTimeString(/*Locale.ShortTimeString*/) + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + Button { + id: timeButton + visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable + contentItem: Item { + ColorIcon { + name: "../images/edit.svg" + color: app.foregroundColor + anchors.centerIn: parent + height: parent.height + width: height + } + } + + onClicked: { + var popup = timePickerComponent.createObject(root, {hour: engine.systemController.serverTime.getHours(), minute: engine.systemController.serverTime.getMinutes()}) + popup.accepted.connect(function() { + var date = new Date(engine.systemController.serverTime) + date.setHours(popup.hour); + date.setMinutes(popup.minute) + engine.systemController.serverTime = date; + }) + popup.open(); + + } + } + } + + + RowLayout { + Layout.fillWidth: true + spacing: app.margins + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + Label { + Layout.fillWidth: true + text: qsTr("Time zone") + } + ComboBox { + Layout.minimumWidth: 200 + model: engine.systemController.timeZones + currentIndex: model.indexOf(engine.systemController.serverTimeZone) + onActivated: { + engine.systemController.serverTimeZone = currentText; + } + } + } + + Button { + Layout.fillWidth: true + text: qsTr("Reboot %1:core").arg(app.systemName) + visible: engine.systemController.powerManagementAvailable + onClicked: { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("Are you sure you want to reboot your %1:core sytem now?").arg(app.systemName) + var popup = dialog.createObject(app, + { + headerIcon: "../images/dialog-warning-symbolic.svg", + title: qsTr("Reboot %1:core").arg(app.systemName), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + engine.systemController.reboot() + }) + } + } + Button { + Layout.fillWidth: true + text: qsTr("Shutdown %1:core").arg(app.systemName) + visible: engine.systemController.powerManagementAvailable + onClicked: { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("Are you sure you want to shut down your %1:core sytem now?").arg(app.systemName) + var popup = dialog.createObject(app, + { + headerIcon: "../images/dialog-warning-symbolic.svg", + title: qsTr("Shut down %1:core").arg(app.systemName), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + engine.systemController.shutdown() + }) + } + } + } + + Component { + id: timePickerComponent + Dialog { + id: timePicker + property int maxSize: Math.min(parent.width, parent.height) + property int size: Math.min(maxSize, 500) + property alias hour: p.hour + property alias minute: p.minute + width: size - 80 + height: size + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + TimePicker { + id: p + width: parent.width + height: parent.height + } + standardButtons: Dialog.Ok | Dialog.Cancel + } + } + + Component { + id: datePickerComponent + Dialog { + id: datePicker + property int maxSize: Math.min(parent.width, parent.height) + property int size: Math.min(maxSize, 500) + property alias dateTime: p.date + width: size - 80 + height: size + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + DatePicker { + id: p + width: parent.width + height: parent.height + date: datePicker.dateTime + } + standardButtons: Dialog.Ok | Dialog.Cancel + } + } +} From 1c10ee5334c54ca27874fd4d026705b0109a62f0 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 4 Feb 2020 11:35:25 +0100 Subject: [PATCH 2/4] More work on the user manager --- libnymea-app-core/engine.cpp | 9 +- libnymea-app-core/engine.h | 4 - libnymea-app-core/libnymea-app-core.h | 7 + libnymea-app-core/libnymea-app-core.pro | 4 +- libnymea-app-core/usermanager.cpp | 64 ++++++ libnymea-app-core/usermanager.h | 47 +++++ libnymea-app-core/usersmanager.cpp | 20 -- libnymea-app-core/usersmanager.h | 23 --- libnymea-common/libnymea-common.pro | 7 + libnymea-common/types/tokeninfo.cpp | 31 +++ libnymea-common/types/tokeninfo.h | 26 +++ libnymea-common/types/tokeninfos.cpp | 47 +++++ libnymea-common/types/tokeninfos.h | 34 ++++ nymea-app/ui/system/UsersSettingsPage.qml | 229 ++-------------------- 14 files changed, 282 insertions(+), 270 deletions(-) create mode 100644 libnymea-app-core/usermanager.cpp create mode 100644 libnymea-app-core/usermanager.h delete mode 100644 libnymea-app-core/usersmanager.cpp delete mode 100644 libnymea-app-core/usersmanager.h create mode 100644 libnymea-common/types/tokeninfo.cpp create mode 100644 libnymea-common/types/tokeninfo.h create mode 100644 libnymea-common/types/tokeninfos.cpp create mode 100644 libnymea-common/types/tokeninfos.h diff --git a/libnymea-app-core/engine.cpp b/libnymea-app-core/engine.cpp index 68dcdd24..f3d07012 100644 --- a/libnymea-app-core/engine.cpp +++ b/libnymea-app-core/engine.cpp @@ -38,7 +38,6 @@ #include "connection/awsclient.h" #include "system/systemcontroller.h" #include "configuration/networkmanager.h" -#include "usersmanager.h" #include "connection/tcpsockettransport.h" #include "connection/websockettransport.h" @@ -55,8 +54,7 @@ Engine::Engine(QObject *parent) : m_logManager(new LogManager(m_jsonRpcClient, this)), m_tagsManager(new TagsManager(m_jsonRpcClient, this)), m_nymeaConfiguration(new NymeaConfiguration(m_jsonRpcClient, this)), - m_systemController(new SystemController(m_jsonRpcClient, this)), - m_usersManager(new UsersManager(m_jsonRpcClient, this)) + m_systemController(new SystemController(m_jsonRpcClient, this)) { m_connection->registerTransport(new TcpSocketTransportFactory()); m_connection->registerTransport(new WebsocketTransportFactory()); @@ -124,11 +122,6 @@ SystemController *Engine::systemController() const return m_systemController; } -UsersManager *Engine::usersManager() const -{ - return m_usersManager; -} - void Engine::deployCertificate() { if (!m_jsonRpcClient->connected()) { diff --git a/libnymea-app-core/engine.h b/libnymea-app-core/engine.h index b077678e..70e87dd0 100644 --- a/libnymea-app-core/engine.h +++ b/libnymea-app-core/engine.h @@ -45,7 +45,6 @@ class TagsManager; class NymeaConfiguration; class SystemController; class NetworkManager; -class UsersManager; class Engine : public QObject { @@ -58,7 +57,6 @@ class Engine : public QObject Q_PROPERTY(JsonRpcClient* jsonRpcClient READ jsonRpcClient CONSTANT) Q_PROPERTY(NymeaConfiguration* nymeaConfiguration READ nymeaConfiguration CONSTANT) Q_PROPERTY(SystemController* systemController READ systemController CONSTANT) - Q_PROPERTY(UsersManager* usersManager READ usersManager CONSTANT) public: explicit Engine(QObject *parent = nullptr); @@ -75,7 +73,6 @@ public: LogManager *logManager() const; NymeaConfiguration *nymeaConfiguration() const; SystemController *systemController() const; - UsersManager *usersManager() const; Q_INVOKABLE void deployCertificate(); @@ -89,7 +86,6 @@ private: TagsManager *m_tagsManager; NymeaConfiguration *m_nymeaConfiguration; SystemController *m_systemController; - UsersManager *m_usersManager; private slots: void onConnectedChanged(); diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index a4d8a699..fc65b699 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -105,6 +105,9 @@ #include "types/script.h" #include "types/scripts.h" #include "types/types.h" +#include "usermanager.h" +#include "types/tokeninfos.h" +#include "types/tokeninfo.h" #include @@ -281,6 +284,10 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "ScriptSyntaxHighlighter"); qmlRegisterType(uri, 1, 0, "CodeCompletion"); qmlRegisterUncreatableType(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter"); + + qmlRegisterType(uri, 1, 0, "UserManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TokenInfos", "Get it from UserManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TokenInfo", "Get it from TokenInfos"); } #endif // LIBNYMEAAPPCORE_H diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index ebf067f5..663b8bd1 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -55,6 +55,7 @@ SOURCES += \ scripting/completionmodel.cpp \ scriptmanager.cpp \ scriptsyntaxhighlighter.cpp \ + usermanager.cpp \ vendorsproxy.cpp \ pluginsproxy.cpp \ interfacesmodel.cpp \ @@ -90,7 +91,6 @@ SOURCES += \ configuration/mqttpolicies.cpp \ models/devicemodel.cpp \ system/systemcontroller.cpp \ - usersmanager.cpp \ HEADERS += \ @@ -124,6 +124,7 @@ HEADERS += \ scripting/completionmodel.h \ scriptmanager.h \ scriptsyntaxhighlighter.h \ + usermanager.h \ vendorsproxy.h \ pluginsproxy.h \ interfacesmodel.h \ @@ -159,7 +160,6 @@ HEADERS += \ configuration/mqttpolicies.h \ models/devicemodel.h \ system/systemcontroller.h \ - usersmanager.h \ ubports: { DEFINES += UBPORTS diff --git a/libnymea-app-core/usermanager.cpp b/libnymea-app-core/usermanager.cpp new file mode 100644 index 00000000..e4355e2c --- /dev/null +++ b/libnymea-app-core/usermanager.cpp @@ -0,0 +1,64 @@ +#include "usermanager.h" +#include "types/tokeninfo.h" + +#include + +UserManager::UserManager(QObject *parent): + JsonHandler(parent) +{ + m_tokenInfos = new TokenInfos(this); +} + +Engine *UserManager::engine() const +{ + return m_engine; +} + +void UserManager::setEngine(Engine *engine) +{ + if (m_engine != engine) { + m_engine = engine; + m_engine->jsonRpcClient()->registerNotificationHandler(this, "notificationReceived"); + emit engineChanged(); + + m_loading = true; + emit loadingChanged(); + + m_engine->jsonRpcClient()->sendCommand("Users.GetTokens", QVariantMap(), this, "getTokensReply"); + } +} + +bool UserManager::loading() const +{ + return m_loading; +} + +TokenInfos *UserManager::tokenInfos() const +{ + return m_tokenInfos; +} + +QString UserManager::nameSpace() const +{ + return "Users"; +} + +void UserManager::notificationReceived(const QVariantMap &data) +{ + qDebug() << "Users notification" << data; +} + +void UserManager::getTokensReply(const QVariantMap ¶ms) +{ + + foreach (const QVariant &tokenVariant, params.value("params").toMap().value("tokenInfoList").toList()) { + qDebug() << "Token received" << tokenVariant.toMap(); + QVariantMap token = tokenVariant.toMap(); + QUuid id = token.value("id").toString(); + QString username = token.value("username").toString(); + QString deviceName = token.value("deviceName").toString(); + QDateTime creationTime = QDateTime::fromSecsSinceEpoch(token.value("creationTime").toInt()); + TokenInfo *tokenInfo = new TokenInfo(id, username, deviceName, creationTime); + m_tokenInfos->addToken(tokenInfo); + } +} diff --git a/libnymea-app-core/usermanager.h b/libnymea-app-core/usermanager.h new file mode 100644 index 00000000..3850963f --- /dev/null +++ b/libnymea-app-core/usermanager.h @@ -0,0 +1,47 @@ +#ifndef USERMANAGER_H +#define USERMANAGER_H + +#include + +#include "jsonrpc/jsonrpcclient.h" +#include "engine.h" + +#include "types/tokeninfos.h" + +class UserManager: public JsonHandler +{ + Q_OBJECT + Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + + Q_PROPERTY(TokenInfos* tokenInfos READ tokenInfos CONSTANT) + +public: + explicit UserManager(QObject *parent = nullptr); + + Engine* engine() const; + void setEngine(Engine* engine); + + bool loading() const; + + TokenInfos* tokenInfos() const; + + QString nameSpace() const override; + +signals: + void engineChanged(); + void loadingChanged(); + +private slots: + void notificationReceived(const QVariantMap &data); + + void getTokensReply(const QVariantMap ¶ms); + +private: + Engine *m_engine = nullptr; + bool m_loading = false; + + TokenInfos *m_tokenInfos = nullptr; +}; + +#endif // USERMANAGER_H diff --git a/libnymea-app-core/usersmanager.cpp b/libnymea-app-core/usersmanager.cpp deleted file mode 100644 index da931238..00000000 --- a/libnymea-app-core/usersmanager.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "usersmanager.h" - -#include - -UsersManager::UsersManager(JsonRpcClient *client, QObject *parent): - JsonHandler(parent), - m_jsonRpcClient(client) -{ - m_jsonRpcClient->registerNotificationHandler(this, "notificationReceived"); -} - -QString UsersManager::nameSpace() const -{ - return "Users"; -} - -void UsersManager::notificationReceived(const QVariantMap &data) -{ - qDebug() << "Users notification" << data; -} diff --git a/libnymea-app-core/usersmanager.h b/libnymea-app-core/usersmanager.h deleted file mode 100644 index 5b3684cc..00000000 --- a/libnymea-app-core/usersmanager.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef USERSMANAGER_H -#define USERSMANAGER_H - -#include - -#include "jsonrpc/jsonrpcclient.h" - -class UsersManager: public JsonHandler -{ - Q_OBJECT -public: - explicit UsersManager(JsonRpcClient *client, QObject *parent = nullptr); - - QString nameSpace() const override; - -private slots: - void notificationReceived(const QVariantMap &data); - -private: - JsonRpcClient *m_jsonRpcClient = nullptr; -}; - -#endif // USERSMANAGER_H diff --git a/libnymea-common/libnymea-common.pro b/libnymea-common/libnymea-common.pro index 26206534..407e6d01 100644 --- a/libnymea-common/libnymea-common.pro +++ b/libnymea-common/libnymea-common.pro @@ -64,6 +64,9 @@ HEADERS += \ types/tags.h \ types/wirelessaccesspoint.h \ types/wirelessaccesspoints.h \ + types/tokeninfo.h \ + types/tokeninfos.h \ + SOURCES += \ types/browseritem.cpp \ @@ -122,3 +125,7 @@ SOURCES += \ types/tags.cpp \ types/wirelessaccesspoint.cpp \ types/wirelessaccesspoints.cpp \ + types/tokeninfo.cpp \ + types/tokeninfos.cpp \ + + diff --git a/libnymea-common/types/tokeninfo.cpp b/libnymea-common/types/tokeninfo.cpp new file mode 100644 index 00000000..b7206cfc --- /dev/null +++ b/libnymea-common/types/tokeninfo.cpp @@ -0,0 +1,31 @@ +#include "tokeninfo.h" + +TokenInfo::TokenInfo(const QUuid &id, const QString &username, const QString &deviceName, const QDateTime &creationTime, QObject *parent): + QObject(parent), + m_id(id), + m_username(username), + m_deviceName(deviceName), + m_creationTime(creationTime) +{ + +} + +QUuid TokenInfo::id() const +{ + return m_id; +} + +QString TokenInfo::username() const +{ + return m_username; +} + +QString TokenInfo::deviceName() const +{ + return m_deviceName; +} + +QDateTime TokenInfo::creationTime() const +{ + return m_creationTime; +} diff --git a/libnymea-common/types/tokeninfo.h b/libnymea-common/types/tokeninfo.h new file mode 100644 index 00000000..c27f10f3 --- /dev/null +++ b/libnymea-common/types/tokeninfo.h @@ -0,0 +1,26 @@ +#ifndef TOKENINFO_H +#define TOKENINFO_H + +#include +#include +#include + +class TokenInfo : public QObject +{ + Q_OBJECT +public: + explicit TokenInfo(const QUuid &id, const QString &username, const QString &deviceName, const QDateTime &creationTime, QObject *parent = nullptr); + + QUuid id() const; + QString username() const; + QString deviceName() const; + QDateTime creationTime() const; + +private: + QUuid m_id; + QString m_username; + QString m_deviceName; + QDateTime m_creationTime; +}; + +#endif // TOKENINFO_H diff --git a/libnymea-common/types/tokeninfos.cpp b/libnymea-common/types/tokeninfos.cpp new file mode 100644 index 00000000..f9430435 --- /dev/null +++ b/libnymea-common/types/tokeninfos.cpp @@ -0,0 +1,47 @@ +#include "tokeninfos.h" +#include "tokeninfo.h" + +TokenInfos::TokenInfos(QObject *parent) : QAbstractListModel(parent) +{ + +} + +int TokenInfos::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant TokenInfos::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleId: + return m_list.at(index.row())->id(); + case RoleUsername: + return m_list.at(index.row())->username(); + case RoleDeviceName: + return m_list.at(index.row())->deviceName(); + case RoleCreationTime: + return m_list.at(index.row())->creationTime(); + } + return QVariant(); +} + +QHash TokenInfos::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleUsername, "username"); + roles.insert(RoleDeviceName, "deviceName"); + roles.insert(RoleCreationTime, "creationTime"); + return roles; +} + +void TokenInfos::addToken(TokenInfo *tokenInfo) +{ + tokenInfo->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(tokenInfo); + endInsertRows(); + emit countChanged(); +} diff --git a/libnymea-common/types/tokeninfos.h b/libnymea-common/types/tokeninfos.h new file mode 100644 index 00000000..a1c332ba --- /dev/null +++ b/libnymea-common/types/tokeninfos.h @@ -0,0 +1,34 @@ +#ifndef TOKENINFOS_H +#define TOKENINFOS_H + +#include + +class TokenInfo; + +class TokenInfos : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleId, + RoleUsername, + RoleDeviceName, + RoleCreationTime + }; + + explicit TokenInfos(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void addToken(TokenInfo *tokenInfo); +signals: + void countChanged(); + +private: + QList m_list; +}; + +#endif // TOKENINFOS_H diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index 97aecc29..717a7e11 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -8,230 +8,33 @@ import "../components" Page { id: root header: NymeaHeader { - text: qsTr("Authentication") + text: qsTr("Authentication") + userManager.tokenInfos.count backButtonVisible: true onBackPressed: pageStack.pop() } + UserManager { + id: userManager + engine: _engine + } + ColumnLayout { id: settingsGrid - anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; margins: app.margins } - width: Math.min(500, parent.width - app.margins * 2) + anchors.fill: parent +// width: Math.min(500, parent.width - app.margins * 2) - RowLayout { + ListView { Layout.fillWidth: true - spacing: app.margins - Label { - text: qsTr("Name") - } - TextField { - id: nameTextField - Layout.fillWidth: true - text: engine.nymeaConfiguration.serverName - } - Button { - text: qsTr("OK") - visible: nameTextField.displayText !== engine.nymeaConfiguration.serverName - onClicked: engine.nymeaConfiguration.serverName = nameTextField.displayText - } - } + Layout.fillHeight: true + model: userManager.tokenInfos - RowLayout { - Layout.fillWidth: true - visible: engine.jsonRpcClient.ensureServerVersion("4.1") && engine.systemController.automaticTimeAvailable - Label { - text: qsTr("Set date and time automatically") - Layout.fillWidth: true - } - CheckBox { - checked: engine.systemController.automaticTime - onClicked: { - engine.systemController.automaticTime = checked - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Layout.preferredHeight: dateButton.implicitHeight - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - Label { - text: qsTr("Date") - Layout.fillWidth: true - } - Label { - text: engine.systemController.serverTime.toLocaleDateString() - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - } - Button { - id: dateButton - visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable - contentItem: Item { - ColorIcon { - name: "../images/edit.svg" - color: app.foregroundColor - anchors.centerIn: parent - height: parent.height - width: height - } - } - - onClicked: { - var popup = datePickerComponent.createObject(root, {dateTime: engine.systemController.serverTime}) - popup.accepted.connect(function() { - print("setting new date", popup.dateTime) - engine.systemController.serverTime = popup.dateTime - }) - popup.open(); - - } - } - } - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Layout.preferredHeight: timeButton.implicitHeight - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - Label { - text: qsTr("Time") - Layout.fillWidth: true - } - Label { - text: engine.systemController.serverTime.toLocaleTimeString(/*Locale.ShortTimeString*/) - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - } - Button { - id: timeButton - visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable - contentItem: Item { - ColorIcon { - name: "../images/edit.svg" - color: app.foregroundColor - anchors.centerIn: parent - height: parent.height - width: height - } - } - - onClicked: { - var popup = timePickerComponent.createObject(root, {hour: engine.systemController.serverTime.getHours(), minute: engine.systemController.serverTime.getMinutes()}) - popup.accepted.connect(function() { - var date = new Date(engine.systemController.serverTime) - date.setHours(popup.hour); - date.setMinutes(popup.minute) - engine.systemController.serverTime = date; - }) - popup.open(); - - } - } - } - - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - Label { - Layout.fillWidth: true - text: qsTr("Time zone") - } - ComboBox { - Layout.minimumWidth: 200 - model: engine.systemController.timeZones - currentIndex: model.indexOf(engine.systemController.serverTimeZone) - onActivated: { - engine.systemController.serverTimeZone = currentText; - } - } - } - - Button { - Layout.fillWidth: true - text: qsTr("Reboot %1:core").arg(app.systemName) - visible: engine.systemController.powerManagementAvailable - onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("Are you sure you want to reboot your %1:core sytem now?").arg(app.systemName) - var popup = dialog.createObject(app, - { - headerIcon: "../images/dialog-warning-symbolic.svg", - title: qsTr("Reboot %1:core").arg(app.systemName), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - engine.systemController.reboot() - }) - } - } - Button { - Layout.fillWidth: true - text: qsTr("Shutdown %1:core").arg(app.systemName) - visible: engine.systemController.powerManagementAvailable - onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("Are you sure you want to shut down your %1:core sytem now?").arg(app.systemName) - var popup = dialog.createObject(app, - { - headerIcon: "../images/dialog-warning-symbolic.svg", - title: qsTr("Shut down %1:core").arg(app.systemName), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - engine.systemController.shutdown() - }) - } - } - } - - Component { - id: timePickerComponent - Dialog { - id: timePicker - property int maxSize: Math.min(parent.width, parent.height) - property int size: Math.min(maxSize, 500) - property alias hour: p.hour - property alias minute: p.minute - width: size - 80 - height: size - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - - TimePicker { - id: p + delegate: NymeaListItemDelegate { width: parent.width - height: parent.height + text: model.deviceName + subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate)) + prominentSubText: false + progressive: false } - standardButtons: Dialog.Ok | Dialog.Cancel - } - } - - Component { - id: datePickerComponent - Dialog { - id: datePicker - property int maxSize: Math.min(parent.width, parent.height) - property int size: Math.min(maxSize, 500) - property alias dateTime: p.date - width: size - 80 - height: size - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - - DatePicker { - id: p - width: parent.width - height: parent.height - date: datePicker.dateTime - } - standardButtons: Dialog.Ok | Dialog.Cancel } } } From 5086d1373a060d859525ea4d04973f73f380afcb Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 6 Feb 2020 12:37:04 +0100 Subject: [PATCH 3/4] finish user settings --- libnymea-app-core/libnymea-app-core.h | 4 +- libnymea-app-core/usermanager.cpp | 71 +- libnymea-app-core/usermanager.h | 28 +- libnymea-common/libnymea-common.pro | 2 + libnymea-common/types/tokeninfos.cpp | 13 + libnymea-common/types/tokeninfos.h | 2 + libnymea-common/types/userinfo.cpp | 27 + libnymea-common/types/userinfo.h | 25 + nymea-app/images.qrc | 1 + nymea-app/resources.qrc | 2 + nymea-app/ui/SettingsPage.qml | 5 +- nymea-app/ui/appsettings/AboutPage.qml | 63 +- nymea-app/ui/appsettings/AppSettingsPage.qml | 3 +- nymea-app/ui/appsettings/CloudLoginPage.qml | 619 ++++++++---------- .../ui/appsettings/DeveloperOptionsPage.qml | 80 +-- .../appsettings/LookAndFeelSettingsPage.qml | 316 +++++---- nymea-app/ui/components/PasswordTextField.qml | 2 +- nymea-app/ui/components/SettingsPageBase.qml | 65 ++ .../components/SettingsPageSectionHeader.qml | 43 ++ nymea-app/ui/connection/LoginPage.qml | 4 +- nymea-app/ui/images/key.svg | 170 +++++ nymea-app/ui/system/AboutNymeaPage.qml | 92 ++- nymea-app/ui/system/CloudSettingsPage.qml | 190 +++--- .../ui/system/ConnectionInterfacesPage.qml | 206 +++--- nymea-app/ui/system/DeveloperTools.qml | 123 ++-- nymea-app/ui/system/GeneralSettingsPage.qml | 363 +++++----- .../ui/system/MqttBrokerSettingsPage.qml | 218 +++--- nymea-app/ui/system/MqttPolicyPage.qml | 325 ++++----- nymea-app/ui/system/NetworkSettingsPage.qml | 538 ++++++++------- nymea-app/ui/system/PluginsPage.qml | 52 +- nymea-app/ui/system/UsersSettingsPage.qml | 131 +++- nymea-app/ui/system/WebServerSettingsPage.qml | 104 ++- 32 files changed, 2155 insertions(+), 1732 deletions(-) create mode 100644 libnymea-common/types/userinfo.cpp create mode 100644 libnymea-common/types/userinfo.h create mode 100644 nymea-app/ui/components/SettingsPageBase.qml create mode 100644 nymea-app/ui/components/SettingsPageSectionHeader.qml create mode 100644 nymea-app/ui/images/key.svg diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index fc65b699..bb98dc68 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -108,6 +108,7 @@ #include "usermanager.h" #include "types/tokeninfos.h" #include "types/tokeninfo.h" +#include "types/userinfo.h" #include @@ -286,8 +287,9 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter"); qmlRegisterType(uri, 1, 0, "UserManager"); - qmlRegisterUncreatableType(uri, 1, 0, "TokenInfos", "Get it from UserManager"); + qmlRegisterUncreatableType(uri, 1, 0, "UserInfo", "Get it from UserManager"); qmlRegisterUncreatableType(uri, 1, 0, "TokenInfo", "Get it from TokenInfos"); + qmlRegisterUncreatableType(uri, 1, 0, "TokenInfos", "Get it from UserManager"); } #endif // LIBNYMEAAPPCORE_H diff --git a/libnymea-app-core/usermanager.cpp b/libnymea-app-core/usermanager.cpp index e4355e2c..4693e372 100644 --- a/libnymea-app-core/usermanager.cpp +++ b/libnymea-app-core/usermanager.cpp @@ -2,10 +2,12 @@ #include "types/tokeninfo.h" #include +#include UserManager::UserManager(QObject *parent): JsonHandler(parent) { + m_userInfo = new UserInfo(this); m_tokenInfos = new TokenInfos(this); } @@ -24,6 +26,7 @@ void UserManager::setEngine(Engine *engine) m_loading = true; emit loadingChanged(); + m_engine->jsonRpcClient()->sendCommand("Users.GetUserInfo", QVariantMap(), this, "getUserInfoReply"); m_engine->jsonRpcClient()->sendCommand("Users.GetTokens", QVariantMap(), this, "getTokensReply"); } } @@ -33,6 +36,11 @@ bool UserManager::loading() const return m_loading; } +UserInfo *UserManager::userInfo() const +{ + return m_userInfo; +} + TokenInfos *UserManager::tokenInfos() const { return m_tokenInfos; @@ -43,16 +51,40 @@ QString UserManager::nameSpace() const return "Users"; } +int UserManager::changePassword(const QString &newPassword) +{ + QVariantMap params; + params.insert("newPassword", newPassword); + int callId = m_engine->jsonRpcClient()->sendCommand("Users.ChangePassword", params, this, "changePasswordReply"); + return callId; +} + +int UserManager::removeToken(const QUuid &id) +{ + QVariantMap params; + params.insert("tokenId", id); + int callId = m_engine->jsonRpcClient()->sendCommand("Users.RemoveToken", params, this, "deleteTokenReply"); + m_tokensToBeRemoved.insert(callId, id); + return callId; +} + void UserManager::notificationReceived(const QVariantMap &data) { qDebug() << "Users notification" << data; } -void UserManager::getTokensReply(const QVariantMap ¶ms) +void UserManager::getUserInfoReply(const QVariantMap &data) +{ + qDebug() << "User info reply" << data; + + m_userInfo->setUsername(data.value("params").toMap().value("userInfo").toMap().value("username").toString()); +} + +void UserManager::getTokensReply(const QVariantMap &data) { - foreach (const QVariant &tokenVariant, params.value("params").toMap().value("tokenInfoList").toList()) { - qDebug() << "Token received" << tokenVariant.toMap(); + foreach (const QVariant &tokenVariant, data.value("params").toMap().value("tokenInfoList").toList()) { + // qDebug() << "Token received" << tokenVariant.toMap(); QVariantMap token = tokenVariant.toMap(); QUuid id = token.value("id").toString(); QString username = token.value("username").toString(); @@ -61,4 +93,37 @@ void UserManager::getTokensReply(const QVariantMap ¶ms) TokenInfo *tokenInfo = new TokenInfo(id, username, deviceName, creationTime); m_tokenInfos->addToken(tokenInfo); } + +} + + + +void UserManager::deleteTokenReply(const QVariantMap &payload) +{ + qDebug() << "Delete token reply" << payload; + int callId = payload.value("id").toInt(); + QUuid tokenId = m_tokensToBeRemoved.take(callId); + QVariantMap params = payload.value("params").toMap(); + QString errorString = params.value("error").toString(); + QMetaEnum metaEnum = QMetaEnum::fromType(); + UserError error = static_cast(metaEnum.keyToValue(errorString.toUtf8())); + + emit deleteTokenResponse(callId, error); + + if (error == UserErrorNoError) { + m_tokenInfos->removeToken(tokenId); + } +} + +void UserManager::changePasswordReply(const QVariantMap &data) +{ + qDebug() << "Change password reply" << data; + + int callId = data.value("id").toInt(); + QVariantMap params = data.value("params").toMap(); + QString errorString = params.value("error").toString(); + QMetaEnum metaEnum = QMetaEnum::fromType(); + UserError error = static_cast(metaEnum.keyToValue(errorString.toUtf8())); + + emit changePasswordResponse(callId, error); } diff --git a/libnymea-app-core/usermanager.h b/libnymea-app-core/usermanager.h index 3850963f..d66cd44a 100644 --- a/libnymea-app-core/usermanager.h +++ b/libnymea-app-core/usermanager.h @@ -7,6 +7,7 @@ #include "engine.h" #include "types/tokeninfos.h" +#include "types/userinfo.h" class UserManager: public JsonHandler { @@ -14,9 +15,21 @@ class UserManager: public JsonHandler Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + Q_PROPERTY(UserInfo* userInfo READ userInfo CONSTANT) Q_PROPERTY(TokenInfos* tokenInfos READ tokenInfos CONSTANT) public: + enum UserError { + UserErrorNoError, + UserErrorBackendError, + UserErrorInvalidUserId, + UserErrorDuplicateUserId, + UserErrorBadPassword, + UserErrorTokenNotFound, + UserErrorPermissionDenied + }; + Q_ENUM(UserError) + explicit UserManager(QObject *parent = nullptr); Engine* engine() const; @@ -24,24 +37,37 @@ public: bool loading() const; + UserInfo* userInfo() const; TokenInfos* tokenInfos() const; QString nameSpace() const override; + Q_INVOKABLE int changePassword(const QString &newPassword); + Q_INVOKABLE int removeToken(const QUuid &id); + signals: void engineChanged(); void loadingChanged(); + void deleteTokenResponse(int id, UserError error); + void changePasswordResponse(int id, UserError error); + private slots: void notificationReceived(const QVariantMap &data); - void getTokensReply(const QVariantMap ¶ms); + void getUserInfoReply(const QVariantMap &data); + void getTokensReply(const QVariantMap &data); + void deleteTokenReply(const QVariantMap &data); + void changePasswordReply(const QVariantMap &data); private: Engine *m_engine = nullptr; bool m_loading = false; + UserInfo *m_userInfo = nullptr; TokenInfos *m_tokenInfos = nullptr; + + QHash m_tokensToBeRemoved; }; #endif // USERMANAGER_H diff --git a/libnymea-common/libnymea-common.pro b/libnymea-common/libnymea-common.pro index 407e6d01..1d5745ff 100644 --- a/libnymea-common/libnymea-common.pro +++ b/libnymea-common/libnymea-common.pro @@ -66,6 +66,7 @@ HEADERS += \ types/wirelessaccesspoints.h \ types/tokeninfo.h \ types/tokeninfos.h \ + types/userinfo.h \ SOURCES += \ @@ -127,5 +128,6 @@ SOURCES += \ types/wirelessaccesspoints.cpp \ types/tokeninfo.cpp \ types/tokeninfos.cpp \ + types/userinfo.cpp \ diff --git a/libnymea-common/types/tokeninfos.cpp b/libnymea-common/types/tokeninfos.cpp index f9430435..e66fb3a6 100644 --- a/libnymea-common/types/tokeninfos.cpp +++ b/libnymea-common/types/tokeninfos.cpp @@ -45,3 +45,16 @@ void TokenInfos::addToken(TokenInfo *tokenInfo) endInsertRows(); emit countChanged(); } + +void TokenInfos::removeToken(const QUuid &tokenId) +{ + for (int i = 0; i < m_list.count(); i++) { + if (m_list.at(i)->id() == tokenId) { + beginRemoveRows(QModelIndex(), i, i); + m_list.takeAt(i)->deleteLater(); + endRemoveRows(); + emit countChanged(); + return; + } + } +} diff --git a/libnymea-common/types/tokeninfos.h b/libnymea-common/types/tokeninfos.h index a1c332ba..ed666253 100644 --- a/libnymea-common/types/tokeninfos.h +++ b/libnymea-common/types/tokeninfos.h @@ -24,6 +24,8 @@ public: QHash roleNames() const override; void addToken(TokenInfo *tokenInfo); + void removeToken(const QUuid &tokenId); + signals: void countChanged(); diff --git a/libnymea-common/types/userinfo.cpp b/libnymea-common/types/userinfo.cpp new file mode 100644 index 00000000..8456d268 --- /dev/null +++ b/libnymea-common/types/userinfo.cpp @@ -0,0 +1,27 @@ +#include "userinfo.h" + +UserInfo::UserInfo(QObject *parent): + QObject(parent) +{ + +} + +UserInfo::UserInfo(const QString &username, QObject *parent): + QObject(parent), + m_username(username) +{ + +} + +QString UserInfo::username() const +{ + return m_username; +} + +void UserInfo::setUsername(const QString &username) +{ + if (m_username != username) { + m_username = username; + emit usernameChanged(); + } +} diff --git a/libnymea-common/types/userinfo.h b/libnymea-common/types/userinfo.h new file mode 100644 index 00000000..1d841da8 --- /dev/null +++ b/libnymea-common/types/userinfo.h @@ -0,0 +1,25 @@ +#ifndef USERINFO_H +#define USERINFO_H + +#include + +class UserInfo : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString username READ username NOTIFY usernameChanged) +public: + explicit UserInfo(QObject *parent = nullptr); + explicit UserInfo(const QString &username, QObject *parent = nullptr); + + QString username() const; + void setUsername(const QString &username); + +signals: + void usernameChanged(); + +private: + QString m_username; + +}; + +#endif // USERINFO_H diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index d06be5ee..2e07698e 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -216,5 +216,6 @@ ui/images/save.svg ui/images/edit-clear.svg ui/images/smartlock.svg + ui/images/key.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 32b0a312..47c5d301 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -209,5 +209,7 @@ ui/magic/scripting/CompletionBox.qml ui/magic/scripting/EditorPane.qml ui/system/UsersSettingsPage.qml + ui/components/SettingsPageBase.qml + ui/components/SettingsPageSectionHeader.qml diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 59a31f73..542834dc 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -74,10 +74,11 @@ Page { Layout.fillWidth: true Material.elevation: layout.isGrid ? 1 : 0 padding: 0 + visible: engine.jsonRpcClient.ensureServerVersion("4.2") NymeaListItemDelegate { width: parent.width - iconName: "../images/lock-closed.svg" - text: qsTr("Authentication") + iconName: "../images/account.svg" + text: qsTr("User settings") subText: qsTr("Configure who can log in") prominentSubText: false wrapTexts: false diff --git a/nymea-app/ui/appsettings/AboutPage.qml b/nymea-app/ui/appsettings/AboutPage.qml index b98c9c24..c39d014e 100644 --- a/nymea-app/ui/appsettings/AboutPage.qml +++ b/nymea-app/ui/appsettings/AboutPage.qml @@ -35,46 +35,37 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("About %1").arg(app.appName) - backButtonVisible: true - onBackPressed: pageStack.pop() - } + title: qsTr("About %1").arg(app.appName) - Flickable { - anchors.fill: parent - contentHeight: imprint.implicitHeight + Imprint { + id: imprint + Layout.fillWidth: true + title: app.appName + additionalLicenses: ListModel { + ListElement { license: "CC-BY-SA-3.0"; component: "Suru icons"; infoText: qsTr("Suru icons by Ubuntu"); platforms: "*" } + ListElement { license: "CC-BY-SA-3.0"; component: "Ubuntu font"; infoText: qsTr("Ubuntu font by Ubuntu"); platforms: "*" } + ListElement { license: "LGPL3"; component: "QtZeroConf"; infoText: qsTr("QtZeroConf library by Jonathan Bagg"); platforms: "android,ios,linux,osx" } + ListElement { license: "OpenSSL"; component: "OpenSSL"; infoText: qsTr("OpenSSL libraries by Eric Young"); platforms: "android,windows" } + ListElement { license: "OFL"; component: "Oswald font"; infoText: qsTr("Oswald font by The Oswald Project"); platforms: "*" } + } - Imprint { - id: imprint - width: parent.width - title: app.appName - additionalLicenses: ListModel { - ListElement { license: "CC-BY-SA-3.0"; component: "Suru icons"; infoText: qsTr("Suru icons by Ubuntu"); platforms: "*" } - ListElement { license: "CC-BY-SA-3.0"; component: "Ubuntu font"; infoText: qsTr("Ubuntu font by Ubuntu"); platforms: "*" } - ListElement { license: "LGPL3"; component: "QtZeroConf"; infoText: qsTr("QtZeroConf library by Jonathan Bagg"); platforms: "android,ios,linux,osx" } - ListElement { license: "OpenSSL"; component: "OpenSSL"; infoText: qsTr("OpenSSL libraries by Eric Young"); platforms: "android,windows" } - ListElement { license: "OFL"; component: "Oswald font"; infoText: qsTr("Oswald font by The Oswald Project"); platforms: "*" } - } + githubLink: "https://github.com/nymea/nymea-app" - githubLink: "https://github.com/nymea/nymea-app" - - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("App version:") - subText: appVersion - progressive: false - prominentSubText: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Qt version:") - subText: qtVersion + (qtBuildVersion !== qtVersion ? " (" + qsTr("Built with %1").arg(qtBuildVersion) + ")" : "") - progressive: false - prominentSubText: false - } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("App version:") + subText: appVersion + progressive: false + prominentSubText: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Qt version:") + subText: qtVersion + (qtBuildVersion !== qtVersion ? " (" + qsTr("Built with %1").arg(qtBuildVersion) + ")" : "") + progressive: false + prominentSubText: false } } } diff --git a/nymea-app/ui/appsettings/AppSettingsPage.qml b/nymea-app/ui/appsettings/AppSettingsPage.qml index 73292180..d90a6d0f 100644 --- a/nymea-app/ui/appsettings/AppSettingsPage.qml +++ b/nymea-app/ui/appsettings/AppSettingsPage.qml @@ -87,12 +87,11 @@ Page { Pane { Layout.fillWidth: true Material.elevation: layout.isGrid ? 1 : 0 - visible: settings.showHiddenOptions padding: 0 NymeaListItemDelegate { width: parent.width text: qsTr("Developer options") - subText: qsTr("Yeehaaa!") + subText: qsTr("Access tools for debugging and error reporting") iconName: "../images/sdk.svg" prominentSubText: false wrapTexts: false diff --git a/nymea-app/ui/appsettings/CloudLoginPage.qml b/nymea-app/ui/appsettings/CloudLoginPage.qml index c7411640..a3a7abf2 100644 --- a/nymea-app/ui/appsettings/CloudLoginPage.qml +++ b/nymea-app/ui/appsettings/CloudLoginPage.qml @@ -34,12 +34,9 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Cloud login") - onBackPressed: pageStack.pop() - } + title: qsTr("%1 cloud login").arg(app.appName) Component.onCompleted: { if (AWSClient.isLoggedIn) { @@ -50,13 +47,13 @@ Page { Connections { target: AWSClient onLoginResult: { - busyOverlay.shown = false; + root.busy = false; if (error === AWSClient.LoginErrorNoError) { AWSClient.fetchDevices(); } } onDeleteAccountResult: { - busyOverlay.shown = false; + root.busy = false; if (error !== AWSClient.LoginErrorNoError) { var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); var text = qsTr("Sorry, an error happened removing the account. Please try again later."); @@ -68,12 +65,15 @@ Page { } ColumnLayout { - anchors.fill: parent + Layout.fillWidth: true visible: AWSClient.isLoggedIn + SettingsPageSectionHeader { + text: qsTr("Login") + } + Label { Layout.fillWidth: true - Layout.topMargin: app.margins Layout.leftMargin: app.margins Layout.rightMargin: app.margins wrapMode: Text.WordWrap @@ -89,11 +89,20 @@ Page { } } - ThinDivider {} + RowLayout { + SettingsPageSectionHeader { + text: qsTr("Connected %1:core systems").arg(app.systemName) + } + BusyIndicator { + running: AWSClient.awsDevices.busy + height: app.iconSize + width: height + } + } + Label { Layout.fillWidth: true - Layout.topMargin: app.margins Layout.leftMargin: app.margins Layout.rightMargin: app.margins wrapMode: Text.WordWrap @@ -101,13 +110,11 @@ Page { qsTr("There are no %1:core systems connected to your cloud yet.").arg(app.systemName) : qsTr("There are %n %1:core systems connected to your cloud.", "", AWSClient.awsDevices.count).arg(app.systemName) } - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true + + Repeater { model: AWSClient.awsDevices delegate: NymeaListItemDelegate { - width: parent.width + Layout.fillWidth: true text: model.name subText: model.id progressive: false @@ -129,66 +136,173 @@ Page { } } - BusyIndicator { - anchors.centerIn: parent - visible: AWSClient.awsDevices.busy + } + } + + + ColumnLayout { + id: loginColumn + visible: !AWSClient.isLoggedIn + Layout.fillWidth: true + SettingsPageSectionHeader { + text: qsTr("Login") + } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + wrapMode: Text.WordWrap + text: qsTr("Log %1 in to %2:cloud in order to connect to %2:core systems from anywhere and receive push notifications from %2:core systems.").arg(app.appName).arg(app.systemName) + } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + text: qsTr("See our privacy policy to find out what information is processed.").arg(app.privacyPolicyUrl) + onLinkActivated: { + Qt.openUrlExternally(link) + } + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + text: "Username (e-mail)" + } + TextField { + id: usernameTextField + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + placeholderText: "john.smith@cooldomain.com" + inputMethodHints: Qt.ImhEmailCharactersOnly + validator: RegExpValidator { regExp:/\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/ } + } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + text: qsTr("Password") + } + RowLayout { + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + PasswordTextField { + id: passwordTextField + Layout.fillWidth: true + signup: false + } + } + + + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("OK") + enabled: usernameTextField.acceptableInput + onClicked: { + root.busy = true + AWSClient.login(usernameTextField.text, passwordTextField.password); + } + } + + Connections { + target: AWSClient + onLoginResult: { + switch (error) { + case AWSClient.LoginErrorInvalidUserOrPass: + errorLabel.text = qsTr("Failed to log in. Please try again. Do you perhaps have forgotten your password?") + break; + case AWSClient.LoginErrorNetworkError: + errorLabel.text = qsTr("Failed to connect to the login server. Please mase sure your network connection is working.") + break; + default: + errorLabel.text = qsTr("An unexpected error happened. Please report this isse. Error code: %1").arg(error) + break; + } + errorLabel.visible = (error !== AWSClient.LoginErrorNoError) + } + } + + Label { + id: errorLabel + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + color: "red" + visible: false + onLinkActivated: { + pageStack.push(resetPasswordComponent, {email: usernameTextField.text}) + } + } + + ThinDivider {} + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + wrapMode: Text.WordWrap + text: qsTr("Don't have a user yet?") + } + + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Sign Up") + onClicked: { + pageStack.push(signupPageComponent) } } } + Component { - id : logoutDialogComponent - MeaDialog { - id: logoutDialog - title: qsTr("Goodbye") - text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1:core systems remotely any more. However, you can come back any time, we'll keep your user account. If you whish to completely delete your account and all the data associated with it, check the box below before hitting ok. If you decide to delete your account, all your personal information will be removed from %1:cloud and cannot be restored.").arg(app.systemName) - headerIcon: "../images/dialog-warning-symbolic.svg" - standardButtons: Dialog.Cancel | Dialog.Ok + id: signupPageComponent + SettingsPageBase { + id: signupPage + title: qsTr("Sign up") - RowLayout { - CheckBox { - id: deleteCheckbox - } - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: qsTr("Delete my account") + Connections { + target: AWSClient + onSignupResult: { + signupPage.busy = false; + var text; + switch (error) { + case AWSClient.LoginErrorNoError: + pageStack.push(enterCodeComponent) + return; + case AWSClient.LoginErrorInvalidUserOrPass: + text = qsTr("The given username or password are not valid.") + break; + default: + text = qsTr("Uh oh, something went wrong. Please try again.") + } + var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); + var popup = errorDialog.createObject(app, {text: text}) + popup.open() } } - onAccepted: { - if (deleteCheckbox.checked) { - busyOverlay.shown = true; - AWSClient.deleteAccount() - } else { - AWSClient.logout() - } + + SettingsPageSectionHeader { + text: qsTr("Welcome to %1:cloud.").arg(app.systemName) } - } - } - - Flickable { - anchors.fill: parent - interactive: contentHeight > height - contentHeight: loginColumn.height - visible: !AWSClient.isLoggedIn - - ColumnLayout { - id: loginColumn - anchors { left: parent.left; right: parent.right; top: parent.top } Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins wrapMode: Text.WordWrap - text: qsTr("Log in to %1:cloud in order to connect to %1:core systems from anywhere.").arg(app.systemName) + text: qsTr("Please enter your email address and pick a password in order to create a new account."); + onLinkActivated: { + print("clicked", link) + Qt.openUrlExternally(link) + } } Label { Layout.fillWidth: true Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins wrapMode: Text.WordWrap font.pixelSize: app.smallFont - text: qsTr("See our privacy policy to find out what information is processed.").arg(app.privacyPolicyUrl) + text: qsTr("See our privacy policy to find out what information is processed. By signing up to %2:cloud you accept those terms and conditions.").arg(app.privacyPolicyUrl).arg(app.systemName) onLinkActivated: { Qt.openUrlExternally(link) } @@ -212,198 +326,32 @@ Page { Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins text: qsTr("Password") } - RowLayout { + PasswordTextField { + id: passwordTextField Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - PasswordTextField { - id: passwordTextField - Layout.fillWidth: true - signup: false - } - } - - - Button { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: qsTr("OK") - enabled: usernameTextField.acceptableInput - onClicked: { - busyOverlay.shown = true - AWSClient.login(usernameTextField.text, passwordTextField.password); - } - } - - Connections { - target: AWSClient - onLoginResult: { - switch (error) { - case AWSClient.LoginErrorInvalidUserOrPass: - errorLabel.text = qsTr("Failed to log in. Please try again. Do you perhaps have forgotten your password?") - break; - case AWSClient.LoginErrorNetworkError: - errorLabel.text = qsTr("Failed to connect to the login server. Please mase sure your network connection is working.") - break; - default: - errorLabel.text = qsTr("An unexpected error happened. Please report this isse. Error code: %1").arg(error) - break; - } - errorLabel.visible = (error !== AWSClient.LoginErrorNoError) - } - } - - Label { - id: errorLabel - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins - wrapMode: Text.WordWrap - font.pixelSize: app.smallFont - color: "red" - visible: false - onLinkActivated: { - pageStack.push(resetPasswordComponent, {email: usernameTextField.text}) - } - } - - ThinDivider {} - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - wrapMode: Text.WordWrap - text: qsTr("Don't have a user yet?") + minPasswordLength: 8 + requireLowerCaseLetter: true + requireUpperCaseLetter: true + requireNumber: true + requireSpecialChar: false } Button { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: qsTr("Sign Up") - onClicked: { - pageStack.push(signupPageComponent) - } - } - } - } - - - BusyOverlay { - id: busyOverlay - } - - Component { - id: signupPageComponent - Page { - id: signupPage - header: NymeaHeader { + Layout.margins: app.margins text: qsTr("Sign up") - onBackPressed: pageStack.pop() - } - - Flickable { - anchors.fill: parent - contentHeight: signupColumn.height - interactive: contentHeight > height - - ColumnLayout { - id: signupColumn - anchors { left: parent.left; top: parent.top; right: parent.right } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - wrapMode: Text.WordWrap - text: qsTr("Welcome to %1:cloud.").arg(app.systemName) - color: app.accentColor - font.pixelSize: app.largeFont - } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - wrapMode: Text.WordWrap - text: qsTr("Please enter your email address and pick a password in order to create a new account."); - onLinkActivated: { - print("clicked", link) - Qt.openUrlExternally(link) - } - } - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - wrapMode: Text.WordWrap - font.pixelSize: app.smallFont - text: qsTr("See our privacy policy to find out what information is processed. By signing up to %2:cloud you accept those terms and conditions.").arg(app.privacyPolicyUrl).arg(app.systemName) - onLinkActivated: { - Qt.openUrlExternally(link) - } - } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: "Username (e-mail)" - } - TextField { - id: usernameTextField - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - placeholderText: "john.smith@cooldomain.com" - inputMethodHints: Qt.ImhEmailCharactersOnly - validator: RegExpValidator { regExp:/\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/ } - } - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: qsTr("Password") - } - PasswordTextField { - id: passwordTextField - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - Layout.fillWidth: true - minPasswordLength: 8 - requireLowerCaseLetter: true - requireUpperCaseLetter: true - requireNumber: true - requireSpecialChar: false - } - - Button { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: qsTr("Sign up") - enabled: usernameTextField.acceptableInput && passwordTextField.isValid - onClicked: { - busyOverlay.shown = true; - AWSClient.signup(usernameTextField.text, passwordTextField.password) - } - } + enabled: usernameTextField.acceptableInput && passwordTextField.isValid + onClicked: { + signupPage.busy = true; + AWSClient.signup(usernameTextField.text, passwordTextField.password) } - - Connections { - target: AWSClient - onSignupResult: { - busyOverlay.shown = false; - var text; - switch (error) { - case AWSClient.LoginErrorNoError: - pageStack.push(enterCodeComponent) - return; - case AWSClient.LoginErrorInvalidUserOrPass: - text = qsTr("The given username or password are not valid.") - break; - default: - text = qsTr("Uh oh, something went wrong. Please try again.") - } - var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); - var popup = errorDialog.createObject(app, {text: text}) - popup.open() - } - } - } - - BusyOverlay { - id: busyOverlay } } + + + + } Component { @@ -436,7 +384,7 @@ Page { Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins text: qsTr("OK") onClicked: { - busyOverlay.shown = true; + root.busy = true; AWSClient.confirmRegistration(confirmationCodeTextField.text) } } @@ -444,7 +392,7 @@ Page { Connections { target: AWSClient onConfirmationResult: { - busyOverlay.shown = false; + root.busy = false; var text switch (error) { case AWSClient.LoginErrorNoError: @@ -473,20 +421,16 @@ Page { Component { id: resetPasswordComponent - Page { + SettingsPageBase { id: resetPasswordPage + title: qsTr("Reset password") property alias email: emailTextField.text - header: NymeaHeader { - text: qsTr("Reset password") - onBackPressed: pageStack.pop() - } - Connections { target: AWSClient onForgotPasswordResult: { - busyOverlay.shown = false + resetPasswordPage.busy = false if (error !== AWSClient.LoginErrorNoError) { var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); var text = qsTr("Sorry, this wasn't right. Did you misspell the email address?"); @@ -501,38 +445,31 @@ Page { } } - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } - spacing: app.margins - Label { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - wrapMode: Text.WordWrap - text: qsTr("Password forgotten?") - font.pixelSize: app.largeFont - color: app.accentColor - } - Label { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - wrapMode: Text.WordWrap - text: qsTr("No problem. Enter your email address here and we'll send you a confirmation code to change your password.") - } - TextField { - id: emailTextField - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - } - Button { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - text: qsTr("Reset password") - onClicked: { - AWSClient.forgotPassword(emailTextField.text) - busyOverlay.shown = true - } - } + Label { + Layout.fillWidth: true + Layout.margins: app.margins + wrapMode: Text.WordWrap + text: qsTr("Password forgotten?") + font.pixelSize: app.largeFont + color: app.accentColor } - - BusyOverlay { - id: busyOverlay + Label { + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + wrapMode: Text.WordWrap + text: qsTr("No problem. Enter your email address here and we'll send you a confirmation code to change your password.") + } + TextField { + id: emailTextField + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + } + Button { + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + text: qsTr("Reset password") + onClicked: { + AWSClient.forgotPassword(emailTextField.text) + resetPasswordPage.busy = true + } } } } @@ -540,13 +477,13 @@ Page { Component { id: confirmResetPasswordComponent - Page { + SettingsPageBase { id: confirmResetPasswordPage Connections { target: AWSClient onConfirmForgotPasswordResult: { - busyOverlay.shown = false + confirmResetPasswordPage.busy = false if (error !== AWSClient.LoginErrorNoError) { var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); var popup = errorDialog.createObject(app, {text: qsTr("Sorry, couldn't reset your password. Did you enter the wrong confirmation code?")}) @@ -564,63 +501,85 @@ Page { } property string email - header: NymeaHeader { - text: qsTr("Reset password") - onBackPressed: pageStack.pop() + title: qsTr("Reset password") + + Label { + Layout.fillWidth: true + Layout.margins: app.margins + wrapMode: Text.WordWrap + text: qsTr("Check your email!") + color: app.accentColor + font.pixelSize: app.largeFont } - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } - spacing: app.margins + Label { + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; + wrapMode: Text.WordWrap + text: qsTr("Enter the confirmation code you've received and a new password for your user %1.").arg(confirmResetPasswordPage.email) + } + + Label { + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; + text: qsTr("Confirmation code:") + } + + TextField { + id: codeTextField + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + } + Label { + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; + text: qsTr("Pick a new password:") + } + + PasswordTextField { + id: passwordTextField + minPasswordLength: 8 + requireLowerCaseLetter: true + requireUpperCaseLetter: true + requireNumber: true + requireSpecialChar: false + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + } + + Button { + Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + text: qsTr("Reset password") + enabled: passwordTextField.isValid && codeTextField.text.length > 0 + onClicked: { + confirmResetPasswordPage.busy = true + AWSClient.confirmForgotPassword(confirmResetPasswordPage.email, codeTextField.text, passwordTextField.password) + } + } + } + } + + Component { + id : logoutDialogComponent + MeaDialog { + id: logoutDialog + title: qsTr("Goodbye") + text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1:core systems remotely any more. However, you can come back any time, we'll keep your user account. If you whish to completely delete your account and all the data associated with it, check the box below before hitting ok. If you decide to delete your account, all your personal information will be removed from %1:cloud and cannot be restored.").arg(app.systemName) + headerIcon: "../images/dialog-warning-symbolic.svg" + standardButtons: Dialog.Cancel | Dialog.Ok + + RowLayout { + CheckBox { + id: deleteCheckbox + } Label { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + Layout.fillWidth: true wrapMode: Text.WordWrap - text: qsTr("Check your email!") - color: app.accentColor - font.pixelSize: app.largeFont + text: qsTr("Delete my account") } + } - Label { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; - wrapMode: Text.WordWrap - text: qsTr("Enter the confirmation code you've received and a new password for your user %1.").arg(confirmResetPasswordPage.email) - } - - Label { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; - text: qsTr("Confirmation code:") - } - - TextField { - id: codeTextField - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - } - Label { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; - text: qsTr("Pick a new password:") - } - - PasswordTextField { - id: passwordTextField - minPasswordLength: 8 - requireLowerCaseLetter: true - requireUpperCaseLetter: true - requireNumber: true - requireSpecialChar: false - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - } - - Button { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - text: qsTr("Reset password") - enabled: passwordTextField.isValid && codeTextField.text.length > 0 - onClicked: { - busyOverlay.shown = true - AWSClient.confirmForgotPassword(confirmResetPasswordPage.email, codeTextField.text, passwordTextField.password) - } - } - BusyOverlay { - id: busyOverlay + onAccepted: { + if (deleteCheckbox.checked) { + root.busy = true; + AWSClient.deleteAccount() + } else { + AWSClient.logout() } } } diff --git a/nymea-app/ui/appsettings/DeveloperOptionsPage.qml b/nymea-app/ui/appsettings/DeveloperOptionsPage.qml index 84928d05..321aad28 100644 --- a/nymea-app/ui/appsettings/DeveloperOptionsPage.qml +++ b/nymea-app/ui/appsettings/DeveloperOptionsPage.qml @@ -34,63 +34,49 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Developer options") - backButtonVisible: true - onBackPressed: pageStack.pop() + title: qsTr("Developer options") + + SettingsPageSectionHeader { + text: qsTr("Logging") } - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } + CheckDelegate { + text: qsTr("Enable app logging") + enabled: AppLogController.canWriteLogs + checked: AppLogController.enabled + onCheckedChanged: AppLogController.enabled = checked; + Layout.fillWidth: true + } - RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("View log") + onClicked: pageStack.push(Qt.resolvedUrl("../appsettings/AppLogPage.qml")) + enabled: AppLogController.enabled + } - Label { - Layout.fillWidth: true - text: qsTr("Cloud environment") - } - ComboBox { - currentIndex: model.indexOf(app.settings.cloudEnvironment) - model: AWSClient.availableConfigs - onActivated: { - app.settings.cloudEnvironment = model[index]; - } - } - } + SettingsPageSectionHeader { + text: qsTr("Advanced options") + visible: settings.showHiddenOptions + } - CheckDelegate { - text: qsTr("Enable app logging") - enabled: AppLogController.canWriteLogs - checked: AppLogController.enabled - onCheckedChanged: AppLogController.enabled = checked; + RowLayout { + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + visible: settings.showHiddenOptions + + Label { Layout.fillWidth: true + text: qsTr("Cloud environment") } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("View log") - onClicked: pageStack.push(Qt.resolvedUrl("../appsettings/AppLogPage.qml")) - enabled: AppLogController.enabled - } - - RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - - Label { - Layout.fillWidth: true - text: qsTr("Experience mode") - } - - ComboBox { - currentIndex: model.indexOf(styleController.currentExperience) - model: styleController.allExperiences - onActivated: { - styleController.currentExperience = model[index] - } + ComboBox { + currentIndex: model.indexOf(app.settings.cloudEnvironment) + model: AWSClient.availableConfigs + onActivated: { + app.settings.cloudEnvironment = model[index]; } } } diff --git a/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml b/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml index 39bf1248..8d0370b1 100644 --- a/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml +++ b/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml @@ -35,167 +35,195 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Look and feel") - backButtonVisible: true - onBackPressed: pageStack.pop() + title: qsTr("Look and feel") + + SettingsPageSectionHeader { + text: qsTr("Appearance") } - ColumnLayout { - id: contentColumn - width: parent.width - - RowLayout { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - visible: !kioskMode && Qt.platform.os !== "ios" - Label { - Layout.fillWidth: true - text: qsTr("View mode") - } - ComboBox { - model: [qsTr("Windowed"), qsTr("Maximized"), qsTr("Fullscreen"), qsTr("Automatic")] - currentIndex: { - switch (settings.viewMode) { - case ApplicationWindow.Windowed: - return 0; - case ApplicationWindow.Maximized: - return 1; - case ApplicationWindow.FullScreen: - return 2; - case ApplicationWindow.AutomaticVisibility: - return 3; - } - } - - onActivated: { - switch (currentIndex) { - case 0: - settings.viewMode = ApplicationWindow.Windowed; - break; - case 1: - settings.viewMode = ApplicationWindow.Maximized; - break; - case 2: - settings.viewMode = ApplicationWindow.FullScreen; - break; - case 3: - settings.viewMode = ApplicationWindow.AutomaticVisibility; - break; - } - } - } - } - - RowLayout { - Layout.fillWidth: true; Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - visible: appBranding.length === 0 - Label { - Layout.fillWidth: true - text: "Style" - } - ComboBox { - model: styleController.allStyles - currentIndex: styleController.allStyles.indexOf(styleController.currentStyle) - - onActivated: { - styleController.currentStyle = model[index] - } - } - - Connections { - target: styleController - onCurrentStyleChanged: { - var popup = styleChangedDialog.createObject(root) - popup.open() - } - } - } - - RowLayout { + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + visible: appBranding.length === 0 + Label { Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - Label { - Layout.fillWidth: true - text: qsTr("Unit system") - } - ComboBox { - id: unitsComboBox - currentIndex: settings.units === "metric" ? 0 : 1 - model: [ qsTr("Metric"), qsTr("Imperial") ] - onActivated: { - settings.units = index == 0 ? "metric" : "imperial"; - } + text: "Style" + } + ComboBox { + model: styleController.allStyles + currentIndex: styleController.allStyles.indexOf(styleController.currentStyle) + + onActivated: { + styleController.currentStyle = model[index] } } - CheckDelegate { - Layout.fillWidth: true - text: qsTr("Return to home on idle") - checked: settings.returnToHome - onClicked: settings.returnToHome = checked - } - CheckDelegate { - Layout.fillWidth: true - text: qsTr("Show connection tabs") - checked: settings.showConnectionTabs - onClicked: settings.showConnectionTabs = checked - } - - CheckDelegate { - id: screenOffCheck - Layout.fillWidth: true - text: qsTr("Turn screen off when idle") - visible: PlatformHelper.canControlScreen - checked: PlatformHelper.screenTimeout > 0 - onClicked: PlatformHelper.screenTimeout = (checked ? 15000 : 0) - } - - ItemDelegate { - Layout.fillWidth: true - Layout.preferredHeight: screenOffCheck.height - visible: PlatformHelper.screenTimeout > 0 - topPadding: 0 - contentItem: RowLayout { - Label { - Layout.fillWidth: true - text: qsTr("Screen off timeout") - } - SpinBox { - value: PlatformHelper.screenTimeout / 1000 - onValueModified: { - PlatformHelper.screenTimeout = value * 1000 - } - } - Label { - text: qsTr("seconds") - } + Connections { + target: styleController + onCurrentStyleChanged: { + var popup = styleChangedDialog.createObject(root) + popup.open() } } + } - ItemDelegate { + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + visible: !kioskMode && Qt.platform.os !== "ios" + Label { Layout.fillWidth: true - visible: PlatformHelper.canControlScreen - topPadding: 0 - contentItem: RowLayout { - Label { - Layout.fillWidth: true - text: qsTr("Screen brightness") + text: qsTr("View mode") + } + ComboBox { + model: [qsTr("Windowed"), qsTr("Maximized"), qsTr("Fullscreen"), qsTr("Automatic")] + currentIndex: { + switch (settings.viewMode) { + case ApplicationWindow.Windowed: + return 0; + case ApplicationWindow.Maximized: + return 1; + case ApplicationWindow.FullScreen: + return 2; + case ApplicationWindow.AutomaticVisibility: + return 3; } - Slider { - Layout.fillWidth: true - value: PlatformHelper.screenBrightness - onMoved: PlatformHelper.screenBrightness = value - from: 0 - to: 100 - stepSize: 1 + } + + onActivated: { + switch (currentIndex) { + case 0: + settings.viewMode = ApplicationWindow.Windowed; + break; + case 1: + settings.viewMode = ApplicationWindow.Maximized; + break; + case 2: + settings.viewMode = ApplicationWindow.FullScreen; + break; + case 3: + settings.viewMode = ApplicationWindow.AutomaticVisibility; + break; } } } } + CheckDelegate { + Layout.fillWidth: true + text: qsTr("Show connection tabs") + checked: settings.showConnectionTabs + onClicked: settings.showConnectionTabs = checked + } + + RowLayout { + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + visible: settings.showHiddenOptions + + Label { + Layout.fillWidth: true + text: qsTr("Experience mode") + } + + ComboBox { + currentIndex: model.indexOf(styleController.currentExperience) + model: styleController.allExperiences + onActivated: { + styleController.currentExperience = model[index] + } + } + } + + SettingsPageSectionHeader { + text: qsTr("Regional") + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + Label { + Layout.fillWidth: true + text: qsTr("Unit system") + } + ComboBox { + id: unitsComboBox + currentIndex: settings.units === "metric" ? 0 : 1 + model: [ qsTr("Metric"), qsTr("Imperial") ] + onActivated: { + settings.units = index == 0 ? "metric" : "imperial"; + } + } + } + + SettingsPageSectionHeader { + text: qsTr("Behavior") + } + + CheckDelegate { + Layout.fillWidth: true + text: qsTr("Return to home on idle") + checked: settings.returnToHome + onClicked: settings.returnToHome = checked + } + + CheckDelegate { + id: screenOffCheck + Layout.fillWidth: true + text: qsTr("Turn screen off when idle") + visible: PlatformHelper.canControlScreen + checked: PlatformHelper.screenTimeout > 0 + onClicked: PlatformHelper.screenTimeout = (checked ? 15000 : 0) + } + + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: screenOffCheck.height + visible: PlatformHelper.screenTimeout > 0 + topPadding: 0 + contentItem: RowLayout { + Label { + Layout.fillWidth: true + text: qsTr("Screen off timeout") + } + SpinBox { + value: PlatformHelper.screenTimeout / 1000 + onValueModified: { + PlatformHelper.screenTimeout = value * 1000 + } + } + Label { + text: qsTr("seconds") + } + } + } + + ItemDelegate { + Layout.fillWidth: true + visible: PlatformHelper.canControlScreen + topPadding: 0 + contentItem: RowLayout { + Label { + Layout.fillWidth: true + text: qsTr("Screen brightness") + } + Slider { + Layout.fillWidth: true + value: PlatformHelper.screenBrightness + onMoved: PlatformHelper.screenBrightness = value + from: 0 + to: 100 + stepSize: 1 + } + } + } + + Component { id: styleChangedDialog Dialog { diff --git a/nymea-app/ui/components/PasswordTextField.qml b/nymea-app/ui/components/PasswordTextField.qml index 993c0680..eef878c7 100644 --- a/nymea-app/ui/components/PasswordTextField.qml +++ b/nymea-app/ui/components/PasswordTextField.qml @@ -99,7 +99,7 @@ ColumnLayout { } var ret = [] for (var i = 0; i < texts.length; i++) { - var entry = "• ".arg(checks[i] ? app.foregroundColor : app.accentColor) + var entry = "• ".arg(checks[i] ? "#ffffff" : app.accentColor) entry += texts[i] entry += "" ret.push(entry) diff --git a/nymea-app/ui/components/SettingsPageBase.qml b/nymea-app/ui/components/SettingsPageBase.qml new file mode 100644 index 00000000..4618763b --- /dev/null +++ b/nymea-app/ui/components/SettingsPageBase.qml @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 + +Page { + id: root + header: NymeaHeader { + text: root.title + backButtonVisible: true + onBackPressed: pageStack.pop() + } + + property alias busy: busyOverlay.shown + default property alias content: contentColumn.data + + Flickable { + anchors.fill: parent + contentHeight: contentColumn.height + interactive: contentHeight > height + + ScrollBar.vertical: ScrollBar {} + + ColumnLayout { + id: contentColumn + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(500, parent.width) + } + } + + BusyOverlay { + id: busyOverlay + } +} diff --git a/nymea-app/ui/components/SettingsPageSectionHeader.qml b/nymea-app/ui/components/SettingsPageSectionHeader.qml new file mode 100644 index 00000000..de274e45 --- /dev/null +++ b/nymea-app/ui/components/SettingsPageSectionHeader.qml @@ -0,0 +1,43 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.2 + +Label { + Layout.fillWidth: true + Layout.topMargin: app.margins * 1.5 + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + Layout.bottomMargin: app.margins + color: app.accentColor + wrapMode: Text.WordWrap +} diff --git a/nymea-app/ui/connection/LoginPage.qml b/nymea-app/ui/connection/LoginPage.qml index 6865ed91..e2e7e44b 100644 --- a/nymea-app/ui/connection/LoginPage.qml +++ b/nymea-app/ui/connection/LoginPage.qml @@ -53,7 +53,7 @@ Page { popup.open(); } onCreateUserSucceeded: { - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app"); + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.device + ")"); } onCreateUserFailed: { @@ -153,7 +153,7 @@ Page { engine.jsonRpcClient.createUser(usernameTextField.text, passwordTextField.password); } else { print("authenticate", usernameTextField.text, passwordTextField.text, "nymea-app") - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app"); + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.device + ")"); } } } diff --git a/nymea-app/ui/images/key.svg b/nymea-app/ui/images/key.svg new file mode 100644 index 00000000..6898fbee --- /dev/null +++ b/nymea-app/ui/images/key.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/system/AboutNymeaPage.qml b/nymea-app/ui/system/AboutNymeaPage.qml index 4e1cbf9c..f26d69aa 100644 --- a/nymea-app/ui/system/AboutNymeaPage.qml +++ b/nymea-app/ui/system/AboutNymeaPage.qml @@ -34,60 +34,52 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { - +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("About %1:core").arg(app.systemName) - onBackPressed: pageStack.pop() - } + title: qsTr("About %1:core").arg(app.systemName) - Flickable { - anchors.fill: parent - contentHeight: imprint.implicitHeight - Imprint { - id: imprint - width: parent.width - title: qsTr("%1:core").arg(app.systemName) - githubLink: "https://github.com/nymea/nymea" + Imprint { + id: imprint + Layout.fillWidth: true + title: qsTr("%1:core").arg(app.systemName) + githubLink: "https://github.com/nymea/nymea" - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Connection:") - subText: engine.connection.currentConnection.url - progressive: false - prominentSubText: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Server UUID:") - subText: engine.jsonRpcClient.serverUuid - progressive: false - prominentSubText: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Server version:") - subText: engine.jsonRpcClient.serverVersion - progressive: false - prominentSubText: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("JSON-RPC version:") - subText: engine.jsonRpcClient.jsonRpcVersion - progressive: false - prominentSubText: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Qt version:") - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - subText: engine.jsonRpcClient.serverQtVersion + (engine.jsonRpcClient.serverQtVersion !== engine.jsonRpcClient.serverQtBuildVersion ? + " (" + qsTr("Built with %1").arg(engine.jsonRpcClient.serverQtBuildVersion) + ")" : "") - progressive: false - prominentSubText: false - } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Connection:") + subText: engine.connection.currentConnection.url + progressive: false + prominentSubText: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Server UUID:") + subText: engine.jsonRpcClient.serverUuid + progressive: false + prominentSubText: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Server version:") + subText: engine.jsonRpcClient.serverVersion + progressive: false + prominentSubText: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("JSON-RPC version:") + subText: engine.jsonRpcClient.jsonRpcVersion + progressive: false + prominentSubText: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Qt version:") + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + subText: engine.jsonRpcClient.serverQtVersion + (engine.jsonRpcClient.serverQtVersion !== engine.jsonRpcClient.serverQtBuildVersion ? + " (" + qsTr("Built with %1").arg(engine.jsonRpcClient.serverQtBuildVersion) + ")" : "") + progressive: false + prominentSubText: false } } } diff --git a/nymea-app/ui/system/CloudSettingsPage.qml b/nymea-app/ui/system/CloudSettingsPage.qml index 72ea9a74..f82b7d97 100644 --- a/nymea-app/ui/system/CloudSettingsPage.qml +++ b/nymea-app/ui/system/CloudSettingsPage.qml @@ -34,12 +34,9 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Cloud settings") - onBackPressed: pageStack.pop(); - } + title: qsTr("%1:core cloud settings").arg(app.systemName) Item { id: d @@ -59,97 +56,120 @@ Page { } } - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } + SettingsPageSectionHeader { + text: qsTr("Cloud connection") + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("Connect %1:core to %1:cloud in order to access it from anywhere and send push notifications from %1:core to %2.").arg(app.systemName).arg(app.appName) + wrapMode: Text.WordWrap + } + + // Button { + // text: "pair" + // onClicked: engine.jsonRpcClient.setupRemoteAccess(AWSClient.idToken, AWSClient.userId) + // } + + SwitchDelegate { + Layout.fillWidth: true + text: qsTr("Cloud connection enabled") + checked: engine.nymeaConfiguration.cloudEnabled + onToggled: { + engine.nymeaConfiguration.cloudEnabled = checked; + } + } + + SettingsPageSectionHeader { + text: qsTr("Status") + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + + ColorIcon { + Layout.preferredHeight: busyIndicator.height + Layout.preferredWidth: height + name: engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateConnected + ? "../images/cloud.svg" + : engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured + ? "../images/cloud-error.svg" + : "../images/cloud-offline.svg" + } Label { Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - Layout.topMargin: app.margins - text: qsTr("You can connect a nymea:box to a nymea:cloud in order to access it from anywhere") wrapMode: Text.WordWrap - } - -// Button { -// text: "pair" -// onClicked: engine.jsonRpcClient.setupRemoteAccess(AWSClient.idToken, AWSClient.userId) -// } - - SwitchDelegate { - Layout.fillWidth: true - text: qsTr("Cloud connection enabled") - checked: engine.nymeaConfiguration.cloudEnabled - onToggled: { - engine.nymeaConfiguration.cloudEnabled = checked; - } - } - - ThinDivider {} - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - - ColorIcon { - Layout.preferredHeight: busyIndicator.height - Layout.preferredWidth: height - name: engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateConnected - ? "../images/cloud.svg" - : engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured - ? "../images/cloud-error.svg" - : "../images/cloud-offline.svg" - } - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: { - switch (engine.jsonRpcClient.cloudConnectionState) { - case JsonRpcClient.CloudConnectionStateDisabled: - return qsTr("This box is not connected to %1:cloud").arg(app.systemName) - case JsonRpcClient.CloudConnectionStateUnconfigured: - if (d.deploymentStarted) { - return qsTr("Registering box in %1:cloud...").arg(app.systemName) - } - return qsTr("This box is not configured to connect to %1:cloud.").arg(app.systemName); - case JsonRpcClient.CloudConnectionStateConnecting: - return qsTr("Connecting the box to %1:cloud...").arg(app.systemName); - case JsonRpcClient.CloudConnectionStateConnected: - return qsTr("The box is connected to %1:cloud.").arg(app.systemName); + text: { + switch (engine.jsonRpcClient.cloudConnectionState) { + case JsonRpcClient.CloudConnectionStateDisabled: + return qsTr("This box is not connected to %1:cloud").arg(app.systemName) + case JsonRpcClient.CloudConnectionStateUnconfigured: + if (d.deploymentStarted) { + return qsTr("Registering box in %1:cloud...").arg(app.systemName) } - return engine.jsonRpcClient.cloudConnectionState + return qsTr("This box is not configured to connect to %1:cloud.").arg(app.systemName); + case JsonRpcClient.CloudConnectionStateConnecting: + return qsTr("Connecting the box to %1:cloud...").arg(app.systemName); + case JsonRpcClient.CloudConnectionStateConnected: + return qsTr("The box is connected to %1:cloud.").arg(app.systemName); } - } - BusyIndicator { - id: busyIndicator - visible: (engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured && d.deploymentStarted) || - engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting + return engine.jsonRpcClient.cloudConnectionState } } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - visible: engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted - text: qsTr("This box is not configured to access the %1:cloud. In order for a box to connect to %1:cloud it needs to be registered first.").arg(app.systemName) - wrapMode: Text.WordWrap + BusyIndicator { + id: busyIndicator + visible: (engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateUnconfigured && d.deploymentStarted) || + engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnecting } + } - Button { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - visible: engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted - text: AWSClient.isLoggedIn ? qsTr("Register box") : qsTr("Log in to cloud") - onClicked: { - if (AWSClient.isLoggedIn) { - d.deploymentStarted = true - engine.deployCertificate(); - } else { - pageStack.push(Qt.resolvedUrl("qrc:/ui/appsettings/CloudLoginPage.qml")) - } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + visible: engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted + text: qsTr("This box is not configured to access the %1:cloud. In order for a box to connect to %1:cloud it needs to be registered first.").arg(app.systemName) + wrapMode: Text.WordWrap + } + + Button { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + visible: engine.jsonRpcClient.cloudConnectionState === JsonRpcClient.CloudConnectionStateUnconfigured && !d.deploymentStarted + text: AWSClient.isLoggedIn ? qsTr("Register box") : qsTr("Log in to cloud") + onClicked: { + if (AWSClient.isLoggedIn) { + d.deploymentStarted = true + engine.deployCertificate(); + } else { + pageStack.push(Qt.resolvedUrl("qrc:/ui/appsettings/CloudLoginPage.qml")) } } } + + SettingsPageSectionHeader { + text: qsTr("Remote connection") + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + wrapMode: Text.WordWrap + text: qsTr("In order to remotely connect to this %1:core, %2 needs to be logged into %1:cloud as well.").arg(app.systemName).arg(app.appName) + } + + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Go to app settings") + subText: qsTr("Set up cloud connection for %1").arg(app.appName) + prominentSubText: false + onClicked: { + pageStack.push(Qt.resolvedUrl("../appsettings/CloudLoginPage.qml")) + } + } } diff --git a/nymea-app/ui/system/ConnectionInterfacesPage.qml b/nymea-app/ui/system/ConnectionInterfacesPage.qml index da9ef82a..ffc96382 100644 --- a/nymea-app/ui/system/ConnectionInterfacesPage.qml +++ b/nymea-app/ui/system/ConnectionInterfacesPage.qml @@ -34,127 +34,101 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Connection interfaces") - onBackPressed: pageStack.pop(); + title: qsTr("Connection interfaces") + + + SettingsPageSectionHeader { + text: qsTr("TCP server interfaces") } - Flickable { - anchors.fill: parent - contentHeight: connectionsColumn.implicitHeight - interactive: contentHeight > height - - ColumnLayout { - id: connectionsColumn - anchors { left: parent.left; top: parent.top; right: parent.right } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - Layout.topMargin: app.margins - text: qsTr("TCP server interfaces") - wrapMode: Text.WordWrap - color: app.accentColor + Repeater { + model: engine.nymeaConfiguration.tcpServerConfigurations + delegate: ConnectionInterfaceDelegate { + Layout.fillWidth: true + canDelete: true + onClicked: { + var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.tcpServerConfigurations.get(index).clone() }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setTcpServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() } - - Repeater { - model: engine.nymeaConfiguration.tcpServerConfigurations - delegate: ConnectionInterfaceDelegate { - Layout.fillWidth: true - canDelete: true - onClicked: { - var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.tcpServerConfigurations.get(index).clone() }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setTcpServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } - onDeleteClicked: { - print("should delete") - engine.nymeaConfiguration.deleteTcpServerConfiguration(model.id) - } - } - } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("Add") - onClicked: { - var config = engine.nymeaConfiguration.createServerConfiguration("0.0.0.0", 2222 + engine.nymeaConfiguration.tcpServerConfigurations.count, false, false); - var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: config }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setTcpServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } - } - - ThinDivider {} - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - Layout.topMargin: app.margins - text: qsTr("WebSocket server interfaces") - wrapMode: Text.WordWrap - color: app.accentColor - } - - Repeater { - model: engine.nymeaConfiguration.webSocketServerConfigurations - delegate: ConnectionInterfaceDelegate { - Layout.fillWidth: true - canDelete: true - onClicked: { - var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.webSocketServerConfigurations.get(index).clone() }); - popup.accepted.connect(function() { - print("configuring:", popup.serverConfiguration.port) - engine.nymeaConfiguration.setWebSocketServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } - onDeleteClicked: { - print("should delete", model.id) - engine.nymeaConfiguration.deleteWebSocketServerConfiguration(model.id) - } - } - } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("Add") - onClicked: { - var config = engine.nymeaConfiguration.createServerConfiguration("0.0.0.0", 4444 + engine.nymeaConfiguration.webSocketServerConfigurations.count, false, false); - var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: config }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setWebSocketServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } + onDeleteClicked: { + print("should delete") + engine.nymeaConfiguration.deleteTcpServerConfiguration(model.id) } } } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Add") + onClicked: { + var config = engine.nymeaConfiguration.createServerConfiguration("0.0.0.0", 2222 + engine.nymeaConfiguration.tcpServerConfigurations.count, false, false); + var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: config }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setTcpServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() + } + } + + SettingsPageSectionHeader { + text: qsTr("WebSocket server interfaces") + } + + Repeater { + model: engine.nymeaConfiguration.webSocketServerConfigurations + delegate: ConnectionInterfaceDelegate { + Layout.fillWidth: true + canDelete: true + onClicked: { + var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.webSocketServerConfigurations.get(index).clone() }); + popup.accepted.connect(function() { + print("configuring:", popup.serverConfiguration.port) + engine.nymeaConfiguration.setWebSocketServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() + } + onDeleteClicked: { + print("should delete", model.id) + engine.nymeaConfiguration.deleteWebSocketServerConfiguration(model.id) + } + } + } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Add") + onClicked: { + var config = engine.nymeaConfiguration.createServerConfiguration("0.0.0.0", 4444 + engine.nymeaConfiguration.webSocketServerConfigurations.count, false, false); + var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: config }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setWebSocketServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() + } + } } diff --git a/nymea-app/ui/system/DeveloperTools.qml b/nymea-app/ui/system/DeveloperTools.qml index a518fa29..f96b2eaa 100644 --- a/nymea-app/ui/system/DeveloperTools.qml +++ b/nymea-app/ui/system/DeveloperTools.qml @@ -34,12 +34,9 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Developer tools") - onBackPressed: pageStack.pop(); - } + title: qsTr("Developer tools") property WebServerConfiguration usedConfig: { var config = null @@ -49,7 +46,7 @@ Page { if (tmp.address === engine.connection.currentConnection.hostAddress || tmp.address === "0.0.0.0") { // This one prefers https over http... -// if (config === null || (!config.sslEnabled && tmp.sslEnabled)) { + // if (config === null || (!config.sslEnabled && tmp.sslEnabled)) { // ...but for now, prefer http because self signed certs cause trouble and this is meant for local debugging only anyways... if (config === null || (config.sslEnabled && !tmp.sslEnabled)) { @@ -61,72 +58,66 @@ Page { return config; } - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - spacing: app.margins - Label { - text: qsTr("Debug server enabled") - Layout.fillWidth: true - } - Switch { - id: debugServerEnabledSwitch - checked: engine.nymeaConfiguration.debugServerEnabled - onClicked: engine.nymeaConfiguration.debugServerEnabled = checked - } - } + SettingsPageSectionHeader { + text: qsTr("Debug server") + } - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - text: qsTr("In order to access the debug interface, please enable the web server.") - font.pixelSize: app.smallFont - color: "red" - wrapMode: Text.WordWrap - visible: engine.nymeaConfiguration.webServerConfigurations.count === 0 - } + SwitchDelegate { + id: debugServerEnabledSwitch + Layout.fillWidth: true + text: qsTr("Debug server enabled") + checked: engine.nymeaConfiguration.debugServerEnabled + onToggled: engine.nymeaConfiguration.debugServerEnabled = checked + } - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - text: qsTr("The web server cannot be reached on %1.").arg(engine.connection.currentConnection.hostAddress) - wrapMode: Text.WordWrap - font.pixelSize: app.smallFont - color: "red" - visible: engine.nymeaConfiguration.webServerConfigurations.count > 0 && root.usedConfig === null - } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("In order to access the debug interface, please enable the web server.") + font.pixelSize: app.smallFont + color: "red" + wrapMode: Text.WordWrap + visible: engine.nymeaConfiguration.webServerConfigurations.count === 0 + } - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - text: qsTr("Please enable the web server to be accessed on this address.") - wrapMode: Text.WordWrap - font.pixelSize: app.smallFont - visible: engine.nymeaConfiguration.webServerConfigurations.count > 0 && root.usedConfig === null - } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("The web server cannot be reached on %1.").arg(engine.connection.currentConnection.hostAddress) + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + color: "red" + visible: engine.nymeaConfiguration.webServerConfigurations.count > 0 && root.usedConfig === null + } - Button { - id: debugServerButton - Layout.fillWidth: true - Layout.margins: app.margins - visible: debugServerEnabledSwitch.checked - enabled: root.usedConfig !== null && engine.nymeaConfiguration.webServerConfigurations.count > 0 - text: qsTr("Open debug interface") - onClicked: { - print("opening:", engine.connection.currentConnection.url) + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("Please enable the web server to be accessed on this address.") + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + visible: engine.nymeaConfiguration.webServerConfigurations.count > 0 && root.usedConfig === null + } - var proto = "http" + (root.usedConfig.sslEnabled ? "s" : "") + "://" - var path = engine.connection.currentConnection.hostAddress + ":" + root.usedConfig.port + "/debug" - print("opening:", proto + path) - Qt.openUrlExternally(proto + path) - } + Button { + id: debugServerButton + Layout.fillWidth: true + Layout.margins: app.margins + visible: debugServerEnabledSwitch.checked + enabled: root.usedConfig !== null && engine.nymeaConfiguration.webServerConfigurations.count > 0 + text: qsTr("Open debug interface") + onClicked: { + print("opening:", engine.connection.currentConnection.url) + + var proto = "http" + (root.usedConfig.sslEnabled ? "s" : "") + "://" + var path = engine.connection.currentConnection.hostAddress + ":" + root.usedConfig.port + "/debug" + print("opening:", proto + path) + Qt.openUrlExternally(proto + path) } } + } diff --git a/nymea-app/ui/system/GeneralSettingsPage.qml b/nymea-app/ui/system/GeneralSettingsPage.qml index ba59744d..7e95c81b 100644 --- a/nymea-app/ui/system/GeneralSettingsPage.qml +++ b/nymea-app/ui/system/GeneralSettingsPage.qml @@ -35,192 +35,213 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("General settings") - backButtonVisible: true - onBackPressed: pageStack.pop() + title: qsTr("General settings") + + + SettingsPageSectionHeader { + text: qsTr("General") } - ColumnLayout { - id: settingsGrid - anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; margins: app.margins } - width: Math.min(500, parent.width - app.margins * 2) + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins - RowLayout { + Label { + text: qsTr("Name") Layout.fillWidth: true - spacing: app.margins - Label { - text: qsTr("Name") - } - TextField { - id: nameTextField - Layout.fillWidth: true - text: engine.nymeaConfiguration.serverName - } - Button { - text: qsTr("OK") - visible: nameTextField.displayText !== engine.nymeaConfiguration.serverName - onClicked: engine.nymeaConfiguration.serverName = nameTextField.displayText - } } - - RowLayout { + TextField { + id: nameTextField Layout.fillWidth: true - visible: engine.jsonRpcClient.ensureServerVersion("4.1") && engine.systemController.automaticTimeAvailable - Label { - text: qsTr("Set date and time automatically") - Layout.fillWidth: true - } - CheckBox { - checked: engine.systemController.automaticTime - onClicked: { - engine.systemController.automaticTime = checked - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Layout.preferredHeight: dateButton.implicitHeight - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - Label { - text: qsTr("Date") - Layout.fillWidth: true - } - Label { - text: engine.systemController.serverTime.toLocaleDateString() - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - } - Button { - id: dateButton - visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable - contentItem: Item { - ColorIcon { - name: "../images/edit.svg" - color: app.foregroundColor - anchors.centerIn: parent - height: parent.height - width: height - } - } - - onClicked: { - var popup = datePickerComponent.createObject(root, {dateTime: engine.systemController.serverTime}) - popup.accepted.connect(function() { - print("setting new date", popup.dateTime) - engine.systemController.serverTime = popup.dateTime - }) - popup.open(); - - } - } - } - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Layout.preferredHeight: timeButton.implicitHeight - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - Label { - text: qsTr("Time") - Layout.fillWidth: true - } - Label { - text: engine.systemController.serverTime.toLocaleTimeString(/*Locale.ShortTimeString*/) - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - } - Button { - id: timeButton - visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable - contentItem: Item { - ColorIcon { - name: "../images/edit.svg" - color: app.foregroundColor - anchors.centerIn: parent - height: parent.height - width: height - } - } - - onClicked: { - var popup = timePickerComponent.createObject(root, {hour: engine.systemController.serverTime.getHours(), minute: engine.systemController.serverTime.getMinutes()}) - popup.accepted.connect(function() { - var date = new Date(engine.systemController.serverTime) - date.setHours(popup.hour); - date.setMinutes(popup.minute) - engine.systemController.serverTime = date; - }) - popup.open(); - - } - } - } - - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - visible: engine.jsonRpcClient.ensureServerVersion("4.1") - Label { - Layout.fillWidth: true - text: qsTr("Time zone") - } - ComboBox { - Layout.minimumWidth: 200 - model: engine.systemController.timeZones - currentIndex: model.indexOf(engine.systemController.serverTimeZone) - onActivated: { - engine.systemController.serverTimeZone = currentText; - } - } - } - - Button { - Layout.fillWidth: true - text: qsTr("Reboot %1:core").arg(app.systemName) - visible: engine.systemController.powerManagementAvailable - onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("Are you sure you want to reboot your %1:core sytem now?").arg(app.systemName) - var popup = dialog.createObject(app, - { - headerIcon: "../images/dialog-warning-symbolic.svg", - title: qsTr("Reboot %1:core").arg(app.systemName), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - engine.systemController.reboot() - }) - } + text: engine.nymeaConfiguration.serverName } Button { + text: qsTr("OK") + visible: nameTextField.displayText !== engine.nymeaConfiguration.serverName + onClicked: engine.nymeaConfiguration.serverName = nameTextField.displayText + } + } + + SettingsPageSectionHeader { + text: qsTr("Date and time") + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + visible: engine.jsonRpcClient.ensureServerVersion("4.1") && engine.systemController.automaticTimeAvailable + Label { + text: qsTr("Set date and time automatically") Layout.fillWidth: true - text: qsTr("Shutdown %1:core").arg(app.systemName) - visible: engine.systemController.powerManagementAvailable + } + CheckBox { + checked: engine.systemController.automaticTime onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("Are you sure you want to shut down your %1:core sytem now?").arg(app.systemName) - var popup = dialog.createObject(app, - { - headerIcon: "../images/dialog-warning-symbolic.svg", - title: qsTr("Shut down %1:core").arg(app.systemName), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - engine.systemController.shutdown() - }) + engine.systemController.automaticTime = checked } } } + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins + Layout.preferredHeight: dateButton.implicitHeight + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + Label { + text: qsTr("Date") + Layout.fillWidth: true + } + Label { + text: engine.systemController.serverTime.toLocaleDateString() + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + Button { + id: dateButton + visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable + contentItem: Item { + ColorIcon { + name: "../images/edit.svg" + color: app.foregroundColor + anchors.centerIn: parent + height: parent.height + width: height + } + } + + onClicked: { + var popup = datePickerComponent.createObject(root, {dateTime: engine.systemController.serverTime}) + popup.accepted.connect(function() { + print("setting new date", popup.dateTime) + engine.systemController.serverTime = popup.dateTime + }) + popup.open(); + + } + } + } + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins + Layout.preferredHeight: timeButton.implicitHeight + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + Label { + text: qsTr("Time") + Layout.fillWidth: true + } + Label { + text: engine.systemController.serverTime.toLocaleTimeString(/*Locale.ShortTimeString*/) + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + Button { + id: timeButton + visible: !engine.systemController.automaticTime && engine.systemController.timeManagementAvailable + contentItem: Item { + ColorIcon { + name: "../images/edit.svg" + color: app.foregroundColor + anchors.centerIn: parent + height: parent.height + width: height + } + } + + onClicked: { + var popup = timePickerComponent.createObject(root, {hour: engine.systemController.serverTime.getHours(), minute: engine.systemController.serverTime.getMinutes()}) + popup.accepted.connect(function() { + var date = new Date(engine.systemController.serverTime) + date.setHours(popup.hour); + date.setMinutes(popup.minute) + engine.systemController.serverTime = date; + }) + popup.open(); + + } + } + } + + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins + visible: engine.jsonRpcClient.ensureServerVersion("4.1") + Label { + Layout.fillWidth: true + text: qsTr("Time zone") + } + ComboBox { + Layout.minimumWidth: 200 + model: engine.systemController.timeZones + currentIndex: model.indexOf(engine.systemController.serverTimeZone) + onActivated: { + engine.systemController.serverTimeZone = currentText; + } + } + } + + SettingsPageSectionHeader { + text: qsTr("System") + visible: engine.systemController.powerManagementAvailable + } + + Button { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("Reboot %1:core").arg(app.systemName) + visible: engine.systemController.powerManagementAvailable + onClicked: { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("Are you sure you want to reboot your %1:core sytem now?").arg(app.systemName) + var popup = dialog.createObject(app, + { + headerIcon: "../images/dialog-warning-symbolic.svg", + title: qsTr("Reboot %1:core").arg(app.systemName), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + engine.systemController.reboot() + }) + } + } + Button { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("Shutdown %1:core").arg(app.systemName) + visible: engine.systemController.powerManagementAvailable + onClicked: { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("Are you sure you want to shut down your %1:core sytem now?").arg(app.systemName) + var popup = dialog.createObject(app, + { + headerIcon: "../images/dialog-warning-symbolic.svg", + title: qsTr("Shut down %1:core").arg(app.systemName), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + engine.systemController.shutdown() + }) + } + } + + Component { id: timePickerComponent Dialog { diff --git a/nymea-app/ui/system/MqttBrokerSettingsPage.qml b/nymea-app/ui/system/MqttBrokerSettingsPage.qml index 4040582f..02fe7d35 100644 --- a/nymea-app/ui/system/MqttBrokerSettingsPage.qml +++ b/nymea-app/ui/system/MqttBrokerSettingsPage.qml @@ -34,140 +34,102 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("MQTT broker") - onBackPressed: pageStack.pop(); + title: qsTr("MQTT broker") + + SettingsPageSectionHeader { + text: qsTr("MQTT Server Interfaces") } -// Flickable { -// anchors.fill: parent -// contentHeight: connectionsColumn.implicitHeight -// interactive: contentHeight > height + Repeater { + model: engine.nymeaConfiguration.mqttServerConfigurations - ColumnLayout { - id: connectionsColumn -// anchors { left: parent.left; top: parent.top; right: parent.right } - anchors.fill: parent -// layoutDirection: Qt. - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - Layout.topMargin: app.margins - text: qsTr("MQTT Server Interfaces") - wrapMode: Text.WordWrap - color: app.accentColor + delegate: ConnectionInterfaceDelegate { + Layout.fillWidth: true + canDelete: true + onClicked: { + var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.mqttServerConfigurations.get(index).clone() }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setMqttServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() } - ListView { - Layout.fillWidth: true - Layout.minimumHeight: 0 - Layout.preferredHeight: Math.min(contentHeight, 120) - model: engine.nymeaConfiguration.mqttServerConfigurations - clip: true - ScrollBar.vertical: ScrollBar {} - - delegate: ConnectionInterfaceDelegate { - width: parent.width - canDelete: true - onClicked: { - var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.mqttServerConfigurations.get(index).clone() }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setMqttServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } - - onDeleteClicked: { - engine.nymeaConfiguration.deleteMqttServerConfiguration(model.id) - } - } - } - Button { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - text: qsTr("Add") - onClicked: { - var config = engine.nymeaConfiguration.createServerConfiguration("0.0.0.0", 1883 + engine.nymeaConfiguration.mqttServerConfigurations.count, false, false); - var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: config }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setMqttServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } - } - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.topMargin: app.margins; Layout.rightMargin: app.margins - text: qsTr("MQTT permissions") - wrapMode: Text.WordWrap - color: app.accentColor - } - - ListView { - Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, parent.height * .4) - model: engine.nymeaConfiguration.mqttPolicies - clip: true - ScrollBar.vertical: ScrollBar {} - delegate: NymeaListItemDelegate { - width: parent.width - iconName: "../images/account.svg" - text: qsTr("Client ID: %1").arg(model.clientId) - subText: qsTr("Username: %1").arg(model.username) - progressive: false - canDelete: true - onClicked: { - var page = pageStack.push(Qt.resolvedUrl("MqttPolicyPage.qml"), { policy: engine.nymeaConfiguration.mqttPolicies.get(index).clone() }); - page.accepted.connect(function() { - if (page.policy.clientId !== model.clientId) { - engine.nymeaConfiguration.deleteMqttPolicy(model.clientId); - } - engine.nymeaConfiguration.updateMqttPolicy(page.policy) - page.policy.destroy(); - }) - page.rejected.connect(function() { - page.policy.destroy(); - }) - } - onDeleteClicked: { - engine.nymeaConfiguration.deleteMqttPolicy(model.clientId) - } - } - } - - Button { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - text: qsTr("Add") - onClicked: { - var page = pageStack.push(Qt.resolvedUrl("MqttPolicyPage.qml"), { policy: engine.nymeaConfiguration.createMqttPolicy() }); - page.accepted.connect(function() { - engine.nymeaConfiguration.updateMqttPolicy(page.policy) - page.policy.destroy(); - }) - page.rejected.connect(function() { - page.policy.destroy(); - }) - } - } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: 0 + onDeleteClicked: { + engine.nymeaConfiguration.deleteMqttServerConfiguration(model.id) } } -// } + } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Add") + onClicked: { + var config = engine.nymeaConfiguration.createServerConfiguration("0.0.0.0", 1883 + engine.nymeaConfiguration.mqttServerConfigurations.count, false, false); + var component = Qt.createComponent(Qt.resolvedUrl("ServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: config }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setMqttServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() + } + } + + SettingsPageSectionHeader { + text: qsTr("MQTT permissions") + } + + Repeater { + model: engine.nymeaConfiguration.mqttPolicies + delegate: NymeaListItemDelegate { + Layout.fillWidth: true + iconName: "../images/account.svg" + text: qsTr("Client ID: %1").arg(model.clientId) + subText: qsTr("Username: %1").arg(model.username) + progressive: false + canDelete: true + onClicked: { + var page = pageStack.push(Qt.resolvedUrl("MqttPolicyPage.qml"), { policy: engine.nymeaConfiguration.mqttPolicies.get(index).clone() }); + page.accepted.connect(function() { + if (page.policy.clientId !== model.clientId) { + engine.nymeaConfiguration.deleteMqttPolicy(model.clientId); + } + engine.nymeaConfiguration.updateMqttPolicy(page.policy) + page.policy.destroy(); + }) + page.rejected.connect(function() { + page.policy.destroy(); + }) + } + onDeleteClicked: { + engine.nymeaConfiguration.deleteMqttPolicy(model.clientId) + } + } + } + + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Add") + onClicked: { + var page = pageStack.push(Qt.resolvedUrl("MqttPolicyPage.qml"), { policy: engine.nymeaConfiguration.createMqttPolicy() }); + page.accepted.connect(function() { + engine.nymeaConfiguration.updateMqttPolicy(page.policy) + page.policy.destroy(); + }) + page.rejected.connect(function() { + page.policy.destroy(); + }) + } + } } diff --git a/nymea-app/ui/system/MqttPolicyPage.qml b/nymea-app/ui/system/MqttPolicyPage.qml index 0416e077..a093ed52 100644 --- a/nymea-app/ui/system/MqttPolicyPage.qml +++ b/nymea-app/ui/system/MqttPolicyPage.qml @@ -35,7 +35,7 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root header: NymeaHeader { text: qsTr("Mqtt permission") @@ -57,175 +57,176 @@ Page { signal accepted(); signal rejected() - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right; bottom: parent.bottom } - RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - spacing: app.margins - Label { - text: qsTr("Client ID:") - Layout.fillWidth: true - } - TextField { - id: clientIdTextField - Layout.fillWidth: true - text: root.policy ? root.policy.clientId : "" - onEditingFinished: root.policy.clientId = text - placeholderText: qsTr("E.g. Sensor_1") - property bool isEmpty: displayText.length === 0 - property bool isDuplicate: clientIdTextField.displayText != root.policy.clientId && engine.nymeaConfiguration.mqttPolicies.getPolicy(clientIdTextField.displayText) !== null - property bool isValid: !isEmpty && !isDuplicate - } - } - Label { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - text: clientIdTextField.isDuplicate ? qsTr("%1 is already used").arg(clientIdTextField.displayText) : qsTr("Can't be blank") - font.pixelSize: app.smallFont - Layout.alignment: Qt.AlignRight - color: "red" - visible: !clientIdTextField.isValid - } + Label { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Client info") + color: app.accentColor + } + RowLayout { + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins + Label { + text: qsTr("Client ID:") + Layout.fillWidth: true + } + TextField { + id: clientIdTextField + Layout.fillWidth: true + text: root.policy ? root.policy.clientId : "" + onEditingFinished: root.policy.clientId = text + placeholderText: qsTr("E.g. Sensor_1") + property bool isEmpty: displayText.length === 0 + property bool isDuplicate: clientIdTextField.displayText != root.policy.clientId && engine.nymeaConfiguration.mqttPolicies.getPolicy(clientIdTextField.displayText) !== null + property bool isValid: !isEmpty && !isDuplicate + } + } + + Label { + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: clientIdTextField.isDuplicate ? qsTr("%1 is already used").arg(clientIdTextField.displayText) : qsTr("Can't be blank") + font.pixelSize: app.smallFont + Layout.alignment: Qt.AlignRight + color: "red" + visible: !clientIdTextField.isValid + } + + RowLayout { + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + Label { + text: qsTr("Username:") + Layout.fillWidth: true + } + TextField { + id: usernameTextField + Layout.fillWidth: true + text: root.policy ? root.policy.username : "" + onEditingFinished: root.policy.username = text + placeholderText: qsTr("Optional") + } + } + + RowLayout { + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + Label { + text: qsTr("Password:") + Layout.fillWidth: true + } RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - Label { - text: qsTr("Username:") - Layout.fillWidth: true - } TextField { - id: usernameTextField + id: passwordTextField Layout.fillWidth: true - text: root.policy ? root.policy.username : "" - onEditingFinished: root.policy.username = text + text: root.policy ? root.policy.password : "" + onEditingFinished: root.policy.password = text placeholderText: qsTr("Optional") + echoMode: hiddenPassword ? TextInput.Password : TextInput.Normal + property bool hiddenPassword: true } - } - - RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - Label { - text: qsTr("Password:") - Layout.fillWidth: true - } - RowLayout { - TextField { - id: passwordTextField - Layout.fillWidth: true - text: root.policy ? root.policy.password : "" - onEditingFinished: root.policy.password = text - placeholderText: qsTr("Optional") - echoMode: hiddenPassword ? TextInput.Password : TextInput.Normal - property bool hiddenPassword: true - } - ColorIcon { - Layout.preferredHeight: app.iconSize - Layout.preferredWidth: height - name: "../images/eye.svg" - color: passwordTextField.hiddenPassword ? keyColor : app.accentColor - MouseArea { - anchors.fill: parent - onClicked: passwordTextField.hiddenPassword = !passwordTextField.hiddenPassword - } - } - } - } - - ThinDivider {} - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: qsTr("Allowed publish topics") - } - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: root.policy.allowedPublishTopicFilters - ScrollBar.vertical: ScrollBar {} - clip: true - delegate: NymeaListItemDelegate { - width: parent.width - text: modelData - canDelete: true - progressive: false - onDeleteClicked: { - root.policy.allowedPublishTopicFilters.splice(index, 1) - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - property bool add: false - TextField { - id: pubField - Layout.fillWidth: parent.add - Layout.preferredWidth: parent.add ? undefined : 0 - Behavior on width { - NumberAnimation {} - } - } - Button { - Layout.fillWidth: !parent.add - text: parent.add ? qsTr("OK") : qsTr("Add") - enabled: !parent.add || pubField.displayText.length > 0 - onClicked: { - if (parent.add) { - root.policy.allowedPublishTopicFilters.push(pubField.displayText) - pubField.clear(); - } - parent.add = !parent.add; - } - } - } - - ThinDivider {} - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins - text: qsTr("Allowed subscribe filters") - } - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: root.policy.allowedSubscribeTopicFilters - ScrollBar.vertical: ScrollBar {} - clip: true - delegate: NymeaListItemDelegate { - width: parent.width - text: modelData - canDelete: true - progressive: false - onDeleteClicked: { - root.policy.allowedSubscribeTopicFilters.splice(index, 1) - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins - property bool add: false - TextField { - id: subField - Layout.fillWidth: parent.add - Layout.preferredWidth: parent.add ? undefined : 0 - } - Button { - Layout.fillWidth: !parent.add; - Behavior on width { NumberAnimation {} } - text: parent.add ? qsTr("OK") : qsTr("Add") - enabled: !parent.add || subField.displayText.length > 0 - onClicked: { - if (parent.add) { - root.policy.allowedSubscribeTopicFilters.push(subField.displayText) - subField.clear(); - } - parent.add = !parent.add; + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + name: "../images/eye.svg" + color: passwordTextField.hiddenPassword ? keyColor : app.accentColor + MouseArea { + anchors.fill: parent + onClicked: passwordTextField.hiddenPassword = !passwordTextField.hiddenPassword } } } } + + Label { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Allowed publish topics") + color: app.accentColor + } + + Repeater { + model: root.policy.allowedPublishTopicFilters + delegate: NymeaListItemDelegate { + Layout.fillWidth: true + text: modelData + canDelete: true + progressive: false + onDeleteClicked: { + root.policy.allowedPublishTopicFilters.splice(index, 1) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + property bool add: false + TextField { + id: pubField + Layout.fillWidth: parent.add + Layout.preferredWidth: parent.add ? undefined : 0 + Behavior on width { + NumberAnimation {} + } + } + Button { + Layout.fillWidth: !parent.add + text: parent.add ? qsTr("OK") : qsTr("Add") + enabled: !parent.add || pubField.displayText.length > 0 + onClicked: { + if (parent.add) { + root.policy.allowedPublishTopicFilters.push(pubField.displayText) + pubField.clear(); + } + parent.add = !parent.add; + } + } + } + + Label { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Allowed subscribe filters") + color: app.accentColor + } + Repeater { + model: root.policy.allowedSubscribeTopicFilters + delegate: NymeaListItemDelegate { + Layout.fillWidth: true + text: modelData + canDelete: true + progressive: false + onDeleteClicked: { + root.policy.allowedSubscribeTopicFilters.splice(index, 1) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + property bool add: false + TextField { + id: subField + Layout.fillWidth: parent.add + Layout.preferredWidth: parent.add ? undefined : 0 + } + Button { + Layout.fillWidth: !parent.add; + Behavior on width { NumberAnimation {} } + text: parent.add ? qsTr("OK") : qsTr("Add") + enabled: !parent.add || subField.displayText.length > 0 + onClicked: { + if (parent.add) { + root.policy.allowedSubscribeTopicFilters.push(subField.displayText) + subField.clear(); + } + parent.add = !parent.add; + } + } + } } diff --git a/nymea-app/ui/system/NetworkSettingsPage.qml b/nymea-app/ui/system/NetworkSettingsPage.qml index d9aec7ec..75d17c27 100644 --- a/nymea-app/ui/system/NetworkSettingsPage.qml +++ b/nymea-app/ui/system/NetworkSettingsPage.qml @@ -35,14 +35,9 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Network settings") - onBackPressed: { - pageStack.pop(); - } - } + title: qsTr("Network settings") NetworkManager { id: networkManager @@ -80,212 +75,222 @@ Page { } } - ColumnLayout { - anchors.fill: parent + SettingsPageSectionHeader { + text: qsTr("General") + } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Current connection state") - prominentSubText: false - subText: { + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Current connection state") + prominentSubText: false + subText: { + switch (networkManager.state) { + case NetworkManager.NetworkManagerStateUnknown: + return qsTr("Unknown"); + case NetworkManager.NetworkManagerStateAsleep: + return qsTr("Asleep"); + case NetworkManager.NetworkManagerStateDisconnected: + return qsTr("Disconnected") + case NetworkManager.NetworkManagerStateDisconnecting: + return qsTr("Disconnecting") + case NetworkManager.NetworkManagerStateConnecting: + return qsTr("Connecting") + case NetworkManager.NetworkManagerStateConnectedLocal: + return qsTr("Locally connected") + case NetworkManager.NetworkManagerStateConnectedSite: + return qsTr("Site connected") + case NetworkManager.NetworkManagerStateConnectedGlobal: + return qsTr("Globally connected") + + } + } + progressive: false + additionalItem: Led { + anchors.verticalCenter: parent.verticalCenter + state: { switch (networkManager.state) { case NetworkManager.NetworkManagerStateUnknown: - return qsTr("Unknown"); case NetworkManager.NetworkManagerStateAsleep: - return qsTr("Asleep"); + return "off"; case NetworkManager.NetworkManagerStateDisconnected: - return qsTr("Disconnected") case NetworkManager.NetworkManagerStateDisconnecting: - return qsTr("Disconnecting") + return "red" case NetworkManager.NetworkManagerStateConnecting: - return qsTr("Connecting") case NetworkManager.NetworkManagerStateConnectedLocal: - return qsTr("Locally connected") case NetworkManager.NetworkManagerStateConnectedSite: - return qsTr("Site connected") + return "orange" case NetworkManager.NetworkManagerStateConnectedGlobal: - return qsTr("Globally connected") + return "green"; } } - progressive: false - additionalItem: Led { - state: { - switch (networkManager.state) { - case NetworkManager.NetworkManagerStateUnknown: - case NetworkManager.NetworkManagerStateAsleep: - return "off"; - case NetworkManager.NetworkManagerStateDisconnected: - case NetworkManager.NetworkManagerStateDisconnecting: - return "red" - case NetworkManager.NetworkManagerStateConnecting: - case NetworkManager.NetworkManagerStateConnectedLocal: - case NetworkManager.NetworkManagerStateConnectedSite: - return "orange" - case NetworkManager.NetworkManagerStateConnectedGlobal: - return "green"; - - } - } - } - } - - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Networking enabled") - subText: qsTr("Enable or disable networking altogether") - prominentSubText: false - progressive: false - additionalItem: Switch { - checked: networkManager.networkingEnabled - onClicked: { - if (!checked) { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("Disabling networking will disconnect all connected clients. Be aware that you will not be able to interact remotely with this %1 system any more. Do not proceed unless you know what your are doing.").arg(app.systemName) - + "\n\n" - + qsTr("Do you want to proceed?") - var popup = dialog.createObject(app, - { - headerIcon: "../images/dialog-warning-symbolic.svg", - title: qsTr("Disable networking?"), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - networkManager.enableNetworking(false); - }) - popup.rejected.connect(function() { - checked = true; - }) - } else { - networkManager.enableNetworking(true); - } - } - } - } - ThinDivider {} - - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Wired network") - subText: qsTr("Shows the current ethernet status") - progressive: false - prominentSubText: false - } - - Repeater { - model: networkManager.wiredNetworkDevices - NymeaListItemDelegate { - Layout.fillWidth: true - iconName: model.pluggedIn ? "../images/network-wired.svg" : "../images/network-wired-offline.svg" - text: model.interface + " (" + model.macAddress + ")" - subText: { - var ret = model.pluggedIn ? qsTr("Plugged in") : qsTr("Unplugged") - ret += " - " - ret += networkStateToString(model.state) - return ret; - } - progressive: false - } - } - - ThinDivider {} - - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Wireless network") - subText: qsTr("Enable or disable WiFi") - progressive: false - prominentSubText: false - additionalItem: Switch { - checked: networkManager.wirelessNetworkingEnabled - onClicked: { - if (!checked) { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("Disabling WiFi will disconnect all clients connected via WiFi. Be aware that you will not be able to interact remotely with this %1 system any more unless a LAN cable is connected.").arg(app.systemName) - + "\n\n" - + qsTr("Do you want to proceed?") - var popup = dialog.createObject(app, - { - headerIcon: "../images/dialog-warning-symbolic.svg", - title: qsTr("Disable WiFi?"), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - networkManager.enableWirelessNetworking(false); - }) - popup.rejected.connect(function() { - checked = true; - }) - } else { - networkManager.enableWirelessNetworking(true); - } - } - } - } - - Repeater { - model: networkManager.wirelessNetworkDevices - NymeaListItemDelegate { - Layout.fillWidth: true - iconName: { - switch (model.state) { - case NetworkDevice.NetworkDeviceStateUnknown: - case NetworkDevice.NetworkDeviceStateUnmanaged: - case NetworkDevice.NetworkDeviceStateUnavailable: - case NetworkDevice.NetworkDeviceStateDisconnected: - case NetworkDevice.NetworkDeviceStateDeactivating: - case NetworkDevice.NetworkDeviceStateFailed: - return "../images/network-wifi-offline.svg" - case NetworkDevice.NetworkDeviceStatePrepare: - return "../images/network-wifi.svg"; - case NetworkDevice.NetworkDeviceStateConfig: - return "../images/network-wifi-offline.svg" - case NetworkDevice.NetworkDeviceStateNeedAuth: - return "../images/network-wifi.svg"; - case NetworkDevice.NetworkDeviceStateIpConfig: - return "../images/network-wifi-offline.svg" - case NetworkDevice.NetworkDeviceStateIpCheck: - return "../images/network-wifi.svg"; - case NetworkDevice.NetworkDeviceStateSecondaries: - return "../images/network-wifi-offline.svg" - case NetworkDevice.NetworkDeviceStateActivated: - return "../images/network-wifi.svg"; - - } - console.warn("Unhandled enum", model.state) - } - text: model.interface + " (" + model.macAddress + ")" - subText: networkStateToString(model.state) - onClicked: { - var wirelessNetworkDevice = networkManager.wirelessNetworkDevices.getWirelessNetworkDevice(model.interface); - if (wirelessNetworkDevice.state === NetworkDevice.NetworkDeviceStateDisconnected) { - networkManager.refreshWifis(model.interface) - pageStack.push(wirelessAccessPointsPageComponent, {wirelessNetworkDevice: wirelessNetworkDevice}) - } else { - pageStack.push(currentApPageComponent, {wirelessNetworkDevice: wirelessNetworkDevice}) - } - } - } - } - Item { - Layout.fillWidth: true - Layout.fillHeight: true } } - Component { - id: wirelessAccessPointsPageComponent - Page { - id: wirelessAccessPointsPage - header: NymeaHeader { - text: qsTr("WiFi networks") - onBackPressed: { - pageStack.pop(); + + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Networking enabled") + subText: qsTr("Enable or disable networking altogether") + prominentSubText: false + progressive: false + additionalItem: Switch { + anchors.verticalCenter: parent.verticalCenter + checked: networkManager.networkingEnabled + onClicked: { + if (!checked) { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("Disabling networking will disconnect all connected clients. Be aware that you will not be able to interact remotely with this %1 system any more. Do not proceed unless you know what your are doing.").arg(app.systemName) + + "\n\n" + + qsTr("Do you want to proceed?") + var popup = dialog.createObject(app, + { + headerIcon: "../images/dialog-warning-symbolic.svg", + title: qsTr("Disable networking?"), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + networkManager.enableNetworking(false); + }) + popup.rejected.connect(function() { + checked = true; + }) + } else { + networkManager.enableNetworking(true); } } + } + } + + SettingsPageSectionHeader { + text: qsTr("Wired network") + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("No wired network interfaces available") + wrapMode: Text.WordWrap + visible: networkManager.wiredNetworkDevices.count == 0 + } + + Repeater { + model: networkManager.wiredNetworkDevices + NymeaListItemDelegate { + Layout.fillWidth: true + iconName: model.pluggedIn ? "../images/network-wired.svg" : "../images/network-wired-offline.svg" + text: model.interface + " (" + model.macAddress + ")" + subText: { + var ret = model.pluggedIn ? qsTr("Plugged in") : qsTr("Unplugged") + ret += " - " + ret += networkStateToString(model.state) + return ret; + } + progressive: false + } + } + + SettingsPageSectionHeader { + text: qsTr("Wireless network") + } + + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Enabled") + subText: qsTr("Enable or disable WiFi") + progressive: false + prominentSubText: false + additionalItem: Switch { + anchors.verticalCenter: parent.verticalCenter + checked: networkManager.wirelessNetworkingEnabled + onClicked: { + if (!checked) { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("Disabling WiFi will disconnect all clients connected via WiFi. Be aware that you will not be able to interact remotely with this %1 system any more unless a LAN cable is connected.").arg(app.systemName) + + "\n\n" + + qsTr("Do you want to proceed?") + var popup = dialog.createObject(app, + { + headerIcon: "../images/dialog-warning-symbolic.svg", + title: qsTr("Disable WiFi?"), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + networkManager.enableWirelessNetworking(false); + }) + popup.rejected.connect(function() { + checked = true; + }) + } else { + networkManager.enableWirelessNetworking(true); + } + } + } + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("No wired network interfaces available") + wrapMode: Text.WordWrap + visible: networkManager.wirelessNetworkDevices.count == 0 + } + + Repeater { + model: networkManager.wirelessNetworkDevices + NymeaListItemDelegate { + Layout.fillWidth: true + iconName: { + switch (model.state) { + case NetworkDevice.NetworkDeviceStateUnknown: + case NetworkDevice.NetworkDeviceStateUnmanaged: + case NetworkDevice.NetworkDeviceStateUnavailable: + case NetworkDevice.NetworkDeviceStateDisconnected: + case NetworkDevice.NetworkDeviceStateDeactivating: + case NetworkDevice.NetworkDeviceStateFailed: + return "../images/network-wifi-offline.svg" + case NetworkDevice.NetworkDeviceStatePrepare: + return "../images/network-wifi.svg"; + case NetworkDevice.NetworkDeviceStateConfig: + return "../images/network-wifi-offline.svg" + case NetworkDevice.NetworkDeviceStateNeedAuth: + return "../images/network-wifi.svg"; + case NetworkDevice.NetworkDeviceStateIpConfig: + return "../images/network-wifi-offline.svg" + case NetworkDevice.NetworkDeviceStateIpCheck: + return "../images/network-wifi.svg"; + case NetworkDevice.NetworkDeviceStateSecondaries: + return "../images/network-wifi-offline.svg" + case NetworkDevice.NetworkDeviceStateActivated: + return "../images/network-wifi.svg"; + + } + console.warn("Unhandled enum", model.state) + } + text: model.interface + " (" + model.macAddress + ")" + subText: networkStateToString(model.state) + onClicked: { + var wirelessNetworkDevice = networkManager.wirelessNetworkDevices.getWirelessNetworkDevice(model.interface); + if (wirelessNetworkDevice.state === NetworkDevice.NetworkDeviceStateDisconnected) { + networkManager.refreshWifis(model.interface) + pageStack.push(wirelessAccessPointsPageComponent, {wirelessNetworkDevice: wirelessNetworkDevice}) + } else { + pageStack.push(currentApPageComponent, {wirelessNetworkDevice: wirelessNetworkDevice}) + } + } + } + } + + Component { + id: wirelessAccessPointsPageComponent + SettingsPageBase { + id: wirelessAccessPointsPage + title: qsTr("WiFi networks") property var wirelessNetworkDevice: null @@ -294,13 +299,11 @@ Page { accessPoints: wirelessAccessPointsPage.wirelessNetworkDevice.accessPoints } - ListView { + Repeater { id: listView - anchors.fill: parent model: apProxy - ScrollBar.vertical: ScrollBar {} delegate: NymeaListItemDelegate { - width: parent.width + Layout.fillWidth: true text: model.ssid subText: model.macAddress iconName: { @@ -334,106 +337,95 @@ Page { } } + Component { id: authPageComponent - Page { + SettingsPageBase { id: authPage - - header: NymeaHeader { - text: qsTr("Authenticate") - onBackPressed: pageStack.pop() - } + title: qsTr("Authenticate") property var wirelessNetworkDevice: null property var wirelessAccessPoint: null - ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } - Label { + Label { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Enter the password for %1").arg(authPage.wirelessAccessPoint.ssid) + wrapMode: Text.WordWrap + } + RowLayout { + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + TextField { + id: passwordTextField Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("Enter the password for %1").arg(authPage.wirelessAccessPoint.ssid) - wrapMode: Text.WordWrap + property bool showPassword: false + echoMode: showPassword ? TextInput.Normal : TextInput.Password } - RowLayout { - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - TextField { - id: passwordTextField - Layout.fillWidth: true - property bool showPassword: false - echoMode: showPassword ? TextInput.Normal : TextInput.Password - } - ColorIcon { - Layout.preferredHeight: app.iconSize - Layout.preferredWidth: app.iconSize - name: "../images/eye.svg" - color: passwordTextField.showPassword ? app.accentColor : keyColor - MouseArea { - anchors.fill: parent - onClicked: passwordTextField.showPassword = !passwordTextField.showPassword - } - } - } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("OK") - enabled: passwordTextField.displayText.length >= 8 - onClicked: { - networkManager.connectToWiFi(authPage.wirelessNetworkDevice.interface, authPage.wirelessAccessPoint.ssid, passwordTextField.text) - pageStack.pop(root); + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "../images/eye.svg" + color: passwordTextField.showPassword ? app.accentColor : keyColor + MouseArea { + anchors.fill: parent + onClicked: passwordTextField.showPassword = !passwordTextField.showPassword } } } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("OK") + enabled: passwordTextField.displayText.length >= 8 + onClicked: { + networkManager.connectToWiFi(authPage.wirelessNetworkDevice.interface, authPage.wirelessAccessPoint.ssid, passwordTextField.text) + pageStack.pop(root); + } + } + } } Component { id: currentApPageComponent - Page { + SettingsPageBase { id: currentApPage - header: NymeaHeader { - text: qsTr("Current connection") - onBackPressed: pageStack.pop(); - } + title: qsTr("Current connection") property WirelessNetworkDevice wirelessNetworkDevice: null - GridLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } - columns: 1 - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("SSID") - subText: currentApPage.wirelessNetworkDevice.currentAccessPoint.ssid - progressive: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("MAC Address") - subText: currentApPage.wirelessNetworkDevice.currentAccessPoint.macAddress - progressive: false - } - NymeaListItemDelegate { - Layout.fillWidth: true - text: qsTr("Signal strength") - subText: currentApPage.wirelessNetworkDevice.currentAccessPoint.signalStrength - progressive: false - } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("SSID") + subText: currentApPage.wirelessNetworkDevice.currentAccessPoint.ssid + progressive: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("MAC Address") + subText: currentApPage.wirelessNetworkDevice.currentAccessPoint.macAddress + progressive: false + } + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Signal strength") + subText: currentApPage.wirelessNetworkDevice.currentAccessPoint.signalStrength + progressive: false + } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("Disconnect") - onClicked: { - networkManager.disconnectInterface(currentApPage.wirelessNetworkDevice.interface) - pageStack.pop(root); - } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Disconnect") + onClicked: { + networkManager.disconnectInterface(currentApPage.wirelessNetworkDevice.interface) + pageStack.pop(root); } } + } } } diff --git a/nymea-app/ui/system/PluginsPage.qml b/nymea-app/ui/system/PluginsPage.qml index 514b6d37..992cca66 100644 --- a/nymea-app/ui/system/PluginsPage.qml +++ b/nymea-app/ui/system/PluginsPage.qml @@ -35,7 +35,7 @@ import QtQuick.Layouts 1.3 import "../components" import Nymea 1.0 -Page { +SettingsPageBase { id: root header: NymeaHeader { text: qsTr("Plugins") @@ -51,36 +51,26 @@ Page { } } - ColumnLayout { - anchors.fill: parent - - Label { - Layout.fillWidth: true - Layout.margins: app.margins - wrapMode: Text.WordWrap - text: qsTr("This list shows the list of installed plugins on this %1 system.").arg(app.systemName) - } - - ThinDivider {} - - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - model: PluginsProxy { - id: pluginsProxy - plugins: engine.deviceManager.plugins - } - clip: true - - delegate: NymeaListItemDelegate { - property var plugin: pluginsProxy.get(index) - width: parent.width - iconName: "../images/plugin.svg" - text: model.name - progressive: plugin.paramTypes.count > 0 - onClicked: pageStack.push(Qt.resolvedUrl("PluginParamsPage.qml"), {plugin: plugin}) - } - } + Label { + Layout.fillWidth: true + Layout.margins: app.margins + wrapMode: Text.WordWrap + text: qsTr("This list shows the list of installed plugins on this %1 system.").arg(app.systemName) } + Repeater { + model: PluginsProxy { + id: pluginsProxy + plugins: engine.deviceManager.plugins + } + + delegate: NymeaListItemDelegate { + Layout.fillWidth: true + property var plugin: pluginsProxy.get(index) + iconName: "../images/plugin.svg" + text: model.name + progressive: plugin.paramTypes.count > 0 + onClicked: pageStack.push(Qt.resolvedUrl("PluginParamsPage.qml"), {plugin: plugin}) + } + } } diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index 717a7e11..51ec3e48 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -5,35 +5,128 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Authentication") + userManager.tokenInfos.count - backButtonVisible: true - onBackPressed: pageStack.pop() - } + title: qsTr("User settings") UserManager { id: userManager engine: _engine + + onChangePasswordResponse: { + if (error != UserManager.UserErrorNoError) { + var component = Qt.createComponent("../components/ErrorDialog.qml") + var text; + switch (error) { + case UserManager.UserErrorBadPassword: + text = qsTr("The given password is not valid."); + break; + case UserManager.UserErrorPermissionDenied: + text = qsTr("Permission denied."); + break; + case UserManager.UserErrorBackendError: + text = qsTr("The new password could not be stored.") + break; + default: + text = qsTr("Un unexpected error happened when changing the password. We're sorry for this. (Error code: %1)").arg(error); + break; + } + + var popup = component.createObject(app, {text: text}); + popup.open() + } + } } - ColumnLayout { - id: settingsGrid - anchors.fill: parent -// width: Math.min(500, parent.width - app.margins * 2) + SettingsPageSectionHeader { + text: qsTr("User info") + } - ListView { + NymeaListItemDelegate { + Layout.fillWidth: true + text: userManager.userInfo.username + subText: qsTr("Username") + progressive: false + prominentSubText: false + iconName: "../images/account.svg" + } + + NymeaListItemDelegate { + Layout.fillWidth: true + text: qsTr("Change password") + iconName: "../images/key.svg" + onClicked: { + var page = pageStack.push(changePasswordComponent) + page.confirmed.connect(function(newPassword) { + userManager.changePassword(newPassword) + }) + } + } + + SettingsPageSectionHeader { + text: qsTr("Devices / Apps accessing nymea:core") + } + + Repeater { + model: userManager.tokenInfos + + delegate: NymeaListItemDelegate { Layout.fillWidth: true - Layout.fillHeight: true - model: userManager.tokenInfos + text: model.deviceName + subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate)) + prominentSubText: false + progressive: false + canDelete: true - delegate: NymeaListItemDelegate { - width: parent.width - text: model.deviceName - subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate)) - prominentSubText: false - progressive: false + onDeleteClicked: { + userManager.removeToken(model.id) + } + } + } + + + Component { + id: changePasswordComponent + SettingsPageBase { + id: changePasswordPage + title: qsTr("Change password") + + signal confirmed(string newPassword) + + SettingsPageSectionHeader { + text: qsTr("Change password") + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: qsTr("Please enter the new password for %1").arg(userManager.userInfo.username) + wrapMode: Text.WordWrap + } + + PasswordTextField { + id: passwordTextField + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + minPasswordLength: 8 + requireLowerCaseLetter: true + requireUpperCaseLetter: true + requireNumber: true + requireSpecialChar: false + signup: true + } + + Button { + text: qsTr("OK") + Layout.fillWidth: true + Layout.margins: app.margins + enabled: passwordTextField.isValid + onClicked: { + changePasswordPage.confirmed(passwordTextField.password) + pageStack.pop(); + } } } } diff --git a/nymea-app/ui/system/WebServerSettingsPage.qml b/nymea-app/ui/system/WebServerSettingsPage.qml index 95f93e23..14762760 100644 --- a/nymea-app/ui/system/WebServerSettingsPage.qml +++ b/nymea-app/ui/system/WebServerSettingsPage.qml @@ -34,73 +34,53 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root - header: NymeaHeader { - text: qsTr("Web server") - onBackPressed: pageStack.pop(); + title: qsTr("Web server") + + SettingsPageSectionHeader { + text: qsTr("Web server interfaces") } - Flickable { - anchors.fill: parent - contentHeight: connectionsColumn.implicitHeight - interactive: contentHeight > height - - ColumnLayout { - id: connectionsColumn - anchors { left: parent.left; top: parent.top; right: parent.right } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - Layout.topMargin: app.margins - text: qsTr("Web server interfaces") - wrapMode: Text.WordWrap - color: app.accentColor + Repeater { + model: engine.nymeaConfiguration.webServerConfigurations + delegate: ConnectionInterfaceDelegate { + Layout.fillWidth: true + canDelete: true + onClicked: { + var component = Qt.createComponent(Qt.resolvedUrl("WebServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.webServerConfigurations.get(index).clone() }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setWebServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() } - - Repeater { - model: engine.nymeaConfiguration.webServerConfigurations - delegate: ConnectionInterfaceDelegate { - Layout.fillWidth: true - canDelete: true - onClicked: { - var component = Qt.createComponent(Qt.resolvedUrl("WebServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: engine.nymeaConfiguration.webServerConfigurations.get(index).clone() }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setWebServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } - onDeleteClicked: { - print("should delete") - engine.nymeaConfiguration.deleteWebServerConfiguration(model.id) - } - } - } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("Add") - onClicked: { - var config = engine.nymeaConfiguration.createWebServerConfiguration("0.0.0.0", 80 + engine.nymeaConfiguration.webServerConfigurations.count, false, false, "/var/www/"); - var component = Qt.createComponent(Qt.resolvedUrl("WebServerConfigurationDialog.qml")); - var popup = component.createObject(root, { serverConfiguration: config }); - popup.accepted.connect(function() { - engine.nymeaConfiguration.setWebServerConfiguration(popup.serverConfiguration) - popup.serverConfiguration.destroy(); - }) - popup.rejected.connect(function() { - popup.serverConfiguration.destroy(); - }) - popup.open() - } + onDeleteClicked: { + print("should delete") + engine.nymeaConfiguration.deleteWebServerConfiguration(model.id) } } } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Add") + onClicked: { + var config = engine.nymeaConfiguration.createWebServerConfiguration("0.0.0.0", 80 + engine.nymeaConfiguration.webServerConfigurations.count, false, false, "/var/www/"); + var component = Qt.createComponent(Qt.resolvedUrl("WebServerConfigurationDialog.qml")); + var popup = component.createObject(root, { serverConfiguration: config }); + popup.accepted.connect(function() { + engine.nymeaConfiguration.setWebServerConfiguration(popup.serverConfiguration) + popup.serverConfiguration.destroy(); + }) + popup.rejected.connect(function() { + popup.serverConfiguration.destroy(); + }) + popup.open() + } + } } From 4d6ba26a59616f5a5bc2682ed70e01075a78aede Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 26 Feb 2020 17:09:08 +0100 Subject: [PATCH 4/4] some fixes --- .../connection/discovery/nymeadiscovery.cpp | 2 +- libnymea-app-core/connection/nymeaconnection.cpp | 1 + libnymea-app-core/jsonrpc/jsonrpcclient.cpp | 15 ++++++++++++++- libnymea-app-core/jsonrpc/jsonrpcclient.h | 4 ++++ nymea-app/ui/SettingsPage.qml | 1 + nymea-app/ui/connection/LoginPage.qml | 4 ++-- 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/libnymea-app-core/connection/discovery/nymeadiscovery.cpp b/libnymea-app-core/connection/discovery/nymeadiscovery.cpp index 33b085fc..f2db34bc 100644 --- a/libnymea-app-core/connection/discovery/nymeadiscovery.cpp +++ b/libnymea-app-core/connection/discovery/nymeadiscovery.cpp @@ -164,7 +164,7 @@ void NymeaDiscovery::cacheHost(NymeaHost *host) settings.beginGroup(QString::number(i++)); settings.setValue("url", connection->url()); settings.setValue("bearerType", connection->bearerType()); - settings.value("secure", connection->secure()); + settings.setValue("secure", connection->secure()); settings.setValue("displayName", connection->displayName()); settings.endGroup(); } diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index bdf60037..2a061bc0 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -161,6 +161,7 @@ Connection *NymeaConnection::currentConnection() const if (!m_currentHost || !m_currentTransport) { return nullptr; } + qDebug() << "secure:" << m_transportCandidates.value(m_currentTransport)->secure(); return m_transportCandidates.value(m_currentTransport); } diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp index 44dfea0f..afd17e94 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp @@ -189,6 +189,11 @@ bool JsonRpcClient::pushButtonAuthAvailable() const return m_pushButtonAuthAvailable; } +bool JsonRpcClient::authenticated() const +{ + return m_authenticated; +} + JsonRpcClient::CloudConnectionState JsonRpcClient::cloudConnectionState() const { return m_cloudConnectionState; @@ -297,6 +302,9 @@ void JsonRpcClient::processAuthenticate(const QVariantMap &data) settings.endGroup(); emit authenticationRequiredChanged(); + m_authenticated = true; + emit authenticated(); + setNotificationsEnabled(); } else { qWarning() << "Authentication failed" << data; @@ -376,6 +384,7 @@ void JsonRpcClient::onInterfaceConnectedChanged(bool connected) qDebug() << "JsonRpcClient: Transport disconnected."; m_initialSetupRequired = false; m_authenticationRequired = false; + m_authenticated = false; m_serverQtVersion.clear(); m_serverQtBuildVersion.clear(); if (m_connected) { @@ -442,6 +451,8 @@ void JsonRpcClient::dataReceived(const QByteArray &data) settings.setValue(m_serverUuid, m_token); settings.endGroup(); emit authenticationRequiredChanged(); + m_authenticated = false; + emit authenticatedChanged(); } if (!reply->caller().isNull() && !reply->callback().isEmpty()) { @@ -508,11 +519,13 @@ void JsonRpcClient::helloReply(const QVariantMap ¶ms) if (m_token.isEmpty()) { return; } + + m_authenticated = true; + emit authenticatedChanged(); } setNotificationsEnabled(); getCloudConnectionStatus(); - } } diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.h b/libnymea-app-core/jsonrpc/jsonrpcclient.h index 6705e4e7..1b193c54 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.h +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.h @@ -50,6 +50,7 @@ class JsonRpcClient : public JsonHandler Q_PROPERTY(bool initialSetupRequired READ initialSetupRequired NOTIFY initialSetupRequiredChanged) Q_PROPERTY(bool authenticationRequired READ authenticationRequired NOTIFY authenticationRequiredChanged) Q_PROPERTY(bool pushButtonAuthAvailable READ pushButtonAuthAvailable NOTIFY pushButtonAuthAvailableChanged) + Q_PROPERTY(bool authenticated READ authenticated NOTIFY authenticatedChanged) Q_PROPERTY(CloudConnectionState cloudConnectionState READ cloudConnectionState NOTIFY cloudConnectionStateChanged) Q_PROPERTY(QString serverVersion READ serverVersion NOTIFY handshakeReceived) Q_PROPERTY(QString jsonRpcVersion READ jsonRpcVersion NOTIFY handshakeReceived) @@ -81,6 +82,7 @@ public: bool initialSetupRequired() const; bool authenticationRequired() const; bool pushButtonAuthAvailable() const; + bool authenticated() const; CloudConnectionState cloudConnectionState() const; void deployCertificate(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint); @@ -103,6 +105,7 @@ signals: void initialSetupRequiredChanged(); void authenticationRequiredChanged(); void pushButtonAuthAvailableChanged(); + void authenticatedChanged(); void connectedChanged(bool connected); void tokenChanged(); void invalidProtocolVersion(const QString &actualVersion, const QString &minimumVersion); @@ -135,6 +138,7 @@ private: bool m_initialSetupRequired = false; bool m_authenticationRequired = false; bool m_pushButtonAuthAvailable = false; + bool m_authenticated = false; CloudConnectionState m_cloudConnectionState = CloudConnectionStateDisabled; int m_pendingPushButtonTransaction = -1; QString m_serverUuid; diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 542834dc..efacf5f0 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -75,6 +75,7 @@ Page { Material.elevation: layout.isGrid ? 1 : 0 padding: 0 visible: engine.jsonRpcClient.ensureServerVersion("4.2") + && engine.jsonRpcClient.authenticated NymeaListItemDelegate { width: parent.width iconName: "../images/account.svg" diff --git a/nymea-app/ui/connection/LoginPage.qml b/nymea-app/ui/connection/LoginPage.qml index e2e7e44b..4affb1b4 100644 --- a/nymea-app/ui/connection/LoginPage.qml +++ b/nymea-app/ui/connection/LoginPage.qml @@ -53,7 +53,7 @@ Page { popup.open(); } onCreateUserSucceeded: { - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.device + ")"); + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.deviceModel + ")"); } onCreateUserFailed: { @@ -153,7 +153,7 @@ Page { engine.jsonRpcClient.createUser(usernameTextField.text, passwordTextField.password); } else { print("authenticate", usernameTextField.text, passwordTextField.text, "nymea-app") - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.device + ")"); + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.deviceModel + ")"); } } }