From 37397e2c659d8f9139bc19e288236f46ad2c0acf Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 29 Nov 2019 17:44:05 +0100 Subject: [PATCH 1/5] initial stab on grouping things --- libnymea-app-core/devicesproxy.cpp | 50 ++- libnymea-app-core/devicesproxy.h | 12 + libnymea-app-core/libnymea-app-core.h | 2 + libnymea-app-core/libnymea-app-core.pro | 2 + libnymea-app-core/models/interfacesproxy.cpp | 23 +- libnymea-app-core/models/interfacesproxy.h | 10 + libnymea-app-core/models/taglistmodel.cpp | 100 ++++++ libnymea-app-core/models/taglistmodel.h | 44 +++ libnymea-app-core/models/tagsproxymodel.cpp | 9 +- libnymea-app-core/models/tagsproxymodel.h | 1 + libnymea-app-core/tagsmanager.cpp | 38 +- libnymea-app-core/tagsmanager.h | 9 +- libnymea-common/types/interfaces.cpp | 5 + libnymea-common/types/tag.cpp | 8 +- libnymea-common/types/tag.h | 17 +- libnymea-common/types/tags.cpp | 15 +- libnymea-common/types/tags.h | 3 +- nymea-app/images.qrc | 1 + nymea-app/resources.qrc | 2 + nymea-app/ui/MainPage.qml | 11 + nymea-app/ui/components/ColorIcon.qml | 4 +- nymea-app/ui/devicepages/DevicePageBase.qml | 72 ++++ nymea-app/ui/grouping/GroupPage.qml | 38 ++ nymea-app/ui/images/view-grid-symbolic.svg | 179 ++++++++++ nymea-app/ui/mainviews/GroupsView.qml | 354 +++++++++++++++++++ 25 files changed, 971 insertions(+), 38 deletions(-) create mode 100644 libnymea-app-core/models/taglistmodel.cpp create mode 100644 libnymea-app-core/models/taglistmodel.h create mode 100644 nymea-app/ui/grouping/GroupPage.qml create mode 100644 nymea-app/ui/images/view-grid-symbolic.svg create mode 100644 nymea-app/ui/mainviews/GroupsView.qml diff --git a/libnymea-app-core/devicesproxy.cpp b/libnymea-app-core/devicesproxy.cpp index 6e7d0761..6fb2aa6c 100644 --- a/libnymea-app-core/devicesproxy.cpp +++ b/libnymea-app-core/devicesproxy.cpp @@ -23,6 +23,7 @@ #include "devicesproxy.h" #include "engine.h" #include "tagsmanager.h" +#include "types/tag.h" DevicesProxy::DevicesProxy(QObject *parent) : QSortFilterProxyModel(parent) @@ -93,7 +94,7 @@ QString DevicesProxy::filterTagId() const void DevicesProxy::setFilterTagId(const QString &filterTag) { - if (m_filterTagId != filterTagId()) { + if (m_filterTagId != filterTag) { m_filterTagId = filterTag; emit filterTagIdChanged(); invalidateFilter(); @@ -101,6 +102,21 @@ void DevicesProxy::setFilterTagId(const QString &filterTag) } } +QString DevicesProxy::filterTagValue() const +{ + return m_filterTagValue; +} + +void DevicesProxy::setFilterTagValue(const QString &tagValue) +{ + if (m_filterTagValue != tagValue) { + m_filterTagValue = tagValue; + emit filterTagValueChanged(); + invalidateFilter(); + emit countChanged(); + } +} + QString DevicesProxy::filterDeviceClassId() const { return m_filterDeviceClassId; @@ -112,6 +128,22 @@ void DevicesProxy::setFilterDeviceClassId(const QString &filterDeviceClassId) m_filterDeviceClassId = filterDeviceClassId; emit filterDeviceClassIdChanged(); invalidateFilter(); + emit countChanged(); + } +} + +QString DevicesProxy::filterDeviceId() const +{ + return m_filterDeviceId; +} + +void DevicesProxy::setFilterDeviceId(const QString &filterDeviceId) +{ + if (m_filterDeviceId != filterDeviceId) { + m_filterDeviceId = filterDeviceId; + emit filterDeviceIdChanged(); + invalidateFilter(); + emit countChanged(); } } @@ -156,7 +188,7 @@ void DevicesProxy::setNameFilter(const QString &nameFilter) m_nameFilter = nameFilter; emit nameFilterChanged(); invalidateFilter(); - countChanged(); + emit countChanged(); } } @@ -201,6 +233,7 @@ void DevicesProxy::setGroupByInterface(bool groupByInterface) m_groupByInterface = groupByInterface; emit groupByInterfaceChanged(); invalidate(); + emit countChanged(); } } @@ -254,12 +287,21 @@ bool DevicesProxy::filterAcceptsRow(int source_row, const QModelIndex &source_pa { Device *device = getInternal(source_row); if (!m_filterTagId.isEmpty()) { - if (!m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId)) { + Tag *tag = m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId); + if (!tag) { + return false; + } + if (!m_filterTagValue.isEmpty() && tag->value() != m_filterTagValue) { return false; } } if (!m_filterDeviceClassId.isEmpty()) { - if (device->deviceClassId() != m_filterDeviceClassId) { + if (device->deviceClassId() != QUuid(m_filterDeviceClassId)) { + return false; + } + } + if (!m_filterDeviceId.isEmpty()) { + if (device->id() != QUuid(m_filterDeviceId)) { return false; } } diff --git a/libnymea-app-core/devicesproxy.h b/libnymea-app-core/devicesproxy.h index b57ba33b..673303ce 100644 --- a/libnymea-app-core/devicesproxy.h +++ b/libnymea-app-core/devicesproxy.h @@ -38,7 +38,9 @@ class DevicesProxy : public QSortFilterProxyModel Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) Q_PROPERTY(DevicesProxy *parentProxy READ parentProxy WRITE setParentProxy NOTIFY parentProxyChanged) Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged) + Q_PROPERTY(QString filterTagValue READ filterTagValue WRITE setFilterTagValue NOTIFY filterTagValueChanged) Q_PROPERTY(QString filterDeviceClassId READ filterDeviceClassId WRITE setFilterDeviceClassId NOTIFY filterDeviceClassIdChanged) + Q_PROPERTY(QString filterDeviceId READ filterDeviceId WRITE setFilterDeviceId NOTIFY filterDeviceIdChanged) Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) Q_PROPERTY(QStringList hiddenInterfaces READ hiddenInterfaces WRITE setHiddenInterfaces NOTIFY hiddenInterfacesChanged) Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged) @@ -63,9 +65,15 @@ public: QString filterTagId() const; void setFilterTagId(const QString &filterTag); + QString filterTagValue() const; + void setFilterTagValue(const QString &tagValue); + QString filterDeviceClassId() const; void setFilterDeviceClassId(const QString &filterDeviceClassId); + QString filterDeviceId() const; + void setFilterDeviceId(const QString &filterDeviceId); + QStringList shownInterfaces() const; void setShownInterfaces(const QStringList &shownInterfaces); @@ -91,7 +99,9 @@ signals: void engineChanged(); void parentProxyChanged(); void filterTagIdChanged(); + void filterTagValueChanged(); void filterDeviceClassIdChanged(); + void filterDeviceIdChanged(); void shownInterfacesChanged(); void hiddenInterfacesChanged(); void nameFilterChanged(); @@ -106,7 +116,9 @@ private: Engine *m_engine = nullptr; DevicesProxy *m_parentProxy = nullptr; QString m_filterTagId; + QString m_filterTagValue; QString m_filterDeviceClassId; + QString m_filterDeviceId; QStringList m_shownInterfaces; QStringList m_hiddenInterfaces; QString m_nameFilter; diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index 75f76421..de616f3b 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -48,6 +48,7 @@ #include "models/wirelessaccesspointsproxy.h" #include "tagsmanager.h" #include "models/tagsproxymodel.h" +#include "models/taglistmodel.h" #include "types/tag.h" #include "ruletemplates/ruletemplates.h" #include "ruletemplates/ruletemplate.h" @@ -189,6 +190,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "Tags", "Get it from TagsManager"); qmlRegisterUncreatableType(uri, 1, 0, "Tag", "Get it from Tags"); qmlRegisterType(uri, 1, 0, "TagsProxyModel"); + qmlRegisterType(uri, 1, 0, "TagListModel"); qmlRegisterType(uri, 1, 0, "NetworkManagerController"); qmlRegisterType(uri, 1, 0, "BluetoothDiscovery"); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index 9cfbcbd5..309dcbe8 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -47,6 +47,7 @@ SOURCES += \ deviceclassesproxy.cpp \ devicediscovery.cpp \ models/packagesfiltermodel.cpp \ + models/taglistmodel.cpp \ vendorsproxy.cpp \ pluginsproxy.cpp \ interfacesmodel.cpp \ @@ -109,6 +110,7 @@ HEADERS += \ deviceclassesproxy.h \ devicediscovery.h \ models/packagesfiltermodel.h \ + models/taglistmodel.h \ vendorsproxy.h \ pluginsproxy.h \ interfacesmodel.h \ diff --git a/libnymea-app-core/models/interfacesproxy.cpp b/libnymea-app-core/models/interfacesproxy.cpp index 708b17fc..a6e6109c 100644 --- a/libnymea-app-core/models/interfacesproxy.cpp +++ b/libnymea-app-core/models/interfacesproxy.cpp @@ -5,6 +5,7 @@ #include "types/device.h" #include "devices.h" +#include "devicesproxy.h" InterfacesProxy::InterfacesProxy(QObject *parent): QSortFilterProxyModel(parent) { @@ -23,6 +24,7 @@ void InterfacesProxy::setShowEvents(bool showEvents) m_showEvents = showEvents; emit showEventsChanged(); invalidateFilter(); + void countChanged(); } } @@ -37,6 +39,7 @@ void InterfacesProxy::setShowActions(bool showActions) m_showActions = showActions; emit showActionsChanged(); invalidateFilter(); + void countChanged(); } } @@ -51,6 +54,7 @@ void InterfacesProxy::setShowStates(bool showStates) m_showStates = showStates; emit showStatesChanged(); invalidateFilter(); + void countChanged(); } } @@ -58,7 +62,6 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source { Q_UNUSED(source_parent) QString interfaceName = m_interfaces->get(source_row)->name(); - qDebug() << "filterAcceptsRow" << interfaceName << m_shownInterfaces; if (!m_shownInterfaces.isEmpty()) { if (!m_shownInterfaces.contains(interfaceName)) { return false; @@ -83,6 +86,24 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source return false; } } + if (m_devicesProxyFilter != nullptr) { + // TODO: This could be improved *a lot* by caching interfaces in the devices model... + bool found = false; + for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) { + Device *d = m_devicesProxyFilter->get(i); + if (!d->deviceClass()) { + qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name(); + return false; + } + if (d->deviceClass()->interfaces().contains(interfaceName)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } Interface* iface = m_interfaces->get(source_row); if (m_showEvents) { diff --git a/libnymea-app-core/models/interfacesproxy.h b/libnymea-app-core/models/interfacesproxy.h index 2ae59058..9151cb2f 100644 --- a/libnymea-app-core/models/interfacesproxy.h +++ b/libnymea-app-core/models/interfacesproxy.h @@ -4,15 +4,18 @@ #include class Devices; +class DevicesProxy; class Interface; class Interfaces; class InterfacesProxy: public QSortFilterProxyModel { Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) Q_PROPERTY(Devices* devicesFilter READ devicesFilter WRITE setDevicesFilter NOTIFY devicesFilterChanged) + Q_PROPERTY(DevicesProxy* devicesProxyFilter READ devicesProxyFilter WRITE setDevicesProxyFilter NOTIFY devicesProxyFilterChanged) Q_PROPERTY(bool showEvents READ showEvents WRITE setShowEvents NOTIFY showEventsChanged) Q_PROPERTY(bool showActions READ showActions WRITE setShowActions NOTIFY showActionsChanged) Q_PROPERTY(bool showStates READ showStates WRITE setShowStates NOTIFY showStatesChanged) @@ -26,6 +29,9 @@ public: Devices* devicesFilter() const { return m_devicesFilter; } void setDevicesFilter(Devices *devices) { m_devicesFilter = devices; emit devicesFilterChanged(); invalidateFilter(); } + DevicesProxy* devicesProxyFilter() const { return m_devicesProxyFilter; } + void setDevicesProxyFilter(DevicesProxy *devicesProxy) { m_devicesProxyFilter = devicesProxy; emit devicesProxyFilterChanged(); invalidateFilter(); } + bool showEvents() const; void setShowEvents(bool showEvents); @@ -42,14 +48,18 @@ public: signals: void shownInterfacesChanged(); void devicesFilterChanged(); + void devicesProxyFilterChanged(); void showEventsChanged(); void showActionsChanged(); void showStatesChanged(); + void countChanged(); + private: Interfaces *m_interfaces = nullptr; QStringList m_shownInterfaces; Devices* m_devicesFilter = nullptr; + DevicesProxy* m_devicesProxyFilter = nullptr; bool m_showEvents = false; bool m_showActions = false; bool m_showStates = false; diff --git a/libnymea-app-core/models/taglistmodel.cpp b/libnymea-app-core/models/taglistmodel.cpp new file mode 100644 index 00000000..34f249a1 --- /dev/null +++ b/libnymea-app-core/models/taglistmodel.cpp @@ -0,0 +1,100 @@ +#include "taglistmodel.h" +#include "tagsproxymodel.h" +#include "types/tag.h" + +#include + +TagListModel::TagListModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +TagsProxyModel *TagListModel::tagsProxy() const +{ + return m_tagsProxy; +} + +void TagListModel::setTagsProxy(TagsProxyModel *tagsProxy) +{ + if (m_tagsProxy != tagsProxy) { + m_tagsProxy = tagsProxy; + emit tagsProxyChanged(); + + connect(tagsProxy, &TagsProxyModel::countChanged, this, &TagListModel::update); + + update(); + } +} + +int TagListModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant TagListModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleTagId: + return m_list.at(index.row())->tagId(); + case RoleValue: + return m_list.at(index.row())->value(); + } + + return QVariant(); +} + +QHash TagListModel::roleNames() const +{ + QHash roles; + roles.insert(RoleTagId, "tagId"); + roles.insert(RoleValue, "value"); + return roles; +} + +bool TagListModel::containsId(const QString &tagId) +{ + foreach (Tag* t, m_list) { + if (t->tagId() == tagId) { + return true; + } + } + return false; +} + +bool TagListModel::containsValue(const QString &tagValue) +{ + foreach (Tag* t, m_list) { + if (t->value() == tagValue) { + return true; + } + } + return false; +} + +void TagListModel::update() +{ + beginResetModel(); + qDeleteAll(m_list); + m_list.clear(); + + for (int i = 0; i < m_tagsProxy->rowCount(); i++) { + Tag *tag = m_tagsProxy->get(i); + + bool found = false; + foreach (Tag* existingTag, m_list) { + if (tag->tagId() == existingTag->tagId() && tag->value() == existingTag->value()) { + found = true; + break; + } + } + if (!found) { + Tag *t = new Tag(tag->tagId(), tag->value(), this); + m_list.append(t); + } + } + + qDebug() << "Model populated" << m_list.count() << this; + endResetModel(); + emit countChanged(); +} diff --git a/libnymea-app-core/models/taglistmodel.h b/libnymea-app-core/models/taglistmodel.h new file mode 100644 index 00000000..cd312b68 --- /dev/null +++ b/libnymea-app-core/models/taglistmodel.h @@ -0,0 +1,44 @@ +#ifndef TAGLISTMODEL_H +#define TAGLISTMODEL_H + +#include + +class TagsProxyModel; +class Tag; + +class TagListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(TagsProxyModel* tagsProxy READ tagsProxy WRITE setTagsProxy NOTIFY tagsProxyChanged) +public: + enum Roles { + RoleTagId, + RoleValue + }; + explicit TagListModel(QObject *parent = nullptr); + + TagsProxyModel* tagsProxy() const; + void setTagsProxy(TagsProxyModel* tagsProxy); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + Q_INVOKABLE bool containsId(const QString &tagId); + Q_INVOKABLE bool containsValue(const QString &tagValue); + +signals: + void countChanged(); + void tagsProxyChanged(); + +private slots: + void update(); + +private: + TagsProxyModel *m_tagsProxy = nullptr; + + QList m_list; +}; + +#endif // TAGLISTMODEL_H diff --git a/libnymea-app-core/models/tagsproxymodel.cpp b/libnymea-app-core/models/tagsproxymodel.cpp index c21df726..3c9f3118 100644 --- a/libnymea-app-core/models/tagsproxymodel.cpp +++ b/libnymea-app-core/models/tagsproxymodel.cpp @@ -94,17 +94,18 @@ bool TagsProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_ Q_UNUSED(source_parent) Tag *tag = m_tags->get(source_row); if (!m_filterTagId.isEmpty()) { - if (tag->tagId() != m_filterTagId) { + QRegExp exp(m_filterTagId); + if (!exp.exactMatch(tag->tagId())) { return false; } } if (!m_filterDeviceId.isEmpty()) { - if (tag->deviceId() != m_filterDeviceId) { + if (QUuid(tag->deviceId()) != QUuid(m_filterDeviceId)) { return false; } } if (!m_filterRuleId.isEmpty()) { - if (tag->ruleId() != m_filterRuleId) { + if (QUuid(tag->ruleId()) != QUuid(m_filterRuleId)) { return false; } } @@ -115,7 +116,7 @@ bool TagsProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex { QString leftValue = m_tags->get(source_left.row())->value(); QString rightValue = m_tags->get(source_right.row())->value(); - bool okLeft, okRight;; + bool okLeft, okRight; qlonglong leftAsNumber = leftValue.toLongLong(&okLeft); qlonglong rightAsNumber = rightValue.toLongLong(&okRight); if (okLeft && okRight) { diff --git a/libnymea-app-core/models/tagsproxymodel.h b/libnymea-app-core/models/tagsproxymodel.h index 4d1205f5..feb7f029 100644 --- a/libnymea-app-core/models/tagsproxymodel.h +++ b/libnymea-app-core/models/tagsproxymodel.h @@ -42,6 +42,7 @@ signals: void filterTagIdChanged(); void filterDeviceIdChanged(); void filterRuleIdChanged(); + void groupSameTagsChanged(); void countChanged(); private: diff --git a/libnymea-app-core/tagsmanager.cpp b/libnymea-app-core/tagsmanager.cpp index e859f824..0912937b 100644 --- a/libnymea-app-core/tagsmanager.cpp +++ b/libnymea-app-core/tagsmanager.cpp @@ -17,10 +17,17 @@ QString TagsManager::nameSpace() const void TagsManager::init() { + m_busy = true; + emit busyChanged(); m_tags->clear(); m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply"); } +bool TagsManager::busy() const +{ + return m_busy; +} + Tags *TagsManager::tags() const { return m_tags; @@ -83,13 +90,16 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) QString notification = params.value("notification").toString(); if (notification == "Tags.TagAdded") { - addTagInternal(tagMap); + Tag *tag = unpackTag(tagMap); + if (tag) { + m_tags->addTag(tag); + } } else if (notification == "Tags.TagRemoved") { for (int i = 0; i < m_tags->rowCount(); i++) { Tag* tag = m_tags->get(i); - if (tagMap.value("deviceId").toString() == tag->deviceId() && - tagMap.value("ruleId").toString() == tag->ruleId() && + if (tagMap.value("deviceId").toUuid() == tag->deviceId() && + tagMap.value("ruleId").toUuid() == tag->ruleId() && tagMap.value("tagId").toString() == tag->tagId()) { m_tags->removeTag(tag); return; @@ -99,8 +109,8 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) qDebug() << "tag value changed"; for (int i = 0; i < m_tags->rowCount(); i++) { Tag* tag = m_tags->get(i); - if (tagMap.value("deviceId").toString() == tag->deviceId() && - tagMap.value("ruleId").toString() == tag->ruleId() && + if (tagMap.value("deviceId").toUuid() == tag->deviceId() && + tagMap.value("ruleId").toUuid() == tag->ruleId() && tagMap.value("tagId").toString() == tag->tagId()) { qDebug() << "Found tag"; tag->setValue(tagMap.value("value").toString()); @@ -111,10 +121,18 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) void TagsManager::getTagsReply(const QVariantMap ¶ms) { + QList tags; foreach (const QVariant &tagVariant, params.value("params").toMap().value("tags").toList()) { - addTagInternal(tagVariant.toMap()); + qDebug() << "aDDING TAG"; + Tag *tag = unpackTag(tagVariant.toMap()); + if (tag) { + tags.append(tag); + } } - emit tagsChanged(); + m_tags->addTags(tags); + + m_busy = false; + emit busyChanged(); } void TagsManager::addTagReply(const QVariantMap ¶ms) @@ -127,7 +145,7 @@ void TagsManager::removeTagReply(const QVariantMap ¶ms) qDebug() << "RemoveTag reply" << params; } -void TagsManager::addTagInternal(const QVariantMap &tagMap) +Tag* TagsManager::unpackTag(const QVariantMap &tagMap) { QString deviceId = tagMap.value("deviceId").toString(); QString ruleId = tagMap.value("ruleId").toString(); @@ -143,8 +161,8 @@ void TagsManager::addTagInternal(const QVariantMap &tagMap) } else { qWarning() << "Invalid tag. Neither deviceId nor ruleId are set. Skipping..."; tag->deleteLater(); - return; + return nullptr; } // qDebug() << "adding tag" << tag->tagId() << tag->value(); - m_tags->addTag(tag); + return tag; } diff --git a/libnymea-app-core/tagsmanager.h b/libnymea-app-core/tagsmanager.h index fefef407..28b7cdcd 100644 --- a/libnymea-app-core/tagsmanager.h +++ b/libnymea-app-core/tagsmanager.h @@ -9,13 +9,15 @@ class TagsManager : public JsonHandler { Q_OBJECT - Q_PROPERTY(Tags* tags READ tags NOTIFY tagsChanged) + Q_PROPERTY(Tags* tags READ tags CONSTANT) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); QString nameSpace() const override; void init(); + bool busy() const; Tags* tags() const; @@ -25,7 +27,7 @@ public: Q_INVOKABLE void untagRule(const QString &ruleId, const QString &tagId); signals: - void tagsChanged(); + void busyChanged(); private slots: void handleTagsNotification(const QVariantMap ¶ms); @@ -34,11 +36,12 @@ private slots: void removeTagReply(const QVariantMap ¶ms); private: - void addTagInternal(const QVariantMap &tagMap); + Tag *unpackTag(const QVariantMap &tagMap); JsonRpcClient *m_jsonClient = nullptr; Tags *m_tags = nullptr; + bool m_busy = false; }; #endif // TAGSMANAGER_H diff --git a/libnymea-common/types/interfaces.cpp b/libnymea-common/types/interfaces.cpp index 9916fc0c..d0d3f41a 100644 --- a/libnymea-common/types/interfaces.cpp +++ b/libnymea-common/types/interfaces.cpp @@ -89,6 +89,11 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent) tr("Daylight"), tr("Daylight changed")); + addInterface("lightsensor", tr("Light intensity sensors")); + addStateType("lightsensor", "lightIntensity", QVariant::Bool, false, + tr("Light intensity"), + tr("Light intensity changed")); + addInterface("evcharger", tr("EV charger")); addStateType("evcharger", "power", QVariant::Bool, true, tr("Charging"), diff --git a/libnymea-common/types/tag.cpp b/libnymea-common/types/tag.cpp index 3ea5e823..9d4750b5 100644 --- a/libnymea-common/types/tag.cpp +++ b/libnymea-common/types/tag.cpp @@ -10,22 +10,22 @@ Tag::Tag(const QString &tagId, const QString &value, QObject *parent): } -QString Tag::deviceId() const +QUuid Tag::deviceId() const { return m_deviceId; } -void Tag::setDeviceId(const QString &deviceId) +void Tag::setDeviceId(const QUuid &deviceId) { m_deviceId = deviceId; } -QString Tag::ruleId() const +QUuid Tag::ruleId() const { return m_ruleId; } -void Tag::setRuleId(const QString &ruleId) +void Tag::setRuleId(const QUuid &ruleId) { m_ruleId = ruleId; } diff --git a/libnymea-common/types/tag.h b/libnymea-common/types/tag.h index e0be291b..eb57579b 100644 --- a/libnymea-common/types/tag.h +++ b/libnymea-common/types/tag.h @@ -2,23 +2,24 @@ #define TAG_H #include +#include class Tag : public QObject { Q_OBJECT - Q_PROPERTY(QString deviceId READ deviceId CONSTANT) - Q_PROPERTY(QString ruleId READ ruleId CONSTANT) + Q_PROPERTY(QUuid deviceId READ deviceId CONSTANT) + Q_PROPERTY(QUuid ruleId READ ruleId CONSTANT) Q_PROPERTY(QString tagId READ tagId CONSTANT) Q_PROPERTY(QString value READ value NOTIFY valueChanged) public: explicit Tag(const QString &tagId, const QString &value, QObject *parent = nullptr); - QString deviceId() const; - void setDeviceId(const QString &deviceId); + QUuid deviceId() const; + void setDeviceId(const QUuid &deviceId); - QString ruleId() const; - void setRuleId(const QString &ruleId); + QUuid ruleId() const; + void setRuleId(const QUuid &ruleId); QString tagId() const; @@ -29,8 +30,8 @@ signals: void valueChanged(); private: - QString m_deviceId; - QString m_ruleId; + QUuid m_deviceId; + QUuid m_ruleId; QString m_tagId; QString m_value; }; diff --git a/libnymea-common/types/tags.cpp b/libnymea-common/types/tags.cpp index 58ef1cce..9d861dea 100644 --- a/libnymea-common/types/tags.cpp +++ b/libnymea-common/types/tags.cpp @@ -46,6 +46,19 @@ void Tags::addTag(Tag *tag) beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); m_list.append(tag); endInsertRows(); + qDebug() << "tags count changed"; + emit countChanged(); +} + +void Tags::addTags(QList tags) +{ + beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + tags.count() - 1); + foreach (Tag *tag, tags) { + tag->setParent(this); + connect(tag, &Tag::valueChanged, this, &Tags::tagValueChanged); + } + m_list.append(tags); + endInsertRows(); emit countChanged(); } @@ -68,7 +81,7 @@ Tag *Tags::get(int index) const return m_list.at(index); } -Tag *Tags::findDeviceTag(const QString &deviceId, const QString &tagId) const +Tag *Tags::findDeviceTag(const QUuid &deviceId, const QString &tagId) const { foreach (Tag *tag, m_list) { if (tag->deviceId() == deviceId && tag->tagId() == tagId) { diff --git a/libnymea-common/types/tags.h b/libnymea-common/types/tags.h index 00373b69..9e624be8 100644 --- a/libnymea-common/types/tags.h +++ b/libnymea-common/types/tags.h @@ -25,11 +25,12 @@ public: QHash roleNames() const override; void addTag(Tag *tag); + void addTags(QList tags); void removeTag(Tag *tag); Tag* get(int index) const; - Q_INVOKABLE Tag* findDeviceTag(const QString &deviceId, const QString &tagId) const; + Q_INVOKABLE Tag* findDeviceTag(const QUuid &deviceId, const QString &tagId) const; Q_INVOKABLE Tag* findRuleTag(const QString &ruleId, const QString &tagId) const; void clear(); diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 0d413d49..85fe2d1f 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -212,5 +212,6 @@ ui/images/browser/MediaBrowserIconNapster.svg ui/images/browser/MediaBrowserIconSoundCloud.svg ui/images/browser/MediaBrowserIconDeezer.svg + ui/images/view-grid-symbolic.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 7a3e5967..8f386481 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -197,5 +197,7 @@ ui/magic/NewMagicPage.qml ui/components/WebViewWrapper.qml ui/magic/NewScenePage.qml + ui/mainviews/GroupsView.qml + ui/grouping/GroupPage.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 4eadf7cf..499ce009 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -79,6 +79,10 @@ Page { } tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++}) tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++}) + if (engine.jsonRpcClient.ensureServerVersion(1.6)) { + tabEntryComponent.createObject(tabBar, {text: qsTr("Groups"), iconSource: "../images/view-grid-symbolic.svg", pageIndex: pi++}) + } + root.tabsReady = true } @@ -292,6 +296,13 @@ Page { } } } + + GroupsView { + id: groupsView + property string title: qsTr("My groups" + count); + width: swipeView.width + height: swipeView.height + } } ColumnLayout { diff --git a/nymea-app/ui/components/ColorIcon.qml b/nymea-app/ui/components/ColorIcon.qml index 16e61690..40cccf73 100644 --- a/nymea-app/ui/components/ColorIcon.qml +++ b/nymea-app/ui/components/ColorIcon.qml @@ -86,7 +86,7 @@ Item { Component.onCompleted: ready = true anchors.fill: parent - anchors.margins: parent.margins + anchors.margins: parent ? parent.margins : 0 source: ready && width > 0 && height > 0 && icon.name ? icon.name : "" sourceSize { width: width @@ -100,7 +100,7 @@ Item { id: colorizedImage anchors.fill: parent - anchors.margins: parent.margins + anchors.margins: parent ? parent.margins : 0 visible: active && image.status == Image.Ready // Whether or not a color has been set. diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 016c8360..f9eda37f 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -70,6 +70,13 @@ Page { iconSource: Qt.binding(function() { return favoritesProxy.count === 0 ? "../images/starred.svg" : "../images/non-starred.svg"}), functionName: "toggleFavorite" })) + + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, + { + text: qsTr("Grouping"), + iconSource: "../images/view-grid-symbolic.svg", + functionName: "addToGroup" + })) } } function openDeviceMagicPage() { @@ -85,6 +92,12 @@ Page { engine.tagsManager.untagDevice(root.device.id, "favorites") } } + function addToGroup() { +// engine.tagsManager.tagDevice(root.device.id, "group", "My group 1") + var dialog = addToGroupDialog.createObject(root) + dialog.open(); + } + function openThingSettingsPage() { pageStack.push(Qt.resolvedUrl("../thingconfiguration/ConfigureThingPage.qml"), {device: root.device}) } @@ -101,6 +114,65 @@ Page { onTriggered: thingMenu[functionName]() } } + + Component { + id: addToGroupDialog + MeaDialog { + title: qsTr("Configure groups") + headerIcon: "../images/view-grid-symbolic.svg" + + RowLayout { + TextField { + id: newGroupdTextField + Layout.fillWidth: true + placeholderText: qsTr("New group") + } + Button { + text: qsTr("OK") + enabled: newGroupdTextField.displayText.length > 0 && !groupTags.containsId("group-" + newGroupdTextField.displayText) + onClicked: { + engine.tagsManager.tagDevice(root.device.id, "group-" + newGroupdTextField.text, 1000) + newGroupdTextField.text = "" + } + } + } + + + ListView { + Layout.fillWidth: true + height: 200 + + model: TagListModel { + id: groupTags + tagsProxy: TagsProxyModel { + tags: engine.tagsManager.tags + filterTagId: "group-.*" + } + } + + delegate: CheckDelegate { + width: parent.width + text: model.tagId.substring(6) + checked: innerProxy.count > 0 + onClicked: { + if (innerProxy.count == 0) { + engine.tagsManager.tagDevice(root.device.id, model.tagId, 1000) + } else { + engine.tagsManager.untagDevice(root.device.id, model.tagId, model.value) + } + } + + DevicesProxy { + id: innerProxy + engine: _engine + filterTagId: model.tagId + filterDeviceId: root.device.id + } + } + } + } + + } } Rectangle { diff --git a/nymea-app/ui/grouping/GroupPage.qml b/nymea-app/ui/grouping/GroupPage.qml new file mode 100644 index 00000000..d1053f0b --- /dev/null +++ b/nymea-app/ui/grouping/GroupPage.qml @@ -0,0 +1,38 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +Page { + id: root + header: NymeaHeader { + text: root.groupTag.substring(6) + onBackPressed: pageStack.pop() + } + + property string groupTag + + + DevicesProxy { + id: devicesInGroup + engine: _engine + filterTagId: root.groupTag + } + + InterfacesProxy { + id: interfacesInGroup + devicesProxyFilter: devicesInGroup + showStates: true + } + + ListView { + anchors.fill: parent + model: devicesInGroup + delegate: Label { + text: model.name + } + } + +} diff --git a/nymea-app/ui/images/view-grid-symbolic.svg b/nymea-app/ui/images/view-grid-symbolic.svg new file mode 100644 index 00000000..bc246a32 --- /dev/null +++ b/nymea-app/ui/images/view-grid-symbolic.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml new file mode 100644 index 00000000..a2762a0e --- /dev/null +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -0,0 +1,354 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import Nymea 1.0 +import QtQuick.Controls.Material 2.2 +import "../components" + +Item { + id: root + + readonly property int count: groupsGridView.count + + GridView { + id: groupsGridView + anchors.fill: parent + anchors.margins: app.margins / 2 + + readonly property int minTileWidth: 180 + readonly property int minTileHeight: 180 + readonly property int tilesPerRow: root.width / minTileWidth + + model: TagListModel { + tagsProxy: TagsProxyModel { + tags: engine.tagsManager.tags + filterTagId: "group-.*" + } + } + cellWidth: width / tilesPerRow + cellHeight: cellWidth + + delegate: Item { + id: groupDelegate + width: groupsGridView.cellWidth + height: groupsGridView.cellHeight + + Pane { + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 2 + padding: 0 + + DevicesProxy { + id: devicesInGroup + engine: _engine + filterTagId: model.tagId + filterTagValue: model.value + } + + InterfacesProxy { + id: controlsInGroup + shownInterfaces: ["light", "simpleclosable"] + devicesProxyFilter: devicesInGroup + showStates: true + showActions: true + } + InterfacesProxy { + id: sensorsInGroup + shownInterfaces: ["temperaturesensor", "lightsensor", "presencesensor"] + devicesProxyFilter: devicesInGroup + showStates: true + } + + contentItem: ItemDelegate { + padding: 0 + + onClicked: { + pageStack.push(Qt.resolvedUrl("../grouping/GroupPage.qml"), {groupTag: model.tagId}) + } + + contentItem: ColumnLayout { + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 30 + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .05) + Label { + anchors.fill: parent + anchors { leftMargin: app.margins; rightMargin: app.margins } + text: model.tagId.substring(6) + elide: Text.ElideRight + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + + Repeater { + model: controlsInGroup + delegate: Loader { + id: controlLoader + Layout.fillWidth: true + Layout.leftMargin: app.margins / 2 + Layout.rightMargin: app.margins / 2 + sourceComponent: { + switch (model.name) { + case "simpleclosable": + return closableDelegate + case "light": + return lightDelegate + } + } + Binding { + target: controlLoader.item + property: "devices" + value: devicesInGroup + } + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize * 1.2 + Layout.alignment: Qt.AlignRight + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, 0.05) + + RowLayout { + anchors.fill: parent + + Repeater { + model: sensorsInGroup + delegate: Row { + ColorIcon { + height: app.iconSize * .8 + width: height + name: app.interfaceToIcon(model.name) + color: app.interfaceToColor(model.name) + } + DevicesProxy { + id: innerProxy + engine: _engine + parentProxy: devicesInGroup + shownInterfaces: [model.name] + } + + Led { + visible: ["presencesensor"].indexOf(model.name) >= 0 + state: { + var stateName = null + switch (model.name) { + case "presencesensor": + stateName = "isPresent" + break; + } + if (!stateName) { + return "off"; + } + var ret = false; + for (var i = 0; i < innerProxy.count; i++) { + ret |= innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value + } + return ret ? "on" : "off"; + } + } + + Label { + text: { + var stateName = null; + switch (model.name) { + case "temperaturesensor": + stateName = "temperature"; + break; + case "lightsensor": + stateName = "lightIntensity" + break; + } + if (!stateName) { + return ""; + } + + var ret = 0 + for (var i = 0; i < innerProxy.count; i++) { + ret += innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value + } + return (ret / innerProxy.count).toFixed(1) + } + } + } + } + } + } + } + } + } + } + } + } + } + + Component { + id: lightDelegate + RowLayout { + property var devices + + DevicesProxy { + id: lights + engine: _engine + parentProxy: devices + shownInterfaces: ["light"] + } + + DevicesProxy { + id: dimmableLights + engine: _engine + parentProxy: devices + shownInterfaces: ["dimmablelight"] + } + + Label { + text: qsTr("Lighting") + Layout.fillWidth: true + Layout.preferredHeight: slider.height + verticalAlignment: Text.AlignVCenter + visible: dimmableLights.count == 0 + } + + Slider { + id: slider + from: 0 + to: 100 + visible: dimmableLights.count > 0 + value: { + var median = 0 + var count = 0; + for (var i = 0; i < dimmableLights.count; i++) { + var device = dimmableLights.get(i); + var brightnessId = device.deviceClass.stateTypes.findByName("brightness").id + median += device.states.getState(brightnessId).value + count++ + } + return median / count; + } + + Layout.fillWidth: true + onPressedChanged: { + for (var i = 0; i < dimmableLights.count; i++) { + var device = dimmableLights.get(i); + var brightnessId = device.deviceClass.actionTypes.findByName("brightness").id + engine.deviceManager.executeAction(device.id, brightnessId, [{paramTypeId: brightnessId, value: value}]); + } + } + } + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: isOn ? "../images/light-on.svg" : "../images/light-off.svg" + color: isOn ? app.accentColor : keyColor + + property bool isOn: { + for (var i = 0; i < lights.count; i++) { + var device = lights.get(i) + var powerId = device.deviceClass.stateTypes.findByName("power").id + if (device.states.getState(powerId).value === true) { + return true + } + } + return false; + } + + MouseArea { + anchors.fill: parent + onClicked: { + for (var i = 0; i < lights.count; i++) { + var device = lights.get(i) + var powerId = device.deviceClass.stateTypes.findByName("power").id + engine.deviceManager.executeAction(device.id, powerId, [{paramTypeId: powerId, value: !parent.isOn}]) + } + } + } + } + } + } + + Component { + id: closableDelegate + + RowLayout { + + property var devices: null + DevicesProxy { + id: simpleClosables + engine: _engine + parentProxy: devices + shownInterfaces: ["simpleclosable"] + } + DevicesProxy { + id: closables + engine: _engine + parentProxy: devices + shownInterfaces: ["closable"] + } + + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + + ColorIcon { + height: parent.height + width: height + anchors.centerIn: parent + name: Qt.resolvedUrl("../images/up.svg") + } + onClicked: { + for (var i = 0; i < simpleClosables.count; i++) { + var device = simpleClosables.get(i) + var openId = device.deviceClass.actionTypes.findByName("open").id + engine.deviceManager.executeAction(device.id, openId) + } + } + } + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + visible: closables.count > 0 + + ColorIcon { + height: parent.height + width: height + anchors.centerIn: parent + name: Qt.resolvedUrl("../images/media-playback-stop.svg") + } + onClicked: { + for (var i = 0; i < closables.count; i++) { + var device = closables.get(i) + var openId = device.deviceClass.actionTypes.findByName("stop").id + engine.deviceManager.executeAction(device.id, openId) + } + } + } + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + + ColorIcon { + height: parent.height + width: height + anchors.centerIn: parent + name: Qt.resolvedUrl("../images/down.svg") + } + onClicked: { + for (var i = 0; i < simpleClosables.count; i++) { + var device = simpleClosables.get(i) + var closeId = device.deviceClass.actionTypes.findByName("close").id + engine.deviceManager.executeAction(device.id, closeId) + } + } + } + } + } +} From 14c7a3de1ecd443c8aea86a31a57b2f7d9bc4404 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 30 Nov 2019 13:18:08 +0100 Subject: [PATCH 2/5] improve groups view --- libnymea-app-core/engine.cpp | 1 + libnymea-app-core/tagsmanager.cpp | 6 +- libnymea-app-core/tagsmanager.h | 1 + libnymea-common/types/interfaces.cpp | 10 + nymea-app/resources.qrc | 1 + nymea-app/ui/MainPage.qml | 2 +- nymea-app/ui/delegates/ThingTile.qml | 376 +++++++++++++++++++ nymea-app/ui/devicepages/DevicePageBase.qml | 6 +- nymea-app/ui/grouping/GroupPage.qml | 24 +- nymea-app/ui/mainviews/DevicesPage.qml | 6 +- nymea-app/ui/mainviews/FavoritesView.qml | 378 +------------------- nymea-app/ui/mainviews/GroupsView.qml | 170 +++++---- nymea-app/ui/mainviews/ScenesView.qml | 6 +- 13 files changed, 543 insertions(+), 444 deletions(-) create mode 100644 nymea-app/ui/delegates/ThingTile.qml diff --git a/libnymea-app-core/engine.cpp b/libnymea-app-core/engine.cpp index 2c521b9f..568cf884 100644 --- a/libnymea-app-core/engine.cpp +++ b/libnymea-app-core/engine.cpp @@ -137,6 +137,7 @@ void Engine::onConnectedChanged() qDebug() << "Engine: connected changed:" << m_jsonRpcClient->connected(); m_deviceManager->clear(); m_ruleManager->clear(); + m_tagsManager->clear(); if (m_jsonRpcClient->connected()) { qDebug() << "Engine: inital setup required:" << m_jsonRpcClient->initialSetupRequired() << "auth required:" << m_jsonRpcClient->authenticationRequired(); if (!m_jsonRpcClient->initialSetupRequired() && !m_jsonRpcClient->authenticationRequired()) { diff --git a/libnymea-app-core/tagsmanager.cpp b/libnymea-app-core/tagsmanager.cpp index 0912937b..1c361721 100644 --- a/libnymea-app-core/tagsmanager.cpp +++ b/libnymea-app-core/tagsmanager.cpp @@ -23,6 +23,11 @@ void TagsManager::init() m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply"); } +void TagsManager::clear() +{ + m_tags->clear(); +} + bool TagsManager::busy() const { return m_busy; @@ -123,7 +128,6 @@ void TagsManager::getTagsReply(const QVariantMap ¶ms) { QList tags; foreach (const QVariant &tagVariant, params.value("params").toMap().value("tags").toList()) { - qDebug() << "aDDING TAG"; Tag *tag = unpackTag(tagVariant.toMap()); if (tag) { tags.append(tag); diff --git a/libnymea-app-core/tagsmanager.h b/libnymea-app-core/tagsmanager.h index 28b7cdcd..ca7e6ba8 100644 --- a/libnymea-app-core/tagsmanager.h +++ b/libnymea-app-core/tagsmanager.h @@ -17,6 +17,7 @@ public: QString nameSpace() const override; void init(); + void clear(); bool busy() const; Tags* tags() const; diff --git a/libnymea-common/types/interfaces.cpp b/libnymea-common/types/interfaces.cpp index d0d3f41a..1466c8bd 100644 --- a/libnymea-common/types/interfaces.cpp +++ b/libnymea-common/types/interfaces.cpp @@ -120,6 +120,13 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent) tr("Playback status"), tr("Playback status changed"), tr("Set playback status")); + + addInterface("mediacontroller", tr("Media controllers")); + addActionType("mediacontroller", "play", tr("Start playback"), new ParamTypes()); + addActionType("mediacontroller", "stop", tr("Stop playback"), new ParamTypes()); + addActionType("mediacontroller", "pause", tr("Pause playback"), new ParamTypes()); + addActionType("mediacontroller", "skipBack", tr("Skip back"), new ParamTypes()); + addActionType("mediacontroller", "skipNext", tr("Skip next"), new ParamTypes()); } int Interfaces::rowCount(const QModelIndex &parent) const @@ -149,6 +156,9 @@ QHash Interfaces::roleNames() const Interface *Interfaces::get(int index) const { + if (index < 0 || index >= m_list.count()) { + return nullptr; + } return m_list.at(index); } diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 8f386481..05d5e5b8 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -199,5 +199,6 @@ ui/magic/NewScenePage.qml ui/mainviews/GroupsView.qml ui/grouping/GroupPage.qml + ui/delegates/ThingTile.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 499ce009..19c96df4 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -299,7 +299,7 @@ Page { GroupsView { id: groupsView - property string title: qsTr("My groups" + count); + property string title: qsTr("My groups"); width: swipeView.width height: swipeView.height } diff --git a/nymea-app/ui/delegates/ThingTile.qml b/nymea-app/ui/delegates/ThingTile.qml new file mode 100644 index 00000000..47dbb6d8 --- /dev/null +++ b/nymea-app/ui/delegates/ThingTile.qml @@ -0,0 +1,376 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.2 +import Nymea 1.0 +import "../components" + +MainPageTile { + id: root + text: device.name.toUpperCase() + iconName: app.interfacesToIcon(deviceClass.interfaces) + iconColor: app.accentColor + batteryCritical: batteryCriticalState && batteryCriticalState.value === true + disconnected: connectedState && connectedState.value === false + + backgroundImage: artworkState && artworkState.value.length > 0 ? artworkState.value : "" + + property Device device: null + readonly property DeviceClass deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + readonly property State connectedState: deviceClass.interfaces.indexOf("connectable") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("connected").id) : null + readonly property State batteryCriticalState: deviceClass.interfaces.indexOf("battery") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("batteryCritical").id) : null + readonly property State artworkState: deviceClass.interfaces.indexOf("mediametadataprovider") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("artwork").id) : null + + contentItem: Loader { + id: loader + anchors.fill: parent + sourceComponent: { + if (root.deviceClass.interfaces.indexOf("closable") >= 0) { + return closableComponent; + } + if (root.deviceClass.interfaces.indexOf("power") >= 0) { + return lightsComponent; + } + if (root.deviceClass.interfaces.indexOf("sensor") >= 0) { + return sensorsComponent; + } + if (root.deviceClass.interfaces.indexOf("weather") >= 0) { + return sensorsComponent; + } + if (root.deviceClass.interfaces.indexOf("smartmeter") >= 0) { + return sensorsComponent; + } + if (root.deviceClass.interfaces.indexOf("mediacontroller") >= 0) { + return mediaComponent; + } + } + Binding { target: loader.item ? loader.item : null; property: "deviceClass"; value: root.deviceClass } + Binding { target: loader.item ? loader.item : null; property: "device"; value: root.device } + } + + Component { + id: lightsComponent + RowLayout { + property var device: null + property var deviceClass: null + + readonly property var powerStateType: deviceClass.stateTypes.findByName("power"); + readonly property var powerState: device.states.getState(powerStateType.id) + + readonly property var brightnessStateType: deviceClass.stateTypes.findByName("brightness"); + readonly property var brightnessState: brightnessStateType ? device.states.getState(brightnessStateType.id) : null + + ThrottledSlider { + Layout.fillWidth: true + Layout.leftMargin: app.margins / 2 + Layout.alignment: Qt.AlignVCenter + opacity: deviceClass.interfaces.indexOf("dimmablelight") >= 0 ? 1 : 0 + enabled: opacity > 0 + from: 0 + to: 100 + value: brightnessState ? brightnessState.value : 0 + onMoved: { + var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var actionType = deviceClass.actionTypes.findByName("brightness"); + var params = []; + var powerParam = {} + powerParam["paramTypeId"] = actionType.paramTypes.get(0).id; + powerParam["value"] = value; + params.push(powerParam) + engine.deviceManager.executeAction(device.id, actionType.id, params); + } + } + + ItemDelegate { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.rightMargin: app.margins / 2 + Layout.alignment: Qt.AlignVCenter + padding: 0; topPadding: 0; bottomPadding: 0 + + contentItem: ColorIcon { + name: deviceClass.interfaces.indexOf("light") >= 0 + ? (powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg") + : app.interfacesToIcon(deviceClass.interfaces) + color: powerState.value === true ? app.accentColor : keyColor + } + onClicked: { + var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var actionType = deviceClass.actionTypes.findByName("power"); + var params = []; + var powerParam = {} + powerParam["paramTypeId"] = actionType.paramTypes.get(0).id; + powerParam["value"] = !powerState.value; + params.push(powerParam) + engine.deviceManager.executeAction(device.id, actionType.id, params); + } + } + } + } + + Component { + id: sensorsComponent + RowLayout { + id: sensorsRoot + property var device: null + property var deviceClass: null + spacing: 0 + + property var shownInterfaces: [] + property int currentStateIndex: -1 + property var currentStateType: deviceClass ? deviceClass.stateTypes.findByName(shownInterfaces[currentStateIndex].state) : null + property var currentState: currentStateType ? device.states.getState(currentStateType.id) : null + + onDeviceClassChanged: { + if (deviceClass == null) { + return; + } + + var tmp = [] + if (deviceClass.interfaces.indexOf("temperaturesensor") >= 0) { + tmp.push({iface: "temperaturesensor", state: "temperature"}); + } + if (deviceClass.interfaces.indexOf("humiditysensor") >= 0) { + tmp.push({iface: "humiditysensor", state: "humidity"}); + } + if (deviceClass.interfaces.indexOf("moisturesensor") >= 0) { + tmp.push({iface: "moisturesensor", state: "moisture"}); + } + if (deviceClass.interfaces.indexOf("pressuresensor") >= 0) { + tmp.push({iface: "pressuresensor", state: "pressure"}); + } + if (deviceClass.interfaces.indexOf("lightsensor") >= 0) { + tmp.push({iface: "lightsensor", state: "lightIntensity"}); + } + if (deviceClass.interfaces.indexOf("conductivitysensor") >= 0) { + tmp.push({iface: "conductivitysensor", state: "conductivity"}); + } + if (deviceClass.interfaces.indexOf("noisesensor") >= 0) { + tmp.push({iface: "noisesensor", state: "noise"}); + } + if (deviceClass.interfaces.indexOf("co2sensor") >= 0) { + tmp.push({iface: "co2sensor", state: "co2"}); + } + if (deviceClass.interfaces.indexOf("smartmeterconsumer") >= 0) { + tmp.push({iface: "smartmeterconsumer", state: "totalEnergyConsumed"}); + } + if (deviceClass.interfaces.indexOf("smartmeterproducer") >= 0) { + tmp.push({iface: "smartmeterproducer", state: "totalEnergyProduced"}); + } + if (deviceClass.interfaces.indexOf("daylightsensor") >= 0) { + tmp.push({iface: "daylightsensor", state: "daylight"}); + } + if (deviceClass.interfaces.indexOf("presencesensor") >= 0) { + tmp.push({iface: "presencesensor", state: "isPresent"}); + } + + if (deviceClass.interfaces.indexOf("weather") >= 0) { + tmp.push({iface: "temperaturesensor", state: "temperature"}); + tmp.push({iface: "humiditysensor", state: "humidity"}); + tmp.push({iface: "pressuresensor", state: "pressure"}); + } + + shownInterfaces = tmp + currentStateIndex = 0 + } + + ItemDelegate { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.leftMargin: app.margins / 2 + Layout.alignment: Qt.AlignVCenter + padding: 0; topPadding: 0; bottomPadding: 0 + visible: sensorsRoot.shownInterfaces.length > 1 + contentItem: ColorIcon { + name: "../images/back.svg" + } + onClicked: { + var newIndex = sensorsRoot.currentStateIndex - 1; + if (newIndex < 0) newIndex = sensorsRoot.shownInterfaces.length - 1 + sensorsRoot.currentStateIndex = newIndex; + } + } + + Item { Layout.fillHeight: true; Layout.fillWidth: true } + ColorIcon { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.alignment: Qt.AlignVCenter + color: app.interfaceToColor(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface) + name: app.interfaceToIcon(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface) + } + + Item { Layout.fillHeight: true; Layout.fillWidth: true } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: sensorsRoot.currentStateType.type.toLowerCase() !== "bool" + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: sensorsRoot.currentStateType.unitString + font.pixelSize: app.smallFont + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: sensorsRoot.currentState.value// + " " + sensorsRoot.currentStateType.unitString + elide: Text.ElideRight + } + } + Led { + state: sensorsRoot.currentState.value === true ? "on" : "off" + visible: sensorsRoot.currentStateType.type.toLowerCase() === "bool" + } + + Item { Layout.fillHeight: true; Layout.fillWidth: true } + + ItemDelegate { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.rightMargin: app.margins / 2 + Layout.alignment: Qt.AlignVCenter + padding: 0; topPadding: 0; bottomPadding: 0 + visible: sensorsRoot.shownInterfaces.length > 1 + contentItem: ColorIcon { + name: "../images/next.svg" + } + onClicked: { + var newIndex = sensorsRoot.currentStateIndex + 1; + if (newIndex >= sensorsRoot.shownInterfaces.length) newIndex = 0; + sensorsRoot.currentStateIndex = newIndex; + } + } + } + } + + Component { + id: closableComponent + RowLayout { + property var device: null + property var deviceClass: null + + ItemDelegate { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.leftMargin: app.margins / 2 + Layout.alignment: Qt.AlignVCenter + padding: 0; topPadding: 0; bottomPadding: 0 + contentItem: ColorIcon { + name: "../images/up.svg" + color: app.accentColor + } + onClicked: { + var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var actionType = deviceClass.actionTypes.findByName("open"); + engine.deviceManager.executeAction(device.id, actionType.id); + } + } + + Slider { + id: closableSlider + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + visible: deviceClass.interfaces.indexOf("extendedclosable") >= 0 + readonly property var percentageStateType: deviceClass.stateTypes.findByName("percentage"); + readonly property var percentateState: percentageStateType ? device.states.getState(percentageStateType.id) : null + from: 0 + to: 100 + value: percentateState ? percentateState.value : 0 + } + Item { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + visible: !closableSlider.visible + } + + ItemDelegate { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.rightMargin: app.margins / 2 + Layout.alignment: Qt.AlignVCenter + padding: 0; topPadding: 0; bottomPadding: 0 + contentItem: ColorIcon { + name: "../images/down.svg" + color: app.accentColor + } + onClicked: { + var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var actionType = deviceClass.actionTypes.findByName("close"); + engine.deviceManager.executeAction(device.id, actionType.id); + } + } + } + } + + Component { + id: mediaComponent + RowLayout { + id: mediaRoot + + property Device device: null + property DeviceClass deviceClass: null + + readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id) + + function executeAction(actionName, params) { + var actionTypeId = deviceClass.actionTypes.findByName(actionName).id; + engine.deviceManager.executeAction(device.id, actionTypeId, params) + } + Item { Layout.fillWidth: true } + + ProgressButton { + Layout.preferredHeight: app.iconSize * .9 + Layout.preferredWidth: height + imageSource: "../images/media-skip-backward.svg" + longpressImageSource: "../images/media-seek-backward.svg" + repeat: true + + onClicked: { + mediaRoot.executeAction("skipBack") + } + onLongpressed: { + mediaRoot.executeAction("fastRewind") + } + } + Item { Layout.fillWidth: true } + + ProgressButton { + Layout.preferredHeight: app.iconSize * 1.3 + Layout.preferredWidth: height + imageSource: mediaRoot.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" + longpressImageSource: "../images/media-playback-stop.svg" + longpressEnabled: mediaRoot.playbackState.value !== "Stopped" + + onClicked: { + if (mediaRoot.playbackState.value === "Playing") { + mediaRoot.executeAction("pause") + } else { + mediaRoot.executeAction("play") + } + } + + onLongpressed: { + mediaRoot.executeAction("stop") + } + } + + Item { Layout.fillWidth: true } + ProgressButton { + Layout.preferredHeight: app.iconSize * .9 + Layout.preferredWidth: height + imageSource: "../images/media-skip-forward.svg" + longpressImageSource: "../images/media-seek-forward.svg" + repeat: true + onClicked: { + mediaRoot.executeAction("skipNext") + } + onLongpressed: { + mediaRoot.executeAction("fastForward") + } + } + Item { Layout.fillWidth: true } + } + } +} diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index f9eda37f..3366e7cd 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -93,7 +93,6 @@ Page { } } function addToGroup() { -// engine.tagsManager.tagDevice(root.device.id, "group", "My group 1") var dialog = addToGroupDialog.createObject(root) dialog.open(); } @@ -122,6 +121,9 @@ Page { headerIcon: "../images/view-grid-symbolic.svg" RowLayout { + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins TextField { id: newGroupdTextField Layout.fillWidth: true @@ -141,6 +143,8 @@ Page { ListView { Layout.fillWidth: true height: 200 + clip: true + ScrollIndicator.vertical: ScrollIndicator {} model: TagListModel { id: groupTags diff --git a/nymea-app/ui/grouping/GroupPage.qml b/nymea-app/ui/grouping/GroupPage.qml index d1053f0b..0e9494da 100644 --- a/nymea-app/ui/grouping/GroupPage.qml +++ b/nymea-app/ui/grouping/GroupPage.qml @@ -4,6 +4,7 @@ import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" +import "../delegates" Page { id: root @@ -27,11 +28,28 @@ Page { showStates: true } - ListView { + GridView { + id: gridView anchors.fill: parent + anchors.margins: app.margins / 2 + model: devicesInGroup - delegate: Label { - text: model.name + + readonly property int minTileWidth: 180 + readonly property int minTileHeight: 240 + readonly property int tilesPerRow: root.width / minTileWidth + + cellWidth: gridView.width / tilesPerRow + cellHeight: cellWidth + + delegate: ThingTile { + width: gridView.cellWidth + height: gridView.cellHeight + + device: devicesInGroup.get(index) + + onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + app.interfaceListToDevicePage(deviceClass.interfaces)), {device: device}) + } } diff --git a/nymea-app/ui/mainviews/DevicesPage.qml b/nymea-app/ui/mainviews/DevicesPage.qml index c2700ec8..dde2e21f 100644 --- a/nymea-app/ui/mainviews/DevicesPage.qml +++ b/nymea-app/ui/mainviews/DevicesPage.qml @@ -5,11 +5,15 @@ import QtQuick.Layouts 1.2 import Nymea 1.0 import "../components" -Item { +MouseArea { id: root property alias count: interfacesGridView.count property alias model: interfacesGridView.model + // Prevent scroll events to swipe left/right in case they fall through the grid + preventStealing: true + onWheel: wheel.accepted = true + GridView { id: interfacesGridView anchors.fill: parent diff --git a/nymea-app/ui/mainviews/FavoritesView.qml b/nymea-app/ui/mainviews/FavoritesView.qml index f6507c65..c2c4c2a7 100644 --- a/nymea-app/ui/mainviews/FavoritesView.qml +++ b/nymea-app/ui/mainviews/FavoritesView.qml @@ -4,13 +4,18 @@ import QtQuick.Layouts 1.3 import QtQuick.Controls.Material 2.2 import Nymea 1.0 import "../components" +import "../delegates" -Item { +MouseArea { id: root property bool editMode: false readonly property int count: tagsProxy.count + // Prevent scroll events to swipe left/right in case they fall through the grid + preventStealing: true + onWheel: wheel.accepted = true + TagsProxyModel { id: tagsProxy tags: engine.tagsManager.tags @@ -29,57 +34,16 @@ Item { cellHeight: cellWidth model: tagsProxy - delegate: MainPageTile { + delegate: ThingTile { id: delegateRoot width: gridView.cellWidth height: gridView.cellHeight - text: device.name.toUpperCase() - iconName: app.interfacesToIcon(deviceClass.interfaces) - iconColor: app.accentColor - visible: !fakeDragItem.visible || fakeDragItem.deviceId !== device.id - batteryCritical: batteryCriticalState && batteryCriticalState.value === true - disconnected: connectedState && connectedState.value === false - - property var modelIndex: index - - property string deviceId: model.deviceId - property string ruleId: model.ruleId - readonly property Device device: engine.deviceManager.devices.getDevice(deviceId) - readonly property DeviceClass deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null - readonly property State connectedState: deviceClass.interfaces.indexOf("connectable") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("connected").id) : null - readonly property State batteryCriticalState: deviceClass.interfaces.indexOf("battery") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("batteryCritical").id) : null + device: engine.deviceManager.devices.getDevice(deviceId) onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + app.interfaceListToDevicePage(deviceClass.interfaces)), {device: device}) onPressAndHold: root.editMode = true - contentItem: Loader { - id: loader - anchors.fill: parent - sourceComponent: { - if (delegateRoot.deviceClass.interfaces.indexOf("closable") >= 0) { - return closableComponent; - } - if (delegateRoot.deviceClass.interfaces.indexOf("power") >= 0) { - return lightsComponent; - } - if (delegateRoot.deviceClass.interfaces.indexOf("sensor") >= 0) { - return sensorsComponent; - } - if (delegateRoot.deviceClass.interfaces.indexOf("weather") >= 0) { - return sensorsComponent; - } - if (delegateRoot.deviceClass.interfaces.indexOf("smartmeter") >= 0) { - return sensorsComponent; - } - if (delegateRoot.deviceClass.interfaces.indexOf("mediacontroller") >= 0) { - return mediaComponent; - } - } - Binding { target: loader.item ? loader.item : null; property: "deviceClass"; value: delegateRoot.deviceClass } - Binding { target: loader.item ? loader.item : null; property: "device"; value: delegateRoot.device } - } - SequentialAnimation { loops: Animation.Infinite running: root.editMode @@ -174,330 +138,4 @@ Item { } } } - - Component { - id: lightsComponent - RowLayout { - property var device: null - property var deviceClass: null - - readonly property var powerStateType: deviceClass.stateTypes.findByName("power"); - readonly property var powerState: device.states.getState(powerStateType.id) - - readonly property var brightnessStateType: deviceClass.stateTypes.findByName("brightness"); - readonly property var brightnessState: brightnessStateType ? device.states.getState(brightnessStateType.id) : null - - ThrottledSlider { - Layout.fillWidth: true - Layout.leftMargin: app.margins / 2 - Layout.alignment: Qt.AlignVCenter - opacity: deviceClass.interfaces.indexOf("dimmablelight") >= 0 ? 1 : 0 - enabled: opacity > 0 - from: 0 - to: 100 - value: brightnessState ? brightnessState.value : 0 - onMoved: { - var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var actionType = deviceClass.actionTypes.findByName("brightness"); - var params = []; - var powerParam = {} - powerParam["paramTypeId"] = actionType.paramTypes.get(0).id; - powerParam["value"] = value; - params.push(powerParam) - engine.deviceManager.executeAction(device.id, actionType.id, params); - } - } - - ItemDelegate { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: width - Layout.rightMargin: app.margins / 2 - Layout.alignment: Qt.AlignVCenter - padding: 0; topPadding: 0; bottomPadding: 0 - - contentItem: ColorIcon { - name: deviceClass.interfaces.indexOf("light") >= 0 - ? (powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg") - : app.interfacesToIcon(deviceClass.interfaces) - color: powerState.value === true ? app.accentColor : keyColor - } - onClicked: { - var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var actionType = deviceClass.actionTypes.findByName("power"); - var params = []; - var powerParam = {} - powerParam["paramTypeId"] = actionType.paramTypes.get(0).id; - powerParam["value"] = !powerState.value; - params.push(powerParam) - engine.deviceManager.executeAction(device.id, actionType.id, params); - } - } - } - } - - Component { - id: sensorsComponent - RowLayout { - id: sensorsRoot - property var device: null - property var deviceClass: null - spacing: 0 - - property var shownInterfaces: [] - property int currentStateIndex: -1 - property var currentStateType: deviceClass ? deviceClass.stateTypes.findByName(shownInterfaces[currentStateIndex].state) : null - property var currentState: currentStateType ? device.states.getState(currentStateType.id) : null - - onDeviceClassChanged: { - if (deviceClass == null) { - return; - } - - var tmp = [] - if (deviceClass.interfaces.indexOf("temperaturesensor") >= 0) { - tmp.push({iface: "temperaturesensor", state: "temperature"}); - } - if (deviceClass.interfaces.indexOf("humiditysensor") >= 0) { - tmp.push({iface: "humiditysensor", state: "humidity"}); - } - if (deviceClass.interfaces.indexOf("moisturesensor") >= 0) { - tmp.push({iface: "moisturesensor", state: "moisture"}); - } - if (deviceClass.interfaces.indexOf("pressuresensor") >= 0) { - tmp.push({iface: "pressuresensor", state: "pressure"}); - } - if (deviceClass.interfaces.indexOf("lightsensor") >= 0) { - tmp.push({iface: "lightsensor", state: "lightIntensity"}); - } - if (deviceClass.interfaces.indexOf("conductivitysensor") >= 0) { - tmp.push({iface: "conductivitysensor", state: "conductivity"}); - } - if (deviceClass.interfaces.indexOf("noisesensor") >= 0) { - tmp.push({iface: "noisesensor", state: "noise"}); - } - if (deviceClass.interfaces.indexOf("co2sensor") >= 0) { - tmp.push({iface: "co2sensor", state: "co2"}); - } - if (deviceClass.interfaces.indexOf("smartmeterconsumer") >= 0) { - tmp.push({iface: "smartmeterconsumer", state: "totalEnergyConsumed"}); - } - if (deviceClass.interfaces.indexOf("smartmeterproducer") >= 0) { - tmp.push({iface: "smartmeterproducer", state: "totalEnergyProduced"}); - } - if (deviceClass.interfaces.indexOf("daylightsensor") >= 0) { - tmp.push({iface: "daylightsensor", state: "daylight"}); - } - if (deviceClass.interfaces.indexOf("presencesensor") >= 0) { - tmp.push({iface: "presencesensor", state: "isPresent"}); - } - - if (deviceClass.interfaces.indexOf("weather") >= 0) { - tmp.push({iface: "temperaturesensor", state: "temperature"}); - tmp.push({iface: "humiditysensor", state: "humidity"}); - tmp.push({iface: "pressuresensor", state: "pressure"}); - } - - shownInterfaces = tmp - currentStateIndex = 0 - } - - ItemDelegate { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: width - Layout.leftMargin: app.margins / 2 - Layout.alignment: Qt.AlignVCenter - padding: 0; topPadding: 0; bottomPadding: 0 - visible: sensorsRoot.shownInterfaces.length > 1 - contentItem: ColorIcon { - name: "../images/back.svg" - } - onClicked: { - var newIndex = sensorsRoot.currentStateIndex - 1; - if (newIndex < 0) newIndex = sensorsRoot.shownInterfaces.length - 1 - sensorsRoot.currentStateIndex = newIndex; - } - } - - Item { Layout.fillHeight: true; Layout.fillWidth: true } - ColorIcon { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: width - Layout.alignment: Qt.AlignVCenter - color: app.interfaceToColor(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface) - name: app.interfaceToIcon(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface) - } - - Item { Layout.fillHeight: true; Layout.fillWidth: true } - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - visible: sensorsRoot.currentStateType.type.toLowerCase() !== "bool" - - Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - text: sensorsRoot.currentStateType.unitString - font.pixelSize: app.smallFont - } - Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - text: sensorsRoot.currentState.value// + " " + sensorsRoot.currentStateType.unitString - elide: Text.ElideRight - } - } - Led { - state: sensorsRoot.currentState.value === true ? "on" : "off" - visible: sensorsRoot.currentStateType.type.toLowerCase() === "bool" - } - - Item { Layout.fillHeight: true; Layout.fillWidth: true } - - ItemDelegate { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: width - Layout.rightMargin: app.margins / 2 - Layout.alignment: Qt.AlignVCenter - padding: 0; topPadding: 0; bottomPadding: 0 - visible: sensorsRoot.shownInterfaces.length > 1 - contentItem: ColorIcon { - name: "../images/next.svg" - } - onClicked: { - var newIndex = sensorsRoot.currentStateIndex + 1; - if (newIndex >= sensorsRoot.shownInterfaces.length) newIndex = 0; - sensorsRoot.currentStateIndex = newIndex; - } - } - } - } - - Component { - id: closableComponent - RowLayout { - property var device: null - property var deviceClass: null - - ItemDelegate { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: width - Layout.leftMargin: app.margins / 2 - Layout.alignment: Qt.AlignVCenter - padding: 0; topPadding: 0; bottomPadding: 0 - contentItem: ColorIcon { - name: "../images/up.svg" - color: app.accentColor - } - onClicked: { - var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var actionType = deviceClass.actionTypes.findByName("open"); - engine.deviceManager.executeAction(device.id, actionType.id); - } - } - - Slider { - id: closableSlider - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - visible: deviceClass.interfaces.indexOf("extendedclosable") >= 0 - readonly property var percentageStateType: deviceClass.stateTypes.findByName("percentage"); - readonly property var percentateState: device.states.getState(percentageStateType.id) - from: 0 - to: 100 - value: percentateState.value - } - Item { - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - visible: !closableSlider.visible - } - - ItemDelegate { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: width - Layout.rightMargin: app.margins / 2 - Layout.alignment: Qt.AlignVCenter - padding: 0; topPadding: 0; bottomPadding: 0 - contentItem: ColorIcon { - name: "../images/down.svg" - color: app.accentColor - } - onClicked: { - var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var actionType = deviceClass.actionTypes.findByName("close"); - engine.deviceManager.executeAction(device.id, actionType.id); - } - } - } - } - - Component { - id: mediaComponent - RowLayout { - id: mediaRoot - - property Device device: null - property DeviceClass deviceClass: null - - readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id) - - function executeAction(actionName, params) { - var actionTypeId = deviceClass.actionTypes.findByName(actionName).id; - engine.deviceManager.executeAction(device.id, actionTypeId, params) - } - Item { Layout.fillWidth: true } - - ProgressButton { - Layout.preferredHeight: app.iconSize * .9 - Layout.preferredWidth: height - imageSource: "../images/media-skip-backward.svg" - longpressImageSource: "../images/media-seek-backward.svg" - repeat: true - - onClicked: { - mediaRoot.executeAction("skipBack") - } - onLongpressed: { - mediaRoot.executeAction("fastRewind") - } - } - Item { Layout.fillWidth: true } - - ProgressButton { - Layout.preferredHeight: app.iconSize * 1.3 - Layout.preferredWidth: height - imageSource: mediaRoot.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" - longpressImageSource: "../images/media-playback-stop.svg" - longpressEnabled: mediaRoot.playbackState.value !== "Stopped" - - onClicked: { - if (mediaRoot.playbackState.value === "Playing") { - mediaRoot.executeAction("pause") - } else { - mediaRoot.executeAction("play") - } - } - - onLongpressed: { - mediaRoot.executeAction("stop") - } - } - - Item { Layout.fillWidth: true } - ProgressButton { - Layout.preferredHeight: app.iconSize * .9 - Layout.preferredWidth: height - imageSource: "../images/media-skip-forward.svg" - longpressImageSource: "../images/media-seek-forward.svg" - repeat: true - onClicked: { - mediaRoot.executeAction("skipNext") - } - onLongpressed: { - mediaRoot.executeAction("fastForward") - } - } - Item { Layout.fillWidth: true } - } - } } diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml index a2762a0e..014b27de 100644 --- a/nymea-app/ui/mainviews/GroupsView.qml +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -5,8 +5,10 @@ import Nymea 1.0 import QtQuick.Controls.Material 2.2 import "../components" -Item { +MouseArea { id: root + preventStealing: true + onWheel: wheel.accepted = true readonly property int count: groupsGridView.count @@ -15,6 +17,7 @@ Item { anchors.fill: parent anchors.margins: app.margins / 2 + readonly property int minTileWidth: 180 readonly property int minTileHeight: 180 readonly property int tilesPerRow: root.width / minTileWidth @@ -48,7 +51,7 @@ Item { InterfacesProxy { id: controlsInGroup - shownInterfaces: ["light", "simpleclosable"] + shownInterfaces: ["light", "simpleclosable", "mediacontroller"] devicesProxyFilter: devicesInGroup showStates: true showActions: true @@ -61,7 +64,10 @@ Item { } contentItem: ItemDelegate { - padding: 0 + leftPadding: 0 + topPadding: 0 + rightPadding: 0 + bottomPadding: 0 onClicked: { pageStack.push(Qt.resolvedUrl("../grouping/GroupPage.qml"), {groupTag: model.tagId}) @@ -74,6 +80,7 @@ Item { color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .05) Label { anchors.fill: parent + verticalAlignment: Text.AlignVCenter anchors { leftMargin: app.margins; rightMargin: app.margins } text: model.tagId.substring(6) elide: Text.ElideRight @@ -82,22 +89,35 @@ Item { Item { Layout.fillHeight: true Layout.fillWidth: true + + ColorIcon { + anchors.centerIn: parent + height: app.iconSize * 2 + width: height + visible: controlsInGroup.count == 0 + color: app.accentColor + name: "../images/view-grid-symbolic.svg" + } + ColumnLayout { anchors.fill: parent Repeater { - model: controlsInGroup + model: Math.min(controlsInGroup.count, parent.height / 50) delegate: Loader { id: controlLoader Layout.fillWidth: true Layout.leftMargin: app.margins / 2 Layout.rightMargin: app.margins / 2 + property string interfaceName: controlsInGroup.get(index).name sourceComponent: { - switch (model.name) { + switch (interfaceName) { case "simpleclosable": return closableDelegate case "light": return lightDelegate + case "mediacontroller": + return mediaControllerDelegate } } Binding { @@ -111,74 +131,77 @@ Item { Layout.fillHeight: true Layout.fillWidth: true } + } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: app.iconSize * 1.2 - Layout.alignment: Qt.AlignRight - color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, 0.05) + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize * 1.2 + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, 0.05) - RowLayout { - anchors.fill: parent + RowLayout { + anchors.fill: parent - Repeater { - model: sensorsInGroup - delegate: Row { - ColorIcon { - height: app.iconSize * .8 - width: height - name: app.interfaceToIcon(model.name) - color: app.interfaceToColor(model.name) + Repeater { + model: sensorsInGroup + delegate: Row { + height: parent.height + + ColorIcon { + height: app.iconSize * .8 + width: height + name: app.interfaceToIcon(model.name) + color: app.interfaceToColor(model.name) + } + DevicesProxy { + id: innerProxy + engine: _engine + parentProxy: devicesInGroup + shownInterfaces: [model.name] + } + + Led { + visible: ["presencesensor"].indexOf(model.name) >= 0 + state: { + var stateName = null + switch (model.name) { + case "presencesensor": + stateName = "isPresent" + break; } - DevicesProxy { - id: innerProxy - engine: _engine - parentProxy: devicesInGroup - shownInterfaces: [model.name] + if (!stateName) { + return "off"; + } + var ret = false; + for (var i = 0; i < innerProxy.count; i++) { + ret |= innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value + } + return ret ? "on" : "off"; + } + } + + Label { + height: parent.height + verticalAlignment: Text.AlignVCenter + text: { + var stateName = null; + switch (model.name) { + case "temperaturesensor": + stateName = "temperature"; + break; + case "lightsensor": + stateName = "lightIntensity" + break; + } + if (!stateName) { + return ""; } - Led { - visible: ["presencesensor"].indexOf(model.name) >= 0 - state: { - var stateName = null - switch (model.name) { - case "presencesensor": - stateName = "isPresent" - break; - } - if (!stateName) { - return "off"; - } - var ret = false; - for (var i = 0; i < innerProxy.count; i++) { - ret |= innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value - } - return ret ? "on" : "off"; - } - } - - Label { - text: { - var stateName = null; - switch (model.name) { - case "temperaturesensor": - stateName = "temperature"; - break; - case "lightsensor": - stateName = "lightIntensity" - break; - } - if (!stateName) { - return ""; - } - - var ret = 0 - for (var i = 0; i < innerProxy.count; i++) { - ret += innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value - } - return (ret / innerProxy.count).toFixed(1) - } + var ret = 0 + for (var i = 0; i < innerProxy.count; i++) { + ret += innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value } + return (ret / innerProxy.count).toFixed(1) } } } @@ -351,4 +374,19 @@ Item { } } } + + Component { + id: mediaControllerDelegate + MediaControls { + property var devices: null + DevicesProxy { + id: mediaControllers + engine: _engine + parentProxy: devices + shownInterfaces: ["mediacontroller"] + } + + device: mediaControllers.parentProxy ? mediaControllers.get(0) : null + } + } } diff --git a/nymea-app/ui/mainviews/ScenesView.qml b/nymea-app/ui/mainviews/ScenesView.qml index 8bac0639..f036af43 100644 --- a/nymea-app/ui/mainviews/ScenesView.qml +++ b/nymea-app/ui/mainviews/ScenesView.qml @@ -5,11 +5,15 @@ import Nymea 1.0 import QtQuick.Controls.Material 2.2 import "../components" -Item { +MouseArea { id: root readonly property int count: interfacesGridView.count + // Prevent scroll events to swipe left/right in case they fall through the grid + preventStealing: true + onWheel: wheel.accepted = true + GridView { id: interfacesGridView anchors.fill: parent From c0ba41d2ef633e4da041724d6de019895960d1a6 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 30 Nov 2019 13:24:02 +0100 Subject: [PATCH 3/5] fix updating of the media component --- nymea-app/ui/mainviews/GroupsView.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml index 014b27de..1f8593b4 100644 --- a/nymea-app/ui/mainviews/GroupsView.qml +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -386,7 +386,8 @@ MouseArea { shownInterfaces: ["mediacontroller"] } - device: mediaControllers.parentProxy ? mediaControllers.get(0) : null + // involve count in the statement to make the binding re-evaluate when the group is changed + device: mediaControllers.count > 0 ? mediaControllers.get(0) : null } } } From 039dc92407d49b59aa1177e4b38e298bde1becba Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 30 Nov 2019 14:16:06 +0100 Subject: [PATCH 4/5] add empty view placeholder for groups --- nymea-app/ui/MainPage.qml | 14 +++++++++++++- nymea-app/ui/components/ClosableArrowAnimation.qml | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 nymea-app/ui/components/ClosableArrowAnimation.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 19c96df4..912c861e 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -269,7 +269,7 @@ Page { anchors { left: parent.left; right: parent.right; margins: app.margins } anchors.verticalCenter: parent.verticalCenter visible: scenesView.count === 0 && !engine.deviceManager.fetchingData - title: qsTr("There are no scenes set up yet") + title: qsTr("There are no scenes set up yet.") text: engine.deviceManager.devices.count === 0 ? qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") : qsTr("Scenes provide a useful way to control your things with just one click.") @@ -302,6 +302,18 @@ Page { property string title: qsTr("My groups"); width: swipeView.width height: swipeView.height + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: groupsView.count == 0 + 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" + buttonVisible: false +// buttonText: qsTr("Create a group") +// onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml")) + } } } diff --git a/nymea-app/ui/components/ClosableArrowAnimation.qml b/nymea-app/ui/components/ClosableArrowAnimation.qml new file mode 100644 index 00000000..9c36e13c --- /dev/null +++ b/nymea-app/ui/components/ClosableArrowAnimation.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 + +Item { + +} From d5f819c98a794392d104a9a38a0da861deb1c4c5 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 30 Nov 2019 14:39:34 +0100 Subject: [PATCH 5/5] update translations --- nymea-app/translations/nymea-app-de.ts | 81 +++++++++++++++++- nymea-app/translations/nymea-app-en.ts | 83 ++++++++++++++++++- nymea-app/translations/nymea-app-en_US.ts | 83 ++++++++++++++++++- nymea-app/translations/nymea-app-ko.ts | 81 +++++++++++++++++- .../ui/components/ClosableArrowAnimation.qml | 5 -- nymea-app/ui/devicepages/DevicePageBase.qml | 2 +- 6 files changed, 319 insertions(+), 16 deletions(-) delete mode 100644 nymea-app/ui/components/ClosableArrowAnimation.qml diff --git a/nymea-app/translations/nymea-app-de.ts b/nymea-app/translations/nymea-app-de.ts index 2c2ab088..57174f4f 100644 --- a/nymea-app/translations/nymea-app-de.ts +++ b/nymea-app/translations/nymea-app-de.ts @@ -1339,6 +1339,22 @@ Logs Protokoll + + Grouping + Gruppen + + + New group + Neue Gruppe + + + OK + OK + + + Groups for %1 + Gruppen für %1 + DeviceRulesPage @@ -2204,6 +2220,13 @@ Bitte versuche es erneut. Entschuldigung, es gibt noch nicht genügend Daten um hier ein Diagramm anzuzeigen! + + GroupsView + + Lighting + Beleuchtung + + HeatingDevicePage @@ -2515,6 +2538,42 @@ Bitte versuche es erneut. Set playback status Setze Wiedergabestatus + + Light intensity sensors + Lichtstärkesensoren + + + Light intensity + Lichtstärke + + + Light intensity changed + Lichtstärke geändert + + + Media controllers + Mediensteuerungen + + + Start playback + Wiedergabe starten + + + Stop playback + Wiedergabe stoppen + + + Pause playback + Wiedergabe pausieren + + + Skip back + Zurück springen + + + Skip next + Weiter springen + InterfacesModels @@ -2893,7 +2952,7 @@ Bitte versuche es erneut. There are no scenes set up yet - Es sind noch keine Szenen eingerichet + Es sind noch keine Szenen eingerichet It appears there are no things set up either yet. In order to use scenes you need to add some things first. @@ -2950,6 +3009,26 @@ Bitte versuche es erneut. In order for your %1 system to be useful, go ahead and add some things. Damit dein %1 System sich nützlich machen kann, füge einige "Things" hinzu. + + Groups + Gruppen + + + There are no scenes set up yet. + Es sind noch keine Szenen eingerichtet. + + + My groups + Meine Gruppen + + + There are no groups set up yet. + Es sind noch keine Gruppen eingerichtet. + + + 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. + "Things" in Gruppen zu verwalten ist hilfreich um mehrere Geräte gleichzeitig zu schalten, z.B. einen ganzen Raum. Halte Ausschau nach dem Gruppensymbol wenn du mit "Things" interagierst und benutze es um sie Gruppen hinzuzufügen. + ManualConnectPage diff --git a/nymea-app/translations/nymea-app-en.ts b/nymea-app/translations/nymea-app-en.ts index e395919f..3547290b 100644 --- a/nymea-app/translations/nymea-app-en.ts +++ b/nymea-app/translations/nymea-app-en.ts @@ -903,6 +903,22 @@ Logs + + Grouping + + + + New group + + + + OK + + + + Groups for %1 + + DeviceRulesPage @@ -1648,6 +1664,13 @@ Please try again. + + GroupsView + + Lighting + + + HeatingDevicePage @@ -1944,6 +1967,42 @@ Please try again. Set playback status + + Light intensity sensors + + + + Light intensity + + + + Light intensity changed + + + + Media controllers + + + + Start playback + + + + Stop playback + + + + Pause playback + + + + Skip back + + + + Skip next + + LightsDeviceListPage @@ -2225,10 +2284,6 @@ Please try again. My scenes - - There are no scenes set up yet - - It appears there are no things set up either yet. In order to use scenes you need to add some things first. @@ -2276,6 +2331,26 @@ Please try again. In order for your %1 system to be useful, go ahead and add some things. + + Groups + + + + There are no scenes set up yet. + + + + My groups + + + + There are no groups set up yet. + + + + 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. + + ManualConnectPage diff --git a/nymea-app/translations/nymea-app-en_US.ts b/nymea-app/translations/nymea-app-en_US.ts index e395919f..3547290b 100644 --- a/nymea-app/translations/nymea-app-en_US.ts +++ b/nymea-app/translations/nymea-app-en_US.ts @@ -903,6 +903,22 @@ Logs + + Grouping + + + + New group + + + + OK + + + + Groups for %1 + + DeviceRulesPage @@ -1648,6 +1664,13 @@ Please try again. + + GroupsView + + Lighting + + + HeatingDevicePage @@ -1944,6 +1967,42 @@ Please try again. Set playback status + + Light intensity sensors + + + + Light intensity + + + + Light intensity changed + + + + Media controllers + + + + Start playback + + + + Stop playback + + + + Pause playback + + + + Skip back + + + + Skip next + + LightsDeviceListPage @@ -2225,10 +2284,6 @@ Please try again. My scenes - - There are no scenes set up yet - - It appears there are no things set up either yet. In order to use scenes you need to add some things first. @@ -2276,6 +2331,26 @@ Please try again. In order for your %1 system to be useful, go ahead and add some things. + + Groups + + + + There are no scenes set up yet. + + + + My groups + + + + There are no groups set up yet. + + + + 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. + + ManualConnectPage diff --git a/nymea-app/translations/nymea-app-ko.ts b/nymea-app/translations/nymea-app-ko.ts index c46086a4..0daffa93 100644 --- a/nymea-app/translations/nymea-app-ko.ts +++ b/nymea-app/translations/nymea-app-ko.ts @@ -932,6 +932,22 @@ Logs + + Grouping + + + + New group + + + + OK + 확인 + + + Groups for %1 + + DeviceRulesPage @@ -1684,6 +1700,13 @@ Please try again. 죄송하지만, 그래프를 표시할 충분한 데이터가 없습니다! + + GroupsView + + Lighting + 전등(조명) + + HeatingDevicePage @@ -1984,6 +2007,42 @@ Please try again. Set playback status 재생 상태 설정 + + Light intensity sensors + + + + Light intensity + + + + Light intensity changed + + + + Media controllers + + + + Start playback + + + + Stop playback + + + + Pause playback + + + + Skip back + + + + Skip next + + LightsDeviceListPage @@ -2267,7 +2326,7 @@ Please try again. There are no scenes set up yet - 아직 설정된 시나리오(연출)가 없습니다 + 아직 설정된 시나리오(연출)가 없습니다 It appears there are no things set up either yet. In order to use scenes you need to add some things first. @@ -2315,6 +2374,26 @@ Please try again. In order for your %1 system to be useful, go ahead and add some things. %1 시스템이 유용하려면 먼저 몇 가지를 추가하십시오. + + Groups + + + + There are no scenes set up yet. + + + + My groups + + + + There are no groups set up yet. + + + + 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. + + ManualConnectPage diff --git a/nymea-app/ui/components/ClosableArrowAnimation.qml b/nymea-app/ui/components/ClosableArrowAnimation.qml deleted file mode 100644 index 9c36e13c..00000000 --- a/nymea-app/ui/components/ClosableArrowAnimation.qml +++ /dev/null @@ -1,5 +0,0 @@ -import QtQuick 2.0 - -Item { - -} diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 3366e7cd..1c2d34ad 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -117,7 +117,7 @@ Page { Component { id: addToGroupDialog MeaDialog { - title: qsTr("Configure groups") + title: qsTr("Groups for %1").arg(root.device.name) headerIcon: "../images/view-grid-symbolic.svg" RowLayout {