diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index 073488bd..eb0da28f 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -21,6 +21,8 @@ #include "devicemanager.h" #include "engine.h" #include "jsonrpc/jsontypes.h" +#include "types/browseritems.h" +#include "types/browseritem.h" #include DeviceManager::DeviceManager(JsonRpcClient* jsonclient, QObject *parent) : @@ -312,7 +314,7 @@ void DeviceManager::editDeviceResponse(const QVariantMap ¶ms) void DeviceManager::executeActionResponse(const QVariantMap ¶ms) { -// qDebug() << "Execute Action response" << params; + qDebug() << "Execute Action response" << params; emit executeActionReply(params); } @@ -425,3 +427,156 @@ int DeviceManager::executeAction(const QUuid &deviceId, const QUuid &actionTypeI return m_jsonClient->sendCommand("Actions.ExecuteAction", p, this, "executeActionResponse"); } + +BrowserItems *DeviceManager::browseDevice(const QUuid &deviceId, const QString &itemId) +{ + QVariantMap params; + params.insert("deviceId", deviceId.toString()); + params.insert("itemId", itemId); + int id = m_jsonClient->sendCommand("Devices.BrowseDevice", params, this, "browseDeviceResponse"); + + // Intentionally not parented. The caller takes ownership and needs to destroy when not needed any more. + BrowserItems *itemModel = new BrowserItems(deviceId, itemId); + itemModel->setBusy(true); + QPointer itemModelPtr(itemModel); + m_browsingRequests.insert(id, itemModelPtr); + + return itemModel; +} + +void DeviceManager::refreshBrowserItems(BrowserItems *browserItems) +{ + QVariantMap params; + params.insert("deviceId", browserItems->deviceId().toString()); + params.insert("itemId", browserItems->itemId()); + int id = m_jsonClient->sendCommand("Devices.BrowseDevice", params, this, "browseDeviceResponse"); + + // Intentionally not parented. The caller takes ownership and needs to destroy when not needed any more. + browserItems->setBusy(true); + QPointer itemModelPtr(browserItems); + m_browsingRequests.insert(id, browserItems); +} + +BrowserItem *DeviceManager::browserItem(const QUuid &deviceId, const QString &itemId) +{ + QVariantMap params; + params.insert("deviceId", deviceId.toString()); + params.insert("itemId", itemId); + int id = m_jsonClient->sendCommand("Devices.GetBrowserItem", params, this, "browserItemResponse"); + + // Intentionally not parented. The caller takes ownership and needs to destroy when not needed any more. + BrowserItem *item = new BrowserItem(itemId); + QPointer itemPtr(item); + m_browserDetailsRequests.insert(id, itemPtr); + + return item; +} + +void DeviceManager::browseDeviceResponse(const QVariantMap ¶ms) +{ + qDebug() << "Browsing response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented)); + int id = params.value("id").toInt(); + if (!m_browsingRequests.contains(id)) { + qWarning() << "Received a browsing reply for an id we don't know."; + return; + } + + QPointer itemModel = m_browsingRequests.take(id); + if (!itemModel) { + qDebug() << "BrowserItems model seems to have disappeared. Discarding browsing result."; + return; + } + + QList itemsToRemove = itemModel->list(); + + foreach (const QVariant &itemVariant, params.value("params").toMap().value("items").toList()) { + QVariantMap itemMap = itemVariant.toMap(); + QString itemId = itemMap.value("id").toString(); + BrowserItem *item = itemModel->getBrowserItem(itemId); + if (!item) { + item = new BrowserItem(itemId, this); + itemModel->addBrowserItem(item); + } + item->setDisplayName(itemMap.value("displayName").toString()); + item->setDescription(itemMap.value("description").toString()); + item->setIcon(itemMap.value("icon").toString()); + item->setThumbnail(itemMap.value("thumbnail").toString()); + item->setExecutable(itemMap.value("executable").toBool()); + item->setBrowsable(itemMap.value("browsable").toBool()); + item->setDisabled(itemMap.value("disabled").toBool()); + item->setActionTypeIds(itemMap.value("actionTypeIds").toStringList()); + + item->setMediaIcon(itemMap.value("mediaIcon").toString()); + + if (itemsToRemove.contains(item)) { + itemsToRemove.removeAll(item); + } + } + + while (!itemsToRemove.isEmpty()) { + BrowserItem *item = itemsToRemove.takeFirst(); + itemModel->removeItem(item); + } + + itemModel->setBusy(false); +} + +void DeviceManager::browserItemResponse(const QVariantMap ¶ms) +{ + qDebug() << "Browser item details response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented)); + int id = params.value("id").toInt(); + if (!m_browserDetailsRequests.contains(id)) { + qWarning() << "Received a browser item details reply for an id we don't know."; + return; + } + + QPointer item = m_browserDetailsRequests.take(id); + if (!item) { + qDebug() << "BrowserItem seems to have disappeared. Discarding browser item details result."; + return; + } + + QVariantMap itemMap = params.value("params").toMap().value("item").toMap(); + item->setDisplayName(itemMap.value("displayName").toString()); + item->setDescription(itemMap.value("description").toString()); + item->setIcon(itemMap.value("icon").toString()); + item->setThumbnail(itemMap.value("thumbnail").toString()); + item->setExecutable(itemMap.value("executable").toBool()); + item->setBrowsable(itemMap.value("browsable").toBool()); + item->setDisabled(itemMap.value("disabled").toBool()); + item->setActionTypeIds(itemMap.value("actionTypeIds").toStringList()); + + item->setMediaIcon(itemMap.value("mediaIcon").toString()); +} + +int DeviceManager::executeBrowserItem(const QUuid &deviceId, const QString &itemId) +{ + QVariantMap params; + params.insert("deviceId", deviceId); + params.insert("itemId", itemId); + return m_jsonClient->sendCommand("Actions.ExecuteBrowserItem", params, this, "executeBrowserItemResponse"); +} + +void DeviceManager::executeBrowserItemResponse(const QVariantMap ¶ms) +{ + qDebug() << "Execute Browser Item finished" << params; + emit executeBrowserItemReply(params); +} + +int DeviceManager::executeBrowserItemAction(const QUuid &deviceId, const QString &itemId, const QUuid &actionTypeId, const QVariantList ¶ms) +{ + QVariantMap data; + data.insert("deviceId", deviceId); + data.insert("itemId", itemId); + data.insert("actionTypeId", actionTypeId); + data.insert("params", params); + qDebug() << "params:" << params; + return m_jsonClient->sendCommand("Actions.ExecuteBrowserItemAction", data, this, "executeBrowserItemActionResponse"); +} + +void DeviceManager::executeBrowserItemActionResponse(const QVariantMap ¶ms) +{ + qDebug() << "Execute Browser Item Action finished" << params; + emit executeBrowserItemActionReply(params); +} + diff --git a/libnymea-app-core/devicemanager.h b/libnymea-app-core/devicemanager.h index 6257a770..57ae3777 100644 --- a/libnymea-app-core/devicemanager.h +++ b/libnymea-app-core/devicemanager.h @@ -31,6 +31,9 @@ #include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonrpcclient.h" +class BrowserItem; +class BrowserItems; + class DeviceManager : public JsonHandler { Q_OBJECT @@ -73,6 +76,11 @@ public: Q_INVOKABLE void reconfigureDevice(const QUuid &deviceId, const QVariantList &deviceParams); Q_INVOKABLE void reconfigureDiscoveredDevice(const QUuid &deviceId, const QUuid &deviceDescriptorId); Q_INVOKABLE int executeAction(const QUuid &deviceId, const QUuid &actionTypeId, const QVariantList ¶ms = QVariantList()); + Q_INVOKABLE BrowserItems* browseDevice(const QUuid &deviceId, const QString &itemId = QString()); + Q_INVOKABLE void refreshBrowserItems(BrowserItems *browserItems); + Q_INVOKABLE BrowserItem* browserItem(const QUuid &deviceId, const QString &itemId); + Q_INVOKABLE int executeBrowserItem(const QUuid &deviceId, const QString &itemId); + Q_INVOKABLE int executeBrowserItemAction(const QUuid &deviceId, const QString &itemId, const QUuid &actionTypeId, const QVariantList ¶ms = QVariantList()); private: Q_INVOKABLE void notificationReceived(const QVariantMap &data); @@ -89,6 +97,10 @@ private: Q_INVOKABLE void editDeviceResponse(const QVariantMap ¶ms); Q_INVOKABLE void executeActionResponse(const QVariantMap ¶ms); Q_INVOKABLE void reconfigureDeviceResponse(const QVariantMap ¶ms); + Q_INVOKABLE void browseDeviceResponse(const QVariantMap ¶ms); + Q_INVOKABLE void browserItemResponse(const QVariantMap ¶ms); + Q_INVOKABLE void executeBrowserItemResponse(const QVariantMap ¶ms); + Q_INVOKABLE void executeBrowserItemActionResponse(const QVariantMap ¶ms); public slots: void savePluginConfig(const QUuid &pluginId); @@ -102,6 +114,8 @@ signals: void editDeviceReply(const QVariantMap ¶ms); void reconfigureDeviceReply(const QVariantMap ¶ms); void executeActionReply(const QVariantMap ¶ms); + void executeBrowserItemReply(const QVariantMap ¶ms); + void executeBrowserItemActionReply(const QVariantMap ¶ms); void fetchingDataChanged(); void notificationReceived(const QString &deviceId, const QString &eventTypeId, const QVariantList ¶ms); @@ -117,6 +131,9 @@ private: JsonRpcClient *m_jsonClient = nullptr; + QHash > m_browsingRequests; + QHash > m_browserDetailsRequests; + }; #endif // DEVICEMANAGER_H diff --git a/libnymea-app-core/jsonrpc/jsontypes.cpp b/libnymea-app-core/jsonrpc/jsontypes.cpp index 9c56b2dc..2c4374a3 100644 --- a/libnymea-app-core/jsonrpc/jsontypes.cpp +++ b/libnymea-app-core/jsonrpc/jsontypes.cpp @@ -75,6 +75,7 @@ DeviceClass *JsonTypes::unpackDeviceClass(const QVariantMap &deviceClassMap, QOb deviceClass->setDisplayName(deviceClassMap.value("displayName").toString()); deviceClass->setId(deviceClassMap.value("id").toUuid()); deviceClass->setVendorId(deviceClassMap.value("vendorId").toUuid()); + deviceClass->setBrowsable(deviceClassMap.value("browsable").toBool()); QVariantList createMethodsList = deviceClassMap.value("createMethods").toList(); QStringList createMethods; foreach (QVariant method, createMethodsList) { @@ -126,6 +127,13 @@ DeviceClass *JsonTypes::unpackDeviceClass(const QVariantMap &deviceClassMap, QOb } deviceClass->setActionTypes(actionTypes); + // BrowserItemActionTypes + ActionTypes *browserItemActionTypes = new ActionTypes(deviceClass); + foreach (QVariant actionType, deviceClassMap.value("browserItemActionTypes").toList()) { + browserItemActionTypes->addActionType(JsonTypes::unpackActionType(actionType.toMap(), actionTypes)); + } + deviceClass->setBrowserItemActionTypes(browserItemActionTypes); + return deviceClass; } @@ -310,6 +318,9 @@ QVariantList JsonTypes::packRuleActions(RuleActions *ruleActions) if (!ra->actionTypeId().isNull() && !ra->deviceId().isNull()) { ruleAction.insert("deviceId", ra->deviceId()); ruleAction.insert("actionTypeId", ra->actionTypeId()); + } else if (!ra->deviceId().isNull() && !ra->browserItemId().isEmpty()) { + ruleAction.insert("deviceId", ra->deviceId()); + ruleAction.insert("browserItemId", ra->browserItemId()); } else { ruleAction.insert("interface", ra->interfaceName()); ruleAction.insert("interfaceAction", ra->interfaceAction()); diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index 7e2d2a46..75f76421 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -31,6 +31,8 @@ #include "types/statedescriptor.h" #include "types/stateevaluator.h" #include "types/stateevaluators.h" +#include "types/browseritems.h" +#include "types/browseritem.h" #include "models/logsmodel.h" #include "models/logsmodelng.h" #include "models/valuelogsproxymodel.h" @@ -109,6 +111,9 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "State", "Can't create this in QML. Get it from the States."); qmlRegisterUncreatableType(uri, 1, 0, "States", "Can't create this in QML. Get it from the Device."); + qmlRegisterUncreatableType(uri, 1, 0, "BrowserItems", "Can't create this in QML. Get it from DeviceManager."); + qmlRegisterUncreatableType(uri, 1, 0, "BrowserItem", "Can't create this in QML. Get it from BroweserItems."); + qmlRegisterUncreatableType(uri, 1, 0, "Vendor", "Can't create this in QML. Get it from the Vendors."); qmlRegisterUncreatableType(uri, 1, 0, "Vendors", "Can't create this in QML. Get it from the DeviceManager."); qmlRegisterType(uri, 1, 0, "VendorsProxy"); diff --git a/libnymea-app-core/rulemanager.cpp b/libnymea-app-core/rulemanager.cpp index 8ea580ca..885232cd 100644 --- a/libnymea-app-core/rulemanager.cpp +++ b/libnymea-app-core/rulemanager.cpp @@ -286,6 +286,9 @@ RuleAction *RuleManager::parseRuleAction(const QVariantMap &ruleAction) if (ruleAction.contains("deviceId") && ruleAction.contains("actionTypeId")) { ret->setDeviceId(ruleAction.value("deviceId").toUuid()); ret->setActionTypeId(ruleAction.value("actionTypeId").toUuid()); + } else if (ruleAction.contains("deviceId") && ruleAction.contains("browserItemId")) { + ret->setDeviceId(ruleAction.value("deviceId").toUuid()); + ret->setBrowserItemId(ruleAction.value("browserItemId").toString()); } else { ret->setInterfaceName(ruleAction.value("interface").toString()); ret->setInterfaceAction(ruleAction.value("interfaceAction").toString()); diff --git a/libnymea-common/libnymea-common.pro b/libnymea-common/libnymea-common.pro index 7b90c204..8e9fbaea 100644 --- a/libnymea-common/libnymea-common.pro +++ b/libnymea-common/libnymea-common.pro @@ -8,6 +8,8 @@ QT -= gui QT += network HEADERS += \ + types/browseritem.h \ + types/browseritems.h \ types/networkdevice.h \ types/networkdevices.h \ types/package.h \ @@ -62,6 +64,8 @@ HEADERS += \ types/wirelessaccesspoints.h \ SOURCES += \ + types/browseritem.cpp \ + types/browseritems.cpp \ types/networkdevice.cpp \ types/networkdevices.cpp \ types/package.cpp \ diff --git a/libnymea-common/types/browseritem.cpp b/libnymea-common/types/browseritem.cpp new file mode 100644 index 00000000..d1db69a5 --- /dev/null +++ b/libnymea-common/types/browseritem.cpp @@ -0,0 +1,130 @@ +#include "browseritem.h" + +BrowserItem::BrowserItem(const QString &id, QObject *parent): + QObject(parent), + m_id(id) +{ + +} + +QString BrowserItem::id() const +{ + return m_id; +} + +QString BrowserItem::displayName() const +{ + return m_displayName; +} + +void BrowserItem::setDisplayName(const QString &displayName) +{ + if (m_displayName != displayName) { + m_displayName = displayName; + emit displayNameChanged(); + } +} + +QString BrowserItem::description() const +{ + return m_description; +} + +void BrowserItem::setDescription(const QString &description) +{ + if (m_description != description) { + m_description = description; + emit descriptionChanged(); + } +} + +QString BrowserItem::icon() const +{ + return m_icon; +} + +void BrowserItem::setIcon(const QString &icon) +{ + if (m_icon != icon) { + m_icon = icon; + emit iconChanged(); + } +} + +QString BrowserItem::thumbnail() const +{ + return m_thumbnail; +} + +void BrowserItem::setThumbnail(const QString &thumbnail) +{ + if (m_thumbnail != thumbnail) { + m_thumbnail = thumbnail; + emit thumbnailChanged(); + } +} + +bool BrowserItem::executable() const +{ + return m_executable; +} + +void BrowserItem::setExecutable(bool executable) +{ + if (m_executable != executable) { + m_executable = executable; + emit executableChanged(); + } +} + +bool BrowserItem::browsable() const +{ + return m_browsable; +} + +void BrowserItem::setBrowsable(bool browsable) +{ + if (m_browsable != browsable) { + m_browsable = browsable; + emit browsableChanged(); + } +} + +bool BrowserItem::disabled() const +{ + return m_disabled; +} + +void BrowserItem::setDisabled(bool disabled) +{ + if (m_disabled != disabled) { + m_disabled = disabled; + emit disabledChanged(); + } +} + +QStringList BrowserItem::actionTypeIds() const +{ + return m_actionTypeIds; +} + +void BrowserItem::setActionTypeIds(const QStringList &actionTypeIds) +{ + if (m_actionTypeIds != actionTypeIds) { + m_actionTypeIds = actionTypeIds; + emit actionTypeIdsChanged(); + } +} + +QString BrowserItem::mediaIcon() const +{ + return m_mediaIcon; +} + +void BrowserItem::setMediaIcon(const QString &mediaIcon) +{ + if (m_mediaIcon != mediaIcon) { + m_mediaIcon = mediaIcon; + emit mediaIconChanged(); + } +} diff --git a/libnymea-common/types/browseritem.h b/libnymea-common/types/browseritem.h new file mode 100644 index 00000000..60b8a49d --- /dev/null +++ b/libnymea-common/types/browseritem.h @@ -0,0 +1,80 @@ +#ifndef BROWSERITEM_H +#define BROWSERITEM_H + +#include +#include + +class BrowserItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QUuid id READ id CONSTANT) + Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QString thumbnail READ thumbnail NOTIFY thumbnailChanged) + Q_PROPERTY(bool executable READ executable NOTIFY executableChanged) + Q_PROPERTY(bool browsable READ browsable NOTIFY browsableChanged) + Q_PROPERTY(bool disabled READ disabled NOTIFY disabledChanged) + Q_PROPERTY(QStringList actionTypeIds READ actionTypeIds NOTIFY actionTypeIdsChanged) + + Q_PROPERTY(QString mediaIcon READ mediaIcon NOTIFY mediaIconChanged) + +public: + explicit BrowserItem(const QString &id, QObject *parent = nullptr); + + QString id() const; + + QString displayName() const; + void setDisplayName(const QString &displayName); + + QString description() const; + void setDescription(const QString &description); + + QString icon() const; + void setIcon(const QString &icon); + + QString thumbnail() const; + void setThumbnail(const QString &thumbnail); + + bool executable() const; + void setExecutable(bool executable); + + bool browsable() const; + void setBrowsable(bool browsable); + + bool disabled() const; + void setDisabled(bool disabled); + + QStringList actionTypeIds() const; + void setActionTypeIds(const QStringList &actionTypeIds); + + QString mediaIcon() const; + void setMediaIcon(const QString &mediaIcon); + +signals: + void displayNameChanged(); + void descriptionChanged(); + void iconChanged(); + void thumbnailChanged(); + void executableChanged(); + void browsableChanged(); + void disabledChanged(); + void actionTypeIdsChanged(); + + void mediaIconChanged(); + +private: + QString m_id; + QString m_displayName; + QString m_description; + QString m_icon; + QString m_thumbnail; + bool m_executable = false; + bool m_browsable = false; + bool m_disabled = false; + QStringList m_actionTypeIds; + + QString m_mediaIcon; +}; + +#endif // BROWSERITEM_H diff --git a/libnymea-common/types/browseritems.cpp b/libnymea-common/types/browseritems.cpp new file mode 100644 index 00000000..f0a64219 --- /dev/null +++ b/libnymea-common/types/browseritems.cpp @@ -0,0 +1,134 @@ +#include "browseritems.h" +#include "browseritem.h" + +#include + +BrowserItems::BrowserItems(const QUuid &deviceId, const QString &itemId, QObject *parent): + QAbstractListModel (parent), + m_deviceId(deviceId), + m_itemId(itemId) +{ + +} + +BrowserItems::~BrowserItems() +{ + qDebug() << "Deleting BrowserItems"; +} + +QUuid BrowserItems::deviceId() const +{ + return m_deviceId; +} + +QString BrowserItems::itemId() const +{ + return m_itemId; +} + +bool BrowserItems::busy() const +{ + return m_busy; +} + +int BrowserItems::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_list.count(); +} + +QVariant BrowserItems::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleId: + return m_list.at(index.row())->id(); + case RoleDisplayName: + return m_list.at(index.row())->displayName(); + case RoleDescription: + return m_list.at(index.row())->description(); + case RoleIcon: + return m_list.at(index.row())->icon(); + case RoleThumbnail: + return m_list.at(index.row())->thumbnail(); + case RoleExecutable: + return m_list.at(index.row())->executable(); + case RoleBrowsable: + return m_list.at(index.row())->browsable(); + case RoleActionTypeIds: + return m_list.at(index.row())->actionTypeIds(); + case RoleDisabled: + return m_list.at(index.row())->disabled(); + + case RoleMediaIcon: + return m_list.at(index.row())->mediaIcon(); + } + return QVariant(); +} + +QHash BrowserItems::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleDisplayName, "displayName"); + roles.insert(RoleDescription, "description"); + roles.insert(RoleIcon, "icon"); + roles.insert(RoleThumbnail, "thumbnail"); + roles.insert(RoleExecutable, "executable"); + roles.insert(RoleBrowsable, "browsable"); + roles.insert(RoleDisabled, "disabled"); + roles.insert(RoleActionTypeIds, "actionTypeIds"); + + roles.insert(RoleMediaIcon, "mediaIcon"); + return roles; +} + +void BrowserItems::addBrowserItem(BrowserItem *browserItem) +{ + browserItem->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(browserItem); + endInsertRows(); + emit countChanged(); +} + +void BrowserItems::removeItem(BrowserItem *browserItem) +{ + int idx = m_list.indexOf(browserItem); + if (idx < 0) { + return; + } + beginRemoveRows(QModelIndex(), idx, idx); + m_list.takeAt(idx)->deleteLater(); + endRemoveRows(); +} + +QList BrowserItems::list() const +{ + return m_list; +} + +void BrowserItems::setBusy(bool busy) +{ + if (m_busy != busy) { + m_busy = busy; + emit busyChanged(); + } +} + +BrowserItem *BrowserItems::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +BrowserItem *BrowserItems::getBrowserItem(const QString &itemId) +{ + foreach (BrowserItem *item, m_list) { + if (item->id() == itemId) { + return item; + } + } + return nullptr; +} diff --git a/libnymea-common/types/browseritems.h b/libnymea-common/types/browseritems.h new file mode 100644 index 00000000..e4d2ca6f --- /dev/null +++ b/libnymea-common/types/browseritems.h @@ -0,0 +1,66 @@ +#ifndef BROWSERITEMS_H +#define BROWSERITEMS_H + +#include +#include + +class BrowserItem; + +class BrowserItems: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) +public: + enum Roles { + RoleId, + RoleDisplayName, + RoleDescription, + RoleIcon, + RoleThumbnail, + RoleBrowsable, + RoleExecutable, + RoleDisabled, + RoleActionTypeIds, + + RoleMediaIcon, + }; + Q_ENUM(Roles) + + explicit BrowserItems(const QUuid &deviceId, const QString &itemId, QObject *parent = nullptr); + virtual ~BrowserItems() override; + + QUuid deviceId() const; + QString itemId() const; + + bool busy() const; + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + virtual QHash roleNames() const override; + + virtual void addBrowserItem(BrowserItem *browserItem); + + void removeItem(BrowserItem *browserItem); + + QList list() const; + void setBusy(bool busy); + + Q_INVOKABLE virtual BrowserItem* get(int index) const; + Q_INVOKABLE virtual BrowserItem* getBrowserItem(const QString &itemId); + +// void clear(); + +signals: + void countChanged(); + void busyChanged(); + +protected: + bool m_busy = false; + QList m_list; + + QUuid m_deviceId; + QString m_itemId; +}; + +#endif // BROWSERITEMS_H diff --git a/libnymea-common/types/deviceclass.cpp b/libnymea-common/types/deviceclass.cpp index 029d9047..6f13d730 100644 --- a/libnymea-common/types/deviceclass.cpp +++ b/libnymea-common/types/deviceclass.cpp @@ -167,6 +167,16 @@ QString DeviceClass::baseInterface() const } +bool DeviceClass::browsable() const +{ + return m_browsable; +} + +void DeviceClass::setBrowsable(bool browsable) +{ + m_browsable = browsable; +} + ParamTypes *DeviceClass::paramTypes() const { return m_paramTypes; @@ -251,6 +261,20 @@ void DeviceClass::setActionTypes(ActionTypes *actionTypes) emit actionTypesChanged(); } +ActionTypes *DeviceClass::browserItemActionTypes() const +{ + return m_browserItemActionTypes; +} + +void DeviceClass::setBrowserItemActionTypes(ActionTypes *browserActionTypes) +{ + if (m_browserItemActionTypes) { + m_browserItemActionTypes->deleteLater(); + } + m_browserItemActionTypes = browserActionTypes; + emit browserItemActionTypesChanged(); +} + bool DeviceClass::hasActionType(const QString &actionTypeId) { foreach (ActionType *actionType, m_actionTypes->actionTypes()) { diff --git a/libnymea-common/types/deviceclass.h b/libnymea-common/types/deviceclass.h index 21ccb3f7..686e613e 100644 --- a/libnymea-common/types/deviceclass.h +++ b/libnymea-common/types/deviceclass.h @@ -46,12 +46,14 @@ class DeviceClass : public QObject Q_PROPERTY(SetupMethod setupMethod READ setupMethod CONSTANT) Q_PROPERTY(QStringList interfaces READ interfaces CONSTANT) Q_PROPERTY(QString baseInterface READ baseInterface CONSTANT) + Q_PROPERTY(bool browsable READ browsable CONSTANT) Q_PROPERTY(ParamTypes *paramTypes READ paramTypes NOTIFY paramTypesChanged) Q_PROPERTY(ParamTypes *settingsTypes READ settingsTypes NOTIFY settingsTypesChanged) Q_PROPERTY(ParamTypes *discoveryParamTypes READ discoveryParamTypes NOTIFY discoveryParamTypesChanged) Q_PROPERTY(StateTypes *stateTypes READ stateTypes NOTIFY stateTypesChanged) Q_PROPERTY(EventTypes *eventTypes READ eventTypes NOTIFY eventTypesChanged) Q_PROPERTY(ActionTypes *actionTypes READ actionTypes NOTIFY actionTypesChanged) + Q_PROPERTY(ActionTypes *browserItemActionTypes READ browserItemActionTypes NOTIFY browserItemActionTypesChanged) public: @@ -91,6 +93,9 @@ public: QString baseInterface() const; + bool browsable() const; + void setBrowsable(bool browsable); + ParamTypes *paramTypes() const; void setParamTypes(ParamTypes *paramTypes); @@ -109,8 +114,20 @@ public: ActionTypes *actionTypes() const; void setActionTypes(ActionTypes *actionTypes); + ActionTypes *browserItemActionTypes() const; + void setBrowserItemActionTypes(ActionTypes *browserActionTypes); + Q_INVOKABLE bool hasActionType(const QString &actionTypeId); +signals: + void paramTypesChanged(); + void settingsTypesChanged(); + void discoveryParamTypesChanged(); + void stateTypesChanged(); + void eventTypesChanged(); + void actionTypesChanged(); + void browserItemActionTypesChanged(); + private: QUuid m_id; QUuid m_vendorId; @@ -120,6 +137,7 @@ private: QStringList m_createMethods; SetupMethod m_setupMethod; QStringList m_interfaces; + bool m_browsable = false; ParamTypes *m_paramTypes = nullptr; ParamTypes *m_settingsTypes = nullptr; @@ -127,13 +145,6 @@ private: StateTypes *m_stateTypes = nullptr; EventTypes *m_eventTypes = nullptr; ActionTypes *m_actionTypes = nullptr; - -signals: - void paramTypesChanged(); - void settingsTypesChanged(); - void discoveryParamTypesChanged(); - void stateTypesChanged(); - void eventTypesChanged(); - void actionTypesChanged(); + ActionTypes *m_browserItemActionTypes = nullptr; }; #endif // DEVICECLASS_H diff --git a/libnymea-common/types/ruleaction.cpp b/libnymea-common/types/ruleaction.cpp index acb2b997..c5ca84f0 100644 --- a/libnymea-common/types/ruleaction.cpp +++ b/libnymea-common/types/ruleaction.cpp @@ -62,6 +62,19 @@ void RuleAction::setInterfaceAction(const QString &interfaceAction) } } +QString RuleAction::browserItemId() const +{ + return m_browserItemId; +} + +void RuleAction::setBrowserItemId(const QString &browserItemId) +{ + if (m_browserItemId != browserItemId) { + m_browserItemId = browserItemId; + emit browserItemIdChanged(); + } +} + RuleActionParams *RuleAction::ruleActionParams() const { return m_ruleActionParams; @@ -72,6 +85,7 @@ RuleAction *RuleAction::clone() const RuleAction *ret = new RuleAction(); ret->setDeviceId(deviceId()); ret->setActionTypeId(actionTypeId()); + ret->setBrowserItemId(browserItemId()); ret->setInterfaceName(interfaceName()); ret->setInterfaceAction(interfaceAction()); for (int i = 0; i < ruleActionParams()->rowCount(); i++) { @@ -88,6 +102,7 @@ bool RuleAction::operator==(RuleAction *other) const COMPARE(m_actionTypeId, other->actionTypeId()); COMPARE(m_interfaceName, other->interfaceName()); COMPARE(m_interfaceAction, other->interfaceAction()); + COMPARE(m_browserItemId, other->browserItemId()); COMPARE_PTR(m_ruleActionParams, other->ruleActionParams()); return true; } diff --git a/libnymea-common/types/ruleaction.h b/libnymea-common/types/ruleaction.h index 9b31c5ec..821424d0 100644 --- a/libnymea-common/types/ruleaction.h +++ b/libnymea-common/types/ruleaction.h @@ -13,6 +13,7 @@ class RuleAction : public QObject Q_PROPERTY(QUuid actionTypeId READ actionTypeId WRITE setActionTypeId NOTIFY actionTypeIdChanged) Q_PROPERTY(QString interfaceName READ interfaceName WRITE setInterfaceName NOTIFY interfaceNameChanged) Q_PROPERTY(QString interfaceAction READ interfaceAction WRITE setInterfaceAction NOTIFY interfaceActionChanged) + Q_PROPERTY(QString browserItemId READ browserItemId WRITE setBrowserItemId NOTIFY browserItemIdChanged) Q_PROPERTY(RuleActionParams* ruleActionParams READ ruleActionParams CONSTANT) public: @@ -30,6 +31,9 @@ public: QString interfaceAction() const; void setInterfaceAction(const QString &interfaceAction); + QString browserItemId() const; + void setBrowserItemId(const QString &browserItemId); + RuleActionParams* ruleActionParams() const; RuleAction *clone() const; @@ -40,12 +44,14 @@ signals: void actionTypeIdChanged(); void interfaceNameChanged(); void interfaceActionChanged(); + bool browserItemIdChanged(); private: QUuid m_deviceId; QUuid m_actionTypeId; QString m_interfaceName; QString m_interfaceAction; + QString m_browserItemId; RuleActionParams *m_ruleActionParams; }; diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 2ce38fb7..03374bf9 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -181,5 +181,32 @@ ui/images/lock-closed.svg ui/images/lock-open.svg ui/images/system-update.svg + ui/images/folder-symbolic.svg + ui/images/browser/BrowserIconFile.svg + ui/images/browser/BrowserIconFolder.svg + ui/images/browser/MediaBrowserIconSpotify.svg + ui/images/browser/MediaBrowserIconAirable.svg + ui/images/browser/MediaBrowserIconAmazon.svg + ui/images/browser/MediaBrowserIconAux.svg + ui/images/browser/MediaBrowserIconLibrary.svg + ui/images/browser/MediaBrowserIconMusicLibrary.svg + ui/images/browser/MediaBrowserIconNetwork.svg + ui/images/browser/MediaBrowserIconPictureLibrary.svg + ui/images/browser/MediaBrowserIconPlaylist.svg + ui/images/browser/MediaBrowserIconRecentlyPlayed.svg + ui/images/browser/MediaBrowserIconSiriusXM.svg + ui/images/browser/MediaBrowserIconTidal.svg + ui/images/browser/MediaBrowserIconTuneIn.svg + ui/images/browser/MediaBrowserIconUSB.svg + ui/images/browser/MediaBrowserIconVideoLibrary.svg + ui/images/browser/MediaBrowserIconVTuner.svg + ui/images/browser/MediaBrowserIconDisk.svg + ui/images/browser/BrowserIconFavorites.svg + ui/images/browser/BrowserIconMusic.svg + ui/images/browser/BrowserIconPicture.svg + ui/images/browser/BrowserIconVideo.svg + ui/images/browser/BrowserIconApplication.svg + ui/images/browser/BrowserIconDocument.svg + ui/images/browser/BrowserIconPackage.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index b70bac89..7e21f360 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -184,5 +184,12 @@ ui/components/UpdateRunningOverlay.qml ui/connection/SetupWizard.qml ui/system/NetworkSettingsPage.qml + ui/devicepages/DeviceBrowserPage.qml + ui/devicelistpages/MediaDeviceListPage.qml + ui/components/MediaControls.qml + ui/components/MediaArtworkImage.qml + ui/components/BrowserContextMenu.qml + ui/magic/SelectBrowserItemActionPage.qml + ui/delegates/BrowserItemDelegate.qml diff --git a/nymea-app/ui/MagicPage.qml b/nymea-app/ui/MagicPage.qml index 74703ded..049b44aa 100644 --- a/nymea-app/ui/MagicPage.qml +++ b/nymea-app/ui/MagicPage.qml @@ -77,8 +77,8 @@ Page { delegate: NymeaListItemDelegate { id: ruleDelegate width: parent.width - iconName: "../images/" + (model.executable ? (iconTag ? iconTag.value : "slideshow") : "magic") + ".svg" - iconColor: model.executable ? (colorTag ? colorTag.value : app.accentColor) : !model.enabled ? "red" : (model.active ? app.accentColor : "grey") + iconName: "../images/" + (model.executable ? (iconTag.value.length > 0 ? iconTag.value : "slideshow") : "magic") + ".svg" + iconColor: model.executable ? (colorTag.value.length > 0 ? colorTag.value : app.accentColor) : !model.enabled ? "red" : (model.active ? app.accentColor : "grey") text: model.name canDelete: true diff --git a/nymea-app/ui/components/BrowserContextMenu.qml b/nymea-app/ui/components/BrowserContextMenu.qml new file mode 100644 index 00000000..96db7d8a --- /dev/null +++ b/nymea-app/ui/components/BrowserContextMenu.qml @@ -0,0 +1,93 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../delegates" + +MeaDialog { + id: root + + property Device device + property string itemId + property alias actionTypeIds: actionListView.model + + signal activated(var actionTypeId, var params) + + standardButtons: Dialog.NoButton + + StackView { + id: stackView + Layout.fillWidth: true + Layout.minimumHeight: actionListView.implicitHeight + + property var actionTypeId + + initialItem: ListView { + id: actionListView + width: parent.width + implicitHeight: contentHeight + + interactive: contentHeight > height + clip: true + delegate: NymeaListItemDelegate { + width: parent.width + text: actionType.displayName + progressive: false + property ActionType actionType: root.device.deviceClass.browserItemActionTypes.getActionType(modelData) + onClicked: { + var hasParams = actionType.paramTypes.count > 0 + if (hasParams) { + stackView.actionTypeId = actionType.id + stackView.push(paramComponent, {model: actionType.paramTypes}) + return; + } + + var params = [] + root.activated(actionType.id, params) + root.accept() + } + } + } + + Component { + id: paramComponent + + Repeater { + id: paramListView + + delegate: ParamDelegate { + width: parent.width + paramType: paramListView.model.get(index) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + Button { + text: qsTr("Cancel") + onClicked: root.reject() + } + Item { Layout.fillWidth: true } + Button { + text: qsTr("OK") + visible: stackView.depth > 1 + onClicked: { + var params = [] + print("k", stackView.currentItem.count) + for (var i = 0; i < stackView.currentItem.count; i++) { + print("juhu", i) + var param = {} + param["paramTypeId"] = stackView.currentItem.itemAt(i).paramType.id + param["value"] = stackView.currentItem.itemAt(i).value + params.push(param) + } + print("have params", params.length) + root.activated(stackView.actionTypeId, params) + root.accept(); + } + } + } +} diff --git a/nymea-app/ui/components/MainPageTile.qml b/nymea-app/ui/components/MainPageTile.qml index 9b31d484..c5801426 100644 --- a/nymea-app/ui/components/MainPageTile.qml +++ b/nymea-app/ui/components/MainPageTile.qml @@ -9,6 +9,7 @@ Item { property alias iconName: colorIcon.name property alias fallbackIconName: fallbackIcon.name property alias iconColor: colorIcon.color + property alias backgroundImage: background.source property string text property bool disconnected: false property bool batteryCritical: false @@ -24,6 +25,21 @@ Item { Material.elevation: 1 padding: 0 + Image { + id: background + anchors.fill: parent + anchors.margins: 1 + z: -1 + fillMode: Image.PreserveAspectCrop +// horizontalAlignment: Image.AlignTop +// opacity: .5 +// Rectangle { +// anchors.fill: parent +// color: Material.background +// opacity: .5 +// } + } + contentItem: ItemDelegate { padding: 0; topPadding: 0; bottomPadding: 0 onClicked: root.clicked() @@ -35,6 +51,7 @@ Item { Layout.fillHeight: true Layout.margins: app.margins Item { + visible: background.status !== Image.Ready Layout.fillWidth: true Layout.fillHeight: true @@ -55,6 +72,7 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true + visible: background.status !== Image.Ready Label { id: label @@ -77,6 +95,11 @@ Item { Layout.preferredHeight: app.iconSize + app.margins * 2 visible: root.contentItem.length > 1 + Rectangle { + anchors.fill: parent + color: Material.background + } + Rectangle { anchors.fill: parent color: Material.foreground diff --git a/nymea-app/ui/components/MediaArtworkImage.qml b/nymea-app/ui/components/MediaArtworkImage.qml new file mode 100644 index 00000000..45fefba4 --- /dev/null +++ b/nymea-app/ui/components/MediaArtworkImage.qml @@ -0,0 +1,47 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 + +Item { + id: root + property Device device: null + + readonly property StateType artworkStateType: device ? device.deviceClass.stateTypes.findByName("artwork") : null + readonly property State artworkState: artworkStateType ? device.states.getState(artworkStateType.id) : null + + readonly property StateType playerTypeStateType: device ? device.deviceClass.stateTypes.findByName("playerType") : null + readonly property State playerTypeState: playerTypeStateType ? device.states.getState(playerTypeStateType.id) : null + + Pane { + Material.elevation: 2 + anchors.centerIn: parent + height: fallback.visible ? Math.min(parent.height, parent.width) : artworkImage.paintedHeight - 1 + width: fallback.visible ? Math.min(parent.height, parent.width) : artworkImage.paintedWidth - 1 + padding: 0 + contentItem: Rectangle { + color: "black" + } + } + + Image { + id: artworkImage + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: root.artworkState.value + visible: source !== "" + } + + ColorIcon { + id: fallback + anchors.centerIn: parent + width: Math.min(parent.height, parent.width) - app.margins * 2 + height: Math.min(parent.height, parent.width) - app.margins * 2 + + name: root.playerTypeState.value === "video" ? "../images/stock_video.svg" : "../images/stock_music.svg" + visible: artworkImage.status !== Image.Ready || artworkImage.source === "" +// color: app.primaryColor + color: "white" + } +} diff --git a/nymea-app/ui/components/MediaControls.qml b/nymea-app/ui/components/MediaControls.qml new file mode 100644 index 00000000..e52432cf --- /dev/null +++ b/nymea-app/ui/components/MediaControls.qml @@ -0,0 +1,76 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 + +RowLayout { + id: root + implicitHeight: iconSize + app.margins + + property Device device: null + property int iconSize: app.iconSize * 1.5 + + readonly property StateType playbackStateType: device ? device.deviceClass.stateTypes.findByName("playbackStatus") : null + readonly property State playbackState: playbackStateType ? device.states.getState(playbackStateType.id) : null + + function executeAction(actionName, params) { + var actionTypeId = device.deviceClass.actionTypes.findByName(actionName).id; + engine.deviceManager.executeAction(device.id, actionTypeId, params) + } + + Item { Layout.fillWidth: true } + ProgressButton { + Layout.preferredHeight: root.iconSize * .6 + Layout.preferredWidth: height + imageSource: "../images/media-skip-backward.svg" + longpressImageSource: "../images/media-seek-backward.svg" + enabled: root.playbackState.value !== "Stopped" + opacity: enabled ? 1 : .5 + + repeat: true + onClicked: { + root.executeAction("skipBack") + } + onLongpressed: { + root.executeAction("fastRewind") + } + } + Item { Layout.fillWidth: true } + ProgressButton { + Layout.preferredHeight: root,iconSize + Layout.preferredWidth: height + imageSource: root.playbackState && root.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" + longpressImageSource: "../images/media-playback-stop.svg" + longpressEnabled: root.playbackState.value !== "Stopped" + + onClicked: { + if (root.playbackState.value === "Playing") { + root.executeAction("pause") + } else { + root.executeAction("play") + } + } + + onLongpressed: { + root.executeAction("stop") + } + } + Item { Layout.fillWidth: true } + ProgressButton { + Layout.preferredHeight: root.iconSize * .6 + Layout.preferredWidth: height + imageSource: "../images/media-skip-forward.svg" + longpressImageSource: "../images/media-seek-forward.svg" + enabled: root.playbackState.value !== "Stopped" + opacity: enabled ? 1 : .5 + repeat: true + onClicked: { + root.executeAction("skipNext") + } + onLongpressed: { + root.executeAction("fastForward") + } + } + Item { Layout.fillWidth: true } +} diff --git a/nymea-app/ui/components/NymeaHeader.qml b/nymea-app/ui/components/NymeaHeader.qml index 1d090e55..a0035316 100644 --- a/nymea-app/ui/components/NymeaHeader.qml +++ b/nymea-app/ui/components/NymeaHeader.qml @@ -3,10 +3,9 @@ import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls.Material 2.1 -ToolBar { +Item { id: root - Material.elevation: 1 - + implicitHeight: toolBar.implicitHeight + infoPane.height property string text property alias backButtonVisible: backButton.visible property alias menuButtonVisible: menuButton.visible @@ -15,33 +14,103 @@ ToolBar { signal backPressed(); signal menuPressed(); - RowLayout { - id: layout - anchors { fill: parent; leftMargin: app.margins; rightMargin: app.margins } + function showInfo(text, isError, isSticky) { + if (isError === undefined) isError = false; + if (isSticky === undefined) isSticky = false; - HeaderButton { - id: menuButton - objectName: "headerMenuButton" - imageSource: "../images/navigation-menu.svg" - visible: false - onClicked: root.menuPressed(); - } + infoPane.text = text; + infoPane.isError = isError; + infoPane.isSticky = isSticky; - HeaderButton { - id: backButton - objectName: "backButton" - imageSource: "../images/back.svg" - onClicked: root.backPressed(); - } - Label { - id: label - Layout.fillWidth: true - Layout.fillHeight: true - verticalAlignment: Text.AlignVCenter - font.pixelSize: app.mediumFont - elide: Text.ElideRight - text: root.text - color: app.headerForegroundColor + if (!isSticky) { + infoPaneTimer.start(); } } + + ToolBar { + id: toolBar + Material.elevation: 3 + anchors { left: parent.left; top: parent.top; right: parent.right } + + RowLayout { + id: layout + anchors { fill: parent; leftMargin: app.margins; rightMargin: app.margins } + + HeaderButton { + id: menuButton + objectName: "headerMenuButton" + imageSource: "../images/navigation-menu.svg" + visible: false + onClicked: root.menuPressed(); + } + + HeaderButton { + id: backButton + objectName: "backButton" + imageSource: "../images/back.svg" + onClicked: root.backPressed(); + } + Label { + id: label + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: Text.AlignVCenter + font.pixelSize: app.mediumFont + elide: Text.ElideRight + text: root.text + color: app.headerForegroundColor + } + } + } + + Pane { + id: infoPane + Material.elevation: 1 + + property string text + property bool isError: false + property bool isSticky: false + property bool shown: isSticky || infoPaneTimer.running + + visible: height > 0 + height: shown ? contentRow.implicitHeight : 0 + Behavior on height { NumberAnimation {} } + anchors { left: parent.left; top: toolBar.bottom; right: parent.right } + + padding: 0 + contentItem: Rectangle { + color: infoPane.isError ? "red" : app.accentColor + implicitHeight: contentRow.implicitHeight + RowLayout { + id: contentRow + anchors { left: parent.left; top: parent.top; right: parent.right; leftMargin: app.margins; rightMargin: app.margins } + Item { + Layout.fillWidth: true + height: app.iconSize + } + + Label { + text: infoPane.text + visible: infoPane.alertState + font.pixelSize: app.smallFont + color: "white" + } + + ColorIcon { + height: app.iconSize / 2 + width: height + visible: true + color: "white" + name: "../images/dialog-warning-symbolic.svg" + } + } + } + } + + Timer { + id: infoPaneTimer + interval: 5000 + repeat: false + running: false + } } diff --git a/nymea-app/ui/components/NymeaListItemDelegate.qml b/nymea-app/ui/components/NymeaListItemDelegate.qml index 1ac96205..ea67ae96 100644 --- a/nymea-app/ui/components/NymeaListItemDelegate.qml +++ b/nymea-app/ui/components/NymeaListItemDelegate.qml @@ -15,31 +15,57 @@ SwipeDelegate { property bool prominentSubText: true property string iconName + property string fallbackIcon property int iconSize: app.iconSize property color iconColor: app.accentColor property alias iconKeyColor: icon.keyColor property alias secondaryIconName: secondaryIcon.name property alias secondaryIconColor: secondaryIcon.color property alias secondaryIconKeyColor: secondaryIcon.keyColor + property alias secondaryIconClickable: secondaryIconMouseArea.enabled property alias tertiaryIconName: tertiaryIcon.name property alias tertiaryIconColor: tertiaryIcon.color property alias tertiaryIconKeyColor: tertiaryIcon.keyColor + property alias tertiaryIconClickable: tertiaryIconMouseArea.enabled property alias additionalItem: additionalItemContainer.children + property alias busy: busyIndicator.running + signal deleteClicked() + signal secondaryIconClicked() contentItem: RowLayout { id: innerLayout spacing: app.margins - ColorIcon { - id: icon + Item { Layout.preferredHeight: root.iconSize Layout.preferredWidth: height - name: root.iconName - color: root.iconColor - visible: root.iconName + visible: root.iconName || root.fallbackIcon + + ColorIcon { + id: icon + anchors.fill: parent + name: root.iconName + color: root.iconColor + visible: root.iconName + } + + ColorIcon { + anchors.fill: parent + name: root.fallbackIcon + color: root.iconColor + visible: root.fallbackIcon && (!root.iconName || icon.status === Image.Error) + } + + BusyIndicator { + id: busyIndicator + anchors.centerIn: parent + visible: running + running: false + } } + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true @@ -72,6 +98,13 @@ SwipeDelegate { Layout.preferredHeight: app.iconSize * .5 Layout.preferredWidth: height visible: name.length > 0 + MouseArea { + id: secondaryIconMouseArea + enabled: false + anchors.fill: parent + anchors.margins: -app.margins + onClicked: root.secondaryIconClicked(); + } } ColorIcon { @@ -79,6 +112,13 @@ SwipeDelegate { Layout.preferredHeight: app.iconSize * .5 Layout.preferredWidth: height visible: name.length > 0 + MouseArea { + id: tertiaryIconMouseArea + enabled: false + anchors.fill: parent + anchors.margins: -app.margins + onClicked: root.tertiaryIconClicked(); + } } ColorIcon { diff --git a/nymea-app/ui/delegates/BrowserItemDelegate.qml b/nymea-app/ui/delegates/BrowserItemDelegate.qml new file mode 100644 index 00000000..c245da99 --- /dev/null +++ b/nymea-app/ui/delegates/BrowserItemDelegate.qml @@ -0,0 +1,40 @@ +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" + +NymeaListItemDelegate { + id: root + width: parent.width + text: model.displayName + progressive: model.browsable + subText: model.description + prominentSubText: false + iconName: model.thumbnail + fallbackIcon: "../images/browser/" + model.icon + ".svg" + enabled: model.browsable || model.executable + secondaryIconName: model.actionTypeIds.length > 0 ? "../images/navigation-menu.svg" : "" + secondaryIconClickable: true + + property Device device: null + + onPressAndHold: openContextMenu() + onSecondaryIconClicked: openContextMenu() + + signal contextMenuActionTriggered(var actionTypeId, var params) + + function openContextMenu() { + if (model.actionTypeIds.length === 0) { + return; + } + + var actionDialogComponent = Qt.createComponent(Qt.resolvedUrl("../components/BrowserContextMenu.qml")); + var popup = actionDialogComponent.createObject(root, {device: root.device, title: model.displayName, itemId: model.id, actionTypeIds: model.actionTypeIds}); + popup.activated.connect(function(actionTypeId, params) { + root.contextMenuActionTriggered(actionTypeId, params) + }) + popup.open() + } +} diff --git a/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml new file mode 100644 index 00000000..9d0afcd5 --- /dev/null +++ b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml @@ -0,0 +1,202 @@ +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 QtGraphicalEffects 1.0 +import "../components" + +DeviceListPageBase { + id: root + + header: NymeaHeader { + text: qsTr("Media") + onBackPressed: pageStack.pop() + } + + Flickable { + anchors.fill: parent + contentHeight: contentGrid.implicitHeight + + GridLayout { + id: contentGrid + width: parent.width + columns: Math.ceil(width / 500) + rowSpacing: 0 + columnSpacing: 0 + + Repeater { + model: root.devicesProxy + + delegate: ItemDelegate { + id: itemDelegate + Layout.preferredWidth: contentGrid.width / contentGrid.columns + + property bool inline: width > 500 + + property Device device: devicesProxy.get(index); + property DeviceClass deviceClass: device.deviceClass + + readonly property StateType playbackStateType: deviceClass.stateTypes.findByName("playbackStatus") + readonly property State playbackState: playbackStateType ? device.states.getState(playbackStateType.id) : null + + readonly property StateType playerTypeStateType: deviceClass.stateTypes.findByName("playerType") + readonly property State playerTypeState: playerTypeStateType ? device.states.getState(playerTypeStateType.id) : null + + bottomPadding: index === ListView.view.count - 1 ? topPadding : 0 + contentItem: Pane { + id: contentItem + Material.elevation: 2 + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + contentItem: ItemDelegate { + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + contentItem: ColumnLayout { + spacing: 0 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: app.mediumFont + app.margins + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .05) + RowLayout { + anchors { verticalCenter: parent.verticalCenter; left: parent.left; right: parent.right; margins: app.margins } + Label { + Layout.fillWidth: true + text: model.name + elide: Text.ElideRight + } + ColorIcon { + Layout.preferredHeight: app.iconSize * .5 + Layout.preferredWidth: height + name: "../images/battery/battery-020.svg" + visible: itemDelegate.deviceClass.interfaces.indexOf("battery") >= 0 && itemDelegate.device.states.getState(itemDelegate.deviceClass.stateTypes.findByName("batteryCritical").id).value === true + } + ColorIcon { + Layout.preferredHeight: app.iconSize * .5 + Layout.preferredWidth: height + name: "../images/dialog-warning-symbolic.svg" + visible: itemDelegate.deviceClass.interfaces.indexOf("connectable") >= 0 && itemDelegate.device.states.getState(itemDelegate.deviceClass.stateTypes.findByName("connected").id).value === false + color: "red" + } + } + + } + RowLayout { + ColumnLayout { + id: leftColummn + Layout.margins: app.margins + Label { + Layout.fillWidth: true + text: itemDelegate.playbackState.value === "Stopped" ? + qsTr("No playback") + : itemDelegate.device.states.getState(itemDelegate.deviceClass.stateTypes.findByName("title").id).value + horizontalAlignment: Text.AlignHCenter + // font.pixelSize: app.largeFont + elide: Text.ElideRight + } + Label { + Layout.fillWidth: true + text: itemDelegate.device.states.getState(itemDelegate.deviceClass.stateTypes.findByName("artist").id).value + font.pixelSize: app.smallFont + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + Label { + Layout.fillWidth: true + text: itemDelegate.device.states.getState(itemDelegate.deviceClass.stateTypes.findByName("collection").id).value + horizontalAlignment: Text.AlignHCenter + font.pixelSize: app.smallFont + elide: Text.ElideRight + } + MediaControls { + visible: itemDelegate.deviceClass.interfaces.indexOf("mediacontroller") >= 0 + device: itemDelegate.device + } + } + Item { + Layout.preferredHeight: leftColummn.height + app.margins * 2 + Layout.preferredWidth: height * .7 + + Item { + id: artworkContainer + anchors.fill: parent + Image { + id: artworkImage + width: artworkImage.sourceSize.width * height / artworkImage.sourceSize.height + anchors { top: parent.top; right: parent.right; bottom: parent.bottom } + readonly property StateType artworkStateType: device ? device.deviceClass.stateTypes.findByName("artwork") : null + readonly property State artworkState: artworkStateType ? device.states.getState(artworkStateType.id) : null + source: artworkState ? artworkState.value : "" + } + Rectangle { + visible: artworkImage.status !== Image.Ready || artworkImage.source === "" + anchors.fill: parent + color: "black" + } + + ColorIcon { + width: Math.min(parent.width, parent.height) - app.margins * 2 + height: width + anchors.centerIn: parent + name: itemDelegate.playerTypeState && itemDelegate.playerTypeState.value === "video" ? "../images/stock_video.svg" : "../images/stock_music.svg" + visible: artworkImage.status !== Image.Ready || artworkImage.source === "" + color: "white" + } + } + + Rectangle { + id: maskRect + anchors.centerIn: parent + height: parent.width + width: parent.height + gradient: Gradient { + GradientStop { position: 0; color: "#00FF0000" } + GradientStop { position: 0.2; color: "#15FF0000" } + GradientStop { position: 1; color: "#FFFF0000" } + } + } + + ShaderEffect { + anchors.fill: parent + property variant source: ShaderEffectSource { + sourceItem: artworkContainer + hideSource: true + } + property variant mask: ShaderEffectSource { + sourceItem: maskRect + hideSource: true + } + + fragmentShader: " + varying highp vec2 qt_TexCoord0; + uniform sampler2D source; + uniform sampler2D mask; + void main(void) + { + highp vec4 sourceColor = texture2D(source, qt_TexCoord0); + highp float alpha = texture2D(mask, vec2(qt_TexCoord0.y, qt_TexCoord0.x)).a; + sourceColor *= alpha; + gl_FragColor = sourceColor; + } + " + } + } + } + } + onClicked: { + enterPage(index, false) + } + } + } + } + } + + + } + } +} diff --git a/nymea-app/ui/devicepages/DeviceBrowserPage.qml b/nymea-app/ui/devicepages/DeviceBrowserPage.qml new file mode 100644 index 00000000..b20dca21 --- /dev/null +++ b/nymea-app/ui/devicepages/DeviceBrowserPage.qml @@ -0,0 +1,89 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../delegates" + +Page { + id: root + header: NymeaHeader { + text: qsTr("Browse %1").arg(root.device.name) + onBackPressed: pageStack.pop() + } + property Device device: null + property string nodeId: "" + + Component.onCompleted: { + d.model = engine.deviceManager.browseDevice(root.device.id, root.nodeId); + } + + function executeBrowserItem(itemId) { + d.pendingItemId = itemId + d.pendingBrowserItemId = engine.deviceManager.executeBrowserItem(root.device.id, itemId) + } + function executeBrowserItemAction(itemId, actionTypeId, params) { + d.pendingItemId = itemId + d.pendingBrowserItemId = engine.deviceManager.executeBrowserItemAction(root.device.id, itemId, actionTypeId, params) + } + + QtObject { + id: d + property BrowserItems model: null + property int pendingBrowserItemId: -1 + property string pendingItemId: "" + } + + Connections { + target: engine.deviceManager + onExecuteBrowserItemReply: actionExecuted(params) + onExecuteBrowserItemActionReply: actionExecuted(params) + } + function actionExecuted(params) { + print("Execute Action reply:", params, params.id, params["id"], d.pendingBrowserItemId) + if (params.id === d.pendingBrowserItemId) { + d.pendingBrowserItemId = -1; + d.pendingItemId = "" + print("yep finished") + if (params.status !== "success") { + header.showInfo(params.error, true); + } else if (params.params.deviceError !== "DeviceErrorNoError") { + header.showInfo(qsTr("Error: %1").arg(params.params.deviceError), true) + } + } + engine.deviceManager.refreshBrowserItems(d.model) + } + + ListView { + id: listView + anchors.fill: parent + model: d.model + ScrollBar.vertical: ScrollBar {} + + delegate: BrowserItemDelegate { + id: delegate + busy: d.pendingItemId === model.id + device: root.device + + onClicked: { + print("clicked:", model.id) + if (model.executable) { + root.executeBrowserItem(model.id) + } else if (model.browsable) { + pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {device: root.device, nodeId: model.id}) + } + } + + onContextMenuActionTriggered: { + root.executeBrowserItemAction(model.id, actionTypeId, params) + } + } + + BusyIndicator { + anchors.centerIn: parent + running: listView.model.busy + visible: running + } + } + +} diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index aec021f7..478e609a 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -11,12 +11,29 @@ Page { property bool showLogsButton: true property bool showDetailsButton: true + property bool showBrowserButton: true + property bool popStackOnBackButton: true default property alias data: contentItem.data + signal backPressed() + header: NymeaHeader { text: device.name - onBackPressed: pageStack.pop() + onBackPressed: { + root.backPressed(); + if (root.popStackOnBackButton) { + pageStack.pop() + } + } + + HeaderButton { + imageSource: "../images/folder-symbolic.svg" + visible: root.deviceClass.browsable && root.showBrowserButton + onClicked: { + pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {device: root.device}) + } + } HeaderButton { imageSource: "../images/navigation-menu.svg" diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 8d0695a6..a7720940 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -6,9 +6,30 @@ import QtGraphicalEffects 1.0 import Nymea 1.0 import "../components" import "../customviews" +import "../delegates" DevicePageBase { id: root + popStackOnBackButton: false + showBrowserButton: false + + onBackPressed: { + if (swipeView.currentIndex > 0) { + if (internalPageStack.depth > 1) { + internalPageStack.pop(); + } else { + swipeView.currentIndex = 0; + } + } else { + pageStack.pop(); + } + } + + Component.onCompleted: { + if (root.deviceClass.browsable && playbackState.value === "Stopped") { + swipeView.currentIndex = 1; + } + } function stateValue(name) { var stateType = root.deviceClass.stateTypes.findByName(name); @@ -22,134 +43,159 @@ DevicePageBase { engine.deviceManager.executeAction(device.id, actionTypeId, params) } + function executeBrowserItem(itemId) { + d.pendingItemId = itemId + d.pendingBrowserItemId = engine.deviceManager.executeBrowserItem(device.id, itemId); + } + function executeBrowserItemAction(itemId, actionTypeId, params) { + print("params2:", JSON.stringify(params)) + d.pendingItemId = itemId + d.pendingBrowserItemId = engine.deviceManager.executeBrowserItemAction(device.id, itemId, actionTypeId, params); + } + readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id) - GridLayout { - id: contentColumn - anchors.fill: parent - anchors.margins: app.margins - columns: app.landscape ? 2 : 1 - columnSpacing: app.margins - rowSpacing: app.margins + QtObject { + id: d + property int pendingBrowserItemId: -1 + property string pendingItemId: "" + } - Pane { - Layout.fillWidth: true - Layout.fillHeight: true - Material.elevation: 2 - padding: 0 - - contentItem: Rectangle { - color: app.foregroundColor - - Image { - id: artworkImage - anchors.fill: parent - fillMode: Image.PreserveAspectFit - source: root.stateValue("artwork") - } - - ColorIcon { - id: fallback - anchors.fill: parent - anchors.margins: app.margins * 2 - name: root.stateValue("playerType") === "video" ? "../images/stock_video.svg" : "../images/stock_music.svg" - visible: artworkImage.status !== Image.Ready || artworkImage.source === "" - color: app.primaryColor - } - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: app.margins - - Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - font.pixelSize: app.largeFont - font.bold: true - text: root.stateValue("title") - } - Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - text: root.stateValue("artist") - } - Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - text: root.stateValue("collection") - } - - RowLayout { - Layout.fillWidth: true - Item { Layout.fillWidth: true } - - ProgressButton { - Layout.preferredHeight: app.iconSize - Layout.preferredWidth: height - imageSource: "../images/media-skip-backward.svg" - longpressImageSource: "../images/media-seek-backward.svg" - repeat: true - - onClicked: { - root.executeAction("skipBack") - } - onLongpressed: { - root.executeAction("fastRewind") - } - } - - Item { Layout.fillWidth: true } - - ProgressButton { - Layout.preferredHeight: app.iconSize * 2 - Layout.preferredWidth: height - imageSource: root.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" - longpressImageSource: "../images/media-playback-stop.svg" - longpressEnabled: root.playbackState.value !== "Stopped" - - onClicked: { - if (root.playbackState.value === "Playing") { - root.executeAction("pause") - } else { - root.executeAction("play") - } - } - - onLongpressed: { - root.executeAction("stop") - } - } - - Item { Layout.fillWidth: true } - ProgressButton { - Layout.preferredHeight: app.iconSize - Layout.preferredWidth: height - imageSource: "../images/media-skip-forward.svg" - longpressImageSource: "../images/media-seek-forward.svg" - repeat: true - onClicked: { - root.executeAction("skipNext") - } - onLongpressed: { - root.executeAction("fastForward") - } - } - Item { Layout.fillWidth: true } + Connections { + target: engine.deviceManager + onExecuteBrowserItemReply: executionFinished(params) + onExecuteBrowserItemActionReply: executionFinished(params) + } + function executionFinished(params) { + print("Execute reply:", params, params.id, params["id"], d.pendingBrowserItemId) + if (params.id === d.pendingBrowserItemId) { + d.pendingBrowserItemId = -1; + d.pendingItemId = "" + print("yep finished") + if (params.params.deviceError === "DeviceErrorNoError") { + swipeView.currentIndex = 0; + } else { + header.showInfo(qsTr("Error: %1").arg(params.params.deviceError), true) } } } + SwipeView { + id: swipeView + anchors.fill: parent + interactive: root.deviceClass.browsable + + Item { + GridLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: app.margins + columns: app.landscape ? 2 : 1 + columnSpacing: app.margins + rowSpacing: app.margins + + MediaArtworkImage { + Layout.fillHeight: true + Layout.preferredWidth: parent.width / parent.columns + device: root.device + } + + ColumnLayout { + Layout.fillWidth: true + spacing: app.margins + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + font.pixelSize: app.largeFont + font.bold: true + text: root.stateValue("title") + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + text: root.stateValue("artist") + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + text: root.stateValue("collection") + } + + MediaControls { + device: root.device + iconSize: app.iconSize * 2 + } + } + } + } + + Item { + + StackView { + id: internalPageStack + anchors.fill: parent + initialItem: internalBrowserPage + + Component { + id: internalBrowserPage + ListView { + id: listView + anchors.fill: parent + model: browserItems + ScrollBar.vertical: ScrollBar {} + + property string nodeId: "" + + // Need to keep a explicit property here or the GC will eat it too early + property BrowserItems browserItems: null + Component.onCompleted: { + browserItems = engine.deviceManager.browseDevice(root.device.id, nodeId); + } + + delegate: BrowserItemDelegate { + fallbackIcon: "../images/browser/" + (model.mediaIcon && model.mediaIcon !== "MediaBrowserIconNone" ? model.mediaIcon : model.icon) + ".svg" + busy: d.pendingItemId === model.id + device: root.device + + onClicked: { + print("clicked:", model.id) + if (model.executable) { + root.executeBrowserItem(model.id) + } else if (model.browsable) { + internalPageStack.push(internalBrowserPage, {device: root.device, nodeId: model.id}) + } + } + + onContextMenuActionTriggered: { + root.executeBrowserItemAction(model.id, actionTypeId, params) + } + } + + BusyIndicator { + anchors.centerIn: parent + running: listView.model.busy + visible: running + } + } + } + + } + + + } + } + + Component { id: volumeSliderPaneComponent Dialog { @@ -158,6 +204,7 @@ DevicePageBase { topPadding: app.margins / 2 rightPadding: 0 bottomPadding: app.margins / 2 + modal: true contentItem: ColumnLayout { Slider { @@ -193,71 +240,103 @@ DevicePageBase { footer: Pane { Material.elevation: 1 - height: 50 + height: 52 padding: 0 - contentItem: RowLayout { -// Item { -// Layout.fillWidth: true; Layout.fillHeight: true -// HeaderButton { -// anchors.centerIn: parent -// imageSource: "../images/media-playlist.svg" -// } -// } + contentItem: ColumnLayout { Item { - Layout.fillWidth: true; Layout.fillHeight: true - visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 - HeaderButton { - anchors.centerIn: parent - imageSource: root.stateValue("repeat") === "One" ? "../images/media-playlist-repeat-one.svg" : "../images/media-playlist-repeat.svg" - color: root.stateValue("repeat") === "None" ? keyColor : app.accentColor - property var allowedValues: ["None", "All", "One"] - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.deviceClass.actionTypes.findByName("repeat").id; - param["value"] = allowedValues[(allowedValues.indexOf(root.stateValue("repeat")) + 1) % 3] - params.push(param) - root.executeAction("repeat", params) - } + Layout.fillWidth: true + Layout.preferredHeight: 2 + visible: root.deviceClass.browsable + Rectangle { + height: parent.height + width: parent.width / 2 + color: app.accentColor + x: swipeView.currentIndex * width + Behavior on x { NumberAnimation { duration: 150 } } } } - Item { - Layout.fillWidth: true; Layout.fillHeight: true - visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 - HeaderButton { - anchors.centerIn: parent - imageSource: "../images/media-playlist-shuffle.svg" - color: root.stateValue("shuffle") ? app.accentColor: keyColor - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.deviceClass.actionTypes.findByName("shuffle").id; - param["value"] = !root.stateValue("shuffle") - params.push(param) - root.executeAction("shuffle", params) + + RowLayout { + Item { + Layout.fillHeight: true + Layout.preferredWidth: root.deviceClass.browsable && swipeView.currentIndex > 0 ? parent.width / 4 : 0 + Behavior on Layout.preferredWidth { NumberAnimation {} } + HeaderButton { + anchors.centerIn: parent + imageSource: "../images/back.svg" + opacity: root.deviceClass.browsable && swipeView.currentIndex == 1 ? 1 : 0 + Behavior on opacity { NumberAnimation {} } + onClicked: swipeView.currentIndex = 0 } } - } - Item { - id: volumeButtonContainer - Layout.fillWidth: true; Layout.fillHeight: true - HeaderButton { - id: volumeButton - anchors.centerIn: parent - imageSource: "../images/audio-speakers-symbolic.svg" - onClicked: { - print("..."); - print(volumeButton.x, volumeButton.y) - print(Qt.point(volumeButton.x, volumeButton.y)) - print(volumeButton.mapToItem(root, volumeButton.x,0)) - var buttonPosition = root.mapFromItem(volumeButtonContainer, volumeButton.x, 0) - var sliderHeight = 200 - var props = {} - props["x"] = buttonPosition.x - props["y"] = root.height - sliderHeight - root.footer.height - props["height"] = sliderHeight - var sliderPane = volumeSliderPaneComponent.createObject(root, props) - sliderPane.open() + Item { + Layout.fillWidth: true; Layout.fillHeight: true + visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 + HeaderButton { + anchors.centerIn: parent + imageSource: root.stateValue("repeat") === "One" ? "../images/media-playlist-repeat-one.svg" : "../images/media-playlist-repeat.svg" + color: root.stateValue("repeat") === "None" ? keyColor : app.accentColor + property var allowedValues: ["None", "All", "One"] + onClicked: { + var params = [] + var param = {} + param["paramTypeId"] = root.deviceClass.actionTypes.findByName("repeat").id; + param["value"] = allowedValues[(allowedValues.indexOf(root.stateValue("repeat")) + 1) % 3] + params.push(param) + root.executeAction("repeat", params) + } + } + } + Item { + Layout.fillWidth: true; Layout.fillHeight: true + visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 + HeaderButton { + anchors.centerIn: parent + imageSource: "../images/media-playlist-shuffle.svg" + color: root.stateValue("shuffle") ? app.accentColor: keyColor + onClicked: { + var params = [] + var param = {} + param["paramTypeId"] = root.deviceClass.actionTypes.findByName("shuffle").id; + param["value"] = !root.stateValue("shuffle") + params.push(param) + root.executeAction("shuffle", params) + } + } + } + Item { + id: volumeButtonContainer + Layout.fillWidth: true; Layout.fillHeight: true + HeaderButton { + id: volumeButton + anchors.centerIn: parent + imageSource: "../images/audio-speakers-symbolic.svg" + onClicked: { + print("..."); + print(volumeButton.x, volumeButton.y) + print(Qt.point(volumeButton.x, volumeButton.y)) + print(volumeButton.mapToItem(root, volumeButton.x,0)) + var buttonPosition = root.mapFromItem(volumeButtonContainer, volumeButton.x, 0) + var sliderHeight = 200 + var props = {} + props["x"] = buttonPosition.x + props["y"] = root.height - sliderHeight - root.footer.height + props["height"] = sliderHeight + var sliderPane = volumeSliderPaneComponent.createObject(root, props) + sliderPane.open() + } + } + } + Item { + Layout.fillHeight: true + Layout.preferredWidth: root.deviceClass.browsable && swipeView.currentIndex == 0 ? parent.width / 4 : 0 + Behavior on Layout.preferredWidth { NumberAnimation {} } + HeaderButton { + anchors.centerIn: parent + imageSource: "../images/next.svg" + onClicked: swipeView.currentIndex = 1 + opacity: root.deviceClass.browsable && swipeView.currentIndex == 0 ? 1 : 0 + Behavior on opacity { NumberAnimation {} } } } } diff --git a/nymea-app/ui/images/browser/BrowserIconApplication.svg b/nymea-app/ui/images/browser/BrowserIconApplication.svg new file mode 100644 index 00000000..87b3400d --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconApplication.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/BrowserIconDocument.svg b/nymea-app/ui/images/browser/BrowserIconDocument.svg new file mode 100644 index 00000000..4110f70e --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconDocument.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/BrowserIconFavorites.svg b/nymea-app/ui/images/browser/BrowserIconFavorites.svg new file mode 120000 index 00000000..2ff3eb55 --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconFavorites.svg @@ -0,0 +1 @@ +../starred.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/BrowserIconFile.svg b/nymea-app/ui/images/browser/BrowserIconFile.svg new file mode 100644 index 00000000..aaa48e15 --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconFile.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/BrowserIconFolder.svg b/nymea-app/ui/images/browser/BrowserIconFolder.svg new file mode 100644 index 00000000..50289d3e --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconFolder.svg @@ -0,0 +1,17 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/BrowserIconMusic.svg b/nymea-app/ui/images/browser/BrowserIconMusic.svg new file mode 100644 index 00000000..b43bc37d --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconMusic.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/BrowserIconPackage.svg b/nymea-app/ui/images/browser/BrowserIconPackage.svg new file mode 120000 index 00000000..22864336 --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconPackage.svg @@ -0,0 +1 @@ +../plugin.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/BrowserIconPicture.svg b/nymea-app/ui/images/browser/BrowserIconPicture.svg new file mode 100644 index 00000000..19bb10c9 --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconPicture.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/BrowserIconVideo.svg b/nymea-app/ui/images/browser/BrowserIconVideo.svg new file mode 100644 index 00000000..56c5a4f4 --- /dev/null +++ b/nymea-app/ui/images/browser/BrowserIconVideo.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconAirable.svg b/nymea-app/ui/images/browser/MediaBrowserIconAirable.svg new file mode 100644 index 00000000..fa2c1016 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconAirable.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconAmazon.svg b/nymea-app/ui/images/browser/MediaBrowserIconAmazon.svg new file mode 100644 index 00000000..e3381f3b --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconAmazon.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconAux.svg b/nymea-app/ui/images/browser/MediaBrowserIconAux.svg new file mode 120000 index 00000000..a3c53ece --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconAux.svg @@ -0,0 +1 @@ +../action.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/MediaBrowserIconDisk.svg b/nymea-app/ui/images/browser/MediaBrowserIconDisk.svg new file mode 100644 index 00000000..1af432c7 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconDisk.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg b/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg new file mode 120000 index 00000000..1762fbfe --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg @@ -0,0 +1 @@ +../mediaplayer-app-symbolic.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/MediaBrowserIconMusicLibrary.svg b/nymea-app/ui/images/browser/MediaBrowserIconMusicLibrary.svg new file mode 120000 index 00000000..cf37aa30 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconMusicLibrary.svg @@ -0,0 +1 @@ +BrowserIconMusic.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/MediaBrowserIconNetwork.svg b/nymea-app/ui/images/browser/MediaBrowserIconNetwork.svg new file mode 100644 index 00000000..0bf511ee --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconNetwork.svg @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconPictureLibrary.svg b/nymea-app/ui/images/browser/MediaBrowserIconPictureLibrary.svg new file mode 120000 index 00000000..87b2075a --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconPictureLibrary.svg @@ -0,0 +1 @@ +BrowserIconPicture.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/MediaBrowserIconPlaylist.svg b/nymea-app/ui/images/browser/MediaBrowserIconPlaylist.svg new file mode 100644 index 00000000..3ab686d4 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconPlaylist.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconRecentlyPlayed.svg b/nymea-app/ui/images/browser/MediaBrowserIconRecentlyPlayed.svg new file mode 120000 index 00000000..ccbc6e10 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconRecentlyPlayed.svg @@ -0,0 +1 @@ +MediaBrowserIconPlaylist.svg \ No newline at end of file diff --git a/nymea-app/ui/images/browser/MediaBrowserIconSiriusXM.svg b/nymea-app/ui/images/browser/MediaBrowserIconSiriusXM.svg new file mode 100644 index 00000000..d6121943 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconSiriusXM.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconSpotify.svg b/nymea-app/ui/images/browser/MediaBrowserIconSpotify.svg new file mode 100644 index 00000000..ef1fdece --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconSpotify.svg @@ -0,0 +1,17 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconTidal.svg b/nymea-app/ui/images/browser/MediaBrowserIconTidal.svg new file mode 100644 index 00000000..b212a6dd --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconTidal.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconTuneIn.svg b/nymea-app/ui/images/browser/MediaBrowserIconTuneIn.svg new file mode 100644 index 00000000..79c73f9b --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconTuneIn.svg @@ -0,0 +1,86 @@ + + + +image/svg+xml + + + + + + + + + + + + + \ No newline at end of file diff --git a/nymea-app/ui/images/browser/MediaBrowserIconUSB.svg b/nymea-app/ui/images/browser/MediaBrowserIconUSB.svg new file mode 100644 index 00000000..9d988a71 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconUSB.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconVTuner.svg b/nymea-app/ui/images/browser/MediaBrowserIconVTuner.svg new file mode 100644 index 00000000..6b293f0e --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconVTuner.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/nymea-app/ui/images/browser/MediaBrowserIconVideoLibrary.svg b/nymea-app/ui/images/browser/MediaBrowserIconVideoLibrary.svg new file mode 120000 index 00000000..e7788653 --- /dev/null +++ b/nymea-app/ui/images/browser/MediaBrowserIconVideoLibrary.svg @@ -0,0 +1 @@ +BrowserIconVideo.svg \ No newline at end of file diff --git a/nymea-app/ui/images/folder-symbolic.svg b/nymea-app/ui/images/folder-symbolic.svg new file mode 100644 index 00000000..50289d3e --- /dev/null +++ b/nymea-app/ui/images/folder-symbolic.svg @@ -0,0 +1,17 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/nymea-app/ui/magic/EditRulePage.qml b/nymea-app/ui/magic/EditRulePage.qml index 9a257fbc..5d749b3c 100644 --- a/nymea-app/ui/magic/EditRulePage.qml +++ b/nymea-app/ui/magic/EditRulePage.qml @@ -317,7 +317,7 @@ Page { Behavior on opacity { NumberAnimation {duration: 200; easing.type: Easing.InOutQuad } } Label { Layout.fillWidth: true - text: qsTr("This is a scene" + root.ruleColor, root.ruleIcon) + text: qsTr("This is a scene") } CheckBox { @@ -603,6 +603,15 @@ Page { implicitWidth: parent.width ruleAction: root.rule.actions.get(index) onRemoveRuleAction: root.rule.actions.removeRuleAction(index) + onClicked: { + var ruleActionPage = pageStack.push(Qt.resolvedUrl("SelectRuleActionPage.qml"), {text: "Select action", ruleAction: ruleAction, rule: root.rule }); + ruleActionPage.onBackPressed.connect(function() { + pageStack.pop(); + }) + ruleActionPage.onDone.connect(function() { + pageStack.pop(root); + }) + } } } diff --git a/nymea-app/ui/magic/RuleActionDelegate.qml b/nymea-app/ui/magic/RuleActionDelegate.qml index e45bec6c..82eeb2b6 100644 --- a/nymea-app/ui/magic/RuleActionDelegate.qml +++ b/nymea-app/ui/magic/RuleActionDelegate.qml @@ -12,18 +12,22 @@ NymeaListItemDelegate { property RuleAction ruleAction: null - property var device: ruleAction.deviceId ? engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null - property var iface: ruleAction.interfaceName ? Interfaces.findByName(ruleAction.interfaceName) : null - property var deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null - property var actionType: deviceClass ? deviceClass.actionTypes.getActionType(ruleAction.actionTypeId) + readonly property Device device: ruleAction.deviceId ? engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null + readonly property Interface iface: ruleAction.interfaceName ? Interfaces.findByName(ruleAction.interfaceName) : null + readonly property DeviceClass deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + readonly property ActionType actionType: deviceClass ? deviceClass.actionTypes.getActionType(ruleAction.actionTypeId) : iface ? iface.actionTypes.findByName(ruleAction.interfaceAction) : null + readonly property string browserItemId: ruleAction.browserItemId + readonly property BrowserItem browserItem: device && browserItemId.length > 0 ? engine.deviceManager.browserItem(device.id, browserItemId) : null signal removeRuleAction() onDeleteClicked: root.removeRuleAction() - iconName: root.device ? "../images/action.svg" : "../images/action-interface.svg" - text: qsTr("%1 - %2").arg(root.device ? root.device.name : root.iface.displayName).arg(root.actionType.displayName) + iconName: root.device ? (root.browserItemId ? "../images/browser/BrowserIconFolder.svg" : "../images/action.svg") : "../images/action-interface.svg" + text: qsTr("%1 - %2") + .arg(root.device ? root.device.name : root.iface.displayName) + .arg(root.actionType ? root.actionType.displayName : (root.browserItem.displayName.length > 0 ? root.browserItem.displayName : qsTr("Unknown item"))) subText: { var ret = []; for (var i = 0; i < root.ruleAction.ruleActionParams.count; i++) { diff --git a/nymea-app/ui/magic/SelectBrowserItemActionPage.qml b/nymea-app/ui/magic/SelectBrowserItemActionPage.qml new file mode 100644 index 00000000..4efb0ef3 --- /dev/null +++ b/nymea-app/ui/magic/SelectBrowserItemActionPage.qml @@ -0,0 +1,48 @@ +import QtQuick 2.4 +import QtQuick.Controls 2.1 +import Nymea 1.0 +import "../components" +import "../delegates" + +Page { + id: root + + property Device device: null + property string itemId: "" + + signal selected(string itemId) + + header: NymeaHeader { + onBackPressed: pageStack.pop() + text: qsTr("Select item") + } + + Component.onCompleted: { + listView.model = engine.deviceManager.browseDevice(root.device.id, root.itemId) + } + + ListView { + id: listView + anchors.fill: parent + ScrollBar.vertical: ScrollBar {} + + delegate: BrowserItemDelegate { + width: parent.width + device: root.device + secondaryIconName: "" // We don't support BrowserItemActions in rules yet + + onClicked: { + if (model.browsable) { + var page = pageStack.push(Qt.resolvedUrl("SelectBrowserItemActionPage.qml"), {device: root.device, itemId: model.id}); + page.selected.connect(function() { + pageStack.pop(); + root.selected(model.id); + }) + } else if (model.executable) { + pageStack.pop(); + root.selected(model.id); + } + } + } + } +} diff --git a/nymea-app/ui/magic/SelectRuleActionPage.qml b/nymea-app/ui/magic/SelectRuleActionPage.qml index 6062b28a..3d29eadd 100644 --- a/nymea-app/ui/magic/SelectRuleActionPage.qml +++ b/nymea-app/ui/magic/SelectRuleActionPage.qml @@ -8,13 +8,13 @@ Page { property alias text: header.text // a ruleAction object needs to be set and prefilled with either deviceId or interfaceName - property var ruleAction: null + property RuleAction ruleAction: null // optionally, a rule which will be used when determining params for the actions - property var rule: null + property Rule rule: null - readonly property var device: ruleAction && ruleAction.deviceId ? engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null - readonly property var deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + readonly property Device device: ruleAction && ruleAction.deviceId ? engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null + readonly property DeviceClass deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null signal backPressed(); signal done(); @@ -65,7 +65,20 @@ Page { } } else { if (root.device) { - listView.model = deviceClass.actionTypes + generatedModel.clear(); + for (var i = 0; i < deviceClass.actionTypes.count; i++) { + var actionType = deviceClass.actionTypes.get(i); + generatedModel.append({displayName: actionType.displayName, actionTypeId: actionType.id}) + } + + // Append an item for browse mode + if (root.deviceClass.browsable) { + generatedModel.append({displayName: qsTr("Open an item on this thing..."), actionTypeId: "browse"}) + } + + listView.model = generatedModel; + +// listView.model = deviceClass.actionTypes } } } @@ -73,14 +86,22 @@ Page { ListView { id: listView anchors.fill: parent + ScrollBar.vertical: ScrollBar {} - delegate: ItemDelegate { + delegate: NymeaListItemDelegate { + id: delegate text: model.displayName width: parent.width + iconName: model.actionTypeId === "browse" ? "../images/browser/BrowserIconFolder.svg" : "../images/action.svg" + property ActionType actionType: root.deviceClass.actionTypes.getActionType(model.actionTypeId) + progressive: model.actionTypeId === "browse" || actionType.paramTypes.count > 0 + onClicked: { if (header.interfacesMode) { if (root.device) { root.ruleAction.actionTypeId = model.actionTypeId; + root.ruleAction.browserItemId = ""; + root.ruleAction.interfaceAction = ""; var actionType = root.deviceClass.actionTypes.getActionType(model.actionTypeId) if (actionType.paramTypes.count > 0) { var paramsPage = pageStack.push(Qt.resolvedUrl("SelectRuleActionParamsPage.qml"), {ruleAction: root.ruleAction, rule: root.rule}) @@ -94,6 +115,8 @@ Page { } } else if (root.ruleAction.interfaceName !== "") { root.ruleAction.interfaceAction = model.name; + root.ruleAction.browserItemId = ""; + root.ruleAction.actionTypeId = ""; if (listView.model.get(index).paramTypes.count > 0) { var paramsPage = pageStack.push(Qt.resolvedUrl("SelectRuleActionParamsPage.qml"), {ruleAction: root.ruleAction, rule: root.rule}) paramsPage.onBackPressed.connect(function() {pageStack.pop()}); @@ -109,18 +132,31 @@ Page { } } else { if (root.device) { - var actionType = root.deviceClass.actionTypes.getActionType(model.id); - console.log("ActionType", actionType.id, "selected. Has", actionType.paramTypes.count, "params"); - root.ruleAction.actionTypeId = actionType.id; - if (actionType.paramTypes.count > 0) { - var paramsPage = pageStack.push(Qt.resolvedUrl("SelectRuleActionParamsPage.qml"), {ruleAction: root.ruleAction, rule: root.rule}) - paramsPage.onBackPressed.connect(function() { pageStack.pop(); }); - paramsPage.onCompleted.connect(function() { + if (model.actionTypeId === "browse") { + var page = pageStack.push(Qt.resolvedUrl("SelectBrowserItemActionPage.qml"), {device: root.device}); + page.selected.connect(function(itemId) { + root.ruleAction.browserItemId = itemId; + root.ruleAction.actionTypeId = ""; + root.ruleAction.interfaceAction = ""; pageStack.pop(); root.done(); }) } else { - root.done(); + var actionType = root.deviceClass.actionTypes.getActionType(model.actionTypeId); + console.log("ActionType", actionType.id, "selected. Has", actionType.paramTypes.count, "params"); + root.ruleAction.actionTypeId = actionType.id; + root.ruleAction.browserItemId = ""; + root.ruleAction.interfaceAction = ""; + if (actionType.paramTypes.count > 0) { + var paramsPage = pageStack.push(Qt.resolvedUrl("SelectRuleActionParamsPage.qml"), {ruleAction: root.ruleAction, rule: root.rule}) + paramsPage.onBackPressed.connect(function() { pageStack.pop(); }); + paramsPage.onCompleted.connect(function() { + pageStack.pop(); + root.done(); + }) + } else { + root.done(); + } } } } diff --git a/nymea-app/ui/mainviews/DevicesPageDelegate.qml b/nymea-app/ui/mainviews/DevicesPageDelegate.qml index da93c081..105f3fa9 100644 --- a/nymea-app/ui/mainviews/DevicesPageDelegate.qml +++ b/nymea-app/ui/mainviews/DevicesPageDelegate.qml @@ -13,6 +13,8 @@ MainPageTile { disconnected: devicesSubProxyConnectables.count > 0 batteryCritical: devicesSubProxyBattery.count > 0 + backgroundImage: inlineControlLoader.item && inlineControlLoader.item.hasOwnProperty("backgroundImage") ? inlineControlLoader.item.backgroundImage : "" + onClicked: { var page; switch (model.name) { @@ -47,6 +49,9 @@ MainPageTile { case "powersocket": page = "PowerSocketsDeviceListPage.qml"; break; + case "media": + page = "MediaDeviceListPage.qml"; + break; default: page = "GenericDeviceListPage.qml" } @@ -76,6 +81,9 @@ MainPageTile { filterBatteryCritical: true } + property int currentDeviceIndex: 0 + readonly property Device currentDevice: devicesProxy.get(currentDeviceIndex) + contentItem: Loader { id: inlineControlLoader anchors { @@ -97,7 +105,6 @@ MainPageTile { // return labelComponent; case "light": - case "media": case "garagegate": case "blind": case "extendedblind": @@ -107,12 +114,66 @@ MainPageTile { case "extendedawning": case "powersocket": return buttonComponent + case "media": + return mediaControlComponent default: console.warn("DevicesPageDelegate, inlineControl: Unhandled interface", model.name) } } } + Component { + id: mediaControlComponent + RowLayout { + id: inlineMediaControl + + property string backgroundImage: artworkState ? artworkState.value : "" + + property int currentDeviceIndex: 0 + readonly property Device currentDevice: devicesProxy.get(currentDeviceIndex) + readonly property StateType playbackStateType: currentDevice.deviceClass.stateTypes.findByName("playbackStatus") + readonly property State playbackState: currentDevice.states.getState(playbackStateType.id) + readonly property StateType artworkStateType: currentDevice.deviceClass.stateTypes.findByName("artwork") + readonly property State artworkState: artworkStateType ? currentDevice.states.getState(artworkStateType.id) : null + + Component.onCompleted: { + for (var i = 0; i < devicesProxy.count; i++) { + var d = devicesProxy.get(i); + var st = d.deviceClass.stateTypes.findByName("playbackStatus") + var s = d.states.getState(st.id) + s.valueChanged.connect(function() {updateTile()}) + } + updateTile(); + } + + function updateTile() { + var playingIndex = -1; + var pausedIndex = -1; + for (var i = 0; i < devicesProxy.count; i++) { + var d = devicesProxy.get(i); + var st = d.deviceClass.stateTypes.findByName("playbackStatus"); + if (!st) continue; + var s = d.states.getState(st.id); + if (playingIndex === -1 && s.value === "Playing") { + playingIndex = i; + } else if (pausedIndex === -1 && s.value === "Paused") { + pausedIndex = -i; + } + } + if (playingIndex !== -1) { + currentDeviceIndex = playingIndex; + } else if (pausedIndex !== -1) { + currentDeviceIndex = pausedIndex; + } + } + + MediaControls { + iconSize: app.iconSize * 1.2 + device: inlineMediaControl.currentDevice + } + } + } + Component { id: buttonComponent ColumnLayout { diff --git a/nymea-app/ui/mainviews/ScenesView.qml b/nymea-app/ui/mainviews/ScenesView.qml index c481adb0..8bac0639 100644 --- a/nymea-app/ui/mainviews/ScenesView.qml +++ b/nymea-app/ui/mainviews/ScenesView.qml @@ -31,7 +31,7 @@ Item { height: interfacesGridView.cellHeight iconName: iconTag ? "../images/" + iconTag.value + ".svg" : "../images/slideshow.svg"; fallbackIconName: "../images/slideshow.svg" - iconColor: colorTag ? colorTag.value : app.accentColor; + iconColor: colorTag.value.length > 0 ? colorTag.value : app.accentColor; text: model.name.toUpperCase() property var colorTag: engine.tagsManager.tags.findRuleTag(model.id, "color")