From 5086d1373a060d859525ea4d04973f73f380afcb Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 6 Feb 2020 12:37:04 +0100 Subject: [PATCH] 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() + } + } }