From 41ebfb48f3b93f15f57268f323d92129905d620a Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 10 Jul 2019 11:40:06 +0200 Subject: [PATCH] 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}) }