Add a dashboard view
This commit is contained in:
parent
0a0710a6d3
commit
652f115b7c
164
libnymea-app/appdata.cpp
Normal file
164
libnymea-app/appdata.cpp
Normal 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 ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
{
|
||||
qCDebug(dcAppData()) << "App data written:" << commandId << params;
|
||||
}
|
||||
|
||||
void AppData::notificationReceived(const QVariantMap ¬ification)
|
||||
{
|
||||
qCDebug(dcAppData()) << "AppData notification" << notification;
|
||||
}
|
||||
57
libnymea-app/appdata.h
Normal file
57
libnymea-app/appdata.h
Normal 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 ¶ms);
|
||||
void appDataWritten(int commandId, const QVariantMap ¶ms);
|
||||
|
||||
void notificationReceived(const QVariantMap ¬ification);
|
||||
private:
|
||||
Engine *m_engine = nullptr;
|
||||
QTimer m_syncTimer;
|
||||
QString m_group;
|
||||
|
||||
bool m_loopLock = false;
|
||||
QHash<int, QString> m_readRequests;
|
||||
|
||||
};
|
||||
|
||||
#endif // APPDATA_H
|
||||
@ -188,15 +188,15 @@ bool AWSClient::confirmationPending() const
|
||||
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()) {
|
||||
qCInfo(dcCloud()) << "AWS config not set. Not logging in.";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (m_loginInProgress) {
|
||||
qCDebug(dcCloud()) << "Login already pending...";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
m_loginInProgress = true;
|
||||
|
||||
@ -243,16 +243,19 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
if (reply->error() == QNetworkReply::HostNotFoundError) {
|
||||
qCWarning(dcCloud()) << "Error logging in to aws due to network connection.";
|
||||
emit loginResult(LoginErrorNetworkError);
|
||||
cancelCallQueue();
|
||||
return;
|
||||
}
|
||||
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
|
||||
qCWarning(dcCloud()) << "Looks like a wrong password.";
|
||||
m_username.clear();
|
||||
m_password.clear();
|
||||
cancelCallQueue();
|
||||
emit loginResult(LoginErrorInvalidUserOrPass);
|
||||
return;
|
||||
}
|
||||
qCWarning(dcCloud()) << "Error logging in to aws. Error:" << reply->error() << reply->errorString();
|
||||
cancelCallQueue();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
@ -263,6 +266,7 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
qCWarning(dcCloud()) << "Failed to parse AWS login response" << error.errorString();
|
||||
m_username.clear();
|
||||
m_password.clear();
|
||||
cancelCallQueue();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
@ -277,6 +281,8 @@ void AWSClient::login(const QString &username, const QString &password)
|
||||
QList<QByteArray> jwtParts = m_idToken.split('.');
|
||||
if (jwtParts.count() != 3) {
|
||||
qCWarning(dcCloud()) << "Error: JWT token doesn't have 3 parts. Cannot retrieve AWS Cognito ID.";
|
||||
cancelCallQueue();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
// 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";
|
||||
getId();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void AWSClient::logout()
|
||||
@ -634,6 +641,7 @@ void AWSClient::getId()
|
||||
reply->deleteLater();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(dcCloud()) << "Error calling GetId" << reply->error() << reply->errorString();
|
||||
cancelCallQueue();
|
||||
return;
|
||||
}
|
||||
QByteArray data = reply->readAll();
|
||||
@ -641,6 +649,7 @@ void AWSClient::getId()
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcCloud()) << "Error parsing json reply for GetId" << error.errorString();
|
||||
cancelCallQueue();
|
||||
return;
|
||||
}
|
||||
m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray();
|
||||
@ -820,6 +829,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
reply->deleteLater();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(dcCloud()) << "Error calling GetCredentialsForIdentity" << reply->errorString();
|
||||
cancelCallQueue();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
return;
|
||||
}
|
||||
@ -828,6 +838,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId)
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcCloud()) << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString();
|
||||
cancelCallQueue();
|
||||
emit loginResult(LoginErrorUnknownError);
|
||||
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
|
||||
{
|
||||
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()) {
|
||||
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";
|
||||
refreshAccessToken();
|
||||
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));
|
||||
|
||||
QPointer<QObject> senderWatcher = QPointer<QObject>(sender);
|
||||
|
||||
// This is somehow broken in AWS...
|
||||
// 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.
|
||||
@ -938,18 +960,18 @@ bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject*
|
||||
// }
|
||||
qCDebug(dcCloud) << "Payload:" << payload;
|
||||
QNetworkReply *reply = m_nam->post(request, payload);
|
||||
QTimer::singleShot(5000, reply, [reply, senderWatcher, callback](){
|
||||
QTimer::singleShot(5000, reply, [reply, sender, callback](){
|
||||
reply->deleteLater();
|
||||
qCWarning(dcCloud) << "Timeout posting to MQTT";
|
||||
if (senderWatcher) {
|
||||
if (!sender.isNull()) {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, senderWatcher, callback]() {
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, sender, callback]() {
|
||||
reply->deleteLater();
|
||||
QByteArray data = reply->readAll();
|
||||
qCDebug(dcCloud()) << "MQTT post reply" << data;
|
||||
if (senderWatcher.isNull()) {
|
||||
// qDebug() << "MQTT post reply" << data;
|
||||
if (sender.isNull()) {
|
||||
qCDebug(dcCloud()) << "Request object disappeared. Discarding MQTT reply...";
|
||||
return;
|
||||
}
|
||||
@ -1052,18 +1074,17 @@ void AWSClient::fetchDevices()
|
||||
});
|
||||
}
|
||||
|
||||
void AWSClient::refreshAccessToken()
|
||||
bool AWSClient::refreshAccessToken()
|
||||
{
|
||||
if (!isLoggedIn()) {
|
||||
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
|
||||
// https://forums.aws.amazon.com/thread.jspa?threadID=287978
|
||||
// Let's re-login instead with user & pass
|
||||
login(m_username, m_password);
|
||||
return;
|
||||
return login(m_username, m_password);
|
||||
|
||||
|
||||
// Non-working block... Enable this if Amazon ever fixes their API...
|
||||
@ -1126,6 +1147,7 @@ void AWSClient::refreshAccessToken()
|
||||
emit isLoggedInChanged();
|
||||
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -139,7 +139,7 @@ public:
|
||||
AWSDevices* awsDevices() 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 signup(const QString &username, const QString &password);
|
||||
Q_INVOKABLE void confirmRegistration(const QString &code);
|
||||
@ -151,7 +151,7 @@ public:
|
||||
|
||||
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 registerPushNotificationEndpoint(const QString ®istrationId, const QString &deviceDisplayName, const QString mobileDeviceId, const QString &mobileDeviceManufacturer, const QString &mobileDeviceModel);
|
||||
@ -185,7 +185,7 @@ private:
|
||||
explicit AWSClient(QObject *parent = nullptr);
|
||||
static AWSClient* s_instance;
|
||||
|
||||
void refreshAccessToken();
|
||||
bool refreshAccessToken();
|
||||
void getCredentialsForIdentity(const QString &identityId);
|
||||
void connectMQTT();
|
||||
|
||||
@ -218,7 +218,7 @@ private:
|
||||
QueuedCall(const QString &method): method(method) { }
|
||||
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, 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 arg1;
|
||||
QString arg2;
|
||||
@ -241,6 +241,7 @@ private:
|
||||
queue.append(call);
|
||||
}
|
||||
};
|
||||
void cancelCallQueue();
|
||||
|
||||
QList<QueuedCall> m_callQueue;
|
||||
|
||||
|
||||
@ -129,6 +129,7 @@
|
||||
#include "zigbee/zigbeenetworks.h"
|
||||
#include "applogcontroller.h"
|
||||
#include "tagwatcher.h"
|
||||
#include "appdata.h"
|
||||
|
||||
#include <QtQml/qqml.h>
|
||||
|
||||
@ -338,6 +339,8 @@ void registerQmlTypes() {
|
||||
qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher");
|
||||
qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher");
|
||||
|
||||
qmlRegisterType<AppData>(uri, 1, 0, "AppData");
|
||||
|
||||
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ INCLUDEPATH += \
|
||||
$$top_srcdir/QtZeroConf
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/appdata.cpp \
|
||||
$$PWD/models/scriptsproxymodel.cpp \
|
||||
$$PWD/tagwatcher.cpp \
|
||||
$${PWD}/logging.cpp \
|
||||
@ -167,6 +168,7 @@ SOURCES += \
|
||||
|
||||
|
||||
HEADERS += \
|
||||
$$PWD/appdata.h \
|
||||
$$PWD/models/scriptsproxymodel.h \
|
||||
$$PWD/tagwatcher.h \
|
||||
$${PWD}/logging.h \
|
||||
|
||||
@ -287,6 +287,10 @@ void LogsModelNg::logsReply(int commandId, const QVariantMap &data)
|
||||
}
|
||||
|
||||
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 (entryStateType->type().toLower() == "bool") {
|
||||
|
||||
@ -74,9 +74,16 @@ void RuleManager::clear()
|
||||
|
||||
void RuleManager::init()
|
||||
{
|
||||
m_fetchingData = true;
|
||||
emit fetchingDataChanged();
|
||||
m_jsonClient->sendCommand("Rules.GetRules", this, "getRulesReply");
|
||||
}
|
||||
|
||||
bool RuleManager::fetchingData() const
|
||||
{
|
||||
return m_fetchingData;
|
||||
}
|
||||
|
||||
Rules *RuleManager::rules() const
|
||||
{
|
||||
return m_rules;
|
||||
@ -176,6 +183,8 @@ void RuleManager::getRulesReply(int /*commandId*/, const QVariantMap ¶ms)
|
||||
requestParams.insert("ruleId", rule->id());
|
||||
m_jsonClient->sendCommand("Rules.GetRuleDetails", requestParams, this, "getRuleDetailsReply");
|
||||
}
|
||||
m_fetchingData = false;
|
||||
emit fetchingDataChanged();
|
||||
}
|
||||
|
||||
void RuleManager::getRuleDetailsReply(int commandId, const QVariantMap ¶ms)
|
||||
|
||||
@ -50,6 +50,7 @@ class RuleManager : public JsonHandler
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(Rules* rules READ rules CONSTANT)
|
||||
Q_PROPERTY(bool fetchingData READ fetchingData NOTIFY fetchingDataChanged)
|
||||
|
||||
public:
|
||||
explicit RuleManager(JsonRpcClient *jsonClient, QObject *parent = nullptr);
|
||||
@ -58,6 +59,7 @@ public:
|
||||
|
||||
void clear();
|
||||
void init();
|
||||
bool fetchingData() const;
|
||||
|
||||
Rules* rules() const;
|
||||
|
||||
@ -99,10 +101,12 @@ private:
|
||||
signals:
|
||||
void addRuleReply(int commandId, const QString &ruleError, const QString &ruleId);
|
||||
void editRuleReply(int commandId, const QString &ruleError);
|
||||
void fetchingDataChanged();
|
||||
|
||||
private:
|
||||
JsonRpcClient *m_jsonClient;
|
||||
Rules* m_rules;
|
||||
bool m_fetchingData = false;
|
||||
};
|
||||
|
||||
#endif // RULEMANAGER_H
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
#include "engine.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QMetaEnum>
|
||||
|
||||
TagsManager::TagsManager(JsonRpcClient *jsonClient, QObject *parent):
|
||||
JsonHandler(parent),
|
||||
@ -52,7 +53,7 @@ void TagsManager::init()
|
||||
m_busy = true;
|
||||
emit busyChanged();
|
||||
m_tags->clear();
|
||||
m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply");
|
||||
m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsResponse");
|
||||
}
|
||||
|
||||
void TagsManager::clear()
|
||||
@ -79,7 +80,7 @@ int TagsManager::tagThing(const QString &thingId, const QString &tagId, const QS
|
||||
tag.insert("tagId", tagId);
|
||||
tag.insert("value", value);
|
||||
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)
|
||||
@ -90,7 +91,7 @@ int TagsManager::untagThing(const QString &thingId, const QString &tagId)
|
||||
tag.insert("appId", "nymea:app");
|
||||
tag.insert("tagId", tagId);
|
||||
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)
|
||||
@ -102,7 +103,7 @@ int TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QStr
|
||||
tag.insert("tagId", tagId);
|
||||
tag.insert("value", value);
|
||||
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)
|
||||
@ -113,7 +114,7 @@ int TagsManager::untagRule(const QString &ruleId, const QString &tagId)
|
||||
tag.insert("appId", "nymea:app");
|
||||
tag.insert("tagId", tagId);
|
||||
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 ¶ms)
|
||||
@ -156,7 +157,7 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms)
|
||||
}
|
||||
}
|
||||
|
||||
void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap ¶ms)
|
||||
void TagsManager::getTagsResponse(int /*commandId*/, const QVariantMap ¶ms)
|
||||
{
|
||||
QList<Tag*> tags;
|
||||
foreach (const QVariant &tagVariant, params.value("tags").toList()) {
|
||||
@ -171,14 +172,18 @@ void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap ¶ms)
|
||||
emit busyChanged();
|
||||
}
|
||||
|
||||
void TagsManager::addTagReply(int commandId, const QVariantMap ¶ms)
|
||||
void TagsManager::addTagResponse(int commandId, const QVariantMap ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
void TagsManager::removeTagResponse(int commandId, const QVariantMap ¶ms)
|
||||
{
|
||||
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)
|
||||
|
||||
@ -43,6 +43,14 @@ class TagsManager : public JsonHandler
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
|
||||
public:
|
||||
enum TagError {
|
||||
TagErrorNoError,
|
||||
TagErrorThingNotFound,
|
||||
TagErrorRuleNotFound,
|
||||
TagErrorTagNotFound
|
||||
};
|
||||
Q_ENUM(TagError)
|
||||
|
||||
explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr);
|
||||
QString nameSpace() const override;
|
||||
|
||||
@ -59,12 +67,14 @@ public:
|
||||
|
||||
signals:
|
||||
void busyChanged();
|
||||
void addTagReply(int commandId, TagError error);
|
||||
void removeTagReply(int commandId, TagError error);
|
||||
|
||||
private slots:
|
||||
void handleTagsNotification(const QVariantMap ¶ms);
|
||||
void getTagsReply(int commandId, const QVariantMap ¶ms);
|
||||
void addTagReply(int commandId, const QVariantMap ¶ms);
|
||||
void removeTagReply(int commandId, const QVariantMap ¶ms);
|
||||
void getTagsResponse(int commandId, const QVariantMap ¶ms);
|
||||
void addTagResponse(int commandId, const QVariantMap ¶ms);
|
||||
void removeTagResponse(int commandId, const QVariantMap ¶ms);
|
||||
|
||||
private:
|
||||
Tag *unpackTag(const QVariantMap &tagMap);
|
||||
|
||||
165
nymea-app/dashboard/dashboarditem.cpp
Normal file
165
nymea-app/dashboard/dashboarditem.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
113
nymea-app/dashboard/dashboarditem.h
Normal file
113
nymea-app/dashboard/dashboarditem.h
Normal 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
|
||||
223
nymea-app/dashboard/dashboardmodel.cpp
Normal file
223
nymea-app/dashboard/dashboardmodel.cpp
Normal 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();
|
||||
}
|
||||
54
nymea-app/dashboard/dashboardmodel.h
Normal file
54
nymea-app/dashboard/dashboardmodel.h
Normal 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
|
||||
@ -157,7 +157,7 @@
|
||||
<file>ui/images/lock-closed.svg</file>
|
||||
<file>ui/images/lock-open.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/BrowserIconFolder.svg</file>
|
||||
<file>ui/images/browser/MediaBrowserIconSpotify.svg</file>
|
||||
@ -189,7 +189,8 @@
|
||||
<file>ui/images/browser/MediaBrowserIconNapster.svg</file>
|
||||
<file>ui/images/browser/MediaBrowserIconSoundCloud.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/save.svg</file>
|
||||
<file>ui/images/edit-clear.svg</file>
|
||||
@ -257,5 +258,6 @@
|
||||
<file>ui/images/zigbee/NXP.svg</file>
|
||||
<file>ui/images/nymea-splash.svg</file>
|
||||
<file>ui/images/cleaning-robot.svg</file>
|
||||
<file>ui/images/chart.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -46,6 +46,9 @@
|
||||
#include "nfchelper.h"
|
||||
#include "nfcthingactionwriter.h"
|
||||
#include "platformhelper.h"
|
||||
#include "dashboard/dashboardmodel.h"
|
||||
#include "dashboard/dashboarditem.h"
|
||||
#include "mouseobserver.h"
|
||||
#include "../config.h"
|
||||
|
||||
#include "logging.h"
|
||||
@ -151,6 +154,16 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterSingletonType<PushNotifications>("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider);
|
||||
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("qtBuildVersion", QT_VERSION_STR);
|
||||
engine->rootContext()->setContextProperty("qtVersion", qVersion());
|
||||
|
||||
37
nymea-app/mouseobserver.cpp
Normal file
37
nymea-app/mouseobserver.cpp
Normal 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
36
nymea-app/mouseobserver.h
Normal 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
|
||||
@ -19,7 +19,10 @@ PRE_TARGETDEPS += ../libnymea-app
|
||||
linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a
|
||||
|
||||
HEADERS += \
|
||||
dashboard/dashboarditem.h \
|
||||
dashboard/dashboardmodel.h \
|
||||
mainmenumodel.h \
|
||||
mouseobserver.h \
|
||||
nfchelper.h \
|
||||
nfcthingactionwriter.h \
|
||||
platformintegration/generic/screenhelper.h \
|
||||
@ -31,7 +34,10 @@ HEADERS += \
|
||||
ruletemplates/messages.h
|
||||
|
||||
SOURCES += main.cpp \
|
||||
dashboard/dashboarditem.cpp \
|
||||
dashboard/dashboardmodel.cpp \
|
||||
mainmenumodel.cpp \
|
||||
mouseobserver.cpp \
|
||||
nfchelper.cpp \
|
||||
nfcthingactionwriter.cpp \
|
||||
platformintegration/generic/screenhelper.cpp \
|
||||
@ -114,6 +120,8 @@ ios: {
|
||||
OBJECTIVE_SOURCES += $$PWD/../packaging/ios/platformhelperios.mm \
|
||||
$$PWD/../packaging/ios/pushnotifications.mm \
|
||||
|
||||
OTHER_FILES += $${OBJECTIVE_SOURCES}
|
||||
|
||||
# Add Firebase SDK
|
||||
QMAKE_LFLAGS += -ObjC $(inherited)
|
||||
firebase_files.files += $$files(../packaging/ios/GoogleService-Info.plist)
|
||||
|
||||
@ -235,5 +235,16 @@
|
||||
<file>ui/appsettings/LoggingCategories.qml</file>
|
||||
<file>ui/ConfigurationBase.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>
|
||||
</RCC>
|
||||
|
||||
@ -70,7 +70,18 @@ Page {
|
||||
qsTr("Configure main view")
|
||||
: 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 {
|
||||
@ -109,13 +120,14 @@ Page {
|
||||
|
||||
ListModel {
|
||||
id: mainMenuBaseModel
|
||||
ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things" }
|
||||
ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred" }
|
||||
ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "view-grid-symbolic" }
|
||||
ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow" }
|
||||
ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100" }
|
||||
ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter" }
|
||||
ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media" }
|
||||
ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things"; minVersion: "0.0" }
|
||||
ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred"; minVersion: "2.0" }
|
||||
ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "groups"; minVersion: "2.0" }
|
||||
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"; minVersion: "2.0" }
|
||||
ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter"; minVersion: "2.0" }
|
||||
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 {
|
||||
@ -143,6 +155,11 @@ Page {
|
||||
|
||||
for (var i = 0; i < mainMenuBaseModel.count; 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);
|
||||
if (idx === -1) {
|
||||
newList[newItems++] = item;
|
||||
@ -340,14 +357,14 @@ Page {
|
||||
id: configListView
|
||||
model: mainMenuModel
|
||||
width: parent.width
|
||||
height: parent.height / 2.5
|
||||
height: parent.height / 3
|
||||
anchors.centerIn: parent
|
||||
orientation: ListView.Horizontal
|
||||
moveDisplaced: Transition {
|
||||
NumberAnimation { properties: "x,y"; duration: 200 }
|
||||
}
|
||||
|
||||
property int delegateWidth: width / 2.5
|
||||
property int delegateWidth: width / 3
|
||||
|
||||
property bool dragging: draggingIndex >= 0
|
||||
property int draggingIndex : -1
|
||||
@ -433,50 +450,35 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
delegate: BigTile {
|
||||
id: configDelegate
|
||||
width: configListView.delegateWidth
|
||||
height: configListView.height
|
||||
property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0
|
||||
visible: configListView.draggingIndex !== index
|
||||
|
||||
Pane {
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins / 2
|
||||
Material.elevation: 2
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
header: RowLayout {
|
||||
id: headerRow
|
||||
Label {
|
||||
text: model.displayName
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ItemDelegate {
|
||||
anchors.fill: parent
|
||||
contentItem: Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: configListView.height - headerRow.height - Style.margins * 2
|
||||
|
||||
padding: app.margins * 2
|
||||
contentItem: GridLayout {
|
||||
columns: 1
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
}
|
||||
}
|
||||
ColorIcon {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * .6
|
||||
height: width
|
||||
name: Qt.resolvedUrl("images/" + model.icon + ".svg")
|
||||
color: configDelegate.isEnabled ? Style.accentColor : Style.iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -500,13 +502,14 @@ Page {
|
||||
target: dndItem
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.9
|
||||
to: 0.95
|
||||
duration: 200
|
||||
}
|
||||
|
||||
Pane {
|
||||
BigTile {
|
||||
id: dndTile
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins / 2
|
||||
// anchors.margins: app.margins / 2
|
||||
Material.elevation: 2
|
||||
|
||||
leftPadding: 0
|
||||
@ -514,32 +517,22 @@ Page {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
contentItem: ItemDelegate {
|
||||
anchors.fill: parent
|
||||
header: RowLayout {
|
||||
Label {
|
||||
text: dndItem.displayName
|
||||
}
|
||||
}
|
||||
|
||||
padding: app.margins * 2
|
||||
contentItem: GridLayout {
|
||||
columns: 1
|
||||
contentItem: Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: configListView.height - header.height
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ColorIcon {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * .8
|
||||
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
|
||||
}
|
||||
ColorIcon {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * .6
|
||||
height: width
|
||||
name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg")
|
||||
color: dndItem.isEnabled ? Style.accentColor : Style.iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ Item {
|
||||
property color backgroundColor: "#fafafa"
|
||||
property color foregroundColor: "#202020"
|
||||
|
||||
property color accentColor: "#ff57baae"
|
||||
property color accentColor: "#57baae"
|
||||
property color iconColor: "#808080"
|
||||
|
||||
property color headerBackgroundColor: "#ffffff"
|
||||
@ -91,6 +91,6 @@ Item {
|
||||
"currentPower": "deepskyblue",
|
||||
}
|
||||
|
||||
readonly property color red: "#ad4754"
|
||||
readonly property color red: "#952727"
|
||||
readonly property color white: "white"
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ Item {
|
||||
|
||||
property alias header: headerContainer.children
|
||||
property alias contentItem: content.contentItem
|
||||
property int contentHeight: root.height - headerContainer.height - content.topPadding - content.bottomPadding
|
||||
|
||||
property alias showHeader: headerContainer.visible
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@ Item {
|
||||
property bool updateStatus: false
|
||||
|
||||
property alias contentItem: innerContent.children
|
||||
property alias lowerText: lowerTextLabel.text
|
||||
|
||||
signal clicked();
|
||||
signal pressAndHold();
|
||||
@ -146,7 +147,7 @@ Item {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: backgroundImg.status !== Image.Ready
|
||||
visible: backgroundImg.status !== Image.Ready && label.text != ""
|
||||
|
||||
Label {
|
||||
id: label
|
||||
@ -155,7 +156,7 @@ Item {
|
||||
text: root.text.toUpperCase()
|
||||
font.pixelSize: app.smallFont
|
||||
font.letterSpacing: 1
|
||||
wrapMode: Text.WordWrap
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
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 {
|
||||
id: innerContent
|
||||
anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: app.margins / 2 }
|
||||
height: Style.iconSize + app.margins * 2
|
||||
Material.foreground: Style.tileOverlayForegroundColor
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
||||
@ -41,6 +41,8 @@ Item {
|
||||
|
||||
property string title: ""
|
||||
|
||||
property var headerButtons: []
|
||||
|
||||
// Prevent scroll events to swipe left/right in case they fall through the grid
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
@ -60,7 +60,7 @@ Dialog {
|
||||
}
|
||||
|
||||
header: Item {
|
||||
implicitHeight: headerRow.height + app.margins * 2
|
||||
implicitHeight: headerRow.height + app.margins
|
||||
implicitWidth: parent.width
|
||||
visible: root.title.length > 0
|
||||
RowLayout {
|
||||
@ -86,9 +86,8 @@ Dialog {
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
id: content
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
|
||||
Label {
|
||||
id: contentLabel
|
||||
|
||||
@ -226,7 +226,7 @@ Item {
|
||||
ProgressButton {
|
||||
longpressEnabled: false
|
||||
visible: root.thing.thingClass.browsable
|
||||
imageSource: "../images/folder-symbolic.svg"
|
||||
imageSource: "../images/folder.svg"
|
||||
onClicked: {
|
||||
if (!d.browser) {
|
||||
d.browser = browserPage.createObject(root, {x: 0, y: root.height})
|
||||
|
||||
@ -65,6 +65,7 @@ Item {
|
||||
buttonDelegate.longpressed = false
|
||||
}
|
||||
onReleased: {
|
||||
print("onReleased!")
|
||||
if (!containsMouse) {
|
||||
print("cancelled")
|
||||
buttonDelegate.longpressed = false;
|
||||
@ -79,6 +80,7 @@ Item {
|
||||
root.clicked();
|
||||
}
|
||||
buttonDelegate.longpressed = false
|
||||
print("released end")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
nymea-app/ui/components/SelectionTabs.qml
Normal file
53
nymea-app/ui/components/SelectionTabs.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,7 @@ AutoSizeMenu {
|
||||
root.addItem(menuEntryComponent.createObject(root,
|
||||
{
|
||||
text: qsTr("Grouping"),
|
||||
iconSource: "../images/view-grid-symbolic.svg",
|
||||
iconSource: "../images/groups.svg",
|
||||
functionName: "addToGroup"
|
||||
}))
|
||||
|
||||
@ -99,7 +99,7 @@ AutoSizeMenu {
|
||||
id: addToGroupDialog
|
||||
MeaDialog {
|
||||
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
|
||||
// https://bugreports.qt.io/browse/QTBUG-56918
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
@ -48,16 +48,16 @@ Item {
|
||||
property string iconSource: ""
|
||||
property alias title: titleLabel.text
|
||||
|
||||
readonly property var valueState: thing.states.getState(stateType.id)
|
||||
readonly property bool hasConnectable: thing.thingClass.interfaces.indexOf("connectable") >= 0
|
||||
readonly property State valueState: thing && stateType ? thing.states.getState(stateType.id) : null
|
||||
readonly property StateType connectedStateType: hasConnectable ? thing.thingClass.stateTypes.findByName("connected") : null
|
||||
readonly property bool hasConnectable: connectedStateType != null
|
||||
|
||||
|
||||
LogsModelNg {
|
||||
id: logsModelNg
|
||||
engine: _engine
|
||||
thingId: root.thing.id
|
||||
typeIds: [root.stateType.id]
|
||||
engine: root.thing ? _engine : null
|
||||
thingId: root.thing ? root.thing.id : ""
|
||||
typeIds: root.stateType ? [root.stateType.id] : []
|
||||
live: true
|
||||
graphSeries: lineSeries1
|
||||
viewStartTime: xAxis.min
|
||||
@ -66,7 +66,7 @@ Item {
|
||||
LogsModelNg {
|
||||
id: connectedLogsModel
|
||||
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] : []
|
||||
live: true
|
||||
graphSeries: connectedLineSeries
|
||||
@ -78,8 +78,8 @@ Item {
|
||||
anchors.fill: parent
|
||||
margins.top: Style.iconSize + app.margins
|
||||
margins.bottom: app.margins / 2
|
||||
margins.left: 0
|
||||
margins.right: 0
|
||||
margins.left: Style.smallMargins
|
||||
margins.right: Style.smallMargins
|
||||
backgroundColor: Style.tileBackgroundColor
|
||||
backgroundRoundness: Style.cornerRadius
|
||||
legend.visible: false
|
||||
@ -104,6 +104,7 @@ Item {
|
||||
Label {
|
||||
id: titleLabel
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
text: root.stateType.type.toLowerCase() === "bool"
|
||||
? 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)
|
||||
@ -118,9 +119,9 @@ Item {
|
||||
}
|
||||
HeaderButton {
|
||||
imageSource: "../images/zoom-in.svg"
|
||||
enabled: xAxis.timeDiff > (60 * 30)
|
||||
enabled: xAxis.timeDiff > (60 * 5)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -129,27 +130,25 @@ Item {
|
||||
ValueAxis {
|
||||
id: yAxis
|
||||
max: {
|
||||
switch (root.stateType.type.toLowerCase()) {
|
||||
case "bool":
|
||||
if (root.stateType && root.stateType.type.toLowerCase() == "bool") {
|
||||
return 1;
|
||||
default:
|
||||
} else {
|
||||
Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05))
|
||||
}
|
||||
}
|
||||
min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05))
|
||||
// onMinChanged: applyNiceNumbers();
|
||||
// onMaxChanged: applyNiceNumbers();
|
||||
labelsFont.pixelSize: app.smallFont
|
||||
labelsFont: Style.smallFont
|
||||
labelFormat: {
|
||||
switch (root.stateType.type.toLowerCase()) {
|
||||
case "bool":
|
||||
if (root.stateType && root.stateType.type.toLowerCase() == "bool") {
|
||||
return "x";
|
||||
default:
|
||||
} else {
|
||||
return "%d";
|
||||
}
|
||||
}
|
||||
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)
|
||||
gridLineColor: color
|
||||
}
|
||||
@ -166,7 +165,7 @@ Item {
|
||||
gridVisible: false
|
||||
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
|
||||
tickCount: chartView.width / 70
|
||||
labelsFont.pixelSize: app.smallFont
|
||||
labelsFont: Style.smallFont
|
||||
labelsColor: Style.foregroundColor
|
||||
property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000
|
||||
|
||||
@ -265,9 +264,9 @@ Item {
|
||||
id: mainSeries
|
||||
axisX: xAxis
|
||||
axisY: yAxis
|
||||
name: root.stateType.displayName
|
||||
name: root.stateType ? root.stateType.displayName : ""
|
||||
borderColor: root.color
|
||||
borderWidth: 4
|
||||
borderWidth: 2
|
||||
lowerSeries: LineSeries {
|
||||
id: lineSeries0
|
||||
XYPoint { x: xAxis.max.getTime(); y: 0 }
|
||||
@ -347,7 +346,7 @@ Item {
|
||||
borderColor: root.color
|
||||
axisX: xAxis
|
||||
axisY: yAxis
|
||||
pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool"
|
||||
pointLabelsVisible: root.stateType && root.stateType.type.toLowerCase() !== "bool"
|
||||
pointLabelsColor: Style.foregroundColor
|
||||
pointLabelsFont.pixelSize: app.smallFont
|
||||
pointLabelsFormat: "@yPoint"
|
||||
|
||||
@ -37,30 +37,34 @@ import "../components"
|
||||
|
||||
MainPageTile {
|
||||
id: root
|
||||
text: thing.name.toUpperCase()
|
||||
iconName: app.interfacesToIcon(thing.thingClass.interfaces)
|
||||
text: thing ? thing.name.toUpperCase() : ""
|
||||
iconName: thing ? app.interfacesToIcon(thing.thingClass.interfaces) : ""
|
||||
iconColor: Style.accentColor
|
||||
isWireless: thing.thingClass.interfaces.indexOf("wirelessconnectable") >= 0
|
||||
isWireless: thing && thing.thingClass.interfaces.indexOf("wirelessconnectable") >= 0
|
||||
batteryCritical: batteryCriticalState && batteryCriticalState.value === true
|
||||
disconnected: connectedState && connectedState.value === false
|
||||
signalStrength: signalStrengthState ? signalStrengthState.value : -1
|
||||
setupStatus: thing.setupStatus
|
||||
setupStatus: thing ? thing.setupStatus : Thing.ThingSetupStatusNone
|
||||
updateStatus: updateStatusState && updateStatusState.value !== "idle"
|
||||
|
||||
backgroundImage: artworkState && artworkState.value.length > 0 ? artworkState.value : ""
|
||||
|
||||
property Thing thing: null
|
||||
property alias device: root.thing
|
||||
readonly property State connectedState: thing.stateByName("connected")
|
||||
readonly property State signalStrengthState: thing.stateByName("signalStrength")
|
||||
readonly property State batteryCriticalState: thing.stateByName("batteryCritical")
|
||||
readonly property State artworkState: thing.stateByName("artwork")
|
||||
readonly property State updateStatusState: thing.stateByName("updateStatus")
|
||||
readonly property State connectedState: thing ? thing.stateByName("connected") : null
|
||||
readonly property State signalStrengthState: thing ? thing.stateByName("signalStrength") : null
|
||||
readonly property State batteryCriticalState: thing ? thing.stateByName("batteryCritical") : null
|
||||
readonly property State artworkState: thing ? thing.stateByName("artwork") : null
|
||||
readonly property State updateStatusState: thing ? thing.stateByName("updateStatus") : null
|
||||
|
||||
contentItem: Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
sourceComponent: {
|
||||
if (!root.thing) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (var i = 0; i < root.thing.thingClass.interfaces.length; i++) {
|
||||
switch (root.thing.thingClass.interfaces[i]) {
|
||||
case "closable":
|
||||
|
||||
@ -285,7 +285,7 @@ ThingPageBase {
|
||||
}
|
||||
ProgressButton {
|
||||
longpressEnabled: false
|
||||
imageSource: "../images/view-grid-symbolic.svg"
|
||||
imageSource: "../images/groups.svg"
|
||||
mode: "normal"
|
||||
size: Style.bigIconSize
|
||||
visible: root.thing.thingClass.browsable
|
||||
|
||||
@ -58,7 +58,7 @@ Page {
|
||||
}
|
||||
|
||||
HeaderButton {
|
||||
imageSource: "../images/folder-symbolic.svg"
|
||||
imageSource: "../images/folder.svg"
|
||||
visible: root.thingClass.browsable && root.showBrowserButton
|
||||
onClicked: {
|
||||
pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {thing: root.thing})
|
||||
|
||||
72
nymea-app/ui/images/chart.svg
Normal file
72
nymea-app/ui/images/chart.svg
Normal 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 |
187
nymea-app/ui/images/dashboard.svg
Normal file
187
nymea-app/ui/images/dashboard.svg
Normal 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 |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
90
nymea-app/ui/mainviews/DashboardView.qml
Normal file
90
nymea-app/ui/mainviews/DashboardView.qml
Normal 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
|
||||
}
|
||||
}
|
||||
@ -60,24 +60,85 @@ MainViewBase {
|
||||
moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 150; easing.type: Easing.InOutQuad } }
|
||||
|
||||
model: tagsProxy
|
||||
delegate: ThingTile {
|
||||
delegate: Item {
|
||||
id: delegateRoot
|
||||
width: gridView.cellWidth
|
||||
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 {
|
||||
loops: Animation.Infinite
|
||||
running: root.editMode
|
||||
alwaysRunToEnd: true
|
||||
NumberAnimation { from: 0; to: 3; target: delegateRoot; duration: 75; property: "rotation" }
|
||||
NumberAnimation { from: 3; to: -3; target: delegateRoot; duration: 150; property: "rotation" }
|
||||
NumberAnimation { from: -3; to: 0; target: delegateRoot; duration: 75; property: "rotation" }
|
||||
ThingTile {
|
||||
id: thingTile
|
||||
anchors.fill: parent
|
||||
thing: delegateRoot.thing
|
||||
enabled: !root.editMode
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing})
|
||||
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
|
||||
property var dragOffset: ({})
|
||||
property var draggedItem: null
|
||||
property var fakeDragItem: null
|
||||
|
||||
onPressed: {
|
||||
var item = gridView.itemAt(mouseX, mouseY)
|
||||
onPressAndHold: {
|
||||
var gridViewCoords = mapToItem(gridView.contentItem, mouseX, mouseY)
|
||||
var item = gridView.itemAt(gridViewCoords.x, gridViewCoords.y);
|
||||
draggedItem = item;
|
||||
dragOffset = mapToItem(item, mouseX, mouseY)
|
||||
fakeDragItem.x = mouseX - dragOffset.x;
|
||||
fakeDragItem.y = mouseY - dragOffset.y;
|
||||
fakeDragItem.text = item.text
|
||||
fakeDragItem.iconName = item.iconName
|
||||
fakeDragItem.iconColor = item.iconColor;
|
||||
fakeDragItem.thingId = item.thing.id
|
||||
fakeDragItem.batteryCritical = item.batteryCritical
|
||||
fakeDragItem.disconnected = item.disconnected
|
||||
|
||||
dragArea.fakeDragItem = dragItemComponent.createObject(dragArea, {
|
||||
x: mouseX - dragOffset.x,
|
||||
y: mouseY - dragOffset.y,
|
||||
thing: draggedItem.thing
|
||||
})
|
||||
|
||||
drag.target = fakeDragItem
|
||||
}
|
||||
onReleased: {
|
||||
drag.target = null
|
||||
draggedItem = null
|
||||
fakeDragItem.thingId = ""
|
||||
if (drag.target) {
|
||||
drag.target = null
|
||||
dragArea.fakeDragItem.destroy()
|
||||
dragArea.fakeDragItem = null
|
||||
dragArea.draggedItem = null
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: fakeDragItem
|
||||
width: gridView.cellWidth
|
||||
height: gridView.cellHeight
|
||||
Drag.active: dragArea.drag.active
|
||||
visible: thingId !== ""
|
||||
property var thingId: ""
|
||||
Component {
|
||||
id: dragItemComponent
|
||||
|
||||
ThingTile {
|
||||
id: fakeDragItem
|
||||
width: gridView.cellWidth
|
||||
height: gridView.cellHeight
|
||||
Drag.active: dragArea.drag.active
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
anchors.fill: gridView
|
||||
@ -130,14 +204,31 @@ MainViewBase {
|
||||
property int from: -1
|
||||
property int to: -1
|
||||
|
||||
property int pendingCommand: -1
|
||||
Connections {
|
||||
target: engine.tagsManager
|
||||
onAddTagReply: {
|
||||
if (commandId == dropArea.pendingCommand) {
|
||||
dropArea.pendingCommand = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
to = index;
|
||||
}
|
||||
|
||||
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) {
|
||||
to = index;
|
||||
print("should move", from, "to", to)
|
||||
@ -159,7 +250,7 @@ MainViewBase {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@ -168,7 +259,7 @@ MainViewBase {
|
||||
}
|
||||
|
||||
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
|
||||
visible: gridView.count === 0 && !engine.thingManager.fetchingData
|
||||
title: qsTr("There are no favorite things yet.")
|
||||
|
||||
@ -61,9 +61,9 @@ MainViewBase {
|
||||
delegate: MainPageTile {
|
||||
width: groupsGridView.cellWidth
|
||||
height: groupsGridView.cellHeight
|
||||
iconName: "../images/view-grid-symbolic.svg"
|
||||
iconName: "../images/groups.svg"
|
||||
iconColor: Style.accentColor
|
||||
text: model.tagId.substring(6)
|
||||
lowerText: model.tagId.substring(6)
|
||||
onClicked: {
|
||||
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
|
||||
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.")
|
||||
imageSource: "../images/view-grid-symbolic.svg"
|
||||
imageSource: "../images/groups.svg"
|
||||
buttonVisible: false
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ MainViewBase {
|
||||
iconName: iconTag ? "../images/" + iconTag.value + ".svg" : "../images/slideshow.svg";
|
||||
fallbackIconName: "../images/slideshow.svg"
|
||||
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 iconTag: engine.tagsManager.tags.findRuleTag(model.id, "icon")
|
||||
|
||||
477
nymea-app/ui/mainviews/dashboard/Dashboard.qml
Normal file
477
nymea-app/ui/mainviews/dashboard/Dashboard.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
400
nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml
Normal file
400
nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
nymea-app/ui/mainviews/dashboard/DashboardDelegateBase.qml
Normal file
60
nymea-app/ui/mainviews/dashboard/DashboardDelegateBase.qml
Normal 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
|
||||
}
|
||||
}
|
||||
117
nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml
Normal file
117
nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
nymea-app/ui/mainviews/dashboard/DashboardGraphDelegate.qml
Normal file
63
nymea-app/ui/mainviews/dashboard/DashboardGraphDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
60
nymea-app/ui/mainviews/dashboard/DashboardPage.qml
Normal file
60
nymea-app/ui/mainviews/dashboard/DashboardPage.qml
Normal 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
|
||||
}
|
||||
}
|
||||
68
nymea-app/ui/mainviews/dashboard/DashboardSceneDelegate.qml
Normal file
68
nymea-app/ui/mainviews/dashboard/DashboardSceneDelegate.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
nymea-app/ui/mainviews/dashboard/DashboardThingDelegate.qml
Normal file
53
nymea-app/ui/mainviews/dashboard/DashboardThingDelegate.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
176
nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml
Normal file
176
nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -51,7 +51,7 @@ SettingsPageBase {
|
||||
|
||||
ThingInfoPane {
|
||||
id: infoPane
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
}
|
||||
|
||||
|
||||
@ -84,4 +84,50 @@ Item {
|
||||
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]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user