Merge PR #597: Add a Dashboard main view

This commit is contained in:
Jenkins nymea 2021-06-06 23:27:35 +02:00
commit 7ce714b321
54 changed files with 3161 additions and 193 deletions

164
libnymea-app/appdata.cpp Normal file
View File

@ -0,0 +1,164 @@
#include "appdata.h"
#include "engine.h"
#include <QMetaProperty>
#include "config.h"
#include "logging.h"
NYMEA_LOGGING_CATEGORY(dcAppData, "AppData")
AppData::AppData(QObject *parent) : JsonHandler(parent)
{
m_syncTimer.setSingleShot(true);
connect(&m_syncTimer, &QTimer::timeout, this, &AppData::store);
}
AppData::~AppData()
{
if (m_engine && m_syncTimer.isActive()) {
store();
m_engine->jsonRpcClient()->unregisterNotificationHandler(this);
}
}
void AppData::classBegin()
{
for (int i = 0; i < metaObject()->propertyCount(); i++) {
qCDebug(dcAppData) << "ClassBegin property:" << metaObject()->property(i).name();
}
}
void AppData::componentComplete()
{
// setup change notifications
for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) {
QMetaProperty prop = metaObject()->property(i);
if (prop.hasNotifySignal()) {
static const int propertyChangedIndex = metaObject()->indexOfSlot("onPropertyChanged()");
QMetaObject::connect(this, prop.notifySignalIndex(), this, propertyChangedIndex);
}
}
load();
}
QString AppData::nameSpace() const
{
return "AppData";
}
Engine *AppData::engine() const
{
return m_engine;
}
void AppData::setEngine(Engine *engine)
{
if (m_engine == engine) {
return;
}
if (m_engine) {
m_engine->jsonRpcClient()->unregisterNotificationHandler(this);
}
m_engine = engine;
if (m_engine) {
m_engine->jsonRpcClient()->registerNotificationHandler(this, "notificationReceived");
}
emit engineChanged();
}
QString AppData::group() const
{
return m_group;
}
void AppData::setGroup(const QString &group)
{
if (m_group != group) {
if (m_syncTimer.isActive()) {
m_syncTimer.stop();
store();
}
m_group = group;
load();
}
}
void AppData::load()
{
if (!m_engine) {
return;
}
for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) {
QMetaProperty prop = metaObject()->property(i);
qCDebug(dcAppData) << "ComponentComplete property:" << prop.name() << prop.isUser() << prop.type() << prop.isScriptable(this) << prop.isScriptable();
QVariantMap params;
params.insert("appId", APPLICATION_NAME);
if (!m_group.isEmpty()) {
params.insert("group", m_group);
}
params.insert("key", prop.name());
int id = m_engine->jsonRpcClient()->sendCommand("AppData.Load", params, this, "appDataReceived");
m_readRequests.insert(id, prop.name());
}
}
void AppData::store()
{
if (!m_engine) {
return;
}
for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) {
QMetaProperty prop = metaObject()->property(i);
QVariantMap params;
params.insert("appId", APPLICATION_NAME);
params.insert("key", prop.name());
if (!m_group.isEmpty()) {
params.insert("group", m_group);
}
params.insert("value", prop.read(this));
m_engine->jsonRpcClient()->sendCommand("AppData.Store", params, this, "appDataWritten");
}
}
void AppData::onPropertyChanged()
{
if (!m_loopLock) {
m_syncTimer.start(500);
}
}
void AppData::appDataReceived(int commandId, const QVariantMap &params)
{
if (m_readRequests.contains(commandId)) {
QString propName = m_readRequests.take(commandId);
for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) {
QMetaProperty prop = metaObject()->property(i);
if (prop.name() == propName) {
m_loopLock = true;
prop.write(this, params.value("value").toString());
m_loopLock = false;
return;
}
}
qCWarning(dcAppData()) << "Retrieved app data property does not exist" << propName;
}
}
void AppData::appDataWritten(int commandId, const QVariantMap &params)
{
qCDebug(dcAppData()) << "App data written:" << commandId << params;
}
void AppData::notificationReceived(const QVariantMap &notification)
{
qCDebug(dcAppData()) << "AppData notification" << notification;
}

57
libnymea-app/appdata.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef APPDATA_H
#define APPDATA_H
#include "jsonrpc/jsonhandler.h"
#include <QQmlParserStatus>
#include <QTimer>
class Engine;
class AppData : public JsonHandler, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged)
Q_PROPERTY(QString group READ group WRITE setGroup NOTIFY groupChanged)
public:
explicit AppData(QObject *parent = nullptr);
~AppData() override;
void classBegin() override;
void componentComplete() override;
QString nameSpace() const override;
Engine *engine() const;
void setEngine(Engine *engine);
QString group() const;
void setGroup(const QString &group);
signals:
void engineChanged();
void groupChanged();
private slots:
void load();
void store();
void onPropertyChanged();
void appDataReceived(int commandId, const QVariantMap &params);
void appDataWritten(int commandId, const QVariantMap &params);
void notificationReceived(const QVariantMap &notification);
private:
Engine *m_engine = nullptr;
QTimer m_syncTimer;
QString m_group;
bool m_loopLock = false;
QHash<int, QString> m_readRequests;
};
#endif // APPDATA_H

View File

@ -188,15 +188,15 @@ bool AWSClient::confirmationPending() const
return m_confirmationPending; return m_confirmationPending;
} }
void AWSClient::login(const QString &username, const QString &password) bool AWSClient::login(const QString &username, const QString &password)
{ {
if (m_usedConfig.isEmpty()) { if (m_usedConfig.isEmpty()) {
qCInfo(dcCloud()) << "AWS config not set. Not logging in."; qCInfo(dcCloud()) << "AWS config not set. Not logging in.";
return; return false;
} }
if (m_loginInProgress) { if (m_loginInProgress) {
qCDebug(dcCloud()) << "Login already pending..."; qCDebug(dcCloud()) << "Login already pending...";
return; return false;
} }
m_loginInProgress = true; m_loginInProgress = true;
@ -243,16 +243,19 @@ void AWSClient::login(const QString &username, const QString &password)
if (reply->error() == QNetworkReply::HostNotFoundError) { if (reply->error() == QNetworkReply::HostNotFoundError) {
qCWarning(dcCloud()) << "Error logging in to aws due to network connection."; qCWarning(dcCloud()) << "Error logging in to aws due to network connection.";
emit loginResult(LoginErrorNetworkError); emit loginResult(LoginErrorNetworkError);
cancelCallQueue();
return; return;
} }
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
qCWarning(dcCloud()) << "Looks like a wrong password."; qCWarning(dcCloud()) << "Looks like a wrong password.";
m_username.clear(); m_username.clear();
m_password.clear(); m_password.clear();
cancelCallQueue();
emit loginResult(LoginErrorInvalidUserOrPass); emit loginResult(LoginErrorInvalidUserOrPass);
return; return;
} }
qCWarning(dcCloud()) << "Error logging in to aws. Error:" << reply->error() << reply->errorString(); qCWarning(dcCloud()) << "Error logging in to aws. Error:" << reply->error() << reply->errorString();
cancelCallQueue();
emit loginResult(LoginErrorUnknownError); emit loginResult(LoginErrorUnknownError);
return; return;
} }
@ -263,6 +266,7 @@ void AWSClient::login(const QString &username, const QString &password)
qCWarning(dcCloud()) << "Failed to parse AWS login response" << error.errorString(); qCWarning(dcCloud()) << "Failed to parse AWS login response" << error.errorString();
m_username.clear(); m_username.clear();
m_password.clear(); m_password.clear();
cancelCallQueue();
emit loginResult(LoginErrorUnknownError); emit loginResult(LoginErrorUnknownError);
return; return;
} }
@ -277,6 +281,8 @@ void AWSClient::login(const QString &username, const QString &password)
QList<QByteArray> jwtParts = m_idToken.split('.'); QList<QByteArray> jwtParts = m_idToken.split('.');
if (jwtParts.count() != 3) { if (jwtParts.count() != 3) {
qCWarning(dcCloud()) << "Error: JWT token doesn't have 3 parts. Cannot retrieve AWS Cognito ID."; qCWarning(dcCloud()) << "Error: JWT token doesn't have 3 parts. Cannot retrieve AWS Cognito ID.";
cancelCallQueue();
emit loginResult(LoginErrorUnknownError);
return; return;
} }
// qDebug() << "decoded header:" << QByteArray::fromBase64(jwtParts.at(0)); // qDebug() << "decoded header:" << QByteArray::fromBase64(jwtParts.at(0));
@ -287,6 +293,7 @@ void AWSClient::login(const QString &username, const QString &password)
// qDebug() << "Getting cognito ID"; // qDebug() << "Getting cognito ID";
getId(); getId();
}); });
return true;
} }
void AWSClient::logout() void AWSClient::logout()
@ -634,6 +641,7 @@ void AWSClient::getId()
reply->deleteLater(); reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcCloud()) << "Error calling GetId" << reply->error() << reply->errorString(); qCWarning(dcCloud()) << "Error calling GetId" << reply->error() << reply->errorString();
cancelCallQueue();
return; return;
} }
QByteArray data = reply->readAll(); QByteArray data = reply->readAll();
@ -641,6 +649,7 @@ void AWSClient::getId()
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCWarning(dcCloud()) << "Error parsing json reply for GetId" << error.errorString(); qCWarning(dcCloud()) << "Error parsing json reply for GetId" << error.errorString();
cancelCallQueue();
return; return;
} }
m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray(); m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray();
@ -820,6 +829,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
reply->deleteLater(); reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcCloud()) << "Error calling GetCredentialsForIdentity" << reply->errorString(); qCWarning(dcCloud()) << "Error calling GetCredentialsForIdentity" << reply->errorString();
cancelCallQueue();
emit loginResult(LoginErrorUnknownError); emit loginResult(LoginErrorUnknownError);
return; return;
} }
@ -828,6 +838,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCWarning(dcCloud()) << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString(); qCWarning(dcCloud()) << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString();
cancelCallQueue();
emit loginResult(LoginErrorUnknownError); emit loginResult(LoginErrorUnknownError);
return; return;
} }
@ -884,12 +895,25 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
}); });
} }
void AWSClient::cancelCallQueue()
{
while (!m_callQueue.isEmpty()) {
QueuedCall qc = m_callQueue.takeFirst();
// Only postToMQTT needs calling a callback with error
if (qc.method == "postToMQTT") {
if (!qc.sender.isNull()) {
qc.callback(false);
}
}
}
}
bool AWSClient::tokensExpired() const bool AWSClient::tokensExpired() const
{ {
return (m_accessTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()) || (m_sessionTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()); return (m_accessTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()) || (m_sessionTokenExpiry.addSecs(-10) < QDateTime::currentDateTime());
} }
bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject* sender, std::function<void (bool)> callback) bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QPointer<QObject> sender, std::function<void (bool)> callback)
{ {
if (!isLoggedIn()) { if (!isLoggedIn()) {
qCWarning(dcCloud()) << "Cannot post to MQTT. Not logged in to AWS"; qCWarning(dcCloud()) << "Cannot post to MQTT. Not logged in to AWS";
@ -899,12 +923,10 @@ bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject*
qCDebug(dcCloud()) << "Cannot post to MQTT. Need to refresh the tokens first"; qCDebug(dcCloud()) << "Cannot post to MQTT. Need to refresh the tokens first";
refreshAccessToken(); refreshAccessToken();
QueuedCall::enqueue(m_callQueue, QueuedCall("postToMQTT", coreId, nonce, sender, callback)); QueuedCall::enqueue(m_callQueue, QueuedCall("postToMQTT", coreId, nonce, sender, callback));
return true; // So far it looks we're doing ok... let's return true return true; // Pretending we're doing fine
} }
QString topic = QString("%1/%2/proxy").arg(coreId).arg(QString(m_identityId)); QString topic = QString("%1/%2/proxy").arg(coreId).arg(QString(m_identityId));
QPointer<QObject> senderWatcher = QPointer<QObject>(sender);
// This is somehow broken in AWS... // This is somehow broken in AWS...
// The Signature needs to be created with having the topic percentage-encoded twice // The Signature needs to be created with having the topic percentage-encoded twice
// while the actual request needs to go out with it only being encoded once. // while the actual request needs to go out with it only being encoded once.
@ -938,18 +960,18 @@ bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject*
// } // }
qCDebug(dcCloud) << "Payload:" << payload; qCDebug(dcCloud) << "Payload:" << payload;
QNetworkReply *reply = m_nam->post(request, payload); QNetworkReply *reply = m_nam->post(request, payload);
QTimer::singleShot(5000, reply, [reply, senderWatcher, callback](){ QTimer::singleShot(5000, reply, [reply, sender, callback](){
reply->deleteLater(); reply->deleteLater();
qCWarning(dcCloud) << "Timeout posting to MQTT"; qCWarning(dcCloud) << "Timeout posting to MQTT";
if (senderWatcher) { if (!sender.isNull()) {
callback(false); callback(false);
} }
}); });
connect(reply, &QNetworkReply::finished, this, [reply, senderWatcher, callback]() { connect(reply, &QNetworkReply::finished, this, [reply, sender, callback]() {
reply->deleteLater(); reply->deleteLater();
QByteArray data = reply->readAll(); QByteArray data = reply->readAll();
qCDebug(dcCloud()) << "MQTT post reply" << data; // qDebug() << "MQTT post reply" << data;
if (senderWatcher.isNull()) { if (sender.isNull()) {
qCDebug(dcCloud()) << "Request object disappeared. Discarding MQTT reply..."; qCDebug(dcCloud()) << "Request object disappeared. Discarding MQTT reply...";
return; return;
} }
@ -1052,18 +1074,17 @@ void AWSClient::fetchDevices()
}); });
} }
void AWSClient::refreshAccessToken() bool AWSClient::refreshAccessToken()
{ {
if (!isLoggedIn()) { if (!isLoggedIn()) {
qCWarning(dcCloud()) << "Cannot refresh tokens. Not logged in to AWS"; qCWarning(dcCloud()) << "Cannot refresh tokens. Not logged in to AWS";
return; return false;
} }
// We should use REFRESH_TOKEN_AUTH to refresh our tokens but it's not working // We should use REFRESH_TOKEN_AUTH to refresh our tokens but it's not working
// https://forums.aws.amazon.com/thread.jspa?threadID=287978 // https://forums.aws.amazon.com/thread.jspa?threadID=287978
// Let's re-login instead with user & pass // Let's re-login instead with user & pass
login(m_username, m_password); return login(m_username, m_password);
return;
// Non-working block... Enable this if Amazon ever fixes their API... // Non-working block... Enable this if Amazon ever fixes their API...
@ -1126,6 +1147,7 @@ void AWSClient::refreshAccessToken()
emit isLoggedInChanged(); emit isLoggedInChanged();
}); });
return true;
} }

View File

@ -139,7 +139,7 @@ public:
AWSDevices* awsDevices() const; AWSDevices* awsDevices() const;
bool confirmationPending() const; bool confirmationPending() const;
Q_INVOKABLE void login(const QString &username, const QString &password); Q_INVOKABLE bool login(const QString &username, const QString &password);
Q_INVOKABLE void logout(); Q_INVOKABLE void logout();
Q_INVOKABLE void signup(const QString &username, const QString &password); Q_INVOKABLE void signup(const QString &username, const QString &password);
Q_INVOKABLE void confirmRegistration(const QString &code); Q_INVOKABLE void confirmRegistration(const QString &code);
@ -151,7 +151,7 @@ public:
Q_INVOKABLE void fetchDevices(); Q_INVOKABLE void fetchDevices();
Q_INVOKABLE bool postToMQTT(const QString &coreId, const QString &nonce, QObject* sender, std::function<void(bool)> callback); Q_INVOKABLE bool postToMQTT(const QString &coreId, const QString &nonce, QPointer<QObject> sender, std::function<void(bool)> callback);
Q_INVOKABLE void getId(); Q_INVOKABLE void getId();
Q_INVOKABLE void registerPushNotificationEndpoint(const QString &registrationId, const QString &deviceDisplayName, const QString mobileDeviceId, const QString &mobileDeviceManufacturer, const QString &mobileDeviceModel); Q_INVOKABLE void registerPushNotificationEndpoint(const QString &registrationId, const QString &deviceDisplayName, const QString mobileDeviceId, const QString &mobileDeviceManufacturer, const QString &mobileDeviceModel);
@ -185,7 +185,7 @@ private:
explicit AWSClient(QObject *parent = nullptr); explicit AWSClient(QObject *parent = nullptr);
static AWSClient* s_instance; static AWSClient* s_instance;
void refreshAccessToken(); bool refreshAccessToken();
void getCredentialsForIdentity(const QString &identityId); void getCredentialsForIdentity(const QString &identityId);
void connectMQTT(); void connectMQTT();
@ -218,7 +218,7 @@ private:
QueuedCall(const QString &method): method(method) { } QueuedCall(const QString &method): method(method) { }
QueuedCall(const QString &method, const QString &arg1): method(method), arg1(arg1) { } QueuedCall(const QString &method, const QString &arg1): method(method), arg1(arg1) { }
QueuedCall(const QString &method, const QString &arg1, const QString &arg2, const QString &arg3, const QString &arg4, const QString &arg5): method(method), arg1(arg1), arg2(arg2), arg3(arg3), arg4(arg4), arg5(arg5) { } QueuedCall(const QString &method, const QString &arg1, const QString &arg2, const QString &arg3, const QString &arg4, const QString &arg5): method(method), arg1(arg1), arg2(arg2), arg3(arg3), arg4(arg4), arg5(arg5) { }
QueuedCall(const QString &method, const QString &arg1, const QString &arg2, QObject* sender, std::function<void(bool)> callback): method(method), arg1(arg1), arg2(arg2), sender(sender), callback(callback) {} QueuedCall(const QString &method, const QString &arg1, const QString &arg2, QPointer<QObject> sender, std::function<void(bool)> callback): method(method), arg1(arg1), arg2(arg2), sender(sender), callback(callback) {}
QString method; QString method;
QString arg1; QString arg1;
QString arg2; QString arg2;
@ -241,6 +241,7 @@ private:
queue.append(call); queue.append(call);
} }
}; };
void cancelCallQueue();
QList<QueuedCall> m_callQueue; QList<QueuedCall> m_callQueue;

View File

@ -129,6 +129,7 @@
#include "zigbee/zigbeenetworks.h" #include "zigbee/zigbeenetworks.h"
#include "applogcontroller.h" #include "applogcontroller.h"
#include "tagwatcher.h" #include "tagwatcher.h"
#include "appdata.h"
#include <QtQml/qqml.h> #include <QtQml/qqml.h>
@ -338,6 +339,8 @@ void registerQmlTypes() {
qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher"); qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher");
qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher"); qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher");
qmlRegisterType<AppData>(uri, 1, 0, "AppData");
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel"); qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
} }

View File

@ -20,6 +20,7 @@ INCLUDEPATH += \
$$top_srcdir/QtZeroConf $$top_srcdir/QtZeroConf
SOURCES += \ SOURCES += \
$$PWD/appdata.cpp \
$$PWD/models/scriptsproxymodel.cpp \ $$PWD/models/scriptsproxymodel.cpp \
$$PWD/tagwatcher.cpp \ $$PWD/tagwatcher.cpp \
$${PWD}/logging.cpp \ $${PWD}/logging.cpp \
@ -167,6 +168,7 @@ SOURCES += \
HEADERS += \ HEADERS += \
$$PWD/appdata.h \
$$PWD/models/scriptsproxymodel.h \ $$PWD/models/scriptsproxymodel.h \
$$PWD/tagwatcher.h \ $$PWD/tagwatcher.h \
$${PWD}/logging.h \ $${PWD}/logging.h \

View File

@ -287,6 +287,10 @@ void LogsModelNg::logsReply(int commandId, const QVariantMap &data)
} }
StateType *entryStateType = thing->thingClass()->stateTypes()->getStateType(entry->typeId()); StateType *entryStateType = thing->thingClass()->stateTypes()->getStateType(entry->typeId());
if (!entryStateType) {
qWarning() << "StateType" << entry->typeId() << "not found on thing" << thing->name();
continue;
}
if (m_graphSeries) { if (m_graphSeries) {
if (entryStateType->type().toLower() == "bool") { if (entryStateType->type().toLower() == "bool") {

View File

@ -74,9 +74,16 @@ void RuleManager::clear()
void RuleManager::init() void RuleManager::init()
{ {
m_fetchingData = true;
emit fetchingDataChanged();
m_jsonClient->sendCommand("Rules.GetRules", this, "getRulesReply"); m_jsonClient->sendCommand("Rules.GetRules", this, "getRulesReply");
} }
bool RuleManager::fetchingData() const
{
return m_fetchingData;
}
Rules *RuleManager::rules() const Rules *RuleManager::rules() const
{ {
return m_rules; return m_rules;
@ -176,6 +183,8 @@ void RuleManager::getRulesReply(int /*commandId*/, const QVariantMap &params)
requestParams.insert("ruleId", rule->id()); requestParams.insert("ruleId", rule->id());
m_jsonClient->sendCommand("Rules.GetRuleDetails", requestParams, this, "getRuleDetailsReply"); m_jsonClient->sendCommand("Rules.GetRuleDetails", requestParams, this, "getRuleDetailsReply");
} }
m_fetchingData = false;
emit fetchingDataChanged();
} }
void RuleManager::getRuleDetailsReply(int commandId, const QVariantMap &params) void RuleManager::getRuleDetailsReply(int commandId, const QVariantMap &params)

View File

@ -50,6 +50,7 @@ class RuleManager : public JsonHandler
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(Rules* rules READ rules CONSTANT) Q_PROPERTY(Rules* rules READ rules CONSTANT)
Q_PROPERTY(bool fetchingData READ fetchingData NOTIFY fetchingDataChanged)
public: public:
explicit RuleManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); explicit RuleManager(JsonRpcClient *jsonClient, QObject *parent = nullptr);
@ -58,6 +59,7 @@ public:
void clear(); void clear();
void init(); void init();
bool fetchingData() const;
Rules* rules() const; Rules* rules() const;
@ -99,10 +101,12 @@ private:
signals: signals:
void addRuleReply(int commandId, const QString &ruleError, const QString &ruleId); void addRuleReply(int commandId, const QString &ruleError, const QString &ruleId);
void editRuleReply(int commandId, const QString &ruleError); void editRuleReply(int commandId, const QString &ruleError);
void fetchingDataChanged();
private: private:
JsonRpcClient *m_jsonClient; JsonRpcClient *m_jsonClient;
Rules* m_rules; Rules* m_rules;
bool m_fetchingData = false;
}; };
#endif // RULEMANAGER_H #endif // RULEMANAGER_H

View File

@ -33,6 +33,7 @@
#include "engine.h" #include "engine.h"
#include <QJsonDocument> #include <QJsonDocument>
#include <QMetaEnum>
TagsManager::TagsManager(JsonRpcClient *jsonClient, QObject *parent): TagsManager::TagsManager(JsonRpcClient *jsonClient, QObject *parent):
JsonHandler(parent), JsonHandler(parent),
@ -52,7 +53,7 @@ void TagsManager::init()
m_busy = true; m_busy = true;
emit busyChanged(); emit busyChanged();
m_tags->clear(); m_tags->clear();
m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply"); m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsResponse");
} }
void TagsManager::clear() void TagsManager::clear()
@ -79,7 +80,7 @@ int TagsManager::tagThing(const QString &thingId, const QString &tagId, const QS
tag.insert("tagId", tagId); tag.insert("tagId", tagId);
tag.insert("value", value); tag.insert("value", value);
params.insert("tag", tag); params.insert("tag", tag);
return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagReply"); return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagResponse");
} }
int TagsManager::untagThing(const QString &thingId, const QString &tagId) int TagsManager::untagThing(const QString &thingId, const QString &tagId)
@ -90,7 +91,7 @@ int TagsManager::untagThing(const QString &thingId, const QString &tagId)
tag.insert("appId", "nymea:app"); tag.insert("appId", "nymea:app");
tag.insert("tagId", tagId); tag.insert("tagId", tagId);
params.insert("tag", tag); params.insert("tag", tag);
return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagReply"); return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagResponse");
} }
int TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QString &value) int TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QString &value)
@ -102,7 +103,7 @@ int TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QStr
tag.insert("tagId", tagId); tag.insert("tagId", tagId);
tag.insert("value", value); tag.insert("value", value);
params.insert("tag", tag); params.insert("tag", tag);
return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagReply"); return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagResponse");
} }
int TagsManager::untagRule(const QString &ruleId, const QString &tagId) int TagsManager::untagRule(const QString &ruleId, const QString &tagId)
@ -113,7 +114,7 @@ int TagsManager::untagRule(const QString &ruleId, const QString &tagId)
tag.insert("appId", "nymea:app"); tag.insert("appId", "nymea:app");
tag.insert("tagId", tagId); tag.insert("tagId", tagId);
params.insert("tag", tag); params.insert("tag", tag);
return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagReply"); return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagResponse");
} }
void TagsManager::handleTagsNotification(const QVariantMap &params) void TagsManager::handleTagsNotification(const QVariantMap &params)
@ -156,7 +157,7 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
} }
} }
void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap &params) void TagsManager::getTagsResponse(int /*commandId*/, const QVariantMap &params)
{ {
QList<Tag*> tags; QList<Tag*> tags;
foreach (const QVariant &tagVariant, params.value("tags").toList()) { foreach (const QVariant &tagVariant, params.value("tags").toList()) {
@ -171,14 +172,18 @@ void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap &params)
emit busyChanged(); emit busyChanged();
} }
void TagsManager::addTagReply(int commandId, const QVariantMap &params) void TagsManager::addTagResponse(int commandId, const QVariantMap &params)
{ {
qCDebug(dcTags()) << "AddTag reply" << commandId << params; qCDebug(dcTags()) << "AddTag reply" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<TagsManager::TagError>();
emit addTagReply(commandId, static_cast<TagsManager::TagError>(metaEnum.keyToValue(params.value("params").toMap().value("error").toByteArray())));
} }
void TagsManager::removeTagReply(int commandId, const QVariantMap &params) void TagsManager::removeTagResponse(int commandId, const QVariantMap &params)
{ {
qCDebug(dcTags()) << "RemoveTag reply" << commandId << params; qCDebug(dcTags()) << "RemoveTag reply" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<TagsManager::TagError>();
emit removeTagReply(commandId, static_cast<TagsManager::TagError>(metaEnum.keyToValue(params.value("params").toMap().value("error").toByteArray())));
} }
Tag* TagsManager::unpackTag(const QVariantMap &tagMap) Tag* TagsManager::unpackTag(const QVariantMap &tagMap)

View File

@ -43,6 +43,14 @@ class TagsManager : public JsonHandler
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public: public:
enum TagError {
TagErrorNoError,
TagErrorThingNotFound,
TagErrorRuleNotFound,
TagErrorTagNotFound
};
Q_ENUM(TagError)
explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr);
QString nameSpace() const override; QString nameSpace() const override;
@ -59,12 +67,14 @@ public:
signals: signals:
void busyChanged(); void busyChanged();
void addTagReply(int commandId, TagError error);
void removeTagReply(int commandId, TagError error);
private slots: private slots:
void handleTagsNotification(const QVariantMap &params); void handleTagsNotification(const QVariantMap &params);
void getTagsReply(int commandId, const QVariantMap &params); void getTagsResponse(int commandId, const QVariantMap &params);
void addTagReply(int commandId, const QVariantMap &params); void addTagResponse(int commandId, const QVariantMap &params);
void removeTagReply(int commandId, const QVariantMap &params); void removeTagResponse(int commandId, const QVariantMap &params);
private: private:
Tag *unpackTag(const QVariantMap &tagMap); Tag *unpackTag(const QVariantMap &tagMap);

View File

@ -0,0 +1,165 @@
#include "dashboarditem.h"
#include "dashboardmodel.h"
#include <QDebug>
DashboardItem::DashboardItem(const QString &type, QObject *parent):
QObject(parent),
m_type(type)
{
}
QString DashboardItem::type() const
{
return m_type;
}
int DashboardItem::columnSpan() const
{
return m_columnSpan;
}
void DashboardItem::setColumnSpan(int columnSpan)
{
if (m_columnSpan != columnSpan) {
m_columnSpan = columnSpan;
emit columnSpanChanged();
emit changed();
}
}
int DashboardItem::rowSpan() const
{
return m_rowSpan;
}
void DashboardItem::setRowSpan(int rowSpan)
{
if (m_rowSpan != rowSpan) {
m_rowSpan = rowSpan;
qCritical() << "emitting changed";
emit rowSpanChanged();
emit changed();
}
}
DashboardThingItem::DashboardThingItem(const QUuid &thingId, QObject *parent):
DashboardItem("thing", parent),
m_thingId(thingId)
{
}
QUuid DashboardThingItem::thingId() const
{
return m_thingId;
}
DashboardFolderItem::DashboardFolderItem(const QString &name, const QString &icon, QObject *parent):
DashboardItem("folder", parent),
m_name(name),
m_icon(icon)
{
m_model = new DashboardModel(this);
connect(m_model, &DashboardModel::changed, this, &DashboardItem::changed);
}
QString DashboardFolderItem::name() const
{
return m_name;
}
void DashboardFolderItem::setName(const QString &name)
{
if (m_name != name) {
m_name = name;
emit nameChanged();
emit changed();
}
}
QString DashboardFolderItem::icon() const
{
return m_icon;
}
void DashboardFolderItem::setIcon(const QString &icon)
{
if (m_icon != icon) {
m_icon = icon;
emit iconChanged();
emit changed();
}
}
DashboardModel *DashboardFolderItem::model() const
{
return m_model;
}
DashboardGraphItem::DashboardGraphItem(const QUuid &thingId, const QUuid &stateTypeId, QObject *parent):
DashboardItem("graph", parent),
m_thingId(thingId),
m_stateTypeId(stateTypeId)
{
setColumnSpan(2);
}
QUuid DashboardGraphItem::thingId() const
{
return m_thingId;
}
QUuid DashboardGraphItem::stateTypeId() const
{
return m_stateTypeId;
}
DashboardSceneItem::DashboardSceneItem(const QUuid &ruleId, QObject *parent):
DashboardItem("scene", parent),
m_ruleId(ruleId)
{
}
QUuid DashboardSceneItem::ruleId() const
{
return m_ruleId;
}
DashboardWebViewItem::DashboardWebViewItem(const QUrl &url, bool interactive, QObject *parent):
DashboardItem("webview", parent),
m_url(url),
m_interactive(interactive)
{
}
QUrl DashboardWebViewItem::url() const
{
return m_url;
}
void DashboardWebViewItem::setUrl(const QUrl &url)
{
if (m_url != url) {
m_url = url;
emit urlChanged();
emit changed();
}
}
bool DashboardWebViewItem::interactive() const
{
return m_interactive;
}
void DashboardWebViewItem::setInteractive(bool interactive)
{
if (m_interactive != interactive) {
m_interactive = interactive;
emit interactiveChanged();
emit changed();
}
}

View File

@ -0,0 +1,113 @@
#ifndef DASHBOARDITEM_H
#define DASHBOARDITEM_H
#include <QObject>
#include <QUuid>
#include <QUrl>
class DashboardModel;
class DashboardItem : public QObject
{
Q_OBJECT
Q_PROPERTY(QString type READ type CONSTANT)
Q_PROPERTY(int columnSpan READ columnSpan WRITE setColumnSpan NOTIFY columnSpanChanged)
Q_PROPERTY(int rowSpan READ rowSpan WRITE setRowSpan NOTIFY rowSpanChanged)
public:
explicit DashboardItem(const QString &type, QObject *parent = nullptr);
QString type() const;
int columnSpan() const;
void setColumnSpan(int columnSpan);
int rowSpan() const;
void setRowSpan(int rowSpan);
signals:
// For convenience when *any* change needs to be tracked
void changed();
void columnSpanChanged();
void rowSpanChanged();
private:
QString m_type;
int m_columnSpan = 1;
int m_rowSpan = 1;
};
class DashboardThingItem: public DashboardItem
{
Q_OBJECT
Q_PROPERTY(QUuid thingId READ thingId CONSTANT)
public:
explicit DashboardThingItem(const QUuid &thingId, QObject *parent = nullptr);
QUuid thingId() const;
private:
QUuid m_thingId;
};
class DashboardFolderItem: public DashboardItem
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(QString icon READ icon WRITE setIcon NOTIFY iconChanged)
Q_PROPERTY(DashboardModel* model READ model CONSTANT)
public:
explicit DashboardFolderItem(const QString &name, const QString &icon, QObject *parent = nullptr);
QString name() const;
void setName(const QString &name);
QString icon() const;
void setIcon(const QString &icon);
DashboardModel *model() const;
signals:
void nameChanged();
void iconChanged();
private:
QString m_name;
QString m_icon;
DashboardModel *m_model= nullptr;
};
class DashboardGraphItem: public DashboardItem
{
Q_OBJECT
Q_PROPERTY(QUuid thingId READ thingId CONSTANT)
Q_PROPERTY(QUuid stateTypeId READ stateTypeId CONSTANT)
public:
explicit DashboardGraphItem(const QUuid &thingId, const QUuid &stateTypeId, QObject *parent = nullptr);
QUuid thingId() const;
QUuid stateTypeId() const;
private:
QUuid m_thingId;
QUuid m_stateTypeId;
};
class DashboardSceneItem: public DashboardItem
{
Q_OBJECT
Q_PROPERTY(QUuid ruleId READ ruleId CONSTANT)
public:
explicit DashboardSceneItem(const QUuid &ruleId, QObject *parent = nullptr);
QUuid ruleId() const;
private:
QUuid m_ruleId;
};
class DashboardWebViewItem: public DashboardItem
{
Q_OBJECT
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged)
public:
explicit DashboardWebViewItem(const QUrl &url, bool interactive = false, QObject *parent = nullptr);
QUrl url() const;
void setUrl(const QUrl &url);
bool interactive() const;
void setInteractive(bool interactive);
signals:
void urlChanged();
void interactiveChanged();
private:
QUrl m_url;
bool m_interactive = false;
};
#endif // DASHBOARDITEM_H

View File

@ -0,0 +1,223 @@
#include "dashboardmodel.h"
#include "dashboarditem.h"
#include <QJsonDocument>
#include <QDebug>
DashboardModel::DashboardModel(QObject *parent) : QAbstractListModel(parent)
{
}
int DashboardModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_list.count();
}
QVariant DashboardModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case RoleType:
return m_list.at(index.row())->type();
case RoleColumnSpan:
return m_list.at(index.row())->columnSpan();
case RoleRowSpan:
return m_list.at(index.row())->rowSpan();
}
Q_ASSERT_X(false, "DashboardModel", "Unhandled role");
return QVariant();
}
QHash<int, QByteArray> DashboardModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(RoleType, "type");
roles.insert(RoleColumnSpan, "columnSpan");
roles.insert(RoleRowSpan, "rowSpan");
return roles;
}
DashboardItem *DashboardModel::get(int index) const
{
if (index < 0 || index >= m_list.count()) {
return nullptr;
}
return m_list.at(index);
}
void DashboardModel::addThingItem(const QUuid &thingId, int index)
{
DashboardThingItem *item = new DashboardThingItem(thingId, this);
addItem(item, index);
}
void DashboardModel::addFolderItem(const QString &name, const QString &icon, int index)
{
DashboardFolderItem *item = new DashboardFolderItem(name, icon, this);
connect(item->model(), &DashboardModel::save, this, &DashboardModel::save);
addItem(item, index);
}
void DashboardModel::addGraphItem(const QUuid &thingId, const QUuid &stateTypeId, int index)
{
DashboardGraphItem *item = new DashboardGraphItem(thingId, stateTypeId, this);
item->setColumnSpan(2);
addItem(item, index);
}
void DashboardModel::addSceneItem(const QUuid &ruleId, int index)
{
DashboardSceneItem *item = new DashboardSceneItem(ruleId, this);
addItem(item, index);
}
void DashboardModel::addWebViewItem(const QUrl &url, int columnSpan, int rowSpan, bool interactive, int index)
{
QUrl fixedUrl = url;
// Correct url if no scheme is given as it would end up being qrc:// by default which no user will want...
if (fixedUrl.scheme().isEmpty()) {
fixedUrl.setScheme("https");
}
DashboardWebViewItem *item = new DashboardWebViewItem(fixedUrl, interactive, this);
item->setColumnSpan(columnSpan);
item->setRowSpan(rowSpan);
addItem(item, index);
}
void DashboardModel::removeItem(int index)
{
qWarning() << "removing" << index;
beginRemoveRows(QModelIndex(), index, index);
m_list.removeAt(index);
endRemoveRows();
emit changed();
emit countChanged();
}
void DashboardModel::move(int from, int to)
{
// QList's and QAbstractItemModel's move implementation differ when moving an item up the list :/
// While QList needs the index in the resulting list, beginMoveRows expects it to be in the current list
// adjust the model's index by +1 in case we're moving upwards
int newModelIndex = to > from ? to+1 : to;
beginMoveRows(QModelIndex(), from, from, QModelIndex(), newModelIndex);
m_list.move(from, to);
endMoveRows();
emit changed();
}
void DashboardModel::loadFromJson(const QByteArray &json)
{
if (toJson() == json) {
return;
}
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
QJsonDocument jsonDoc = QJsonDocument::fromJson(json);
foreach (const QVariant &itemVariant, jsonDoc.toVariant().toList()) {
QVariantMap itemMap = itemVariant.toMap();
QString type = itemMap.value("type").toString();
DashboardItem *item;
if (type == "folder") {
DashboardFolderItem *folderItem = new DashboardFolderItem(itemMap.value("name").toString(), itemMap.value("icon", "folder").toString(), this);
folderItem->model()->loadFromJson(QJsonDocument::fromVariant(itemMap.value("model").toList()).toJson(QJsonDocument::Compact));
connect(folderItem->model(), &DashboardModel::save, this, &DashboardModel::save);
item = folderItem;
} else if (type == "thing") {
item = new DashboardThingItem(itemMap.value("thingId").toUuid(), this);
} else if (type == "graph") {
item = new DashboardGraphItem(itemMap.value("thingId").toUuid(), itemMap.value("stateTypeId").toUuid(), this);
} else if (type == "scene") {
item = new DashboardSceneItem(itemMap.value("ruleId").toUuid(), this);
} else if (type == "webview") {
item = new DashboardWebViewItem(itemMap.value("url").toUrl(), itemMap.value("interactive", false).toBool(), this);
} else {
qWarning() << "Dashboard item type" << type << "is not implemented. Skipping...";
continue;
}
item->setColumnSpan(itemMap.value("columnSpan", 1).toInt());
item->setRowSpan(itemMap.value("rowSpan", 1).toInt());
addItem(item);
// connect(item, &DashboardItem::changed, this, &DashboardModel::changed);
// m_list.append(item);
}
endResetModel();
emit countChanged();
}
QByteArray DashboardModel::toJson() const
{
QVariantList list;
foreach (DashboardItem* item, m_list) {
QVariantMap map;
map.insert("type", item->type());
if (item->type() == "thing") {
DashboardThingItem *thingItem = dynamic_cast<DashboardThingItem*>(item);
map.insert("thingId", thingItem->thingId());
} else if (item->type() == "folder") {
DashboardFolderItem *folderItem = dynamic_cast<DashboardFolderItem*>(item);
map.insert("name", folderItem->name());
map.insert("icon", folderItem->icon());
QJsonDocument modelDoc = QJsonDocument::fromJson(folderItem->model()->toJson());
map.insert("model", modelDoc.toVariant());
} else if (item->type() == "graph") {
DashboardGraphItem *grapItem = dynamic_cast<DashboardGraphItem*>(item);
map.insert("thingId", grapItem->thingId());
map.insert("stateTypeId", grapItem->stateTypeId());
} else if (item->type() == "scene") {
DashboardSceneItem *sceneItem = dynamic_cast<DashboardSceneItem*>(item);
map.insert("ruleId", sceneItem->ruleId());
} else if (item->type() == "webview") {
DashboardWebViewItem *webViewItem = dynamic_cast<DashboardWebViewItem*>(item);
map.insert("url", webViewItem->url());
if (webViewItem->interactive()) {
map.insert("interactive", true);
}
} else {
Q_ASSERT_X(false, Q_FUNC_INFO, "Type " + item->type().toUtf8() + " not implemented!");
continue;
}
if (item->columnSpan() != 1) {
map.insert("columnSpan", item->columnSpan());
}
if (item->rowSpan() != 1) {
map.insert("rowSpan", item->rowSpan());
}
list.append(map);
}
QJsonDocument jsonDoc = QJsonDocument::fromVariant(list);
return jsonDoc.toJson(QJsonDocument::Compact);
}
void DashboardModel::addItem(DashboardItem *item, int index)
{
if (index < 0 || index > m_list.count()) {
index = m_list.count();
}
connect(item, &DashboardItem::rowSpanChanged, this, [this, item](){
int idx = m_list.indexOf(item);
if (idx >= 0) {
emit dataChanged(this->index(idx), this->index(idx), {RoleRowSpan});
}
});
connect(item, &DashboardItem::columnSpanChanged, this, [this, item](){
int idx = m_list.indexOf(item);
if (idx >= 0) {
emit dataChanged(this->index(idx), this->index(idx), {RoleColumnSpan});
}
});
connect(item, &DashboardItem::changed, this, [this]() {
emit changed();
});
beginInsertRows(QModelIndex(), index, index);
m_list.insert(index, item);
endInsertRows();
emit changed();
emit countChanged();
}

View File

@ -0,0 +1,54 @@
#ifndef DASHBOARDMODEL_H
#define DASHBOARDMODEL_H
#include <QAbstractListModel>
class DashboardItem;
class DashboardModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
enum Roles {
RoleType,
RoleColumnSpan,
RoleRowSpan,
};
Q_ENUM(Roles)
explicit DashboardModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE DashboardItem* get(int index) const;
Q_INVOKABLE void addThingItem(const QUuid &thingId, int index = -1);
Q_INVOKABLE void addFolderItem(const QString &name, const QString &icon, int index = -1);
Q_INVOKABLE void addGraphItem(const QUuid &thingId, const QUuid &stateTypeId, int index = -1);
Q_INVOKABLE void addSceneItem(const QUuid &ruleId, int index = -1);
Q_INVOKABLE void addWebViewItem(const QUrl &url, int columnSpan, int rowSpan, bool interactive, int index = -1);
Q_INVOKABLE void removeItem(int index);
Q_INVOKABLE void move(int from, int to);
Q_INVOKABLE void loadFromJson(const QByteArray &json);
Q_INVOKABLE QByteArray toJson() const;
signals:
void changed();
void countChanged();
void save();
private:
void addItem(DashboardItem *item, int index = -1);
private:
QList<DashboardItem*> m_list;
};
#endif // DASHBOARDMODEL_H

View File

@ -157,7 +157,7 @@
<file>ui/images/lock-closed.svg</file> <file>ui/images/lock-closed.svg</file>
<file>ui/images/lock-open.svg</file> <file>ui/images/lock-open.svg</file>
<file>ui/images/system-update.svg</file> <file>ui/images/system-update.svg</file>
<file>ui/images/folder-symbolic.svg</file> <file>ui/images/folder.svg</file>
<file>ui/images/browser/BrowserIconFile.svg</file> <file>ui/images/browser/BrowserIconFile.svg</file>
<file>ui/images/browser/BrowserIconFolder.svg</file> <file>ui/images/browser/BrowserIconFolder.svg</file>
<file>ui/images/browser/MediaBrowserIconSpotify.svg</file> <file>ui/images/browser/MediaBrowserIconSpotify.svg</file>
@ -189,7 +189,8 @@
<file>ui/images/browser/MediaBrowserIconNapster.svg</file> <file>ui/images/browser/MediaBrowserIconNapster.svg</file>
<file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file> <file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file>
<file>ui/images/browser/MediaBrowserIconDeezer.svg</file> <file>ui/images/browser/MediaBrowserIconDeezer.svg</file>
<file>ui/images/view-grid-symbolic.svg</file> <file>ui/images/groups.svg</file>
<file>ui/images/dashboard.svg</file>
<file>ui/images/script.svg</file> <file>ui/images/script.svg</file>
<file>ui/images/save.svg</file> <file>ui/images/save.svg</file>
<file>ui/images/edit-clear.svg</file> <file>ui/images/edit-clear.svg</file>
@ -257,5 +258,6 @@
<file>ui/images/zigbee/NXP.svg</file> <file>ui/images/zigbee/NXP.svg</file>
<file>ui/images/nymea-splash.svg</file> <file>ui/images/nymea-splash.svg</file>
<file>ui/images/cleaning-robot.svg</file> <file>ui/images/cleaning-robot.svg</file>
<file>ui/images/chart.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -46,6 +46,9 @@
#include "nfchelper.h" #include "nfchelper.h"
#include "nfcthingactionwriter.h" #include "nfcthingactionwriter.h"
#include "platformhelper.h" #include "platformhelper.h"
#include "dashboard/dashboardmodel.h"
#include "dashboard/dashboarditem.h"
#include "mouseobserver.h"
#include "../config.h" #include "../config.h"
#include "logging.h" #include "logging.h"
@ -151,6 +154,16 @@ int main(int argc, char *argv[])
qmlRegisterSingletonType<PushNotifications>("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider); qmlRegisterSingletonType<PushNotifications>("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider);
qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" );
qmlRegisterType<DashboardModel>("Nymea", 1, 0, "DashboardModel");
qmlRegisterUncreatableType<DashboardItem>("Nymea", 1, 0, "DashboardItem", "");
qmlRegisterUncreatableType<DashboardThingItem>("Nymea", 1, 0, "DashboardThingItem", "");
qmlRegisterUncreatableType<DashboardFolderItem>("Nymea", 1, 0, "DashboardFolderItem", "");
qmlRegisterUncreatableType<DashboardGraphItem>("Nymea", 1, 0, "DashboardGraphItem", "");
qmlRegisterUncreatableType<DashboardSceneItem>("Nymea", 1, 0, "DashboardSceneItem", "");
qmlRegisterUncreatableType<DashboardWebViewItem>("Nymea", 1, 0, "DashboardWebViewItem", "");
qmlRegisterType<MouseObserver>("Nymea", 1, 0, "MouseObserver");
engine->rootContext()->setContextProperty("appVersion", APP_VERSION); engine->rootContext()->setContextProperty("appVersion", APP_VERSION);
engine->rootContext()->setContextProperty("qtBuildVersion", QT_VERSION_STR); engine->rootContext()->setContextProperty("qtBuildVersion", QT_VERSION_STR);
engine->rootContext()->setContextProperty("qtVersion", qVersion()); engine->rootContext()->setContextProperty("qtVersion", qVersion());

View File

@ -0,0 +1,37 @@
#include "mouseobserver.h"
#include <QCursor>
#include <QQuickWindow>
MouseObserver::MouseObserver(QQuickItem *parent) : QQuickItem(parent)
{
qCritical() << "*************************** creating observer" << window();
EventFilter *filter = new EventFilter(this);
connect(filter, &EventFilter::pressed, this, [=](){
m_timer.start();
});
connect(filter, &EventFilter::released, this, [=](){
m_timer.stop();
});
installEventFilter(filter);
setAcceptedMouseButtons(Qt::AllButtons);
m_timer.setInterval(200);
m_timer.setSingleShot(true);
connect(&m_timer, &QTimer::timeout, this, &MouseObserver::longPressed);
}
EventFilter::EventFilter(QObject *parent): QObject(parent)
{
}
bool EventFilter::eventFilter(QObject *watched, QEvent *event)
{
qWarning() << "************ eventfilter" << event->type();
return QObject::eventFilter(watched, event);
}

36
nymea-app/mouseobserver.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef MOUSEOBSERVER_H
#define MOUSEOBSERVER_H
#include <QQuickItem>
#include <QTimer>
class MouseObserver : public QQuickItem
{
Q_OBJECT
public:
explicit MouseObserver(QQuickItem *parent = nullptr);
signals:
void longPressed();
private:
QTimer m_timer;
};
class EventFilter: public QObject
{
Q_OBJECT
public:
explicit EventFilter(QObject *parent = nullptr);
bool eventFilter(QObject *watched, QEvent *event) override;
signals:
void pressed();
void released();
};
#endif // MOUSEOBSERVER_H

View File

@ -19,7 +19,10 @@ PRE_TARGETDEPS += ../libnymea-app
linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a
HEADERS += \ HEADERS += \
dashboard/dashboarditem.h \
dashboard/dashboardmodel.h \
mainmenumodel.h \ mainmenumodel.h \
mouseobserver.h \
nfchelper.h \ nfchelper.h \
nfcthingactionwriter.h \ nfcthingactionwriter.h \
platformintegration/generic/screenhelper.h \ platformintegration/generic/screenhelper.h \
@ -31,7 +34,10 @@ HEADERS += \
ruletemplates/messages.h ruletemplates/messages.h
SOURCES += main.cpp \ SOURCES += main.cpp \
dashboard/dashboarditem.cpp \
dashboard/dashboardmodel.cpp \
mainmenumodel.cpp \ mainmenumodel.cpp \
mouseobserver.cpp \
nfchelper.cpp \ nfchelper.cpp \
nfcthingactionwriter.cpp \ nfcthingactionwriter.cpp \
platformintegration/generic/screenhelper.cpp \ platformintegration/generic/screenhelper.cpp \
@ -114,6 +120,8 @@ ios: {
OBJECTIVE_SOURCES += $${IOS_PACKAGE_DIR}/platformhelperios.mm \ OBJECTIVE_SOURCES += $${IOS_PACKAGE_DIR}/platformhelperios.mm \
$${IOS_PACKAGE_DIR}/pushnotifications.mm \ $${IOS_PACKAGE_DIR}/pushnotifications.mm \
OTHER_FILES += $${OBJECTIVE_SOURCES}
# Add Firebase SDK # Add Firebase SDK
QMAKE_LFLAGS += -ObjC $(inherited) QMAKE_LFLAGS += -ObjC $(inherited)
firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist) firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist)

View File

@ -235,5 +235,16 @@
<file>ui/appsettings/LoggingCategories.qml</file> <file>ui/appsettings/LoggingCategories.qml</file>
<file>ui/ConfigurationBase.qml</file> <file>ui/ConfigurationBase.qml</file>
<file>ui/devicepages/CleaningRobotThingPage.qml</file> <file>ui/devicepages/CleaningRobotThingPage.qml</file>
<file>ui/mainviews/DashboardView.qml</file>
<file>ui/mainviews/dashboard/DashboardThingDelegate.qml</file>
<file>ui/mainviews/dashboard/DashboardFolderDelegate.qml</file>
<file>ui/mainviews/dashboard/Dashboard.qml</file>
<file>ui/mainviews/dashboard/DashboardPage.qml</file>
<file>ui/mainviews/dashboard/DashboardDelegateBase.qml</file>
<file>ui/mainviews/dashboard/DashboardAddWizard.qml</file>
<file>ui/mainviews/dashboard/DashboardGraphDelegate.qml</file>
<file>ui/mainviews/dashboard/DashboardSceneDelegate.qml</file>
<file>ui/mainviews/dashboard/DashboardWebViewDelegate.qml</file>
<file>ui/components/SelectionTabs.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -70,7 +70,18 @@ Page {
qsTr("Configure main view") qsTr("Configure main view")
: swipeView.currentItem.item.title.length > 0 ? swipeView.currentItem.item.title : filteredContentModel.modelData(swipeView.currentIndex, "displayName") : swipeView.currentItem.item.title.length > 0 ? swipeView.currentItem.item.title : filteredContentModel.modelData(swipeView.currentIndex, "displayName")
} }
Repeater {
model: swipeView.currentItem.item.hasOwnProperty("headerButtons") ? swipeView.currentItem.item.headerButtons : 0
delegate: HeaderButton {
imageSource: swipeView.currentItem.item.headerButtons[index].iconSource
onClicked: swipeView.currentItem.item.headerButtons[index].trigger()
visible: swipeView.currentItem.item.headerButtons[index].visible
color: swipeView.currentItem.item.headerButtons[index].color
}
}
} }
} }
Connections { Connections {
@ -109,13 +120,14 @@ Page {
ListModel { ListModel {
id: mainMenuBaseModel id: mainMenuBaseModel
ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things" } ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things"; minVersion: "0.0" }
ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred" } ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred"; minVersion: "2.0" }
ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "view-grid-symbolic" } ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "groups"; minVersion: "2.0" }
ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow" } ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow"; minVersion: "2.0" }
ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100" } ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100"; minVersion: "2.0" }
ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter" } ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter"; minVersion: "2.0" }
ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media" } ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media"; minVersion: "2.0" }
ListElement { name: "dashboard"; source: "DashboardView"; displayName: qsTr("Dashboard"); icon: "dashboard"; minVersion: "5.5" }
} }
ListModel { ListModel {
@ -143,6 +155,11 @@ Page {
for (var i = 0; i < mainMenuBaseModel.count; i++) { for (var i = 0; i < mainMenuBaseModel.count; i++) {
var item = mainMenuBaseModel.get(i); var item = mainMenuBaseModel.get(i);
if (!engine.jsonRpcClient.ensureServerVersion(item.minVersion)) {
console.log("Skipping main view", item.name, "as the minimum required server version isn't met:", engine.jsonRpcClient.jsonRpcVersion, "<", item.minVersion)
continue;
}
var idx = mainViewSettings.sortOrder.indexOf(item.name); var idx = mainViewSettings.sortOrder.indexOf(item.name);
if (idx === -1) { if (idx === -1) {
newList[newItems++] = item; newList[newItems++] = item;
@ -340,14 +357,14 @@ Page {
id: configListView id: configListView
model: mainMenuModel model: mainMenuModel
width: parent.width width: parent.width
height: parent.height / 2.5 height: parent.height / 3
anchors.centerIn: parent anchors.centerIn: parent
orientation: ListView.Horizontal orientation: ListView.Horizontal
moveDisplaced: Transition { moveDisplaced: Transition {
NumberAnimation { properties: "x,y"; duration: 200 } NumberAnimation { properties: "x,y"; duration: 200 }
} }
property int delegateWidth: width / 2.5 property int delegateWidth: width / 3
property bool dragging: draggingIndex >= 0 property bool dragging: draggingIndex >= 0
property int draggingIndex : -1 property int draggingIndex : -1
@ -433,50 +450,35 @@ Page {
} }
} }
delegate: Item { delegate: BigTile {
id: configDelegate id: configDelegate
width: configListView.delegateWidth width: configListView.delegateWidth
height: configListView.height height: configListView.height
property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0 property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0
visible: configListView.draggingIndex !== index visible: configListView.draggingIndex !== index
Pane { leftPadding: 0
anchors.fill: parent rightPadding: 0
anchors.margins: app.margins / 2 topPadding: 0
Material.elevation: 2 bottomPadding: 0
leftPadding: 0 header: RowLayout {
rightPadding: 0 id: headerRow
topPadding: 0 Label {
bottomPadding: 0 text: model.displayName
}
}
contentItem: ItemDelegate { contentItem: Item {
anchors.fill: parent Layout.fillWidth: true
implicitHeight: configListView.height - headerRow.height - Style.margins * 2
padding: app.margins * 2 ColorIcon {
contentItem: GridLayout { anchors.centerIn: parent
columns: 1 width: Math.min(parent.width, parent.height) * .6
height: width
Item { name: Qt.resolvedUrl("images/" + model.icon + ".svg")
Layout.fillWidth: true color: configDelegate.isEnabled ? Style.accentColor : Style.iconColor
Layout.fillHeight: true
ColorIcon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * .8
height: width
name: Qt.resolvedUrl("images/" + model.icon + ".svg")
color: configDelegate.isEnabled ? Style.accentColor : Style.iconColor
}
}
Label {
text: model.displayName
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: app.largeFont
}
}
} }
} }
} }
@ -500,13 +502,14 @@ Page {
target: dndItem target: dndItem
property: "scale" property: "scale"
from: 1 from: 1
to: 0.9 to: 0.95
duration: 200 duration: 200
} }
Pane { BigTile {
id: dndTile
anchors.fill: parent anchors.fill: parent
anchors.margins: app.margins / 2 // anchors.margins: app.margins / 2
Material.elevation: 2 Material.elevation: 2
leftPadding: 0 leftPadding: 0
@ -514,32 +517,22 @@ Page {
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
contentItem: ItemDelegate { header: RowLayout {
anchors.fill: parent Label {
text: dndItem.displayName
}
}
padding: app.margins * 2 contentItem: Item {
contentItem: GridLayout { Layout.fillWidth: true
columns: 1 implicitHeight: configListView.height - header.height
Item { ColorIcon {
Layout.fillWidth: true anchors.centerIn: parent
Layout.fillHeight: true width: Math.min(parent.width, parent.height) * .6
ColorIcon { height: width
anchors.centerIn: parent name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg")
width: Math.min(parent.width, parent.height) * .8 color: dndItem.isEnabled ? Style.accentColor : Style.iconColor
height: width
name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg")
color: dndItem.isEnabled ? Style.accentColor : Style.iconColor
}
}
Label {
text: dndItem.displayName
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: app.largeFont
}
} }
} }
} }

View File

@ -4,7 +4,7 @@ Item {
property color backgroundColor: "#fafafa" property color backgroundColor: "#fafafa"
property color foregroundColor: "#202020" property color foregroundColor: "#202020"
property color accentColor: "#ff57baae" property color accentColor: "#57baae"
property color iconColor: "#808080" property color iconColor: "#808080"
property color headerBackgroundColor: "#ffffff" property color headerBackgroundColor: "#ffffff"
@ -92,6 +92,6 @@ Item {
"currentPower": "deepskyblue", "currentPower": "deepskyblue",
} }
readonly property color red: "#ad4754" readonly property color red: "#952727"
readonly property color white: "white" readonly property color white: "white"
} }

View File

@ -10,6 +10,7 @@ Item {
property alias header: headerContainer.children property alias header: headerContainer.children
property alias contentItem: content.contentItem property alias contentItem: content.contentItem
property int contentHeight: root.height - headerContainer.height - content.topPadding - content.bottomPadding
property alias showHeader: headerContainer.visible property alias showHeader: headerContainer.visible

View File

@ -51,6 +51,7 @@ Item {
property bool updateStatus: false property bool updateStatus: false
property alias contentItem: innerContent.children property alias contentItem: innerContent.children
property alias lowerText: lowerTextLabel.text
signal clicked(); signal clicked();
signal pressAndHold(); signal pressAndHold();
@ -146,7 +147,7 @@ Item {
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
visible: backgroundImg.status !== Image.Ready visible: backgroundImg.status !== Image.Ready && label.text != ""
Label { Label {
id: label id: label
@ -155,7 +156,7 @@ Item {
text: root.text.toUpperCase() text: root.text.toUpperCase()
font.pixelSize: app.smallFont font.pixelSize: app.smallFont
font.letterSpacing: 1 font.letterSpacing: 1
wrapMode: Text.WordWrap wrapMode: Text.WrapAtWordBoundaryOrAnywhere
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2 maximumLineCount: 2
elide: Text.ElideRight elide: Text.ElideRight
@ -166,15 +167,26 @@ Item {
} }
} }
Label {
id: lowerTextLabel
anchors.fill: innerContent
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
maximumLineCount: 2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
padding: app.margins / 2
visible: root.contentItem.length === 0
}
MouseArea {
anchors.fill: innerContent
}
Item { Item {
id: innerContent id: innerContent
anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: app.margins / 2 } anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: app.margins / 2 }
height: Style.iconSize + app.margins * 2 height: Style.iconSize + app.margins * 2
Material.foreground: Style.tileOverlayForegroundColor Material.foreground: Style.tileOverlayForegroundColor
MouseArea {
anchors.fill: parent
}
} }
RowLayout { RowLayout {

View File

@ -41,6 +41,8 @@ Item {
property string title: "" property string title: ""
property var headerButtons: []
// Prevent scroll events to swipe left/right in case they fall through the grid // Prevent scroll events to swipe left/right in case they fall through the grid
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent

View File

@ -60,7 +60,7 @@ Dialog {
} }
header: Item { header: Item {
implicitHeight: headerRow.height + app.margins * 2 implicitHeight: headerRow.height + app.margins
implicitWidth: parent.width implicitWidth: parent.width
visible: root.title.length > 0 visible: root.title.length > 0
RowLayout { RowLayout {
@ -86,9 +86,8 @@ Dialog {
} }
} }
} }
ColumnLayout { contentItem: ColumnLayout {
id: content id: content
anchors { left: parent.left; top: parent.top; right: parent.right }
Label { Label {
id: contentLabel id: contentLabel

View File

@ -226,7 +226,7 @@ Item {
ProgressButton { ProgressButton {
longpressEnabled: false longpressEnabled: false
visible: root.thing.thingClass.browsable visible: root.thing.thingClass.browsable
imageSource: "../images/folder-symbolic.svg" imageSource: "../images/folder.svg"
onClicked: { onClicked: {
if (!d.browser) { if (!d.browser) {
d.browser = browserPage.createObject(root, {x: 0, y: root.height}) d.browser = browserPage.createObject(root, {x: 0, y: root.height})

View File

@ -65,6 +65,7 @@ Item {
buttonDelegate.longpressed = false buttonDelegate.longpressed = false
} }
onReleased: { onReleased: {
print("onReleased!")
if (!containsMouse) { if (!containsMouse) {
print("cancelled") print("cancelled")
buttonDelegate.longpressed = false; buttonDelegate.longpressed = false;
@ -79,6 +80,7 @@ Item {
root.clicked(); root.clicked();
} }
buttonDelegate.longpressed = false buttonDelegate.longpressed = false
print("released end")
} }
} }

View File

@ -0,0 +1,53 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Nymea 1.0
Rectangle {
id: root
color: Style.tileBackgroundColor
radius: Style.smallCornerRadius
implicitHeight: layout.implicitHeight
property int currentIndex: 0
property alias model: repeater.model
readonly property var currentValue: model.hasOwnProperty("get") ? model.get(currentIndex) : model[currentIndex]
Rectangle {
x: repeater.count > 0 ? repeater.itemAt(root.currentIndex).x + 1 : 0
anchors.verticalCenter: parent.verticalCenter
Behavior on x { NumberAnimation { duration: 150; easing.type: Easing.InOutQuad } }
height: layout.height - 2
width: Math.floor(root.width / repeater.count) - 2
color: Style.tileOverlayColor
radius: Style.smallCornerRadius
}
RowLayout {
id: layout
anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter }
spacing: 0
Repeater {
id: repeater
delegate: Item {
Layout.fillWidth: true
height: label.implicitHeight + Style.smallMargins
Label {
id: label
anchors.centerIn: parent
text: modelData
}
MouseArea {
anchors.fill: parent
onClicked: {
print("current index:", index)
root.currentIndex = index
}
}
}
}
}
}

View File

@ -32,7 +32,7 @@ AutoSizeMenu {
root.addItem(menuEntryComponent.createObject(root, root.addItem(menuEntryComponent.createObject(root,
{ {
text: qsTr("Grouping"), text: qsTr("Grouping"),
iconSource: "../images/view-grid-symbolic.svg", iconSource: "../images/groups.svg",
functionName: "addToGroup" functionName: "addToGroup"
})) }))
@ -99,7 +99,7 @@ AutoSizeMenu {
id: addToGroupDialog id: addToGroupDialog
MeaDialog { MeaDialog {
title: qsTr("Groups for %1").arg(root.thing.name) title: qsTr("Groups for %1").arg(root.thing.name)
headerIcon: "../images/view-grid-symbolic.svg" headerIcon: "../images/groups.svg"
// NOTE: If CloseOnPressOutside is active (default) it will break the QtVirtualKeyboard // NOTE: If CloseOnPressOutside is active (default) it will break the QtVirtualKeyboard
// https://bugreports.qt.io/browse/QTBUG-56918 // https://bugreports.qt.io/browse/QTBUG-56918
closePolicy: Popup.CloseOnEscape closePolicy: Popup.CloseOnEscape

View File

@ -48,16 +48,16 @@ Item {
property string iconSource: "" property string iconSource: ""
property alias title: titleLabel.text property alias title: titleLabel.text
readonly property var valueState: thing.states.getState(stateType.id) readonly property State valueState: thing && stateType ? thing.states.getState(stateType.id) : null
readonly property bool hasConnectable: thing.thingClass.interfaces.indexOf("connectable") >= 0
readonly property StateType connectedStateType: hasConnectable ? thing.thingClass.stateTypes.findByName("connected") : null readonly property StateType connectedStateType: hasConnectable ? thing.thingClass.stateTypes.findByName("connected") : null
readonly property bool hasConnectable: connectedStateType != null
LogsModelNg { LogsModelNg {
id: logsModelNg id: logsModelNg
engine: _engine engine: root.thing ? _engine : null
thingId: root.thing.id thingId: root.thing ? root.thing.id : ""
typeIds: [root.stateType.id] typeIds: root.stateType ? [root.stateType.id] : []
live: true live: true
graphSeries: lineSeries1 graphSeries: lineSeries1
viewStartTime: xAxis.min viewStartTime: xAxis.min
@ -66,7 +66,7 @@ Item {
LogsModelNg { LogsModelNg {
id: connectedLogsModel id: connectedLogsModel
engine: root.hasConnectable ? _engine : null // don't even try to poll if we don't have a connectable interface engine: root.hasConnectable ? _engine : null // don't even try to poll if we don't have a connectable interface
thingId: root.thing.id thingId: root.thing ? root.thing.id : ""
typeIds: root.hasConnectable ? [root.connectedStateType.id] : [] typeIds: root.hasConnectable ? [root.connectedStateType.id] : []
live: true live: true
graphSeries: connectedLineSeries graphSeries: connectedLineSeries
@ -78,8 +78,8 @@ Item {
anchors.fill: parent anchors.fill: parent
margins.top: Style.iconSize + app.margins margins.top: Style.iconSize + app.margins
margins.bottom: app.margins / 2 margins.bottom: app.margins / 2
margins.left: 0 margins.left: Style.smallMargins
margins.right: 0 margins.right: Style.smallMargins
backgroundColor: Style.tileBackgroundColor backgroundColor: Style.tileBackgroundColor
backgroundRoundness: Style.cornerRadius backgroundRoundness: Style.cornerRadius
legend.visible: false legend.visible: false
@ -104,6 +104,7 @@ Item {
Label { Label {
id: titleLabel id: titleLabel
Layout.fillWidth: true Layout.fillWidth: true
elide: Text.ElideRight
text: root.stateType.type.toLowerCase() === "bool" text: root.stateType.type.toLowerCase() === "bool"
? root.stateType.displayName ? root.stateType.displayName
: 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit) : 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit)
@ -118,9 +119,9 @@ Item {
} }
HeaderButton { HeaderButton {
imageSource: "../images/zoom-in.svg" imageSource: "../images/zoom-in.svg"
enabled: xAxis.timeDiff > (60 * 30) enabled: xAxis.timeDiff > (60 * 5)
onClicked: { onClicked: {
var newTime = new Date(Math.min(xAxis.min.getTime() + (xAxis.timeDiff * 1000 / 4), xAxis.max.getTime() - (1000 * 60 * 30))) var newTime = new Date(Math.min(xAxis.min.getTime() + (xAxis.timeDiff * 1000 / 4), xAxis.max.getTime() - (1000 * 60 * 5)))
xAxis.min = newTime; xAxis.min = newTime;
} }
} }
@ -129,27 +130,25 @@ Item {
ValueAxis { ValueAxis {
id: yAxis id: yAxis
max: { max: {
switch (root.stateType.type.toLowerCase()) { if (root.stateType && root.stateType.type.toLowerCase() == "bool") {
case "bool":
return 1; return 1;
default: } else {
Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05)) Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05))
} }
} }
min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05))
// onMinChanged: applyNiceNumbers(); // onMinChanged: applyNiceNumbers();
// onMaxChanged: applyNiceNumbers(); // onMaxChanged: applyNiceNumbers();
labelsFont.pixelSize: app.smallFont labelsFont: Style.smallFont
labelFormat: { labelFormat: {
switch (root.stateType.type.toLowerCase()) { if (root.stateType && root.stateType.type.toLowerCase() == "bool") {
case "bool":
return "x"; return "x";
default: } else {
return "%d"; return "%d";
} }
} }
labelsColor: Style.foregroundColor labelsColor: Style.foregroundColor
tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 tickCount: root.stateType && root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
gridLineColor: color gridLineColor: color
} }
@ -166,7 +165,7 @@ Item {
gridVisible: false gridVisible: false
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
tickCount: chartView.width / 70 tickCount: chartView.width / 70
labelsFont.pixelSize: app.smallFont labelsFont: Style.smallFont
labelsColor: Style.foregroundColor labelsColor: Style.foregroundColor
property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000
@ -265,9 +264,9 @@ Item {
id: mainSeries id: mainSeries
axisX: xAxis axisX: xAxis
axisY: yAxis axisY: yAxis
name: root.stateType.displayName name: root.stateType ? root.stateType.displayName : ""
borderColor: root.color borderColor: root.color
borderWidth: 4 borderWidth: 2
lowerSeries: LineSeries { lowerSeries: LineSeries {
id: lineSeries0 id: lineSeries0
XYPoint { x: xAxis.max.getTime(); y: 0 } XYPoint { x: xAxis.max.getTime(); y: 0 }
@ -347,7 +346,7 @@ Item {
borderColor: root.color borderColor: root.color
axisX: xAxis axisX: xAxis
axisY: yAxis axisY: yAxis
pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool" pointLabelsVisible: root.stateType && root.stateType.type.toLowerCase() !== "bool"
pointLabelsColor: Style.foregroundColor pointLabelsColor: Style.foregroundColor
pointLabelsFont.pixelSize: app.smallFont pointLabelsFont.pixelSize: app.smallFont
pointLabelsFormat: "@yPoint" pointLabelsFormat: "@yPoint"

View File

@ -37,30 +37,34 @@ import "../components"
MainPageTile { MainPageTile {
id: root id: root
text: thing.name.toUpperCase() text: thing ? thing.name.toUpperCase() : ""
iconName: app.interfacesToIcon(thing.thingClass.interfaces) iconName: thing ? app.interfacesToIcon(thing.thingClass.interfaces) : ""
iconColor: Style.accentColor iconColor: Style.accentColor
isWireless: thing.thingClass.interfaces.indexOf("wirelessconnectable") >= 0 isWireless: thing && thing.thingClass.interfaces.indexOf("wirelessconnectable") >= 0
batteryCritical: batteryCriticalState && batteryCriticalState.value === true batteryCritical: batteryCriticalState && batteryCriticalState.value === true
disconnected: connectedState && connectedState.value === false disconnected: connectedState && connectedState.value === false
signalStrength: signalStrengthState ? signalStrengthState.value : -1 signalStrength: signalStrengthState ? signalStrengthState.value : -1
setupStatus: thing.setupStatus setupStatus: thing ? thing.setupStatus : Thing.ThingSetupStatusNone
updateStatus: updateStatusState && updateStatusState.value !== "idle" updateStatus: updateStatusState && updateStatusState.value !== "idle"
backgroundImage: artworkState && artworkState.value.length > 0 ? artworkState.value : "" backgroundImage: artworkState && artworkState.value.length > 0 ? artworkState.value : ""
property Thing thing: null property Thing thing: null
property alias device: root.thing property alias device: root.thing
readonly property State connectedState: thing.stateByName("connected") readonly property State connectedState: thing ? thing.stateByName("connected") : null
readonly property State signalStrengthState: thing.stateByName("signalStrength") readonly property State signalStrengthState: thing ? thing.stateByName("signalStrength") : null
readonly property State batteryCriticalState: thing.stateByName("batteryCritical") readonly property State batteryCriticalState: thing ? thing.stateByName("batteryCritical") : null
readonly property State artworkState: thing.stateByName("artwork") readonly property State artworkState: thing ? thing.stateByName("artwork") : null
readonly property State updateStatusState: thing.stateByName("updateStatus") readonly property State updateStatusState: thing ? thing.stateByName("updateStatus") : null
contentItem: Loader { contentItem: Loader {
id: loader id: loader
anchors.fill: parent anchors.fill: parent
sourceComponent: { sourceComponent: {
if (!root.thing) {
return null
}
for (var i = 0; i < root.thing.thingClass.interfaces.length; i++) { for (var i = 0; i < root.thing.thingClass.interfaces.length; i++) {
switch (root.thing.thingClass.interfaces[i]) { switch (root.thing.thingClass.interfaces[i]) {
case "closable": case "closable":

View File

@ -285,7 +285,7 @@ ThingPageBase {
} }
ProgressButton { ProgressButton {
longpressEnabled: false longpressEnabled: false
imageSource: "../images/view-grid-symbolic.svg" imageSource: "../images/groups.svg"
mode: "normal" mode: "normal"
size: Style.bigIconSize size: Style.bigIconSize
visible: root.thing.thingClass.browsable visible: root.thing.thingClass.browsable

View File

@ -58,7 +58,7 @@ Page {
} }
HeaderButton { HeaderButton {
imageSource: "../images/folder-symbolic.svg" imageSource: "../images/folder.svg"
visible: root.thingClass.browsable && root.showBrowserButton visible: root.thingClass.browsable && root.showBrowserButton
onClicked: { onClicked: {
pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {thing: root.thing}) pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {thing: root.thing})

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg4874"
height="96"
viewBox="0 0 96 96.000001"
width="96"
version="1.1"
sodipodi:docname="chart.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<defs
id="defs9" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1464"
inkscape:window-height="933"
id="namedview7"
showgrid="false"
inkscape:zoom="1.1184636"
inkscape:cx="100.03185"
inkscape:cy="67.877248"
inkscape:window-x="72"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4874" />
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(67.857 -78.505)">
<rect
id="rect4782"
style="color:#000000;fill:none"
transform="rotate(90)"
height="96"
width="96"
y="-28.143"
x="78.505" />
<path
id="path4643"
style="color-rendering:auto;text-decoration-color:#000000;color:#000000;font-variant-numeric:normal;shape-rendering:auto;solid-color:#000000;text-decoration-line:none;fill:#808080;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;image-rendering:auto;white-space:normal;text-decoration-style:solid;font-variant-ligatures:none;isolation:auto;text-transform:none"
d="m-43.869 86.504-0.01172 0.002c-5.0328 0.05818-8.7136-0.12027-11.725 1.541-1.5055 0.83064-2.6968 2.2356-3.3555 3.9902-0.65866 1.7547-0.89648 3.8364-0.89648 6.4668v56.002c0 2.6304 0.23782 4.7121 0.89648 6.4668 0.65866 1.7546 1.85 3.1596 3.3555 3.9902 3.011 1.6613 6.6918 1.4848 11.725 1.543h0.01172 48.023 0.011719c5.0328-0.0582 8.7136 0.11832 11.725-1.543 1.5055-0.83064 2.6968-2.2356 3.3555-3.9902 0.65866-1.7547 0.89648-3.8364 0.89648-6.4668v-56.002c0-2.6304-0.23782-4.7121-0.89648-6.4668-0.66-1.759-1.851-3.163-3.356-3.994-3.011-1.661-6.6922-1.483-11.725-1.541l-0.011719-0.002h-48.023zm0.01172 4h48c5.0383 0.05877 8.3519 0.23688 9.8164 1.0449 0.73364 0.40478 1.1527 0.85491 1.543 1.8945 0.39025 1.0396 0.64062 2.691 0.64062 5.0605v56.002c0 2.3696-0.25037 4.0209-0.64062 5.0606-0.39025 1.0396-0.80933 1.4898-1.543 1.8945-1.4645 0.80804-4.7782 0.98616-9.8164 1.0449h-47.977-0.02344c-5.0383-0.0588-8.3519-0.23688-9.8164-1.0449-0.73364-0.40478-1.1508-0.85491-1.541-1.8945-0.39025-1.0396-0.64258-2.691-0.64258-5.0606v-56.002c0-2.3696 0.25232-4.0209 0.64258-5.0605 0.39025-1.0396 0.80738-1.4898 1.541-1.8945 1.4645-0.80804 4.7782-0.98616 9.8164-1.0449z" />
</g>
<path
style="fill:none;stroke:#808080;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 17.060932,61.930162 34.675536,43.851696 45.192182,54.173667 59.709302,34.435564 69.637773,46.020641 77.792946,39.18423"
id="path11"
sodipodi:nodetypes="cccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
viewBox="0 0 96 96.000001"
sodipodi:docname="dashboard.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6199994"
inkscape:cx="45.775452"
inkscape:cy="62.909239"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true"
inkscape:document-rotation="0"
inkscape:window-width="1848"
inkscape:window-height="1173"
inkscape:window-x="72"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-16,48"
orientation="0,1"
id="guide4172" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 429.99805,433.36328 h -2 -34.01366 v -40.00195 h 36.01366 z m -4,-4.00195 v -31.99805 h -28.01171 v 31.99805 z"
id="rect4201"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 429.99805,385.36133 h -2 -34.01367 v -32 h 36.01367 z m -4,-4 v -23.99805 h -28.01172 v 23.99805 z"
id="rect4203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 385.98243,433.36328 h -2 -34.01563 v -32.00195 h 36.01563 z m -4.00195,-4.00195 v -23.99805 h -28.01173 v 23.99805 z"
id="rect4205"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 385.98242,393.36133 h -2 -34.01562 v -40 h 36.01562 z m -4.00195,-4 v -31.99805 h -28.01172 v 31.99805 z"
id="rect4207"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,90 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import Qt.labs.settings 1.1
import "../components"
import "dashboard"
MainViewBase {
id: root
headerButtons: [
{
iconSource: "/ui/images/configure.svg",
color: dashboard.editMode ? Style.accentColor : Style.iconColor,
trigger: function() {
dashboard.editMode = !dashboard.editMode;
},
visible: true
}
]
DashboardModel {
id: dashboardModel
Component.onCompleted: {
print("loading dashboard:", dashboardSettings.dashboardConfig)
loadFromJson(dashboardSettings.dashboardConfig)
}
onSave: {
print("saving dashboard");
dashboardSettings.dashboardConfig = dashboardModel.toJson()
}
}
AppData {
id: dashboardSettings
engine: _engine
group: "dashboard-1"
property string dashboardConfig: ""
onDashboardConfigChanged: {
print("dashboard changed on server! Reloading...")
dashboardModel.loadFromJson(dashboardConfig)
}
}
Settings {
category: engine.jsonRpcClient.currentHost.uuid
property string dashboardConfig
}
Dashboard {
id: dashboard
anchors.fill: parent
model: dashboardModel
}
}

View File

@ -60,24 +60,85 @@ MainViewBase {
moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 150; easing.type: Easing.InOutQuad } } moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 150; easing.type: Easing.InOutQuad } }
model: tagsProxy model: tagsProxy
delegate: ThingTile { delegate: Item {
id: delegateRoot id: delegateRoot
width: gridView.cellWidth width: gridView.cellWidth
height: gridView.cellHeight height: gridView.cellHeight
thing: engine.thingManager.things.getThing(thingId)
visible: thingId !== fakeDragItem.thingId
onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing}) property Thing thing: engine.thingManager.things.getThing(thingId)
onPressAndHold: root.editMode = true property alias tile: thingTile
SequentialAnimation { ThingTile {
loops: Animation.Infinite id: thingTile
running: root.editMode anchors.fill: parent
alwaysRunToEnd: true thing: delegateRoot.thing
NumberAnimation { from: 0; to: 3; target: delegateRoot; duration: 75; property: "rotation" } enabled: !root.editMode
NumberAnimation { from: 3; to: -3; target: delegateRoot; duration: 150; property: "rotation" } onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing})
NumberAnimation { from: -3; to: 0; target: delegateRoot; duration: 75; property: "rotation" } onPressAndHold: root.editMode = true
opacity: dragArea.fakeDragItem !== null && delegateRoot.thing === dragArea.fakeDragItem.thing ? .3 : 1
}
Rectangle {
anchors.fill: parent
anchors.margins: Style.smallMargins
color: "transparent"
border.color: Style.accentColor
border.width: 4
radius: Style.cornerRadius
visible: dragArea.fakeDragItem !== null && delegateRoot.thing === dragArea.fakeDragItem.thing
}
Rectangle {
z: 2
anchors.fill: parent
anchors.margins: Style.smallMargins
visible: opacity > 0
radius: Style.cornerRadius
color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, .5)
opacity: root.editMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: root.editMode = false
}
Rectangle {
anchors {
left: parent.left; top: parent.top;
margins: Style.smallMargins
}
height: Style.largeIconSize
width: Style.largeIconSize
color: Style.red
radius: Style.cornerRadius
opacity: dragArea.fakeDragItem == null
Rectangle {
anchors.fill: parent
radius: Style.cornerRadius
color: Style.foregroundColor
opacity: deleteMouseArea.pressed || deleteMouseArea.containsMouse ? .08 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
ColorIcon {
name: "/ui/images/delete.svg"
size: Style.iconSize
anchors.centerIn: parent
color: Style.white
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
print("delete clicked")
engine.tagsManager.untagThing(model.thingId, "favorites")
}
}
}
} }
} }
@ -88,41 +149,54 @@ MainViewBase {
propagateComposedEvents: true propagateComposedEvents: true
property var dragOffset: ({}) property var dragOffset: ({})
property var draggedItem: null property var draggedItem: null
property var fakeDragItem: null
onPressed: { onPressAndHold: {
var item = gridView.itemAt(mouseX, mouseY) var gridViewCoords = mapToItem(gridView.contentItem, mouseX, mouseY)
var item = gridView.itemAt(gridViewCoords.x, gridViewCoords.y);
draggedItem = item; draggedItem = item;
dragOffset = mapToItem(item, mouseX, mouseY) dragOffset = mapToItem(item, mouseX, mouseY)
fakeDragItem.x = mouseX - dragOffset.x;
fakeDragItem.y = mouseY - dragOffset.y; dragArea.fakeDragItem = dragItemComponent.createObject(dragArea, {
fakeDragItem.text = item.text x: mouseX - dragOffset.x,
fakeDragItem.iconName = item.iconName y: mouseY - dragOffset.y,
fakeDragItem.iconColor = item.iconColor; thing: draggedItem.thing
fakeDragItem.thingId = item.thing.id })
fakeDragItem.batteryCritical = item.batteryCritical
fakeDragItem.disconnected = item.disconnected
drag.target = fakeDragItem drag.target = fakeDragItem
} }
onReleased: { onReleased: {
drag.target = null if (drag.target) {
draggedItem = null drag.target = null
fakeDragItem.thingId = "" dragArea.fakeDragItem.destroy()
dragArea.fakeDragItem = null
dragArea.draggedItem = null
}
} }
onClicked: { onClicked: {
root.editMode = false var gridViewCoords = mapToItem(gridView.contentItem, mouseX, mouseY)
var itemUnderMouse = gridView.itemAt(gridViewCoords.x, gridViewCoords.y);
if (itemUnderMouse !== null) {
mouse.accepted = false
} else {
root.editMode = false
}
} }
} }
MainPageTile { Component {
id: fakeDragItem id: dragItemComponent
width: gridView.cellWidth
height: gridView.cellHeight ThingTile {
Drag.active: dragArea.drag.active id: fakeDragItem
visible: thingId !== "" width: gridView.cellWidth
property var thingId: "" height: gridView.cellHeight
Drag.active: dragArea.drag.active
}
} }
DropArea { DropArea {
id: dropArea id: dropArea
anchors.fill: gridView anchors.fill: gridView
@ -130,14 +204,31 @@ MainViewBase {
property int from: -1 property int from: -1
property int to: -1 property int to: -1
property int pendingCommand: -1
Connections {
target: engine.tagsManager
onAddTagReply: {
if (commandId == dropArea.pendingCommand) {
dropArea.pendingCommand = -1
}
}
}
onEntered: { onEntered: {
var index = gridView.indexAt(drag.x + dragArea.dragOffset.x, drag.y + dragArea.dragOffset.y); var gridViewCoords = mapToItem(gridView.contentItem, drag.x, drag.y)
var index = gridView.indexAt(gridViewCoords.x + dragArea.dragOffset.x, gridViewCoords.y + dragArea.dragOffset.y);
from = index; from = index;
to = index; to = index;
} }
onPositionChanged: { onPositionChanged: {
var index = gridView.indexAt(drag.x + dragArea.dragOffset.x, drag.y + dragArea.dragOffset.y); if (dropArea.pendingCommand != -1) {
// busy
return
}
var gridViewCoords = mapToItem(gridView.contentItem, drag.x, drag.y)
var index = gridView.indexAt(gridViewCoords.x + dragArea.dragOffset.x, gridViewCoords.y + dragArea.dragOffset.y);
if (to !== index && from !== index && index >= 0 && index <= tagsProxy.count) { if (to !== index && from !== index && index >= 0 && index <= tagsProxy.count) {
to = index; to = index;
print("should move", from, "to", to) print("should move", from, "to", to)
@ -159,7 +250,7 @@ MainViewBase {
} }
var tag = tagsProxy.get(i); var tag = tagsProxy.get(i);
engine.tagsManager.tagThing(tag.thingId, tag.tagId, newIdx); dropArea.pendingCommand = engine.tagsManager.tagThing(tag.thingId, tag.tagId, newIdx);
} }
from = index; from = index;
} }
@ -168,7 +259,7 @@ MainViewBase {
} }
EmptyViewPlaceholder { EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins } anchors { left: parent.left; right: parent.right; margins: Style.margins }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: gridView.count === 0 && !engine.thingManager.fetchingData visible: gridView.count === 0 && !engine.thingManager.fetchingData
title: qsTr("There are no favorite things yet.") title: qsTr("There are no favorite things yet.")

View File

@ -61,9 +61,9 @@ MainViewBase {
delegate: MainPageTile { delegate: MainPageTile {
width: groupsGridView.cellWidth width: groupsGridView.cellWidth
height: groupsGridView.cellHeight height: groupsGridView.cellHeight
iconName: "../images/view-grid-symbolic.svg" iconName: "../images/groups.svg"
iconColor: Style.accentColor iconColor: Style.accentColor
text: model.tagId.substring(6) lowerText: model.tagId.substring(6)
onClicked: { onClicked: {
pageStack.push(Qt.resolvedUrl("../grouping/GroupInterfacesPage.qml"), {groupTag: model.tagId}) pageStack.push(Qt.resolvedUrl("../grouping/GroupInterfacesPage.qml"), {groupTag: model.tagId})
} }
@ -76,7 +76,7 @@ MainViewBase {
visible: groupsGridView.count == 0 && !engine.thingManager.fetchingData && !engine.tagsManager.busy visible: groupsGridView.count == 0 && !engine.thingManager.fetchingData && !engine.tagsManager.busy
title: qsTr("There are no groups set up yet.") title: qsTr("There are no groups set up yet.")
text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.") text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.")
imageSource: "../images/view-grid-symbolic.svg" imageSource: "../images/groups.svg"
buttonVisible: false buttonVisible: false
} }
} }

View File

@ -59,7 +59,7 @@ MainViewBase {
iconName: iconTag ? "../images/" + iconTag.value + ".svg" : "../images/slideshow.svg"; iconName: iconTag ? "../images/" + iconTag.value + ".svg" : "../images/slideshow.svg";
fallbackIconName: "../images/slideshow.svg" fallbackIconName: "../images/slideshow.svg"
iconColor: colorTag && colorTag.value.length > 0 ? colorTag.value : Style.accentColor; iconColor: colorTag && colorTag.value.length > 0 ? colorTag.value : Style.accentColor;
text: model.name.toUpperCase() lowerText: model.name
property var colorTag: engine.tagsManager.tags.findRuleTag(model.id, "color") property var colorTag: engine.tagsManager.tags.findRuleTag(model.id, "color")
property var iconTag: engine.tagsManager.tags.findRuleTag(model.id, "icon") property var iconTag: engine.tagsManager.tags.findRuleTag(model.id, "icon")

View File

@ -0,0 +1,477 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
Item {
id: root
property var model: null
property bool editMode: false
function addItem(index) {
if (index === undefined) {
index = root.model.count
}
var addComponent = Qt.createComponent(Qt.resolvedUrl("DashboardAddWizard.qml"))
var popup = addComponent.createObject(root, {dashboardModel: root.model, index: index})
popup.open()
}
readonly property var componentMap: {
"thing": "DashboardThingDelegate.qml",
"folder": "DashboardFolderDelegate.qml",
"graph": "DashboardGraphDelegate.qml",
"scene": "DashboardSceneDelegate.qml",
"webview": "DashboardWebViewDelegate.qml"
}
onEditModeChanged: {
if (!editMode) {
root.model.save()
}
}
Flickable {
id: flickable
anchors.fill: parent
anchors.margins: app.margins / 2
contentHeight: Math.max(layout.implicitHeight, height)
contentWidth: width
MouseArea {
width: flickable.contentWidth
height: flickable.contentHeight
onPressAndHold: root.editMode = true
}
GridLayout {
id: layout
width: Math.min(root.model.count * cellWidth, flickable.width)
readonly property int minTileWidth: 172
readonly property int tilesPerRow: root.width / minTileWidth
columns: tilesPerRow
columnSpacing: 0
rowSpacing: 0
property int cellWidth: flickable.width / tilesPerRow
property int cellHeight: cellWidth
Repeater {
id: repeater
model: root.model
Loader {
id: loader
Layout.preferredWidth: layout.cellWidth * columnSpan
Layout.preferredHeight: layout.cellHeight * rowSpan
property int columnSpan: model.columnSpan
property int rowSpan: model.rowSpan
Layout.columnSpan: columnSpan
Layout.rowSpan: rowSpan
property string type: model.type
property DashboardItem dashboardItem: root.model.get(index)
opacity: dragArea.fromIndex == index ? .3 : 1
Component.onCompleted: {
setSource(Qt.resolvedUrl(componentMap[model.type]), {item: loader.dashboardItem})
}
Binding {
target: loader.item
property: "enabled"
value: !root.editMode// dragArea.editIndex === index
}
Binding {
target: loader.item
property: "editMode"
value: root.editMode
}
Binding {
target: loader.item
property: "bottomClip"
value: loader.bottomClip
when: ["android", "ios"].indexOf(Qt.platform.os) >= 0
}
Binding {
target: loader.item
property: "topClip"
value: loader.topClip
when: ["android", "ios"].indexOf(Qt.platform.os) >= 0
}
Connections {
target: loader.item
onOpenDialog: {
dialogComponent.createObject(root).open()
}
}
property int topClip: Math.min(height, Math.max(0, -y + (flickable.contentY - flickable.originY) - Style.margins))
property int bottomClip: Math.max(0, y + height - flickable.height - Style.margins - (flickable.contentY - flickable.originY))
Rectangle {
anchors.fill: parent
anchors.margins: Style.smallMargins
color: "transparent"
border.color: Style.accentColor
border.width: 4
radius: Style.cornerRadius
visible: dragArea.fromIndex == index
}
Rectangle {
z: 2
anchors.fill: parent
anchors.margins: Style.smallMargins
visible: opacity > 0
radius: Style.cornerRadius
color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, .5)
opacity: root.editMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: root.editMode = false
}
Rectangle {
anchors {
left: parent.left; top: parent.top;
margins: Style.smallMargins
}
height: Style.largeIconSize
width: Style.largeIconSize
color: Style.red
radius: Style.cornerRadius
opacity: dragArea.fromIndex == -1
Rectangle {
anchors.fill: parent
radius: Style.cornerRadius
color: Style.foregroundColor
opacity: deleteMouseArea.pressed || deleteMouseArea.containsMouse ? .08 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
ColorIcon {
name: "/ui/images/delete.svg"
size: Style.iconSize
anchors.centerIn: parent
color: Style.white
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
print("delete clicked")
root.model.removeItem(index)
}
}
}
Rectangle {
anchors {
right: parent.right
top: parent.top
margins: Style.smallMargins
}
height: Style.largeIconSize
width: Style.largeIconSize
color: Style.tileOverlayColor
radius: Style.cornerRadius
visible: opacity > 0
opacity: dragArea.fromIndex == -1 && loader.item.configurable
Behavior on opacity { NumberAnimation { duration: 200 } }
Rectangle {
anchors.fill: parent
radius: Style.cornerRadius
color: Style.foregroundColor
opacity: configureMouseArea.pressed || configureMouseArea.containsMouse ? .08 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
ColorIcon {
name: "/ui/images/configure.svg"
size: Style.iconSize
anchors.centerIn: parent
// color: Style.white
}
MouseArea {
id: configureMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
loader.item.configure()
}
}
}
}
}
}
MouseArea {
id: addTile
Layout.preferredWidth: layout.cellWidth
Layout.preferredHeight: layout.cellHeight
hoverEnabled: true
opacity: root.editMode ? 1 : 0
visible: opacity > 0
Behavior on opacity { NumberAnimation { duration: 200 } }
onClicked: {
print("add clicked")
root.addItem()
}
Rectangle {
anchors.fill: parent
anchors.margins: Style.smallMargins
border.width: 4
border.color: Style.tileOverlayColor
color: Qt.rgba( Style.tileBackgroundColor.r, Style.tileBackgroundColor.g, Style.tileBackgroundColor.b, addTile.containsMouse ? 1 : 0)
Behavior on color { ColorAnimation { duration: 200 } }
radius: Style.cornerRadius
ColorIcon {
name: "/ui/images/add.svg"
size: Style.bigIconSize
anchors.centerIn: parent
color: Style.tileOverlayColor
}
}
}
}
MouseArea {
id: dragArea
anchors.fill: parent
enabled: root.editMode
propagateComposedEvents: true
preventStealing: fakeDragItem != null
property var fakeDragItem: null
property int fromIndex: -1
property int editIndex: -1
property int fakeDragOffsetX: 0
property int fakeDragOffsetY: 0
Timer {
id: scrollTimer
interval: 10
repeat: true
running: dragArea.fakeDragItem !== null
property int scrollOffset: 0
onTriggered: {
var mappedPos = dragArea.mapToItem(flickable, dragArea.mouseX, dragArea.mouseY)
var scrollPixels = 0
if (mappedPos.y + scrollOffset < 60) {
scrollPixels = Math.max(-2, -flickable.contentY)
} else if (mappedPos.y + scrollOffset > flickable.height - 60) {
scrollPixels = Math.min(2, flickable.contentHeight - flickable.height - flickable.contentY)
}
flickable.contentY += scrollPixels
scrollOffset += scrollPixels
dragArea.fakeDragItem.y += scrollPixels
}
}
onClicked: {
var ret = itemUnderMouse()
if (ret.item) {
dragArea.editIndex = ret.index
// Let the click pass through
mouse.accepted = false
} else if (itemContainsMouse(addTile)) {
mouse.accepted = false
} else {
root.editMode = false
}
}
onPressAndHold: {
print("position", mouseX, mouseY)
var draggedItem = null
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i);
print("item coords:", item.x, item.y, item.width, item.height)
if (itemContainsMouse(item)) {
print("Yes!, Item at:", i)
fromIndex = i;
draggedItem = item;
break;
}
}
if (!draggedItem) {
return
}
var mappedCursor = dragArea.mapToItem(item, mouseX, mouseY)
fakeDragOffsetX = mappedCursor.x
fakeDragOffsetY = mappedCursor.y
dragArea.fakeDragItem = dragItemComponent.createObject(dragArea,
{
x: mouseX - fakeDragOffsetX,
y: mouseY - fakeDragOffsetY,
draggedItem: draggedItem
})
}
function itemUnderMouse() {
var ret = {}
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i);
if (itemContainsMouse(item)) {
ret.item = item;
ret.index = i
break;
}
}
return ret
}
function itemContainsMouse(item) {
var mapped = dragArea.mapToItem(item, mouseX, mouseY)
return mapped.x > 0 && mapped.x < item.width && mapped.y > 0 && mapped.y < item.height
}
onPositionChanged: {
if (!fakeDragItem) {
return
}
scrollTimer.scrollOffset = 0;
fakeDragItem.x = mouseX - fakeDragOffsetX
fakeDragItem.y = mouseY - fakeDragOffsetY
var itemUnderCursor = null
var itemIndex = -1
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i);
if (itemContainsMouse(item)) {
print("Yes!, Item at:", i)
itemUnderCursor = item;
itemIndex = i
break;
}
}
if (!itemUnderCursor) {
return
}
if (fromIndex === itemIndex) {
return
}
print("over item:", itemIndex)
root.model.move(fromIndex, itemIndex)
fromIndex = itemIndex
}
onReleased: {
if (dragArea.fakeDragItem) {
dragArea.fakeDragItem.destroy();
dragArea.fakeDragItem = null
dragArea.fromIndex = -1
}
}
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: Style.margins }
anchors.verticalCenter: parent.verticalCenter
visible: root.model.count === 0 && !root.editMode
title: qsTr("Dashboard is empty")
text: qsTr("Start with adding a new item to this dashboard.")
buttonText: qsTr("Add item")
imageSource: "/ui/images/dashboard.svg"
onButtonClicked: {
root.addItem()
}
}
Component {
id: dragItemComponent
Item {
property Item draggedItem: null
layer.enabled: true
layer.effect: ShaderEffectSource {
sourceItem: draggedItem
live: true
}
height: draggedItem.height
width: draggedItem.width
}
}
Component {
id: editDialogComponent
MeaDialog {
id: editDialog
standardButtons: Dialog.NoButton
property DashboardItem dashboardItem: null
property int index: -1
ColumnLayout {
Button {
text: qsTr("Remove")
Layout.fillWidth: true
onClicked: {
root.model.removeItem(editDialog.index)
editDialog.close()
}
}
}
}
}
}

View File

@ -0,0 +1,400 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
MeaDialog {
id: root
title: qsTr("Add item")
standardButtons: Dialog.NoButton
property DashboardModel dashboardModel: null
property int index: 0
padding: Style.margins
contentItem: StackView {
id: internalPageStack
implicitHeight: currentItem.implicitHeight
clip: true
initialItem: ColumnLayout {
id: contentColumn
implicitHeight: childrenRect.height
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Thing")
iconName: "things"
onClicked: {
internalPageStack.push(addThingSelectionComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
iconName: "folder"
text: qsTr("Folder")
onClicked: {
internalPageStack.push(addFolderComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Chart")
iconName: "chart"
onClicked: {
internalPageStack.push(addGraphSelectThingComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Scene")
iconName: "slideshow"
onClicked: {
internalPageStack.push(addSceneComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Web view")
iconName: "stock_website"
visible: Qt.platform.os != "android"
onClicked: {
internalPageStack.push(addWebViewComponent)
}
}
}
Component {
id: addThingSelectionComponent
ColumnLayout {
RowLayout {
ColorIcon {
name: "/ui/images/find.svg"
}
TextField {
id: filterTextField
Layout.fillWidth: true
}
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.delegateHeight * 6
clip: true
model: ThingsProxy {
id: thingsProxy
engine: _engine
nameFilter: filterTextField.displayText
}
ScrollBar.vertical: ScrollBar {}
delegate: NymeaItemDelegate {
width: parent.width
text: model.name
iconName: app.interfacesToIcon(thingsProxy.get(index).thingClass.interfaces)
progressive: false
onClicked: {
root.dashboardModel.addThingItem(model.id, root.index)
root.close();
}
}
}
}
}
Component {
id: addFolderComponent
ColumnLayout {
property bool needsOkButton: true
TextField {
id: folderNameTextField
Layout.fillWidth: true
placeholderText: qsTr("Name")
}
GridView {
id: iconsGrid
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.bigIconSize * 6
model: Object.entries(NymeaUtils.namedIcons)
property int columns: width / Style.bigIconSize - 1
cellWidth: width / columns
cellHeight: cellWidth
property string currentIcon: "dashboard"
clip: true
delegate: MouseArea {
width: iconsGrid.cellWidth
height: iconsGrid.cellHeight
onClicked: {
print("clicked", modelData[0])
iconsGrid.currentIcon = modelData[0]
}
ColorIcon {
anchors.centerIn: parent
name: modelData[1]
color: modelData[0] == iconsGrid.currentIcon ? Style.accentColor : Style.iconColor
size: Style.bigIconSize
}
}
}
Connections {
target: okButton
onClicked: {
root.dashboardModel.addFolderItem(folderNameTextField.text, iconsGrid.currentIcon, root.index)
root.close();
}
}
}
}
Component {
id: addGraphSelectThingComponent
ColumnLayout {
RowLayout {
ColorIcon {
name: "/ui/images/find.svg"
}
TextField {
id: filterTextField
Layout.fillWidth: true
}
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.delegateHeight * 6
clip: true
ScrollBar.vertical: ScrollBar {}
model: ThingsProxy {
id: thingsProxy
engine: _engine
nameFilter: filterTextField.displayText
}
delegate: NymeaItemDelegate {
text: model.name
width: parent ? parent.width : 0 // silence warning on delegate descruction
iconName: app.interfacesToIcon(thingsProxy.get(index).thingClass.interfaces)
onClicked: {
internalPageStack.push(addGraphSelectStateComponent, {thing: thingsProxy.get(index)})
}
}
}
}
}
Component {
id: addGraphSelectStateComponent
ListView {
implicitHeight: Style.delegateHeight * 6
clip: true
ScrollBar.vertical: ScrollBar {}
property Thing thing: null
model: thing.thingClass.stateTypes
width: parent.width
delegate: NymeaItemDelegate {
width: parent.width
text: model.displayName
onClicked: {
root.dashboardModel.addGraphItem(thing.id, model.id, root.index)
root.close()
}
}
}
}
Component {
id: addSceneComponent
ListView {
width: parent.width
implicitHeight: Style.delegateHeight * 6
ScrollBar.vertical: ScrollBar {}
model: RulesFilterModel {
rules: engine.ruleManager.rules
filterExecutable: true
}
delegate: NymeaItemDelegate {
width: parent.width
text: model.name
iconName: iconTag.tag.value
iconColor: colorTag.tag.value
TagWatcher {
id: iconTag
tags: engine.tagsManager.tags
ruleId: model.id
tagId: "icon"
}
TagWatcher {
id: colorTag
tags: engine.tagsManager.tags
ruleId: model.id
tagId: "color"
}
onClicked: {
root.dashboardModel.addSceneItem(model.id, root.index)
root.close()
}
}
}
}
Component {
id: addWebViewComponent
ColumnLayout {
property bool needsOkButton: true
property bool okButtonEnabled: urlTextField.displayText.length > 0
Connections {
target: okButton
onClicked: {
root.dashboardModel.addWebViewItem(urlTextField.text, columnsTabs.currentValue, rowsTabs.currentValue, interactiveSwitch.checked, root.index)
root.close();
}
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Location")
}
TextField {
id: urlTextField
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
placeholderText: qsTr("Enter a URL")
text: "https://"
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Size")
}
GridLayout {
columns: width > 300 ? 2 : 1
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
columnSpacing: Style.smallMargins
rowSpacing: Style.smallMargins
Label {
text: qsTr("Columns")
}
SelectionTabs {
id: columnsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.columnSpan - 1
}
Label {
text: qsTr("Rows")
}
SelectionTabs {
id: rowsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.rowSpan - 1
}
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Behavior")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
SwitchDelegate {
id: interactiveSwitch
Layout.fillWidth: true
checked: root.item.interactive
text: qsTr("Interactive")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
}
}
}
footer: Item {
implicitHeight: buttonRow.implicitHeight + Style.margins
RowLayout {
id: buttonRow
anchors { left: parent.left; right: parent.right; bottom: parent.bottom; margins: Style.margins}
spacing: Style.smallMargins
Button {
text: qsTr("Cancel")
onClicked: root.close()
}
Button {
text: qsTr("Back")
visible: internalPageStack.depth > 1
onClicked: internalPageStack.pop()
}
Item {
Layout.fillWidth: true
}
Button {
id: okButton
text: qsTr("OK")
visible: internalPageStack.currentItem.hasOwnProperty("needsOkButton") && internalPageStack.currentItem.needsOkButton === true
enabled: !internalPageStack.currentItem.hasOwnProperty("okButtonEnabled") || internalPageStack.currentItem.okButtonEnabled
}
}
}
}

View File

@ -0,0 +1,60 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
Item {
id: root
property alias contentItem: contentContainer.children
property bool configurable: false
function configure() {
console.warn("Dashboard item claims to be configurable but doesn't implement configure() function")
}
signal openDialog(var dialogComponent);
property bool editMode: false
property int topClip: 0
property int bottomClip: 0
Item {
id: contentContainer
anchors.fill: parent
}
}

View File

@ -0,0 +1,117 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
DashboardDelegateBase {
id: root
property DashboardFolderItem item: null
configurable: true
function configure() {
print("configure called")
root.openDialog(configDialogComponent)
}
contentItem: MainPageTile {
id: delegateRoot
height: root.height
width: root.width
// text: root.item.name
iconName: NymeaUtils.namedIcon(root.item.icon)
iconColor: Style.accentColor
onClicked: pageStack.push(Qt.resolvedUrl("DashboardPage.qml"), {item: root.item})
onPressAndHold: root.longPressed();
contentItem: Label {
text: root.item.name
width: parent.width
height: parent.height
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
maximumLineCount: 2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
padding: app.margins / 2
}
}
Component {
id: configDialogComponent
MeaDialog {
id: configDialog
onAccepted: {
root.item.name = nameTextField.text
}
TextField {
id: nameTextField
text: root.item.name
Layout.fillWidth: true
placeholderText: qsTr("Name")
}
GridView {
id: iconsGrid
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.bigIconSize * 6
model: Object.entries(NymeaUtils.namedIcons)
property int columns: width / Style.bigIconSize - 1
cellWidth: width / columns
cellHeight: cellWidth
clip: true
delegate: MouseArea {
width: iconsGrid.cellWidth
height: iconsGrid.cellHeight
onClicked: {
print("clicked", modelData[0])
root.item.icon = modelData[0]
}
ColorIcon {
anchors.centerIn: parent
name: modelData[1]
color: modelData[0] == root.item.icon ? Style.accentColor : Style.iconColor
size: Style.bigIconSize
}
}
}
}
}
}

View File

@ -0,0 +1,63 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../customviews"
DashboardDelegateBase {
id: root
property DashboardGraphItem item: null
readonly property Thing thing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(item.thingId)
readonly property StateType stateType: thing ? thing.thingClass.stateTypes.getStateType(item.stateTypeId) : null
readonly property State state: thing ? thing.states.getState(item.stateTypeId) : null
contentItem: GenericTypeGraph {
id: graph
width: root.width
height: root.height
title: root.state && root.stateType ? root.thing.name + " " + Types.toUiValue(root.state.value, root.stateType.unit) + Types.toUiUnit(root.stateType.unit) : ""
thing: root.thing
color: "blue"//app.interfaceToColor(interfaceName)
iconSource: ""// app.interfaceToIcon(interfaceName)
implicitHeight: width * .6
// property string interfaceName: parent.interfaceName
stateType: root.stateType
property State state: root.state
}
}

View File

@ -0,0 +1,60 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
Page {
id: root
property DashboardFolderItem item: null
header: NymeaHeader {
text: root.item.name
onBackPressed: pageStack.pop()
HeaderButton {
imageSource: "configure"
onClicked: dashboard.editMode = !dashboard.editMode
color: dashboard.editMode ? Style.accentColor : Style.iconColor
}
}
Dashboard {
id: dashboard
anchors.fill: parent
model: root.item.model
}
}

View File

@ -0,0 +1,68 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
DashboardDelegateBase {
id: root
property DashboardSceneItem item: null
readonly property Rule rule: item && !engine.ruleManager.fetchingData ? engine.ruleManager.rules.getRule(item.ruleId) : null
property var colorTag: engine.tagsManager.tags.findRuleTag(root.item.ruleId, "color")
property var iconTag: engine.tagsManager.tags.findRuleTag(root.item.ruleId, "icon")
contentItem: MainPageTile {
width: root.width
height: root.height
iconName: iconTag ? "/ui/images/" + iconTag.value + ".svg" : "/ui/images/slideshow.svg";
fallbackIconName: "/ui/images/slideshow.svg"
iconColor: colorTag && colorTag.value.length > 0 ? colorTag.value : Style.accentColor;
lowerText: root.rule ? root.rule.name : ""
onClicked: engine.ruleManager.executeActions(root.item.ruleId)
onPressAndHold: root.longPressed()
Connections {
target: engine.tagsManager.tags
onCountChanged: {
colorTag = engine.tagsManager.tags.findRuleTag(root.item.ruleId, "color")
iconTag = engine.tagsManager.tags.findRuleTag(root.item.ruleId, "icon")
}
}
}
}

View File

@ -0,0 +1,53 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
DashboardDelegateBase {
id: root
property DashboardThingItem item: null
contentItem: ThingTile {
id: delegateRoot
width: root.width
height: root.height
thing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(root.item.thingId)
onClicked: pageStack.push(Qt.resolvedUrl("../../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing})
onPressAndHold: root.longPressed()
}
}

View File

@ -0,0 +1,176 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
//import QtWebView 1.1
import QtGraphicalEffects 1.1
DashboardDelegateBase {
id: root
property DashboardWebViewItem item: null
configurable: true
function configure() {
root.openDialog(configDialogComponent)
}
contentItem: MouseArea {
id: delegateRoot
width: root.width
height: root.height
Component.onCompleted: {
// This might fail if qml-module-qtwebview isn't around
var webView = Qt.createQmlObject(webViewString, webViewContainer);
print("created webView", webView)
}
property string webViewString:
'
import QtQuick 2.8;
import QtWebView 1.1;
import Nymea 1.0;
WebView {
id: webView
anchors.fill: parent
anchors.bottomMargin: root.bottomClip + Style.smallMargins
anchors.topMargin: root.topClip + Style.smallMargins
anchors.margins: Style.smallMargins
url: root.item.url
enabled: root.item.interactive
visible: !app.mainMenu.visible && !root.editMode && root.topClip < root.height && root.bottomClip < height
}
'
Item {
id: webViewContainer
anchors.fill: parent
}
Rectangle {
id: mask
anchors.fill: parent
anchors.margins: Style.smallMargins
radius: Style.cornerRadius
}
OpacityMask {
anchors.fill: parent
anchors.margins: Style.smallMargins
source: ShaderEffectSource {
sourceItem: webViewContainer
recursive: true
hideSource: true
}
maskSource: mask
}
}
Component {
id: configDialogComponent
MeaDialog {
id: configDialog
onAccepted: {
root.item.url = urlTextField.text
root.item.columnSpan = columnsTabs.currentValue
root.item.rowSpan = rowsTabs.currentValue
root.item.interactive = interactiveSwitch.checked
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Location")
}
TextField {
id: urlTextField
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
placeholderText: qsTr("Enter a URL")
text: root.item.url
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Size")
}
GridLayout {
columns: width > 300 ? 2 : 1
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
columnSpacing: Style.smallMargins
rowSpacing: Style.smallMargins
Label {
text: qsTr("Columns")
}
SelectionTabs {
id: columnsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.columnSpan - 1
}
Label {
text: qsTr("Rows")
}
SelectionTabs {
id: rowsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.rowSpan - 1
}
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Behavior")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
SwitchDelegate {
id: interactiveSwitch
Layout.fillWidth: true
checked: root.item.interactive
text: qsTr("Interactive")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
}
}
}

View File

@ -51,7 +51,7 @@ SettingsPageBase {
ThingInfoPane { ThingInfoPane {
id: infoPane id: infoPane
anchors { left: parent.left; top: parent.top; right: parent.right } Layout.fillWidth: true
thing: root.thing thing: root.thing
} }

View File

@ -84,4 +84,50 @@ Item {
return ((r * 299 + g * 587 + b * 114) / 1000) < 128 return ((r * 299 + g * 587 + b * 114) / 1000) < 128
} }
property var namedIcons: {
"dashboard": "/ui/images/dashboard.svg",
"group": "/ui/images/groups.svg",
"folder": "/ui/images/folder.svg",
"star": "/ui/images/starred.svg",
"heart": "/ui/images/like.svg",
"wrench": "/ui/images/configure.svg",
"light": "/ui/images/light-on.svg",
"sensor": "/ui/images/sensors.svg",
"media": "/ui/images/media.svg",
"powersocket": "/ui/images/powersocket.svg",
"power": "/ui/images/system-shutdown.svg",
"weather": "/ui/images/weather-app-symbolic.svg",
"attention": "/ui/images/attention.svg",
"shutter": "/ui/images/shutter/shutter-040.svg",
"garage": "/ui/images/garage/garage-100.svg",
"awning": "/ui/images/awning/awning-100.svg",
"uncategorized": "/ui/images/select-none.svg",
"closable": "/ui/images/closable-move.svg",
"smartmeter": "/ui/images/smartmeter.svg",
"heating": "/ui/images/thermostat/heating.svg",
"cooling": "/ui/images/thermostat/cooling.svg",
"meter": "/ui/images/dial.svg",
"ev-charger": "/ui/images/ev-charger.svg",
"battery": "/ui/images/battery/battery-100.svg",
"message": "/ui/images/notification.svg",
"irrigation": "/ui/images/irrigation.svg",
"ventilation": "/ui/images/ventilation.svg",
"lock": "/ui/images/smartlock.svg",
"qrcode": "/ui/images/qrcode.svg",
"cleaningrobot": "/ui/images/cleaning-robot.svg",
"plant": "/ui/images/sensors/conductivity.svg",
"water": "/ui/images/sensors/water.svg",
"wind": "/ui/images/sensors/windspeed.svg",
"cloud": "/ui/images/weathericons/weather-clouds.svg",
"send": "/ui/images/send.svg"
}
function namedIcon(name) {
if (!namedIcons.hasOwnProperty(name)) {
console.error("No such named icon:", name)
return
}
return namedIcons[name]
}
} }