From 2ad624329c552d18e9fc3cbbf4852be3a35ab485 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 8 Jul 2019 12:16:56 +0200 Subject: [PATCH] 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 {