From 9dfde053caf83c292a05c43cb5a2a207e0d75208 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 4 Jul 2019 18:40:51 +0200 Subject: [PATCH 1/7] Initial work on the device browser --- libnymea-app-core/devicemanager.cpp | 61 ++++++++++++++++ libnymea-app-core/devicemanager.h | 7 ++ libnymea-app-core/jsonrpc/jsontypes.cpp | 1 + libnymea-app-core/libnymea-app-core.h | 5 ++ libnymea-common/libnymea-common.pro | 4 + libnymea-common/types/browseritem.cpp | 63 ++++++++++++++++ libnymea-common/types/browseritem.h | 46 ++++++++++++ libnymea-common/types/browseritems.cpp | 73 +++++++++++++++++++ libnymea-common/types/browseritems.h | 50 +++++++++++++ libnymea-common/types/deviceclass.cpp | 10 +++ libnymea-common/types/deviceclass.h | 5 ++ nymea-app/images.qrc | 1 + nymea-app/resources.qrc | 1 + .../ui/devicepages/DeviceBrowserPage.qml | 55 ++++++++++++++ nymea-app/ui/devicepages/DevicePageBase.qml | 8 ++ nymea-app/ui/images/folder-symbolic.svg | 17 +++++ 16 files changed, 407 insertions(+) create mode 100644 libnymea-common/types/browseritem.cpp create mode 100644 libnymea-common/types/browseritem.h create mode 100644 libnymea-common/types/browseritems.cpp create mode 100644 libnymea-common/types/browseritems.h create mode 100644 nymea-app/ui/devicepages/DeviceBrowserPage.qml create mode 100644 nymea-app/ui/images/folder-symbolic.svg diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index 073488bd..2e59dc56 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) : @@ -425,3 +427,62 @@ 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 &nodeId) +{ + QVariantMap params; + params.insert("deviceId", deviceId.toString()); + params.insert("nodeId", nodeId); + 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(); + itemModel->setBusy(true); + QPointer itemModelPtr(itemModel); + m_browsingRequests.insert(id, itemModelPtr); + + return itemModel; +} + +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; + } + + foreach (const QVariant &itemVariant, params.value("params").toMap().value("items").toList()) { + QVariantMap itemMap = itemVariant.toMap(); + BrowserItem *item = new BrowserItem(itemMap.value("id").toString(), this); + item->setDisplayName(itemMap.value("displayName").toString()); + item->setDescription(itemMap.value("description").toString()); + item->setThumbnail(itemMap.value("thumbnail").toString()); + item->setExecutable(itemMap.value("executable").toBool()); + item->setBrowsable(itemMap.value("browsable").toBool()); + itemModel->addBrowserItem(item); + } + + itemModel->setBusy(false); +} + +void DeviceManager::executeBrowserItem(const QUuid &deviceId, const QString &nodeId) +{ + QVariantMap params; + params.insert("deviceId", deviceId); + params.insert("nodeId", nodeId); + m_jsonClient->sendCommand("Devices.ExecuteBrowserItem", params, this, "executeBrowserItemResponse"); +} + +void DeviceManager::executeBrowserItemResponse(const QVariantMap ¶ms) +{ + qDebug() << "Execute Browser Item finished" << params; +} + diff --git a/libnymea-app-core/devicemanager.h b/libnymea-app-core/devicemanager.h index 6257a770..5ae9ff77 100644 --- a/libnymea-app-core/devicemanager.h +++ b/libnymea-app-core/devicemanager.h @@ -30,6 +30,7 @@ #include "types/plugins.h" #include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonrpcclient.h" +class BrowserItems; class DeviceManager : public JsonHandler { @@ -73,6 +74,8 @@ 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 &nodeId = QString()); + Q_INVOKABLE void executeBrowserItem(const QUuid &deviceId, const QString &nodeId); private: Q_INVOKABLE void notificationReceived(const QVariantMap &data); @@ -89,6 +92,8 @@ 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 executeBrowserItemResponse(const QVariantMap ¶ms); public slots: void savePluginConfig(const QUuid &pluginId); @@ -117,6 +122,8 @@ private: JsonRpcClient *m_jsonClient = nullptr; + QHash > m_browsingRequests; + }; #endif // DEVICEMANAGER_H diff --git a/libnymea-app-core/jsonrpc/jsontypes.cpp b/libnymea-app-core/jsonrpc/jsontypes.cpp index 9c56b2dc..79dd7218 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) { 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-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..98250990 --- /dev/null +++ b/libnymea-common/types/browseritem.cpp @@ -0,0 +1,63 @@ +#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) +{ + m_displayName = displayName; +} + +QString BrowserItem::description() const +{ + return m_description; +} + +void BrowserItem::setDescription(const QString &description) +{ + m_description = description; +} + +QString BrowserItem::thumbnail() const +{ + return m_thumbnail; +} + +void BrowserItem::setThumbnail(const QString &thumbnail) +{ + m_thumbnail = thumbnail; +} + +bool BrowserItem::executable() const +{ + return m_executable; +} + +void BrowserItem::setExecutable(bool executable) +{ + m_executable = executable; +} + +bool BrowserItem::browsable() const +{ + return m_browsable; +} + +void BrowserItem::setBrowsable(bool browsable) +{ + m_browsable = browsable; +} diff --git a/libnymea-common/types/browseritem.h b/libnymea-common/types/browseritem.h new file mode 100644 index 00000000..14bd51cd --- /dev/null +++ b/libnymea-common/types/browseritem.h @@ -0,0 +1,46 @@ +#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 CONSTANT) + Q_PROPERTY(QString description READ description CONSTANT) + Q_PROPERTY(QString thumbnail READ thumbnail CONSTANT) + Q_PROPERTY(bool executable READ executable CONSTANT) + Q_PROPERTY(bool browsable READ browsable CONSTANT) + +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 thumbnail() const; + void setThumbnail(const QString &thumbnail); + + bool executable() const; + void setExecutable(bool executable); + + bool browsable() const; + void setBrowsable(bool browsable); + +private: + QString m_id; + QString m_displayName; + QString m_description; + QString m_thumbnail; + bool m_executable = false; + bool m_browsable = false; +}; + +#endif // BROWSERITEM_H diff --git a/libnymea-common/types/browseritems.cpp b/libnymea-common/types/browseritems.cpp new file mode 100644 index 00000000..bfcae8d4 --- /dev/null +++ b/libnymea-common/types/browseritems.cpp @@ -0,0 +1,73 @@ +#include "browseritems.h" +#include "browseritem.h" + +#include + +BrowserItems::BrowserItems(QObject *parent): QAbstractListModel(parent) +{ + +} + +BrowserItems::~BrowserItems() +{ + qDebug() << "Deleting BrowserItems"; +} + +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 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(); + } + return QVariant(); +} + +QHash BrowserItems::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleDisplayName, "displayName"); + roles.insert(RoleDescription, "description"); + roles.insert(RoleThumbnail, "thumbnail"); + roles.insert(RoleExecutable, "executable"); + roles.insert(RoleBrowsable, "browsable"); + 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::setBusy(bool busy) +{ + if (m_busy != busy) { + m_busy = busy; + emit busyChanged(); + } +} diff --git a/libnymea-common/types/browseritems.h b/libnymea-common/types/browseritems.h new file mode 100644 index 00000000..07257942 --- /dev/null +++ b/libnymea-common/types/browseritems.h @@ -0,0 +1,50 @@ +#ifndef BROWSERITEMS_H +#define BROWSERITEMS_H + +#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, + RoleThumbnail, + RoleBrowsable, + RoleExecutable + }; + Q_ENUM(Roles) + + explicit BrowserItems(QObject *parent = nullptr); + virtual ~BrowserItems() override; + + 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 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; +}; + +#endif // BROWSERITEMS_H diff --git a/libnymea-common/types/deviceclass.cpp b/libnymea-common/types/deviceclass.cpp index 029d9047..a9fcc81e 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; diff --git a/libnymea-common/types/deviceclass.h b/libnymea-common/types/deviceclass.h index 21ccb3f7..a86ae8b4 100644 --- a/libnymea-common/types/deviceclass.h +++ b/libnymea-common/types/deviceclass.h @@ -46,6 +46,7 @@ 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) @@ -91,6 +92,9 @@ public: QString baseInterface() const; + bool browsable() const; + void setBrowsable(bool browsable); + ParamTypes *paramTypes() const; void setParamTypes(ParamTypes *paramTypes); @@ -120,6 +124,7 @@ private: QStringList m_createMethods; SetupMethod m_setupMethod; QStringList m_interfaces; + bool m_browsable = false; ParamTypes *m_paramTypes = nullptr; ParamTypes *m_settingsTypes = nullptr; diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 2ce38fb7..37db3d92 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -181,5 +181,6 @@ ui/images/lock-closed.svg ui/images/lock-open.svg ui/images/system-update.svg + ui/images/folder-symbolic.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index b70bac89..1b9790da 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -184,5 +184,6 @@ ui/components/UpdateRunningOverlay.qml ui/connection/SetupWizard.qml ui/system/NetworkSettingsPage.qml + ui/devicepages/DeviceBrowserPage.qml diff --git a/nymea-app/ui/devicepages/DeviceBrowserPage.qml b/nymea-app/ui/devicepages/DeviceBrowserPage.qml new file mode 100644 index 00000000..c53a6366 --- /dev/null +++ b/nymea-app/ui/devicepages/DeviceBrowserPage.qml @@ -0,0 +1,55 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +Page { + id: root + header: GuhHeader { + 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); + } + + QtObject { + id: d + property BrowserItems model: null + } + + ListView { + id: listView + anchors.fill: parent + model: d.model + + delegate: MeaListItemDelegate { + width: parent.width + text: model.displayName + progressive: model.browsable + subText: model.description + prominentSubText: false + iconName: model.thumbnail + + onClicked: { + print("clicked:", model.id) + if (model.executable) { + engine.deviceManager.executeBrowserItem(root.device.id, model.id) + } else if (model.browsable) { + pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {device: root.device, nodeId: model.id}) + } + } + } + + 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..40621a1c 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -18,6 +18,14 @@ Page { text: device.name onBackPressed: pageStack.pop() + HeaderButton { + imageSource: "../images/folder-symbolic.svg" + visible: root.deviceClass.browsable + onClicked: { + pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {device: root.device}) + } + } + HeaderButton { imageSource: "../images/navigation-menu.svg" onClicked: thingMenu.open(); 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 + + + + + + + + + + From 8ad40dcf561afa1c4b4cb30003e397391d49e22a Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 7 Jul 2019 16:10:56 +0200 Subject: [PATCH 2/7] More work on the item browser/media browser --- libnymea-app-core/devicemanager.cpp | 3 + libnymea-common/types/browseritem.cpp | 20 + libnymea-common/types/browseritem.h | 12 + libnymea-common/types/browseritems.cpp | 8 + libnymea-common/types/browseritems.h | 5 +- nymea-app/images.qrc | 26 + nymea-app/ui/components/MainPageTile.qml | 21 + .../ui/components/NymeaListItemDelegate.qml | 24 +- .../ui/devicepages/DeviceBrowserPage.qml | 7 +- nymea-app/ui/devicepages/DevicePageBase.qml | 13 +- nymea-app/ui/devicepages/MediaDevicePage.qml | 447 +++++++++++------- .../images/browser/BrowserIconApplication.svg | 167 +++++++ .../ui/images/browser/BrowserIconDocument.svg | 177 +++++++ .../images/browser/BrowserIconFavorites.svg | 1 + .../ui/images/browser/BrowserIconFile.svg | 147 ++++++ .../ui/images/browser/BrowserIconFolder.svg | 17 + .../ui/images/browser/BrowserIconMusic.svg | 173 +++++++ .../ui/images/browser/BrowserIconPackage.svg | 1 + .../ui/images/browser/BrowserIconPicture.svg | 175 +++++++ .../ui/images/browser/BrowserIconVideo.svg | 164 +++++++ .../browser/MediaBrowserIconAirable.svg | 67 +++ .../images/browser/MediaBrowserIconAmazon.svg | 160 +++++++ .../ui/images/browser/MediaBrowserIconAux.svg | 1 + .../images/browser/MediaBrowserIconDisk.svg | 166 +++++++ .../browser/MediaBrowserIconLibrary.svg | 1 + .../browser/MediaBrowserIconMusicLibrary.svg | 1 + .../browser/MediaBrowserIconNetwork.svg | 216 +++++++++ .../MediaBrowserIconPictureLibrary.svg | 1 + .../browser/MediaBrowserIconPlaylist.svg | 193 ++++++++ .../MediaBrowserIconRecentlyPlayed.svg | 1 + .../browser/MediaBrowserIconSiriusXM.svg | 67 +++ .../browser/MediaBrowserIconSpotify.svg | 17 + .../images/browser/MediaBrowserIconTidal.svg | 99 ++++ .../images/browser/MediaBrowserIconTuneIn.svg | 86 ++++ .../ui/images/browser/MediaBrowserIconUSB.svg | 203 ++++++++ .../images/browser/MediaBrowserIconVTuner.svg | 78 +++ .../browser/MediaBrowserIconVideoLibrary.svg | 1 + .../ui/mainviews/DevicesPageDelegate.qml | 8 + 38 files changed, 2796 insertions(+), 178 deletions(-) create mode 100644 nymea-app/ui/images/browser/BrowserIconApplication.svg create mode 100644 nymea-app/ui/images/browser/BrowserIconDocument.svg create mode 120000 nymea-app/ui/images/browser/BrowserIconFavorites.svg create mode 100644 nymea-app/ui/images/browser/BrowserIconFile.svg create mode 100644 nymea-app/ui/images/browser/BrowserIconFolder.svg create mode 100644 nymea-app/ui/images/browser/BrowserIconMusic.svg create mode 120000 nymea-app/ui/images/browser/BrowserIconPackage.svg create mode 100644 nymea-app/ui/images/browser/BrowserIconPicture.svg create mode 100644 nymea-app/ui/images/browser/BrowserIconVideo.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconAirable.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconAmazon.svg create mode 120000 nymea-app/ui/images/browser/MediaBrowserIconAux.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconDisk.svg create mode 120000 nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg create mode 120000 nymea-app/ui/images/browser/MediaBrowserIconMusicLibrary.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconNetwork.svg create mode 120000 nymea-app/ui/images/browser/MediaBrowserIconPictureLibrary.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconPlaylist.svg create mode 120000 nymea-app/ui/images/browser/MediaBrowserIconRecentlyPlayed.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconSiriusXM.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconSpotify.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconTidal.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconTuneIn.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconUSB.svg create mode 100644 nymea-app/ui/images/browser/MediaBrowserIconVTuner.svg create mode 120000 nymea-app/ui/images/browser/MediaBrowserIconVideoLibrary.svg diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index 2e59dc56..6e26deed 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -464,9 +464,12 @@ void DeviceManager::browseDeviceResponse(const QVariantMap ¶ms) BrowserItem *item = new BrowserItem(itemMap.value("id").toString(), this); 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->setMediaIcon(itemMap.value("mediaIcon").toString()); itemModel->addBrowserItem(item); } diff --git a/libnymea-common/types/browseritem.cpp b/libnymea-common/types/browseritem.cpp index 98250990..ea46d4b4 100644 --- a/libnymea-common/types/browseritem.cpp +++ b/libnymea-common/types/browseritem.cpp @@ -32,6 +32,16 @@ void BrowserItem::setDescription(const QString &description) m_description = description; } +QString BrowserItem::icon() const +{ + return m_icon; +} + +void BrowserItem::setIcon(const QString &icon) +{ + m_icon = icon; +} + QString BrowserItem::thumbnail() const { return m_thumbnail; @@ -61,3 +71,13 @@ void BrowserItem::setBrowsable(bool browsable) { m_browsable = browsable; } + +QString BrowserItem::mediaIcon() const +{ + return m_mediaIcon; +} + +void BrowserItem::setMediaIcon(const QString &mediaIcon) +{ + m_mediaIcon = mediaIcon; +} diff --git a/libnymea-common/types/browseritem.h b/libnymea-common/types/browseritem.h index 14bd51cd..3208bc73 100644 --- a/libnymea-common/types/browseritem.h +++ b/libnymea-common/types/browseritem.h @@ -10,10 +10,13 @@ class BrowserItem : public QObject Q_PROPERTY(QUuid id READ id CONSTANT) Q_PROPERTY(QString displayName READ displayName CONSTANT) Q_PROPERTY(QString description READ description CONSTANT) + Q_PROPERTY(QString icon READ icon CONSTANT) Q_PROPERTY(QString thumbnail READ thumbnail CONSTANT) Q_PROPERTY(bool executable READ executable CONSTANT) Q_PROPERTY(bool browsable READ browsable CONSTANT) + Q_PROPERTY(QString mediaIcon READ mediaIcon CONSTANT) + public: explicit BrowserItem(const QString &id, QObject *parent = nullptr); @@ -25,6 +28,9 @@ public: QString description() const; void setDescription(const QString &description); + QString icon() const; + void setIcon(const QString &icon); + QString thumbnail() const; void setThumbnail(const QString &thumbnail); @@ -34,13 +40,19 @@ public: bool browsable() const; void setBrowsable(bool browsable); + QString mediaIcon() const; + void setMediaIcon(const QString &mediaIcon); + private: QString m_id; QString m_displayName; QString m_description; + QString m_icon; QString m_thumbnail; bool m_executable = false; bool m_browsable = false; + + QString m_mediaIcon; }; #endif // BROWSERITEM_H diff --git a/libnymea-common/types/browseritems.cpp b/libnymea-common/types/browseritems.cpp index bfcae8d4..e5a67509 100644 --- a/libnymea-common/types/browseritems.cpp +++ b/libnymea-common/types/browseritems.cpp @@ -33,12 +33,17 @@ QVariant BrowserItems::data(const QModelIndex &index, int role) const 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 RoleMediaIcon: + return m_list.at(index.row())->mediaIcon(); } return QVariant(); } @@ -49,9 +54,12 @@ QHash BrowserItems::roleNames() const 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(RoleMediaIcon, "mediaIcon"); return roles; } diff --git a/libnymea-common/types/browseritems.h b/libnymea-common/types/browseritems.h index 07257942..4429b28e 100644 --- a/libnymea-common/types/browseritems.h +++ b/libnymea-common/types/browseritems.h @@ -15,9 +15,12 @@ public: RoleId, RoleDisplayName, RoleDescription, + RoleIcon, RoleThumbnail, RoleBrowsable, - RoleExecutable + RoleExecutable, + + RoleMediaIcon, }; Q_ENUM(Roles) diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 37db3d92..03374bf9 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -182,5 +182,31 @@ 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/ui/components/MainPageTile.qml b/nymea-app/ui/components/MainPageTile.qml index 9b31d484..cd0a3c0c 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,19 @@ Item { Material.elevation: 1 padding: 0 + Image { + id: background + anchors.fill: parent + anchors.margins: 1 + z: -1 +// opacity: .5 +// Rectangle { +// anchors.fill: parent +// color: Material.background +// opacity: .5 +// } + } + contentItem: ItemDelegate { padding: 0; topPadding: 0; bottomPadding: 0 onClicked: root.clicked() @@ -35,6 +49,7 @@ Item { Layout.fillHeight: true Layout.margins: app.margins Item { + visible: background.status !== Image.Ready Layout.fillWidth: true Layout.fillHeight: true @@ -55,6 +70,7 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true + visible: background.status !== Image.Ready Label { id: label @@ -77,6 +93,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/NymeaListItemDelegate.qml b/nymea-app/ui/components/NymeaListItemDelegate.qml index 1ac96205..b0768229 100644 --- a/nymea-app/ui/components/NymeaListItemDelegate.qml +++ b/nymea-app/ui/components/NymeaListItemDelegate.qml @@ -15,6 +15,7 @@ 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 @@ -32,14 +33,27 @@ SwipeDelegate { 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) + } } + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true diff --git a/nymea-app/ui/devicepages/DeviceBrowserPage.qml b/nymea-app/ui/devicepages/DeviceBrowserPage.qml index c53a6366..535234cc 100644 --- a/nymea-app/ui/devicepages/DeviceBrowserPage.qml +++ b/nymea-app/ui/devicepages/DeviceBrowserPage.qml @@ -6,7 +6,7 @@ import "../components" Page { id: root - header: GuhHeader { + header: NymeaHeader { text: qsTr("Browse %1").arg(root.device.name) onBackPressed: pageStack.pop() } @@ -26,14 +26,17 @@ Page { id: listView anchors.fill: parent model: d.model + ScrollBar.vertical: ScrollBar {} - delegate: MeaListItemDelegate { + delegate: NymeaListItemDelegate { 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 onClicked: { print("clicked:", model.id) diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 40621a1c..478e609a 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -11,16 +11,25 @@ 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 + visible: root.deviceClass.browsable && root.showBrowserButton onClicked: { pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {device: root.device}) } diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 8d0695a6..33b8ec23 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -9,6 +9,20 @@ import "../customviews" 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(); + } + } function stateValue(name) { var stateType = root.deviceClass.stateTypes.findByName(name); @@ -24,132 +38,197 @@ DevicePageBase { readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id) - GridLayout { - id: contentColumn + SwipeView { + id: swipeView anchors.fill: parent - anchors.margins: app.margins - columns: app.landscape ? 2 : 1 - columnSpacing: app.margins - rowSpacing: app.margins - Pane { - Layout.fillWidth: true - Layout.fillHeight: true - Material.elevation: 2 - padding: 0 + Item { + GridLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: app.margins + columns: app.landscape ? 2 : 1 + columnSpacing: app.margins + rowSpacing: app.margins - contentItem: Rectangle { - color: app.foregroundColor + Pane { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumWidth: parent.width / 2 + Material.elevation: 2 + padding: 0 - Image { - id: artworkImage - anchors.fill: parent - fillMode: Image.PreserveAspectFit - source: root.stateValue("artwork") - } + contentItem: Rectangle { + color: app.foregroundColor - 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 - } - } - } + Image { + id: artworkImage + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: root.stateValue("artwork") + } - 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") + 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 } } - - 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") + 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") } - onLongpressed: { - root.executeAction("fastForward") + 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 } } } - Item { Layout.fillWidth: true } } } + + 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: NymeaListItemDelegate { + width: parent.width + text: model.displayName + progressive: model.browsable + subText: model.description + prominentSubText: false + iconName: model.thumbnail + fallbackIcon: "../images/browser/" + (model.mediaIcon && model.mediaIcon !== "MediaBrowserIconNone" ? model.mediaIcon : model.icon) + ".svg" + enabled: model.browsable || model.executable + + onClicked: { + print("clicked:", model.id) + if (model.executable) { + engine.deviceManager.executeBrowserItem(root.device.id, model.id) + } else if (model.browsable) { + internalPageStack.push(internalBrowserPage, {device: root.device, nodeId: model.id}) + } + } + } + + BusyIndicator { + anchors.centerIn: parent + running: listView.model.busy + visible: running + } + } + } + + } + + + } } + Component { id: volumeSliderPaneComponent Dialog { @@ -158,6 +237,7 @@ DevicePageBase { topPadding: app.margins / 2 rightPadding: 0 bottomPadding: app.margins / 2 + modal: true contentItem: ColumnLayout { Slider { @@ -193,71 +273,102 @@ 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 + 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/mainviews/DevicesPageDelegate.qml b/nymea-app/ui/mainviews/DevicesPageDelegate.qml index da93c081..a93fc9c4 100644 --- a/nymea-app/ui/mainviews/DevicesPageDelegate.qml +++ b/nymea-app/ui/mainviews/DevicesPageDelegate.qml @@ -13,6 +13,14 @@ MainPageTile { disconnected: devicesSubProxyConnectables.count > 0 batteryCritical: devicesSubProxyBattery.count > 0 + backgroundImage: currentDevice.deviceClass.interfaces.indexOf("mediametadataprovider") >= 0 ? + currentDevice.states.getState(currentDevice.deviceClass.stateTypes.findByName("artwork").id).value : "" + + property int currentDeviceIndex: 0 + readonly property Device currentDevice: devicesProxy.get(currentDeviceIndex) +// readonly property State currentBackgroundState: currentDevice.state + + onClicked: { var page; switch (model.name) { From 2ad624329c552d18e9fc3cbbf4852be3a35ab485 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 8 Jul 2019 12:16:56 +0200 Subject: [PATCH 3/7] More work on Multimedia --- nymea-app/resources.qrc | 3 + nymea-app/ui/components/MainPageTile.qml | 2 + nymea-app/ui/components/MediaArtworkImage.qml | 47 +++++ nymea-app/ui/components/MediaControls.qml | 73 ++++++++ .../devicelistpages/MediaDeviceListPage.qml | 170 ++++++++++++++++++ nymea-app/ui/devicepages/MediaDevicePage.qml | 89 +-------- .../ui/mainviews/DevicesPageDelegate.qml | 69 ++++++- 7 files changed, 364 insertions(+), 89 deletions(-) create mode 100644 nymea-app/ui/components/MediaArtworkImage.qml create mode 100644 nymea-app/ui/components/MediaControls.qml create mode 100644 nymea-app/ui/devicelistpages/MediaDeviceListPage.qml diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 1b9790da..b3a8aa00 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -185,5 +185,8 @@ ui/connection/SetupWizard.qml ui/system/NetworkSettingsPage.qml ui/devicepages/DeviceBrowserPage.qml + ui/devicelistpages/MediaDeviceListPage.qml + ui/components/MediaControls.qml + ui/components/MediaArtworkImage.qml diff --git a/nymea-app/ui/components/MainPageTile.qml b/nymea-app/ui/components/MainPageTile.qml index cd0a3c0c..c5801426 100644 --- a/nymea-app/ui/components/MainPageTile.qml +++ b/nymea-app/ui/components/MainPageTile.qml @@ -30,6 +30,8 @@ Item { anchors.fill: parent anchors.margins: 1 z: -1 + fillMode: Image.PreserveAspectCrop +// horizontalAlignment: Image.AlignTop // opacity: .5 // Rectangle { // anchors.fill: parent 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..b6719c5b --- /dev/null +++ b/nymea-app/ui/components/MediaControls.qml @@ -0,0 +1,73 @@ +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" + 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" + repeat: true + onClicked: { + root.executeAction("skipNext") + } + onLongpressed: { + root.executeAction("fastForward") + } + } + Item { Layout.fillWidth: true } +} diff --git a/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml new file mode 100644 index 00000000..3b6629f9 --- /dev/null +++ b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml @@ -0,0 +1,170 @@ +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() + } + + ListView { + anchors.fill: parent + model: root.devicesProxy + + delegate: ItemDelegate { + id: itemDelegate + width: parent.width + + 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 + + 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 { + id: maskRect + anchors.centerIn: parent + height: parent.width + width: parent.height + gradient: Gradient { + GradientStop { position: 0; color: "transparent" } + GradientStop { position: 1; color: "red" } + } + } + + 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/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 33b8ec23..d7066726 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -41,6 +41,7 @@ DevicePageBase { SwipeView { id: swipeView anchors.fill: parent + interactive: root.deviceClass.browsable Item { GridLayout { @@ -51,32 +52,10 @@ DevicePageBase { columnSpacing: app.margins rowSpacing: app.margins - Pane { - Layout.fillWidth: true + MediaArtworkImage { Layout.fillHeight: true - Layout.minimumWidth: parent.width / 2 - 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 - } - } + Layout.preferredWidth: parent.width / parent.columns + device: root.device } ColumnLayout { @@ -110,62 +89,9 @@ DevicePageBase { 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 } + MediaControls { + device: root.device + iconSize: app.iconSize * 2 } } } @@ -279,6 +205,7 @@ DevicePageBase { Item { Layout.fillWidth: true Layout.preferredHeight: 2 + visible: root.deviceClass.browsable Rectangle { height: parent.height width: parent.width / 2 diff --git a/nymea-app/ui/mainviews/DevicesPageDelegate.qml b/nymea-app/ui/mainviews/DevicesPageDelegate.qml index a93fc9c4..105f3fa9 100644 --- a/nymea-app/ui/mainviews/DevicesPageDelegate.qml +++ b/nymea-app/ui/mainviews/DevicesPageDelegate.qml @@ -13,13 +13,7 @@ MainPageTile { disconnected: devicesSubProxyConnectables.count > 0 batteryCritical: devicesSubProxyBattery.count > 0 - backgroundImage: currentDevice.deviceClass.interfaces.indexOf("mediametadataprovider") >= 0 ? - currentDevice.states.getState(currentDevice.deviceClass.stateTypes.findByName("artwork").id).value : "" - - property int currentDeviceIndex: 0 - readonly property Device currentDevice: devicesProxy.get(currentDeviceIndex) -// readonly property State currentBackgroundState: currentDevice.state - + backgroundImage: inlineControlLoader.item && inlineControlLoader.item.hasOwnProperty("backgroundImage") ? inlineControlLoader.item.backgroundImage : "" onClicked: { var page; @@ -55,6 +49,9 @@ MainPageTile { case "powersocket": page = "PowerSocketsDeviceListPage.qml"; break; + case "media": + page = "MediaDeviceListPage.qml"; + break; default: page = "GenericDeviceListPage.qml" } @@ -84,6 +81,9 @@ MainPageTile { filterBatteryCritical: true } + property int currentDeviceIndex: 0 + readonly property Device currentDevice: devicesProxy.get(currentDeviceIndex) + contentItem: Loader { id: inlineControlLoader anchors { @@ -105,7 +105,6 @@ MainPageTile { // return labelComponent; case "light": - case "media": case "garagegate": case "blind": case "extendedblind": @@ -115,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 { From 41ebfb48f3b93f15f57268f323d92129905d620a Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 10 Jul 2019 11:40:06 +0200 Subject: [PATCH 4/7] BrowserItemActions --- libnymea-app-core/devicemanager.cpp | 66 +++- libnymea-app-core/devicemanager.h | 9 +- libnymea-app-core/jsonrpc/jsontypes.cpp | 7 + libnymea-common/types/browseritem.cpp | 61 +++- libnymea-common/types/browseritem.h | 36 ++- libnymea-common/types/browseritems.cpp | 55 +++- libnymea-common/types/browseritems.h | 19 +- libnymea-common/types/deviceclass.cpp | 14 + libnymea-common/types/deviceclass.h | 22 +- nymea-app/ui/components/MediaControls.qml | 3 + nymea-app/ui/components/NymeaHeader.qml | 125 ++++++-- .../ui/components/NymeaListItemDelegate.qml | 9 + .../devicelistpages/MediaDeviceListPage.qml | 298 ++++++++++-------- .../ui/devicepages/DeviceBrowserPage.qml | 72 ++++- nymea-app/ui/devicepages/MediaDevicePage.qml | 36 ++- 15 files changed, 631 insertions(+), 201 deletions(-) diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index 6e26deed..2e06d1b9 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -314,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); } @@ -428,15 +428,15 @@ 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 &nodeId) +BrowserItems *DeviceManager::browseDevice(const QUuid &deviceId, const QString &itemId) { QVariantMap params; params.insert("deviceId", deviceId.toString()); - params.insert("nodeId", nodeId); + 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(); + BrowserItems *itemModel = new BrowserItems(deviceId, itemId); itemModel->setBusy(true); QPointer itemModelPtr(itemModel); m_browsingRequests.insert(id, itemModelPtr); @@ -444,6 +444,19 @@ BrowserItems *DeviceManager::browseDevice(const QUuid &deviceId, const QString & 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); +} + void DeviceManager::browseDeviceResponse(const QVariantMap ¶ms) { qDebug() << "Browsing response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented)); @@ -459,33 +472,68 @@ void DeviceManager::browseDeviceResponse(const QVariantMap ¶ms) return; } + QList itemsToRemove = itemModel->list(); + + foreach (const QVariant &itemVariant, params.value("params").toMap().value("items").toList()) { QVariantMap itemMap = itemVariant.toMap(); - BrowserItem *item = new BrowserItem(itemMap.value("id").toString(), this); + 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()); - itemModel->addBrowserItem(item); + + if (itemsToRemove.contains(item)) { + itemsToRemove.removeAll(item); + } + } + + while (!itemsToRemove.isEmpty()) { + BrowserItem *item = itemsToRemove.takeFirst(); + itemModel->removeItem(item); } itemModel->setBusy(false); } -void DeviceManager::executeBrowserItem(const QUuid &deviceId, const QString &nodeId) +int DeviceManager::executeBrowserItem(const QUuid &deviceId, const QString &itemId) { QVariantMap params; params.insert("deviceId", deviceId); - params.insert("nodeId", nodeId); - m_jsonClient->sendCommand("Devices.ExecuteBrowserItem", params, this, "executeBrowserItemResponse"); + 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 QVariantMap ¶ms) +{ + QVariantMap data; + data.insert("deviceId", deviceId); + data.insert("itemId", itemId); + data.insert("actionTypeId", actionTypeId); + data.insert("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 5ae9ff77..11225ce3 100644 --- a/libnymea-app-core/devicemanager.h +++ b/libnymea-app-core/devicemanager.h @@ -74,8 +74,10 @@ 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 &nodeId = QString()); - Q_INVOKABLE void executeBrowserItem(const QUuid &deviceId, const QString &nodeId); + Q_INVOKABLE BrowserItems* browseDevice(const QUuid &deviceId, const QString &itemId = QString()); + Q_INVOKABLE void refreshBrowserItems(BrowserItems *browserItems); + Q_INVOKABLE int executeBrowserItem(const QUuid &deviceId, const QString &itemId); + Q_INVOKABLE int executeBrowserItemAction(const QUuid &deviceId, const QString &itemId, const QUuid &actionTypeId, const QVariantMap ¶ms = QVariantMap()); private: Q_INVOKABLE void notificationReceived(const QVariantMap &data); @@ -94,6 +96,7 @@ private: Q_INVOKABLE void reconfigureDeviceResponse(const QVariantMap ¶ms); Q_INVOKABLE void browseDeviceResponse(const QVariantMap ¶ms); Q_INVOKABLE void executeBrowserItemResponse(const QVariantMap ¶ms); + Q_INVOKABLE void executeBrowserItemActionResponse(const QVariantMap ¶ms); public slots: void savePluginConfig(const QUuid &pluginId); @@ -107,6 +110,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); diff --git a/libnymea-app-core/jsonrpc/jsontypes.cpp b/libnymea-app-core/jsonrpc/jsontypes.cpp index 79dd7218..5635506d 100644 --- a/libnymea-app-core/jsonrpc/jsontypes.cpp +++ b/libnymea-app-core/jsonrpc/jsontypes.cpp @@ -127,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; } diff --git a/libnymea-common/types/browseritem.cpp b/libnymea-common/types/browseritem.cpp index ea46d4b4..d1db69a5 100644 --- a/libnymea-common/types/browseritem.cpp +++ b/libnymea-common/types/browseritem.cpp @@ -19,7 +19,10 @@ QString BrowserItem::displayName() const void BrowserItem::setDisplayName(const QString &displayName) { - m_displayName = displayName; + if (m_displayName != displayName) { + m_displayName = displayName; + emit displayNameChanged(); + } } QString BrowserItem::description() const @@ -29,7 +32,10 @@ QString BrowserItem::description() const void BrowserItem::setDescription(const QString &description) { - m_description = description; + if (m_description != description) { + m_description = description; + emit descriptionChanged(); + } } QString BrowserItem::icon() const @@ -39,7 +45,10 @@ QString BrowserItem::icon() const void BrowserItem::setIcon(const QString &icon) { - m_icon = icon; + if (m_icon != icon) { + m_icon = icon; + emit iconChanged(); + } } QString BrowserItem::thumbnail() const @@ -49,7 +58,10 @@ QString BrowserItem::thumbnail() const void BrowserItem::setThumbnail(const QString &thumbnail) { - m_thumbnail = thumbnail; + if (m_thumbnail != thumbnail) { + m_thumbnail = thumbnail; + emit thumbnailChanged(); + } } bool BrowserItem::executable() const @@ -59,7 +71,10 @@ bool BrowserItem::executable() const void BrowserItem::setExecutable(bool executable) { - m_executable = executable; + if (m_executable != executable) { + m_executable = executable; + emit executableChanged(); + } } bool BrowserItem::browsable() const @@ -69,7 +84,36 @@ bool BrowserItem::browsable() const void BrowserItem::setBrowsable(bool browsable) { - m_browsable = 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 @@ -79,5 +123,8 @@ QString BrowserItem::mediaIcon() const void BrowserItem::setMediaIcon(const QString &mediaIcon) { - m_mediaIcon = mediaIcon; + if (m_mediaIcon != mediaIcon) { + m_mediaIcon = mediaIcon; + emit mediaIconChanged(); + } } diff --git a/libnymea-common/types/browseritem.h b/libnymea-common/types/browseritem.h index 3208bc73..60b8a49d 100644 --- a/libnymea-common/types/browseritem.h +++ b/libnymea-common/types/browseritem.h @@ -8,14 +8,16 @@ class BrowserItem : public QObject { Q_OBJECT Q_PROPERTY(QUuid id READ id CONSTANT) - Q_PROPERTY(QString displayName READ displayName CONSTANT) - Q_PROPERTY(QString description READ description CONSTANT) - Q_PROPERTY(QString icon READ icon CONSTANT) - Q_PROPERTY(QString thumbnail READ thumbnail CONSTANT) - Q_PROPERTY(bool executable READ executable CONSTANT) - Q_PROPERTY(bool browsable READ browsable 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 CONSTANT) + Q_PROPERTY(QString mediaIcon READ mediaIcon NOTIFY mediaIconChanged) public: explicit BrowserItem(const QString &id, QObject *parent = nullptr); @@ -40,9 +42,27 @@ public: 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; @@ -51,6 +71,8 @@ private: QString m_thumbnail; bool m_executable = false; bool m_browsable = false; + bool m_disabled = false; + QStringList m_actionTypeIds; QString m_mediaIcon; }; diff --git a/libnymea-common/types/browseritems.cpp b/libnymea-common/types/browseritems.cpp index e5a67509..f0a64219 100644 --- a/libnymea-common/types/browseritems.cpp +++ b/libnymea-common/types/browseritems.cpp @@ -3,7 +3,10 @@ #include -BrowserItems::BrowserItems(QObject *parent): QAbstractListModel(parent) +BrowserItems::BrowserItems(const QUuid &deviceId, const QString &itemId, QObject *parent): + QAbstractListModel (parent), + m_deviceId(deviceId), + m_itemId(itemId) { } @@ -13,6 +16,16 @@ 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; @@ -41,6 +54,10 @@ QVariant BrowserItems::data(const QModelIndex &index, int role) const 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(); @@ -58,6 +75,8 @@ QHash BrowserItems::roleNames() const 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; @@ -72,6 +91,22 @@ void BrowserItems::addBrowserItem(BrowserItem *browserItem) 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) { @@ -79,3 +114,21 @@ void BrowserItems::setBusy(bool 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 index 4429b28e..e4d2ca6f 100644 --- a/libnymea-common/types/browseritems.h +++ b/libnymea-common/types/browseritems.h @@ -2,6 +2,7 @@ #define BROWSERITEMS_H #include +#include class BrowserItem; @@ -19,14 +20,19 @@ public: RoleThumbnail, RoleBrowsable, RoleExecutable, + RoleDisabled, + RoleActionTypeIds, RoleMediaIcon, }; Q_ENUM(Roles) - explicit BrowserItems(QObject *parent = nullptr); + 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; @@ -34,10 +40,14 @@ public: 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); + Q_INVOKABLE virtual BrowserItem* get(int index) const; + Q_INVOKABLE virtual BrowserItem* getBrowserItem(const QString &itemId); // void clear(); @@ -48,6 +58,9 @@ signals: 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 a9fcc81e..6f13d730 100644 --- a/libnymea-common/types/deviceclass.cpp +++ b/libnymea-common/types/deviceclass.cpp @@ -261,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 a86ae8b4..686e613e 100644 --- a/libnymea-common/types/deviceclass.h +++ b/libnymea-common/types/deviceclass.h @@ -53,6 +53,7 @@ class DeviceClass : public QObject 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: @@ -113,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; @@ -132,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/nymea-app/ui/components/MediaControls.qml b/nymea-app/ui/components/MediaControls.qml index b6719c5b..e52432cf 100644 --- a/nymea-app/ui/components/MediaControls.qml +++ b/nymea-app/ui/components/MediaControls.qml @@ -26,6 +26,8 @@ RowLayout { 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") @@ -61,6 +63,7 @@ RowLayout { 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") 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 b0768229..69cfdd04 100644 --- a/nymea-app/ui/components/NymeaListItemDelegate.qml +++ b/nymea-app/ui/components/NymeaListItemDelegate.qml @@ -28,6 +28,8 @@ SwipeDelegate { property alias additionalItem: additionalItemContainer.children + property alias busy: busyIndicator.running + signal deleteClicked() contentItem: RowLayout { @@ -52,6 +54,13 @@ SwipeDelegate { color: root.iconColor visible: root.fallbackIcon && (!root.iconName || icon.status === Image.Error) } + + BusyIndicator { + id: busyIndicator + anchors.centerIn: parent + visible: running + running: false + } } ColumnLayout { diff --git a/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml index 3b6629f9..9d0afcd5 100644 --- a/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml @@ -14,157 +14,189 @@ DeviceListPageBase { onBackPressed: pageStack.pop() } - ListView { + Flickable { anchors.fill: parent - model: root.devicesProxy + contentHeight: contentGrid.implicitHeight - delegate: ItemDelegate { - id: itemDelegate + GridLayout { + id: contentGrid width: parent.width + columns: Math.ceil(width / 500) + rowSpacing: 0 + columnSpacing: 0 - property bool inline: width > 500 + Repeater { + model: root.devicesProxy - property Device device: devicesProxy.get(index); - property DeviceClass deviceClass: device.deviceClass + delegate: ItemDelegate { + id: itemDelegate + Layout.preferredWidth: contentGrid.width / contentGrid.columns - readonly property StateType playbackStateType: deviceClass.stateTypes.findByName("playbackStatus") - readonly property State playbackState: playbackStateType ? device.states.getState(playbackStateType.id) : null + property bool inline: width > 500 - bottomPadding: index === ListView.view.count - 1 ? topPadding : 0 - contentItem: Pane { - id: contentItem - Material.elevation: 2 - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 + property Device device: devicesProxy.get(index); + property DeviceClass deviceClass: device.deviceClass - 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" - } - } + readonly property StateType playbackStateType: deviceClass.stateTypes.findByName("playbackStatus") + readonly property State playbackState: playbackStateType ? device.states.getState(playbackStateType.id) : null - } - 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 + readonly property StateType playerTypeStateType: deviceClass.stateTypes.findByName("playerType") + readonly property State playerTypeState: playerTypeStateType ? device.states.getState(playerTypeStateType.id) : null - 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 : "" - } - } + 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 { - id: maskRect - anchors.centerIn: parent - height: parent.width - width: parent.height - gradient: Gradient { - GradientStop { position: 0; color: "transparent" } - GradientStop { position: 1; color: "red" } - } - } - - 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; + 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) } } } - onClicked: { - enterPage(index, false) - } - } } + } + + } } } diff --git a/nymea-app/ui/devicepages/DeviceBrowserPage.qml b/nymea-app/ui/devicepages/DeviceBrowserPage.qml index 535234cc..5bf8209e 100644 --- a/nymea-app/ui/devicepages/DeviceBrowserPage.qml +++ b/nymea-app/ui/devicepages/DeviceBrowserPage.qml @@ -17,9 +17,40 @@ Page { 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 { @@ -36,16 +67,25 @@ Page { prominentSubText: false iconName: model.thumbnail fallbackIcon: "../images/browser/" + model.icon + ".svg" - enabled: model.browsable || model.executable + enabled: !model.disabled + busy: d.pendingItemId === model.id onClicked: { print("clicked:", model.id) if (model.executable) { - engine.deviceManager.executeBrowserItem(root.device.id, model.id) + root.executeBrowserItem(model.id) } else if (model.browsable) { pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {device: root.device, nodeId: model.id}) } } + + + onPressAndHold: { + print("show actions:", model.actionTypeIds) + var popup = actionDialogComponent.createObject(this, {title: model.displayName, itemId: model.id, actionTypeIds: model.actionTypeIds}); + popup.open() +// root.device.deviceClass.browserItemActionTypes.getActionType() + } } BusyIndicator { @@ -55,4 +95,32 @@ Page { } } + Component { + id: actionDialogComponent + MeaDialog { + id: actionDialog + + property string itemId + property alias actionTypeIds: actionListView.model + + ListView { + id: actionListView + Layout.fillWidth: true + implicitHeight: count * 50 + 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 params = [] + root.executeBrowserItemAction(actionDialog.itemId, actionType.id, params) + actionDialog.close() + } + } + } + } + } } diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index d7066726..947f0d71 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -24,6 +24,12 @@ DevicePageBase { } } + Component.onCompleted: { + if (root.deviceClass.browsable && playbackState.value === "Stopped") { + swipeView.currentIndex = 1; + } + } + function stateValue(name) { var stateType = root.deviceClass.stateTypes.findByName(name); if (!stateType) return null @@ -36,8 +42,36 @@ DevicePageBase { engine.deviceManager.executeAction(device.id, actionTypeId, params) } + function executeBrowserItem(itemId) { + d.pendingItemId = itemId + d.pendingBrowserItemId = engine.deviceManager.executeBrowserItem(device.id, itemId); + } + readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id) + QtObject { + id: d + property int pendingBrowserItemId: -1 + property string pendingItemId: "" + } + + Connections { + target: engine.deviceManager + onExecuteBrowserItemReply: { + 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: %").arg(params.params.deviceError), true) + } + } + } + } + SwipeView { id: swipeView anchors.fill: parent @@ -133,7 +167,7 @@ DevicePageBase { onClicked: { print("clicked:", model.id) if (model.executable) { - engine.deviceManager.executeBrowserItem(root.device.id, model.id) + root.executeBrowserItem(model.id) } else if (model.browsable) { internalPageStack.push(internalBrowserPage, {device: root.device, nodeId: model.id}) } From 62b2a33145ef8dbcb413f1879cd241a370659daf Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 16 Jul 2019 00:03:33 +0200 Subject: [PATCH 5/7] browseritemactions --- libnymea-app-core/devicemanager.cpp | 3 +- libnymea-app-core/devicemanager.h | 2 +- nymea-app/resources.qrc | 1 + .../ui/components/BrowserContextMenu.qml | 93 +++++++++++++++++++ .../ui/devicepages/DeviceBrowserPage.qml | 35 +------ nymea-app/ui/devicepages/MediaDevicePage.qml | 41 +++++--- 6 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 nymea-app/ui/components/BrowserContextMenu.qml diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index 2e06d1b9..d5666ab4 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -521,13 +521,14 @@ void DeviceManager::executeBrowserItemResponse(const QVariantMap ¶ms) emit executeBrowserItemReply(params); } -int DeviceManager::executeBrowserItemAction(const QUuid &deviceId, const QString &itemId, const QUuid &actionTypeId, const QVariantMap ¶ms) +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"); } diff --git a/libnymea-app-core/devicemanager.h b/libnymea-app-core/devicemanager.h index 11225ce3..d26fec3a 100644 --- a/libnymea-app-core/devicemanager.h +++ b/libnymea-app-core/devicemanager.h @@ -77,7 +77,7 @@ public: Q_INVOKABLE BrowserItems* browseDevice(const QUuid &deviceId, const QString &itemId = QString()); Q_INVOKABLE void refreshBrowserItems(BrowserItems *browserItems); Q_INVOKABLE int executeBrowserItem(const QUuid &deviceId, const QString &itemId); - Q_INVOKABLE int executeBrowserItemAction(const QUuid &deviceId, const QString &itemId, const QUuid &actionTypeId, const QVariantMap ¶ms = QVariantMap()); + 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); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index b3a8aa00..2dfbea2f 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -188,5 +188,6 @@ ui/devicelistpages/MediaDeviceListPage.qml ui/components/MediaControls.qml ui/components/MediaArtworkImage.qml + ui/components/BrowserContextMenu.qml 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/devicepages/DeviceBrowserPage.qml b/nymea-app/ui/devicepages/DeviceBrowserPage.qml index 5bf8209e..05d23ab5 100644 --- a/nymea-app/ui/devicepages/DeviceBrowserPage.qml +++ b/nymea-app/ui/devicepages/DeviceBrowserPage.qml @@ -82,9 +82,12 @@ Page { onPressAndHold: { print("show actions:", model.actionTypeIds) - var popup = actionDialogComponent.createObject(this, {title: model.displayName, itemId: model.id, actionTypeIds: model.actionTypeIds}); + var actionDialogComponent = Qt.createComponent(Qt.resolvedUrl("../components/BrowserContextMenu.qml")); + var popup = actionDialogComponent.createObject(this, {device: root.device, title: model.displayName, itemId: model.id, actionTypeIds: model.actionTypeIds}); + popup.activated.connect(function(actionTypeId, params) { + root.executeBrowserItemAction(model.id, actionTypeId, params) + }) popup.open() -// root.device.deviceClass.browserItemActionTypes.getActionType() } } @@ -95,32 +98,4 @@ Page { } } - Component { - id: actionDialogComponent - MeaDialog { - id: actionDialog - - property string itemId - property alias actionTypeIds: actionListView.model - - ListView { - id: actionListView - Layout.fillWidth: true - implicitHeight: count * 50 - 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 params = [] - root.executeBrowserItemAction(actionDialog.itemId, actionType.id, params) - actionDialog.close() - } - } - } - } - } } diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 947f0d71..f4ef6ef5 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -46,6 +46,11 @@ DevicePageBase { 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) @@ -57,17 +62,19 @@ DevicePageBase { Connections { target: engine.deviceManager - onExecuteBrowserItemReply: { - 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: %").arg(params.params.deviceError), true) - } + 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) } } } @@ -163,6 +170,8 @@ DevicePageBase { iconName: model.thumbnail fallbackIcon: "../images/browser/" + (model.mediaIcon && model.mediaIcon !== "MediaBrowserIconNone" ? model.mediaIcon : model.icon) + ".svg" enabled: model.browsable || model.executable + busy: d.pendingItemId === model.id + secondaryIconName: model.actionTypeIds.length > 0 ? "../images/navigation-menu.svg" : "" onClicked: { print("clicked:", model.id) @@ -172,6 +181,16 @@ DevicePageBase { internalPageStack.push(internalBrowserPage, {device: root.device, nodeId: model.id}) } } + onPressAndHold: { + print("show actions:", model.actionTypeIds) + 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) { + print("params:", JSON.stringify(params)) + root.executeBrowserItemAction(model.id, actionTypeId, params) + }) + popup.open() + } } BusyIndicator { From f735c5edfbbffe598197ca82e7b3f711508e66b4 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 17 Jul 2019 22:49:04 +0200 Subject: [PATCH 6/7] Browser Items in rules --- libnymea-app-core/jsonrpc/jsontypes.cpp | 3 + libnymea-app-core/rulemanager.cpp | 3 + libnymea-common/types/ruleaction.cpp | 15 +++++ libnymea-common/types/ruleaction.h | 6 ++ nymea-app/resources.qrc | 2 + nymea-app/ui/MagicPage.qml | 4 +- .../ui/components/NymeaListItemDelegate.qml | 17 ++++++ .../ui/delegates/BrowserItemDelegate.qml | 40 +++++++++++++ .../ui/devicepages/DeviceBrowserPage.qml | 24 ++------ nymea-app/ui/devicepages/MediaDevicePage.qml | 24 ++------ nymea-app/ui/magic/EditRulePage.qml | 2 +- nymea-app/ui/magic/RuleActionDelegate.qml | 5 +- .../ui/magic/SelectBrowserItemActionPage.qml | 48 ++++++++++++++++ nymea-app/ui/magic/SelectRuleActionPage.qml | 56 ++++++++++++++----- nymea-app/ui/mainviews/ScenesView.qml | 2 +- 15 files changed, 195 insertions(+), 56 deletions(-) create mode 100644 nymea-app/ui/delegates/BrowserItemDelegate.qml create mode 100644 nymea-app/ui/magic/SelectBrowserItemActionPage.qml diff --git a/libnymea-app-core/jsonrpc/jsontypes.cpp b/libnymea-app-core/jsonrpc/jsontypes.cpp index 5635506d..2c4374a3 100644 --- a/libnymea-app-core/jsonrpc/jsontypes.cpp +++ b/libnymea-app-core/jsonrpc/jsontypes.cpp @@ -318,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/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/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/resources.qrc b/nymea-app/resources.qrc index 2dfbea2f..7e21f360 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -189,5 +189,7 @@ 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/NymeaListItemDelegate.qml b/nymea-app/ui/components/NymeaListItemDelegate.qml index 69cfdd04..ea67ae96 100644 --- a/nymea-app/ui/components/NymeaListItemDelegate.qml +++ b/nymea-app/ui/components/NymeaListItemDelegate.qml @@ -22,15 +22,18 @@ SwipeDelegate { 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 @@ -95,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 { @@ -102,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/devicepages/DeviceBrowserPage.qml b/nymea-app/ui/devicepages/DeviceBrowserPage.qml index 05d23ab5..b20dca21 100644 --- a/nymea-app/ui/devicepages/DeviceBrowserPage.qml +++ b/nymea-app/ui/devicepages/DeviceBrowserPage.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" +import "../delegates" Page { id: root @@ -59,16 +60,10 @@ Page { model: d.model ScrollBar.vertical: ScrollBar {} - delegate: NymeaListItemDelegate { - 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.disabled + delegate: BrowserItemDelegate { + id: delegate busy: d.pendingItemId === model.id + device: root.device onClicked: { print("clicked:", model.id) @@ -79,15 +74,8 @@ Page { } } - - onPressAndHold: { - print("show actions:", model.actionTypeIds) - var actionDialogComponent = Qt.createComponent(Qt.resolvedUrl("../components/BrowserContextMenu.qml")); - var popup = actionDialogComponent.createObject(this, {device: root.device, title: model.displayName, itemId: model.id, actionTypeIds: model.actionTypeIds}); - popup.activated.connect(function(actionTypeId, params) { - root.executeBrowserItemAction(model.id, actionTypeId, params) - }) - popup.open() + onContextMenuActionTriggered: { + root.executeBrowserItemAction(model.id, actionTypeId, params) } } diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index f4ef6ef5..a7720940 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -6,6 +6,7 @@ import QtGraphicalEffects 1.0 import Nymea 1.0 import "../components" import "../customviews" +import "../delegates" DevicePageBase { id: root @@ -161,17 +162,10 @@ DevicePageBase { browserItems = engine.deviceManager.browseDevice(root.device.id, nodeId); } - delegate: NymeaListItemDelegate { - width: parent.width - text: model.displayName - progressive: model.browsable - subText: model.description - prominentSubText: false - iconName: model.thumbnail + delegate: BrowserItemDelegate { fallbackIcon: "../images/browser/" + (model.mediaIcon && model.mediaIcon !== "MediaBrowserIconNone" ? model.mediaIcon : model.icon) + ".svg" - enabled: model.browsable || model.executable busy: d.pendingItemId === model.id - secondaryIconName: model.actionTypeIds.length > 0 ? "../images/navigation-menu.svg" : "" + device: root.device onClicked: { print("clicked:", model.id) @@ -181,15 +175,9 @@ DevicePageBase { internalPageStack.push(internalBrowserPage, {device: root.device, nodeId: model.id}) } } - onPressAndHold: { - print("show actions:", model.actionTypeIds) - 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) { - print("params:", JSON.stringify(params)) - root.executeBrowserItemAction(model.id, actionTypeId, params) - }) - popup.open() + + onContextMenuActionTriggered: { + root.executeBrowserItemAction(model.id, actionTypeId, params) } } diff --git a/nymea-app/ui/magic/EditRulePage.qml b/nymea-app/ui/magic/EditRulePage.qml index 9a257fbc..1a148987 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 { diff --git a/nymea-app/ui/magic/RuleActionDelegate.qml b/nymea-app/ui/magic/RuleActionDelegate.qml index e45bec6c..f662942c 100644 --- a/nymea-app/ui/magic/RuleActionDelegate.qml +++ b/nymea-app/ui/magic/RuleActionDelegate.qml @@ -17,13 +17,14 @@ NymeaListItemDelegate { property var deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null property var actionType: deviceClass ? deviceClass.actionTypes.getActionType(ruleAction.actionTypeId) : iface ? iface.actionTypes.findByName(ruleAction.interfaceAction) : null + property var browserItemId: ruleAction.browserItemId 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 : qsTr("Launch an 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..ec61f756 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,10 +86,16 @@ 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) { @@ -109,18 +128,27 @@ 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; 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; + 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/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") From ba4ace5796fdae075446c6d3c70b0b758604715f Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 18 Jul 2019 01:12:19 +0200 Subject: [PATCH 7/7] Browser items in rules --- libnymea-app-core/devicemanager.cpp | 44 ++++++++++++++++++++- libnymea-app-core/devicemanager.h | 5 +++ nymea-app/ui/magic/EditRulePage.qml | 9 +++++ nymea-app/ui/magic/RuleActionDelegate.qml | 15 ++++--- nymea-app/ui/magic/SelectRuleActionPage.qml | 10 ++++- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index d5666ab4..eb0da28f 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -457,6 +457,21 @@ void DeviceManager::refreshBrowserItems(BrowserItems *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)); @@ -474,7 +489,6 @@ void DeviceManager::browseDeviceResponse(const QVariantMap ¶ms) 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(); @@ -507,6 +521,34 @@ void DeviceManager::browseDeviceResponse(const QVariantMap ¶ms) 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; diff --git a/libnymea-app-core/devicemanager.h b/libnymea-app-core/devicemanager.h index d26fec3a..57ae3777 100644 --- a/libnymea-app-core/devicemanager.h +++ b/libnymea-app-core/devicemanager.h @@ -30,6 +30,8 @@ #include "types/plugins.h" #include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonrpcclient.h" + +class BrowserItem; class BrowserItems; class DeviceManager : public JsonHandler @@ -76,6 +78,7 @@ public: 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()); @@ -95,6 +98,7 @@ private: 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); @@ -128,6 +132,7 @@ private: JsonRpcClient *m_jsonClient = nullptr; QHash > m_browsingRequests; + QHash > m_browserDetailsRequests; }; diff --git a/nymea-app/ui/magic/EditRulePage.qml b/nymea-app/ui/magic/EditRulePage.qml index 1a148987..5d749b3c 100644 --- a/nymea-app/ui/magic/EditRulePage.qml +++ b/nymea-app/ui/magic/EditRulePage.qml @@ -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 f662942c..82eeb2b6 100644 --- a/nymea-app/ui/magic/RuleActionDelegate.qml +++ b/nymea-app/ui/magic/RuleActionDelegate.qml @@ -12,19 +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 - property var browserItemId: ruleAction.browserItemId + 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 ? (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 : qsTr("Launch an item")) + 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/SelectRuleActionPage.qml b/nymea-app/ui/magic/SelectRuleActionPage.qml index ec61f756..3d29eadd 100644 --- a/nymea-app/ui/magic/SelectRuleActionPage.qml +++ b/nymea-app/ui/magic/SelectRuleActionPage.qml @@ -100,6 +100,8 @@ Page { 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}) @@ -113,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()}); @@ -132,13 +136,17 @@ Page { 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 { - var actionType = root.deviceClass.actionTypes.getActionType(model.actiontypeId); + 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(); });