diff --git a/libnymea-app-core/connection/discovery/nymeadiscovery.cpp b/libnymea-app-core/connection/discovery/nymeadiscovery.cpp index 33b085fc..f2db34bc 100644 --- a/libnymea-app-core/connection/discovery/nymeadiscovery.cpp +++ b/libnymea-app-core/connection/discovery/nymeadiscovery.cpp @@ -164,7 +164,7 @@ void NymeaDiscovery::cacheHost(NymeaHost *host) settings.beginGroup(QString::number(i++)); settings.setValue("url", connection->url()); settings.setValue("bearerType", connection->bearerType()); - settings.value("secure", connection->secure()); + settings.setValue("secure", connection->secure()); settings.setValue("displayName", connection->displayName()); settings.endGroup(); } diff --git a/libnymea-app-core/connection/nymeaconnection.cpp b/libnymea-app-core/connection/nymeaconnection.cpp index bdf60037..2a061bc0 100644 --- a/libnymea-app-core/connection/nymeaconnection.cpp +++ b/libnymea-app-core/connection/nymeaconnection.cpp @@ -161,6 +161,7 @@ Connection *NymeaConnection::currentConnection() const if (!m_currentHost || !m_currentTransport) { return nullptr; } + qDebug() << "secure:" << m_transportCandidates.value(m_currentTransport)->secure(); return m_transportCandidates.value(m_currentTransport); } diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp index 44dfea0f..afd17e94 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp @@ -189,6 +189,11 @@ bool JsonRpcClient::pushButtonAuthAvailable() const return m_pushButtonAuthAvailable; } +bool JsonRpcClient::authenticated() const +{ + return m_authenticated; +} + JsonRpcClient::CloudConnectionState JsonRpcClient::cloudConnectionState() const { return m_cloudConnectionState; @@ -297,6 +302,9 @@ void JsonRpcClient::processAuthenticate(const QVariantMap &data) settings.endGroup(); emit authenticationRequiredChanged(); + m_authenticated = true; + emit authenticated(); + setNotificationsEnabled(); } else { qWarning() << "Authentication failed" << data; @@ -376,6 +384,7 @@ void JsonRpcClient::onInterfaceConnectedChanged(bool connected) qDebug() << "JsonRpcClient: Transport disconnected."; m_initialSetupRequired = false; m_authenticationRequired = false; + m_authenticated = false; m_serverQtVersion.clear(); m_serverQtBuildVersion.clear(); if (m_connected) { @@ -442,6 +451,8 @@ void JsonRpcClient::dataReceived(const QByteArray &data) settings.setValue(m_serverUuid, m_token); settings.endGroup(); emit authenticationRequiredChanged(); + m_authenticated = false; + emit authenticatedChanged(); } if (!reply->caller().isNull() && !reply->callback().isEmpty()) { @@ -508,11 +519,13 @@ void JsonRpcClient::helloReply(const QVariantMap ¶ms) if (m_token.isEmpty()) { return; } + + m_authenticated = true; + emit authenticatedChanged(); } setNotificationsEnabled(); getCloudConnectionStatus(); - } } diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.h b/libnymea-app-core/jsonrpc/jsonrpcclient.h index 6705e4e7..1b193c54 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.h +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.h @@ -50,6 +50,7 @@ class JsonRpcClient : public JsonHandler Q_PROPERTY(bool initialSetupRequired READ initialSetupRequired NOTIFY initialSetupRequiredChanged) Q_PROPERTY(bool authenticationRequired READ authenticationRequired NOTIFY authenticationRequiredChanged) Q_PROPERTY(bool pushButtonAuthAvailable READ pushButtonAuthAvailable NOTIFY pushButtonAuthAvailableChanged) + Q_PROPERTY(bool authenticated READ authenticated NOTIFY authenticatedChanged) Q_PROPERTY(CloudConnectionState cloudConnectionState READ cloudConnectionState NOTIFY cloudConnectionStateChanged) Q_PROPERTY(QString serverVersion READ serverVersion NOTIFY handshakeReceived) Q_PROPERTY(QString jsonRpcVersion READ jsonRpcVersion NOTIFY handshakeReceived) @@ -81,6 +82,7 @@ public: bool initialSetupRequired() const; bool authenticationRequired() const; bool pushButtonAuthAvailable() const; + bool authenticated() const; CloudConnectionState cloudConnectionState() const; void deployCertificate(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint); @@ -103,6 +105,7 @@ signals: void initialSetupRequiredChanged(); void authenticationRequiredChanged(); void pushButtonAuthAvailableChanged(); + void authenticatedChanged(); void connectedChanged(bool connected); void tokenChanged(); void invalidProtocolVersion(const QString &actualVersion, const QString &minimumVersion); @@ -135,6 +138,7 @@ private: bool m_initialSetupRequired = false; bool m_authenticationRequired = false; bool m_pushButtonAuthAvailable = false; + bool m_authenticated = false; CloudConnectionState m_cloudConnectionState = CloudConnectionStateDisabled; int m_pendingPushButtonTransaction = -1; QString m_serverUuid; diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index a4d8a699..bb98dc68 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -105,6 +105,10 @@ #include "types/script.h" #include "types/scripts.h" #include "types/types.h" +#include "usermanager.h" +#include "types/tokeninfos.h" +#include "types/tokeninfo.h" +#include "types/userinfo.h" #include @@ -281,6 +285,11 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "ScriptSyntaxHighlighter"); qmlRegisterType(uri, 1, 0, "CodeCompletion"); qmlRegisterUncreatableType(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter"); + + qmlRegisterType(uri, 1, 0, "UserManager"); + qmlRegisterUncreatableType(uri, 1, 0, "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/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index c1fc4788..663b8bd1 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -55,6 +55,7 @@ SOURCES += \ scripting/completionmodel.cpp \ scriptmanager.cpp \ scriptsyntaxhighlighter.cpp \ + usermanager.cpp \ vendorsproxy.cpp \ pluginsproxy.cpp \ interfacesmodel.cpp \ @@ -91,6 +92,7 @@ SOURCES += \ models/devicemodel.cpp \ system/systemcontroller.cpp \ + HEADERS += \ configuration/networkmanager.h \ engine.h \ @@ -122,6 +124,7 @@ HEADERS += \ scripting/completionmodel.h \ scriptmanager.h \ scriptsyntaxhighlighter.h \ + usermanager.h \ vendorsproxy.h \ pluginsproxy.h \ interfacesmodel.h \ diff --git a/libnymea-app-core/usermanager.cpp b/libnymea-app-core/usermanager.cpp new file mode 100644 index 00000000..4693e372 --- /dev/null +++ b/libnymea-app-core/usermanager.cpp @@ -0,0 +1,129 @@ +#include "usermanager.h" +#include "types/tokeninfo.h" + +#include +#include + +UserManager::UserManager(QObject *parent): + JsonHandler(parent) +{ + m_userInfo = new UserInfo(this); + m_tokenInfos = new TokenInfos(this); +} + +Engine *UserManager::engine() const +{ + return m_engine; +} + +void UserManager::setEngine(Engine *engine) +{ + if (m_engine != engine) { + m_engine = engine; + m_engine->jsonRpcClient()->registerNotificationHandler(this, "notificationReceived"); + emit engineChanged(); + + m_loading = true; + emit loadingChanged(); + + m_engine->jsonRpcClient()->sendCommand("Users.GetUserInfo", QVariantMap(), this, "getUserInfoReply"); + m_engine->jsonRpcClient()->sendCommand("Users.GetTokens", QVariantMap(), this, "getTokensReply"); + } +} + +bool UserManager::loading() const +{ + return m_loading; +} + +UserInfo *UserManager::userInfo() const +{ + return m_userInfo; +} + +TokenInfos *UserManager::tokenInfos() const +{ + return m_tokenInfos; +} + +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::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, 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(); + QString deviceName = token.value("deviceName").toString(); + QDateTime creationTime = QDateTime::fromSecsSinceEpoch(token.value("creationTime").toInt()); + TokenInfo *tokenInfo = new TokenInfo(id, username, deviceName, creationTime); + m_tokenInfos->addToken(tokenInfo); + } + +} + + + +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 new file mode 100644 index 00000000..d66cd44a --- /dev/null +++ b/libnymea-app-core/usermanager.h @@ -0,0 +1,73 @@ +#ifndef USERMANAGER_H +#define USERMANAGER_H + +#include + +#include "jsonrpc/jsonrpcclient.h" +#include "engine.h" + +#include "types/tokeninfos.h" +#include "types/userinfo.h" + +class UserManager: public JsonHandler +{ + Q_OBJECT + Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + + Q_PROPERTY(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; + void setEngine(Engine* engine); + + 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 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 26206534..1d5745ff 100644 --- a/libnymea-common/libnymea-common.pro +++ b/libnymea-common/libnymea-common.pro @@ -64,6 +64,10 @@ HEADERS += \ types/tags.h \ types/wirelessaccesspoint.h \ types/wirelessaccesspoints.h \ + types/tokeninfo.h \ + types/tokeninfos.h \ + types/userinfo.h \ + SOURCES += \ types/browseritem.cpp \ @@ -122,3 +126,8 @@ SOURCES += \ types/tags.cpp \ types/wirelessaccesspoint.cpp \ types/wirelessaccesspoints.cpp \ + types/tokeninfo.cpp \ + types/tokeninfos.cpp \ + types/userinfo.cpp \ + + diff --git a/libnymea-common/types/tokeninfo.cpp b/libnymea-common/types/tokeninfo.cpp new file mode 100644 index 00000000..b7206cfc --- /dev/null +++ b/libnymea-common/types/tokeninfo.cpp @@ -0,0 +1,31 @@ +#include "tokeninfo.h" + +TokenInfo::TokenInfo(const QUuid &id, const QString &username, const QString &deviceName, const QDateTime &creationTime, QObject *parent): + QObject(parent), + m_id(id), + m_username(username), + m_deviceName(deviceName), + m_creationTime(creationTime) +{ + +} + +QUuid TokenInfo::id() const +{ + return m_id; +} + +QString TokenInfo::username() const +{ + return m_username; +} + +QString TokenInfo::deviceName() const +{ + return m_deviceName; +} + +QDateTime TokenInfo::creationTime() const +{ + return m_creationTime; +} diff --git a/libnymea-common/types/tokeninfo.h b/libnymea-common/types/tokeninfo.h new file mode 100644 index 00000000..c27f10f3 --- /dev/null +++ b/libnymea-common/types/tokeninfo.h @@ -0,0 +1,26 @@ +#ifndef TOKENINFO_H +#define TOKENINFO_H + +#include +#include +#include + +class TokenInfo : public QObject +{ + Q_OBJECT +public: + explicit TokenInfo(const QUuid &id, const QString &username, const QString &deviceName, const QDateTime &creationTime, QObject *parent = nullptr); + + QUuid id() const; + QString username() const; + QString deviceName() const; + QDateTime creationTime() const; + +private: + QUuid m_id; + QString m_username; + QString m_deviceName; + QDateTime m_creationTime; +}; + +#endif // TOKENINFO_H diff --git a/libnymea-common/types/tokeninfos.cpp b/libnymea-common/types/tokeninfos.cpp new file mode 100644 index 00000000..e66fb3a6 --- /dev/null +++ b/libnymea-common/types/tokeninfos.cpp @@ -0,0 +1,60 @@ +#include "tokeninfos.h" +#include "tokeninfo.h" + +TokenInfos::TokenInfos(QObject *parent) : QAbstractListModel(parent) +{ + +} + +int TokenInfos::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant TokenInfos::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleId: + return m_list.at(index.row())->id(); + case RoleUsername: + return m_list.at(index.row())->username(); + case RoleDeviceName: + return m_list.at(index.row())->deviceName(); + case RoleCreationTime: + return m_list.at(index.row())->creationTime(); + } + return QVariant(); +} + +QHash TokenInfos::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleUsername, "username"); + roles.insert(RoleDeviceName, "deviceName"); + roles.insert(RoleCreationTime, "creationTime"); + return roles; +} + +void TokenInfos::addToken(TokenInfo *tokenInfo) +{ + tokenInfo->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(tokenInfo); + endInsertRows(); + emit countChanged(); +} + +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 new file mode 100644 index 00000000..ed666253 --- /dev/null +++ b/libnymea-common/types/tokeninfos.h @@ -0,0 +1,36 @@ +#ifndef TOKENINFOS_H +#define TOKENINFOS_H + +#include + +class TokenInfo; + +class TokenInfos : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleId, + RoleUsername, + RoleDeviceName, + RoleCreationTime + }; + + explicit TokenInfos(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void addToken(TokenInfo *tokenInfo); + void removeToken(const QUuid &tokenId); + +signals: + void countChanged(); + +private: + QList m_list; +}; + +#endif // TOKENINFOS_H 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 966f82c6..47c5d301 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -208,5 +208,8 @@ ui/magic/scripting/LineNumbers.qml 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 6a68d943..efacf5f0 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -70,6 +70,23 @@ Page { } } + Pane { + Layout.fillWidth: true + Material.elevation: layout.isGrid ? 1 : 0 + padding: 0 + visible: engine.jsonRpcClient.ensureServerVersion("4.2") + && engine.jsonRpcClient.authenticated + NymeaListItemDelegate { + width: parent.width + iconName: "../images/account.svg" + text: qsTr("User settings") + subText: qsTr("Configure who can log in") + prominentSubText: false + wrapTexts: false + onClicked: pageStack.push(Qt.resolvedUrl("system/UsersSettingsPage.qml")) + } + } + Pane { Layout.fillWidth: true Material.elevation: layout.isGrid ? 1 : 0 diff --git a/nymea-app/ui/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..4affb1b4 100644 --- a/nymea-app/ui/connection/LoginPage.qml +++ b/nymea-app/ui/connection/LoginPage.qml @@ -53,7 +53,7 @@ Page { popup.open(); } onCreateUserSucceeded: { - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app"); + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.deviceModel + ")"); } onCreateUserFailed: { @@ -153,7 +153,7 @@ Page { engine.jsonRpcClient.createUser(usernameTextField.text, passwordTextField.password); } else { print("authenticate", usernameTextField.text, passwordTextField.text, "nymea-app") - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app"); + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.deviceModel + ")"); } } } 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 new file mode 100644 index 00000000..51ec3e48 --- /dev/null +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -0,0 +1,133 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +SettingsPageBase { + id: root + 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() + } + } + } + + SettingsPageSectionHeader { + text: qsTr("User info") + } + + 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 + text: model.deviceName + subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate)) + prominentSubText: false + progressive: false + canDelete: true + + 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() + } + } }