Merge PR #597: Add a Dashboard main view

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

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

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

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

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

View File

@ -188,15 +188,15 @@ bool AWSClient::confirmationPending() const
return m_confirmationPending;
}
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;
}

View File

@ -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 &registrationId, 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;

View File

@ -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");
}

View File

@ -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 \

View File

@ -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") {

View File

@ -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 &params)
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 &params)

View File

@ -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

View File

@ -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 &params)
@ -156,7 +157,7 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
}
}
void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap &params)
void TagsManager::getTagsResponse(int /*commandId*/, const QVariantMap &params)
{
QList<Tag*> tags;
foreach (const QVariant &tagVariant, params.value("tags").toList()) {
@ -171,14 +172,18 @@ void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap &params)
emit busyChanged();
}
void TagsManager::addTagReply(int commandId, const QVariantMap &params)
void TagsManager::addTagResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcTags()) << "AddTag reply" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<TagsManager::TagError>();
emit addTagReply(commandId, static_cast<TagsManager::TagError>(metaEnum.keyToValue(params.value("params").toMap().value("error").toByteArray())));
}
void TagsManager::removeTagReply(int commandId, const QVariantMap &params)
void TagsManager::removeTagResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcTags()) << "RemoveTag reply" << commandId << params;
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)

View File

@ -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 &params);
void getTagsReply(int commandId, const QVariantMap &params);
void addTagReply(int commandId, const QVariantMap &params);
void removeTagReply(int commandId, const QVariantMap &params);
void getTagsResponse(int commandId, const QVariantMap &params);
void addTagResponse(int commandId, const QVariantMap &params);
void removeTagResponse(int commandId, const QVariantMap &params);
private:
Tag *unpackTag(const QVariantMap &tagMap);

View File

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

View File

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

View File

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

View File

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

View File

@ -157,7 +157,7 @@
<file>ui/images/lock-closed.svg</file>
<file>ui/images/lock-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>

View File

@ -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());

View File

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

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

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

View File

@ -19,7 +19,10 @@ PRE_TARGETDEPS += ../libnymea-app
linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a
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 += $${IOS_PACKAGE_DIR}/platformhelperios.mm \
$${IOS_PACKAGE_DIR}/pushnotifications.mm \
OTHER_FILES += $${OBJECTIVE_SOURCES}
# Add Firebase SDK
QMAKE_LFLAGS += -ObjC $(inherited)
firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist)

View File

@ -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>

View File

@ -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
}
}
}

View File

@ -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"
@ -92,6 +92,6 @@ Item {
"currentPower": "deepskyblue",
}
readonly property color red: "#ad4754"
readonly property color red: "#952727"
readonly property color white: "white"
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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})

View File

@ -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")
}
}

View File

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

View File

@ -32,7 +32,7 @@ AutoSizeMenu {
root.addItem(menuEntryComponent.createObject(root,
{
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

View File

@ -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"

View File

@ -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":

View File

@ -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

View File

@ -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})

View File

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

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,90 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import Qt.labs.settings 1.1
import "../components"
import "dashboard"
MainViewBase {
id: root
headerButtons: [
{
iconSource: "/ui/images/configure.svg",
color: dashboard.editMode ? Style.accentColor : Style.iconColor,
trigger: function() {
dashboard.editMode = !dashboard.editMode;
},
visible: true
}
]
DashboardModel {
id: dashboardModel
Component.onCompleted: {
print("loading dashboard:", dashboardSettings.dashboardConfig)
loadFromJson(dashboardSettings.dashboardConfig)
}
onSave: {
print("saving dashboard");
dashboardSettings.dashboardConfig = dashboardModel.toJson()
}
}
AppData {
id: dashboardSettings
engine: _engine
group: "dashboard-1"
property string dashboardConfig: ""
onDashboardConfigChanged: {
print("dashboard changed on server! Reloading...")
dashboardModel.loadFromJson(dashboardConfig)
}
}
Settings {
category: engine.jsonRpcClient.currentHost.uuid
property string dashboardConfig
}
Dashboard {
id: dashboard
anchors.fill: parent
model: dashboardModel
}
}

View File

@ -60,24 +60,85 @@ MainViewBase {
moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 150; easing.type: Easing.InOutQuad } }
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.")

View File

@ -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
}
}

View File

@ -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")

View File

@ -0,0 +1,477 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
Item {
id: root
property var model: null
property bool editMode: false
function addItem(index) {
if (index === undefined) {
index = root.model.count
}
var addComponent = Qt.createComponent(Qt.resolvedUrl("DashboardAddWizard.qml"))
var popup = addComponent.createObject(root, {dashboardModel: root.model, index: index})
popup.open()
}
readonly property var componentMap: {
"thing": "DashboardThingDelegate.qml",
"folder": "DashboardFolderDelegate.qml",
"graph": "DashboardGraphDelegate.qml",
"scene": "DashboardSceneDelegate.qml",
"webview": "DashboardWebViewDelegate.qml"
}
onEditModeChanged: {
if (!editMode) {
root.model.save()
}
}
Flickable {
id: flickable
anchors.fill: parent
anchors.margins: app.margins / 2
contentHeight: Math.max(layout.implicitHeight, height)
contentWidth: width
MouseArea {
width: flickable.contentWidth
height: flickable.contentHeight
onPressAndHold: root.editMode = true
}
GridLayout {
id: layout
width: Math.min(root.model.count * cellWidth, flickable.width)
readonly property int minTileWidth: 172
readonly property int tilesPerRow: root.width / minTileWidth
columns: tilesPerRow
columnSpacing: 0
rowSpacing: 0
property int cellWidth: flickable.width / tilesPerRow
property int cellHeight: cellWidth
Repeater {
id: repeater
model: root.model
Loader {
id: loader
Layout.preferredWidth: layout.cellWidth * columnSpan
Layout.preferredHeight: layout.cellHeight * rowSpan
property int columnSpan: model.columnSpan
property int rowSpan: model.rowSpan
Layout.columnSpan: columnSpan
Layout.rowSpan: rowSpan
property string type: model.type
property DashboardItem dashboardItem: root.model.get(index)
opacity: dragArea.fromIndex == index ? .3 : 1
Component.onCompleted: {
setSource(Qt.resolvedUrl(componentMap[model.type]), {item: loader.dashboardItem})
}
Binding {
target: loader.item
property: "enabled"
value: !root.editMode// dragArea.editIndex === index
}
Binding {
target: loader.item
property: "editMode"
value: root.editMode
}
Binding {
target: loader.item
property: "bottomClip"
value: loader.bottomClip
when: ["android", "ios"].indexOf(Qt.platform.os) >= 0
}
Binding {
target: loader.item
property: "topClip"
value: loader.topClip
when: ["android", "ios"].indexOf(Qt.platform.os) >= 0
}
Connections {
target: loader.item
onOpenDialog: {
dialogComponent.createObject(root).open()
}
}
property int topClip: Math.min(height, Math.max(0, -y + (flickable.contentY - flickable.originY) - Style.margins))
property int bottomClip: Math.max(0, y + height - flickable.height - Style.margins - (flickable.contentY - flickable.originY))
Rectangle {
anchors.fill: parent
anchors.margins: Style.smallMargins
color: "transparent"
border.color: Style.accentColor
border.width: 4
radius: Style.cornerRadius
visible: dragArea.fromIndex == index
}
Rectangle {
z: 2
anchors.fill: parent
anchors.margins: Style.smallMargins
visible: opacity > 0
radius: Style.cornerRadius
color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, .5)
opacity: root.editMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: root.editMode = false
}
Rectangle {
anchors {
left: parent.left; top: parent.top;
margins: Style.smallMargins
}
height: Style.largeIconSize
width: Style.largeIconSize
color: Style.red
radius: Style.cornerRadius
opacity: dragArea.fromIndex == -1
Rectangle {
anchors.fill: parent
radius: Style.cornerRadius
color: Style.foregroundColor
opacity: deleteMouseArea.pressed || deleteMouseArea.containsMouse ? .08 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
ColorIcon {
name: "/ui/images/delete.svg"
size: Style.iconSize
anchors.centerIn: parent
color: Style.white
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
print("delete clicked")
root.model.removeItem(index)
}
}
}
Rectangle {
anchors {
right: parent.right
top: parent.top
margins: Style.smallMargins
}
height: Style.largeIconSize
width: Style.largeIconSize
color: Style.tileOverlayColor
radius: Style.cornerRadius
visible: opacity > 0
opacity: dragArea.fromIndex == -1 && loader.item.configurable
Behavior on opacity { NumberAnimation { duration: 200 } }
Rectangle {
anchors.fill: parent
radius: Style.cornerRadius
color: Style.foregroundColor
opacity: configureMouseArea.pressed || configureMouseArea.containsMouse ? .08 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
ColorIcon {
name: "/ui/images/configure.svg"
size: Style.iconSize
anchors.centerIn: parent
// color: Style.white
}
MouseArea {
id: configureMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
loader.item.configure()
}
}
}
}
}
}
MouseArea {
id: addTile
Layout.preferredWidth: layout.cellWidth
Layout.preferredHeight: layout.cellHeight
hoverEnabled: true
opacity: root.editMode ? 1 : 0
visible: opacity > 0
Behavior on opacity { NumberAnimation { duration: 200 } }
onClicked: {
print("add clicked")
root.addItem()
}
Rectangle {
anchors.fill: parent
anchors.margins: Style.smallMargins
border.width: 4
border.color: Style.tileOverlayColor
color: Qt.rgba( Style.tileBackgroundColor.r, Style.tileBackgroundColor.g, Style.tileBackgroundColor.b, addTile.containsMouse ? 1 : 0)
Behavior on color { ColorAnimation { duration: 200 } }
radius: Style.cornerRadius
ColorIcon {
name: "/ui/images/add.svg"
size: Style.bigIconSize
anchors.centerIn: parent
color: Style.tileOverlayColor
}
}
}
}
MouseArea {
id: dragArea
anchors.fill: parent
enabled: root.editMode
propagateComposedEvents: true
preventStealing: fakeDragItem != null
property var fakeDragItem: null
property int fromIndex: -1
property int editIndex: -1
property int fakeDragOffsetX: 0
property int fakeDragOffsetY: 0
Timer {
id: scrollTimer
interval: 10
repeat: true
running: dragArea.fakeDragItem !== null
property int scrollOffset: 0
onTriggered: {
var mappedPos = dragArea.mapToItem(flickable, dragArea.mouseX, dragArea.mouseY)
var scrollPixels = 0
if (mappedPos.y + scrollOffset < 60) {
scrollPixels = Math.max(-2, -flickable.contentY)
} else if (mappedPos.y + scrollOffset > flickable.height - 60) {
scrollPixels = Math.min(2, flickable.contentHeight - flickable.height - flickable.contentY)
}
flickable.contentY += scrollPixels
scrollOffset += scrollPixels
dragArea.fakeDragItem.y += scrollPixels
}
}
onClicked: {
var ret = itemUnderMouse()
if (ret.item) {
dragArea.editIndex = ret.index
// Let the click pass through
mouse.accepted = false
} else if (itemContainsMouse(addTile)) {
mouse.accepted = false
} else {
root.editMode = false
}
}
onPressAndHold: {
print("position", mouseX, mouseY)
var draggedItem = null
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i);
print("item coords:", item.x, item.y, item.width, item.height)
if (itemContainsMouse(item)) {
print("Yes!, Item at:", i)
fromIndex = i;
draggedItem = item;
break;
}
}
if (!draggedItem) {
return
}
var mappedCursor = dragArea.mapToItem(item, mouseX, mouseY)
fakeDragOffsetX = mappedCursor.x
fakeDragOffsetY = mappedCursor.y
dragArea.fakeDragItem = dragItemComponent.createObject(dragArea,
{
x: mouseX - fakeDragOffsetX,
y: mouseY - fakeDragOffsetY,
draggedItem: draggedItem
})
}
function itemUnderMouse() {
var ret = {}
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i);
if (itemContainsMouse(item)) {
ret.item = item;
ret.index = i
break;
}
}
return ret
}
function itemContainsMouse(item) {
var mapped = dragArea.mapToItem(item, mouseX, mouseY)
return mapped.x > 0 && mapped.x < item.width && mapped.y > 0 && mapped.y < item.height
}
onPositionChanged: {
if (!fakeDragItem) {
return
}
scrollTimer.scrollOffset = 0;
fakeDragItem.x = mouseX - fakeDragOffsetX
fakeDragItem.y = mouseY - fakeDragOffsetY
var itemUnderCursor = null
var itemIndex = -1
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i);
if (itemContainsMouse(item)) {
print("Yes!, Item at:", i)
itemUnderCursor = item;
itemIndex = i
break;
}
}
if (!itemUnderCursor) {
return
}
if (fromIndex === itemIndex) {
return
}
print("over item:", itemIndex)
root.model.move(fromIndex, itemIndex)
fromIndex = itemIndex
}
onReleased: {
if (dragArea.fakeDragItem) {
dragArea.fakeDragItem.destroy();
dragArea.fakeDragItem = null
dragArea.fromIndex = -1
}
}
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: Style.margins }
anchors.verticalCenter: parent.verticalCenter
visible: root.model.count === 0 && !root.editMode
title: qsTr("Dashboard is empty")
text: qsTr("Start with adding a new item to this dashboard.")
buttonText: qsTr("Add item")
imageSource: "/ui/images/dashboard.svg"
onButtonClicked: {
root.addItem()
}
}
Component {
id: dragItemComponent
Item {
property Item draggedItem: null
layer.enabled: true
layer.effect: ShaderEffectSource {
sourceItem: draggedItem
live: true
}
height: draggedItem.height
width: draggedItem.width
}
}
Component {
id: editDialogComponent
MeaDialog {
id: editDialog
standardButtons: Dialog.NoButton
property DashboardItem dashboardItem: null
property int index: -1
ColumnLayout {
Button {
text: qsTr("Remove")
Layout.fillWidth: true
onClicked: {
root.model.removeItem(editDialog.index)
editDialog.close()
}
}
}
}
}
}

View File

@ -0,0 +1,400 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
MeaDialog {
id: root
title: qsTr("Add item")
standardButtons: Dialog.NoButton
property DashboardModel dashboardModel: null
property int index: 0
padding: Style.margins
contentItem: StackView {
id: internalPageStack
implicitHeight: currentItem.implicitHeight
clip: true
initialItem: ColumnLayout {
id: contentColumn
implicitHeight: childrenRect.height
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Thing")
iconName: "things"
onClicked: {
internalPageStack.push(addThingSelectionComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
iconName: "folder"
text: qsTr("Folder")
onClicked: {
internalPageStack.push(addFolderComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Chart")
iconName: "chart"
onClicked: {
internalPageStack.push(addGraphSelectThingComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Scene")
iconName: "slideshow"
onClicked: {
internalPageStack.push(addSceneComponent)
}
}
NymeaItemDelegate {
Layout.fillWidth: true
text: qsTr("Web view")
iconName: "stock_website"
visible: Qt.platform.os != "android"
onClicked: {
internalPageStack.push(addWebViewComponent)
}
}
}
Component {
id: addThingSelectionComponent
ColumnLayout {
RowLayout {
ColorIcon {
name: "/ui/images/find.svg"
}
TextField {
id: filterTextField
Layout.fillWidth: true
}
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.delegateHeight * 6
clip: true
model: ThingsProxy {
id: thingsProxy
engine: _engine
nameFilter: filterTextField.displayText
}
ScrollBar.vertical: ScrollBar {}
delegate: NymeaItemDelegate {
width: parent.width
text: model.name
iconName: app.interfacesToIcon(thingsProxy.get(index).thingClass.interfaces)
progressive: false
onClicked: {
root.dashboardModel.addThingItem(model.id, root.index)
root.close();
}
}
}
}
}
Component {
id: addFolderComponent
ColumnLayout {
property bool needsOkButton: true
TextField {
id: folderNameTextField
Layout.fillWidth: true
placeholderText: qsTr("Name")
}
GridView {
id: iconsGrid
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.bigIconSize * 6
model: Object.entries(NymeaUtils.namedIcons)
property int columns: width / Style.bigIconSize - 1
cellWidth: width / columns
cellHeight: cellWidth
property string currentIcon: "dashboard"
clip: true
delegate: MouseArea {
width: iconsGrid.cellWidth
height: iconsGrid.cellHeight
onClicked: {
print("clicked", modelData[0])
iconsGrid.currentIcon = modelData[0]
}
ColorIcon {
anchors.centerIn: parent
name: modelData[1]
color: modelData[0] == iconsGrid.currentIcon ? Style.accentColor : Style.iconColor
size: Style.bigIconSize
}
}
}
Connections {
target: okButton
onClicked: {
root.dashboardModel.addFolderItem(folderNameTextField.text, iconsGrid.currentIcon, root.index)
root.close();
}
}
}
}
Component {
id: addGraphSelectThingComponent
ColumnLayout {
RowLayout {
ColorIcon {
name: "/ui/images/find.svg"
}
TextField {
id: filterTextField
Layout.fillWidth: true
}
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.delegateHeight * 6
clip: true
ScrollBar.vertical: ScrollBar {}
model: ThingsProxy {
id: thingsProxy
engine: _engine
nameFilter: filterTextField.displayText
}
delegate: NymeaItemDelegate {
text: model.name
width: parent ? parent.width : 0 // silence warning on delegate descruction
iconName: app.interfacesToIcon(thingsProxy.get(index).thingClass.interfaces)
onClicked: {
internalPageStack.push(addGraphSelectStateComponent, {thing: thingsProxy.get(index)})
}
}
}
}
}
Component {
id: addGraphSelectStateComponent
ListView {
implicitHeight: Style.delegateHeight * 6
clip: true
ScrollBar.vertical: ScrollBar {}
property Thing thing: null
model: thing.thingClass.stateTypes
width: parent.width
delegate: NymeaItemDelegate {
width: parent.width
text: model.displayName
onClicked: {
root.dashboardModel.addGraphItem(thing.id, model.id, root.index)
root.close()
}
}
}
}
Component {
id: addSceneComponent
ListView {
width: parent.width
implicitHeight: Style.delegateHeight * 6
ScrollBar.vertical: ScrollBar {}
model: RulesFilterModel {
rules: engine.ruleManager.rules
filterExecutable: true
}
delegate: NymeaItemDelegate {
width: parent.width
text: model.name
iconName: iconTag.tag.value
iconColor: colorTag.tag.value
TagWatcher {
id: iconTag
tags: engine.tagsManager.tags
ruleId: model.id
tagId: "icon"
}
TagWatcher {
id: colorTag
tags: engine.tagsManager.tags
ruleId: model.id
tagId: "color"
}
onClicked: {
root.dashboardModel.addSceneItem(model.id, root.index)
root.close()
}
}
}
}
Component {
id: addWebViewComponent
ColumnLayout {
property bool needsOkButton: true
property bool okButtonEnabled: urlTextField.displayText.length > 0
Connections {
target: okButton
onClicked: {
root.dashboardModel.addWebViewItem(urlTextField.text, columnsTabs.currentValue, rowsTabs.currentValue, interactiveSwitch.checked, root.index)
root.close();
}
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Location")
}
TextField {
id: urlTextField
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
placeholderText: qsTr("Enter a URL")
text: "https://"
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Size")
}
GridLayout {
columns: width > 300 ? 2 : 1
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
columnSpacing: Style.smallMargins
rowSpacing: Style.smallMargins
Label {
text: qsTr("Columns")
}
SelectionTabs {
id: columnsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.columnSpan - 1
}
Label {
text: qsTr("Rows")
}
SelectionTabs {
id: rowsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.rowSpan - 1
}
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Behavior")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
SwitchDelegate {
id: interactiveSwitch
Layout.fillWidth: true
checked: root.item.interactive
text: qsTr("Interactive")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
}
}
}
footer: Item {
implicitHeight: buttonRow.implicitHeight + Style.margins
RowLayout {
id: buttonRow
anchors { left: parent.left; right: parent.right; bottom: parent.bottom; margins: Style.margins}
spacing: Style.smallMargins
Button {
text: qsTr("Cancel")
onClicked: root.close()
}
Button {
text: qsTr("Back")
visible: internalPageStack.depth > 1
onClicked: internalPageStack.pop()
}
Item {
Layout.fillWidth: true
}
Button {
id: okButton
text: qsTr("OK")
visible: internalPageStack.currentItem.hasOwnProperty("needsOkButton") && internalPageStack.currentItem.needsOkButton === true
enabled: !internalPageStack.currentItem.hasOwnProperty("okButtonEnabled") || internalPageStack.currentItem.okButtonEnabled
}
}
}
}

View File

@ -0,0 +1,60 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
Item {
id: root
property alias contentItem: contentContainer.children
property bool configurable: false
function configure() {
console.warn("Dashboard item claims to be configurable but doesn't implement configure() function")
}
signal openDialog(var dialogComponent);
property bool editMode: false
property int topClip: 0
property int bottomClip: 0
Item {
id: contentContainer
anchors.fill: parent
}
}

View File

@ -0,0 +1,117 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
DashboardDelegateBase {
id: root
property DashboardFolderItem item: null
configurable: true
function configure() {
print("configure called")
root.openDialog(configDialogComponent)
}
contentItem: MainPageTile {
id: delegateRoot
height: root.height
width: root.width
// text: root.item.name
iconName: NymeaUtils.namedIcon(root.item.icon)
iconColor: Style.accentColor
onClicked: pageStack.push(Qt.resolvedUrl("DashboardPage.qml"), {item: root.item})
onPressAndHold: root.longPressed();
contentItem: Label {
text: root.item.name
width: parent.width
height: parent.height
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
maximumLineCount: 2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
padding: app.margins / 2
}
}
Component {
id: configDialogComponent
MeaDialog {
id: configDialog
onAccepted: {
root.item.name = nameTextField.text
}
TextField {
id: nameTextField
text: root.item.name
Layout.fillWidth: true
placeholderText: qsTr("Name")
}
GridView {
id: iconsGrid
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: Style.bigIconSize * 6
model: Object.entries(NymeaUtils.namedIcons)
property int columns: width / Style.bigIconSize - 1
cellWidth: width / columns
cellHeight: cellWidth
clip: true
delegate: MouseArea {
width: iconsGrid.cellWidth
height: iconsGrid.cellHeight
onClicked: {
print("clicked", modelData[0])
root.item.icon = modelData[0]
}
ColorIcon {
anchors.centerIn: parent
name: modelData[1]
color: modelData[0] == root.item.icon ? Style.accentColor : Style.iconColor
size: Style.bigIconSize
}
}
}
}
}
}

View File

@ -0,0 +1,63 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../customviews"
DashboardDelegateBase {
id: root
property DashboardGraphItem item: null
readonly property Thing thing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(item.thingId)
readonly property StateType stateType: thing ? thing.thingClass.stateTypes.getStateType(item.stateTypeId) : null
readonly property State state: thing ? thing.states.getState(item.stateTypeId) : null
contentItem: GenericTypeGraph {
id: graph
width: root.width
height: root.height
title: root.state && root.stateType ? root.thing.name + " " + Types.toUiValue(root.state.value, root.stateType.unit) + Types.toUiUnit(root.stateType.unit) : ""
thing: root.thing
color: "blue"//app.interfaceToColor(interfaceName)
iconSource: ""// app.interfaceToIcon(interfaceName)
implicitHeight: width * .6
// property string interfaceName: parent.interfaceName
stateType: root.stateType
property State state: root.state
}
}

View File

@ -0,0 +1,60 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
Page {
id: root
property DashboardFolderItem item: null
header: NymeaHeader {
text: root.item.name
onBackPressed: pageStack.pop()
HeaderButton {
imageSource: "configure"
onClicked: dashboard.editMode = !dashboard.editMode
color: dashboard.editMode ? Style.accentColor : Style.iconColor
}
}
Dashboard {
id: dashboard
anchors.fill: parent
model: root.item.model
}
}

View File

@ -0,0 +1,68 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
DashboardDelegateBase {
id: root
property DashboardSceneItem item: null
readonly property Rule rule: item && !engine.ruleManager.fetchingData ? engine.ruleManager.rules.getRule(item.ruleId) : null
property var colorTag: engine.tagsManager.tags.findRuleTag(root.item.ruleId, "color")
property var iconTag: engine.tagsManager.tags.findRuleTag(root.item.ruleId, "icon")
contentItem: MainPageTile {
width: root.width
height: root.height
iconName: iconTag ? "/ui/images/" + iconTag.value + ".svg" : "/ui/images/slideshow.svg";
fallbackIconName: "/ui/images/slideshow.svg"
iconColor: colorTag && colorTag.value.length > 0 ? colorTag.value : Style.accentColor;
lowerText: root.rule ? root.rule.name : ""
onClicked: engine.ruleManager.executeActions(root.item.ruleId)
onPressAndHold: root.longPressed()
Connections {
target: engine.tagsManager.tags
onCountChanged: {
colorTag = engine.tagsManager.tags.findRuleTag(root.item.ruleId, "color")
iconTag = engine.tagsManager.tags.findRuleTag(root.item.ruleId, "icon")
}
}
}
}

View File

@ -0,0 +1,53 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
DashboardDelegateBase {
id: root
property DashboardThingItem item: null
contentItem: ThingTile {
id: delegateRoot
width: root.width
height: root.height
thing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(root.item.thingId)
onClicked: pageStack.push(Qt.resolvedUrl("../../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing})
onPressAndHold: root.longPressed()
}
}

View File

@ -0,0 +1,176 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtCharts 2.2
import Nymea 1.0
import "../../components"
import "../../delegates"
//import QtWebView 1.1
import QtGraphicalEffects 1.1
DashboardDelegateBase {
id: root
property DashboardWebViewItem item: null
configurable: true
function configure() {
root.openDialog(configDialogComponent)
}
contentItem: MouseArea {
id: delegateRoot
width: root.width
height: root.height
Component.onCompleted: {
// This might fail if qml-module-qtwebview isn't around
var webView = Qt.createQmlObject(webViewString, webViewContainer);
print("created webView", webView)
}
property string webViewString:
'
import QtQuick 2.8;
import QtWebView 1.1;
import Nymea 1.0;
WebView {
id: webView
anchors.fill: parent
anchors.bottomMargin: root.bottomClip + Style.smallMargins
anchors.topMargin: root.topClip + Style.smallMargins
anchors.margins: Style.smallMargins
url: root.item.url
enabled: root.item.interactive
visible: !app.mainMenu.visible && !root.editMode && root.topClip < root.height && root.bottomClip < height
}
'
Item {
id: webViewContainer
anchors.fill: parent
}
Rectangle {
id: mask
anchors.fill: parent
anchors.margins: Style.smallMargins
radius: Style.cornerRadius
}
OpacityMask {
anchors.fill: parent
anchors.margins: Style.smallMargins
source: ShaderEffectSource {
sourceItem: webViewContainer
recursive: true
hideSource: true
}
maskSource: mask
}
}
Component {
id: configDialogComponent
MeaDialog {
id: configDialog
onAccepted: {
root.item.url = urlTextField.text
root.item.columnSpan = columnsTabs.currentValue
root.item.rowSpan = rowsTabs.currentValue
root.item.interactive = interactiveSwitch.checked
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Location")
}
TextField {
id: urlTextField
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
placeholderText: qsTr("Enter a URL")
text: root.item.url
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Size")
}
GridLayout {
columns: width > 300 ? 2 : 1
Layout.fillWidth: true
Layout.leftMargin: Style.margins
Layout.rightMargin: Style.margins
columnSpacing: Style.smallMargins
rowSpacing: Style.smallMargins
Label {
text: qsTr("Columns")
}
SelectionTabs {
id: columnsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.columnSpan - 1
}
Label {
text: qsTr("Rows")
}
SelectionTabs {
id: rowsTabs
Layout.fillWidth: true
model: [1, 2, 3, 4, 5, 6]
currentIndex: root.item.rowSpan - 1
}
}
SettingsPageSectionHeader {
Layout.fillWidth: true
text: qsTr("Behavior")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
SwitchDelegate {
id: interactiveSwitch
Layout.fillWidth: true
checked: root.item.interactive
text: qsTr("Interactive")
visible: ["android", "ios"].indexOf(Qt.platform.os) < 0
}
}
}
}

View File

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

View File

@ -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]
}
}