diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index a5c271a4..1bd386d1 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -161,5 +161,11 @@ ui/images/mqtt.svg ui/images/sensors/co2.svg ui/images/sensors/noise.svg + ui/images/media-playlist-repeat-one.svg + ui/images/media-playlist-repeat.svg + ui/images/media-playlist-shuffle.svg + ui/images/media-playlist.svg + ui/images/stock_music.svg + ui/images/stock_video.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 4a4d41d0..8961c881 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -51,6 +51,8 @@ ui/components/FingerprintVisual.qml ui/components/ListSectionHeader.qml ui/components/ListFilterInput.qml + ui/components/Led.qml + ui/components/ProgressButton.qml ui/customviews/GenericTypeLogView.qml ui/customviews/CustomViewBase.qml ui/customviews/WeatherView.qml @@ -77,6 +79,7 @@ ui/devicepages/NotificationsDevicePage.qml ui/devicepages/LightDevicePage.qml ui/devicepages/FingerprintReaderDevicePage.qml + ui/devicepages/DeviceLogPage.qml ui/devicelistpages/GenericDeviceListPage.qml ui/devicelistpages/ClosablesDeviceListPage.qml ui/devicelistpages/GarageDeviceListPage.qml @@ -141,7 +144,5 @@ ui/system/MqttBrokerSettingsPage.qml ui/system/ServerConfigurationDialog.qml ui/system/MqttPolicyPage.qml - ui/devicepages/DeviceLogPage.qml - ui/components/Led.qml diff --git a/nymea-app/ui/components/ProgressButton.qml b/nymea-app/ui/components/ProgressButton.qml new file mode 100644 index 00000000..529f6e4d --- /dev/null +++ b/nymea-app/ui/components/ProgressButton.qml @@ -0,0 +1,122 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.3 + +Item { + id: root + + property string imageSource + property string longpressImageSource: imageSource + property bool repeat: false + + property bool longpressEnabled: true + + signal clicked(); + signal longpressed(); + + MouseArea { + id: buttonDelegate + anchors.fill: parent + + property bool longpressed: false + + onPressed: { + canvas.inverted = false + buttonDelegate.longpressed = false + } + onReleased: { + if (!containsMouse) { + print("cancelled") + buttonDelegate.longpressed = false; + return; + } + + if (buttonDelegate.longpressed) { + if (!repeat) { + root.longpressed(); + } + } else { + root.clicked(); + } + buttonDelegate.longpressed = false + } + + NumberAnimation { + target: canvas + properties: "progress" + from: 0.0 + to: 1.0 + running: root.longpressEnabled && buttonDelegate.pressed + duration: 750 + onRunningChanged: { + if (!running && canvas.progress == 1) { + buttonDelegate.longpressed = true; + if (root.repeat) { + root.longpressed(); + start(); + canvas.inverted = !canvas.inverted + } + } + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: -app.margins / 2 + radius: width / 2 + color: app.foregroundColor + opacity: buttonDelegate.pressed ? .08 : 0 + Behavior on opacity { + NumberAnimation { duration: 200 } + } + } + + Canvas { + id: canvas + anchors.fill: parent + anchors.margins: -app.margins / 2 + + property real progress: 0 + property bool inverted: false + + readonly property int penWidth: 2 + onProgressChanged: { + requestPaint() + } + Connections { + target: buttonDelegate + onPressedChanged: { + if (!buttonDelegate.pressed) { + canvas.progress = 0; + canvas.requestPaint() + } + } + } + + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = Qt.rgba(1, 0, 0, 1); + ctx.lineWidth = canvas.penWidth + ctx.strokeStyle = app.accentColor + + var start = -Math.PI / 2; + var stop = -Math.PI / 2; + if (inverted) { + start += canvas.progress * 2 * Math.PI + } else { + stop += canvas.progress * 2 * Math.PI + } + + ctx.beginPath(); + ctx.arc(canvas.width / 2, canvas.height / 2, ((canvas.width - canvas.penWidth) / 2), start, stop); + ctx.stroke(); + } + } + + ColorIcon { + anchors.fill: parent + name: buttonDelegate.longpressed ? root.longpressImageSource : root.imageSource + } + } +} + diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 58a4ba27..8d0695a6 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -1,6 +1,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 import Nymea 1.0 import "../components" import "../customviews" @@ -8,23 +10,257 @@ import "../customviews" DevicePageBase { id: root - ColumnLayout { + function stateValue(name) { + var stateType = root.deviceClass.stateTypes.findByName(name); + if (!stateType) return null + return root.device.states.getState(stateType.id).value + } + + function executeAction(actionName, params) { + var actionTypeId = deviceClass.actionTypes.findByName(actionName).id; + print("executing", device, device.id, actionTypeId, actionName, deviceClass.actionTypes, params) + engine.deviceManager.executeAction(device.id, actionTypeId, params) + } + + readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id) + + GridLayout { id: contentColumn anchors.fill: parent - spacing: app.margins + anchors.margins: app.margins + columns: app.landscape ? 2 : 1 + columnSpacing: app.margins + rowSpacing: app.margins - ExtendedVolumeController { + Pane { Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass -// visible: deviceClass.interfaces.indexOf("extendedvolumecontroller") >= 0 + Layout.fillHeight: true + Material.elevation: 2 + padding: 0 + + contentItem: Rectangle { + color: app.foregroundColor + + Image { + id: artworkImage + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: root.stateValue("artwork") + } + + ColorIcon { + id: fallback + anchors.fill: parent + anchors.margins: app.margins * 2 + name: root.stateValue("playerType") === "video" ? "../images/stock_video.svg" : "../images/stock_music.svg" + visible: artworkImage.status !== Image.Ready || artworkImage.source === "" + color: app.primaryColor + } + } } - MediaControllerView { + ColumnLayout { Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass - visible: root.deviceClass.interfaces.indexOf("mediacontroller") >= 0 + spacing: app.margins + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + font.pixelSize: app.largeFont + font.bold: true + text: root.stateValue("title") + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + text: root.stateValue("artist") + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + text: root.stateValue("collection") + } + + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + + ProgressButton { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + imageSource: "../images/media-skip-backward.svg" + longpressImageSource: "../images/media-seek-backward.svg" + repeat: true + + onClicked: { + root.executeAction("skipBack") + } + onLongpressed: { + root.executeAction("fastRewind") + } + } + + Item { Layout.fillWidth: true } + + ProgressButton { + Layout.preferredHeight: app.iconSize * 2 + Layout.preferredWidth: height + imageSource: root.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" + longpressImageSource: "../images/media-playback-stop.svg" + longpressEnabled: root.playbackState.value !== "Stopped" + + onClicked: { + if (root.playbackState.value === "Playing") { + root.executeAction("pause") + } else { + root.executeAction("play") + } + } + + onLongpressed: { + root.executeAction("stop") + } + } + + Item { Layout.fillWidth: true } + ProgressButton { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + imageSource: "../images/media-skip-forward.svg" + longpressImageSource: "../images/media-seek-forward.svg" + repeat: true + onClicked: { + root.executeAction("skipNext") + } + onLongpressed: { + root.executeAction("fastForward") + } + } + Item { Layout.fillWidth: true } + } + } + } + + Component { + id: volumeSliderPaneComponent + Dialog { + + leftPadding: 0 + topPadding: app.margins / 2 + rightPadding: 0 + bottomPadding: app.margins / 2 + + contentItem: ColumnLayout { + Slider { + Layout.fillHeight: true + orientation: Qt.Vertical + from: 0 + to: 100 + value: root.stateValue("volume") + onMoved: { + var params = [] + var volParam = {} + volParam["paramTypeId"] = root.deviceClass.actionTypes.findByName("volume").id + volParam["value"] = value; + params.push(volParam) + root.executeAction("volume", params); + } + } + HeaderButton { + imageSource: "../images/audio-speakers-muted-symbolic.svg" + color: root.stateValue("mute") ? app.accentColor : keyColor + onClicked: { + var params = [] + var muteParam = {} + muteParam["paramTypeId"] = root.deviceClass.actionTypes.findByName("mute").id + muteParam["value"] = !root.stateValue("mute"); + params.push(muteParam) + root.executeAction("mute", params); + } + } + } + } + } + + footer: Pane { + Material.elevation: 1 + height: 50 + padding: 0 + contentItem: RowLayout { +// Item { +// Layout.fillWidth: true; Layout.fillHeight: true +// HeaderButton { +// anchors.centerIn: parent +// imageSource: "../images/media-playlist.svg" +// } +// } + 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() + } + } + } } } } diff --git a/nymea-app/ui/images/media-playlist-repeat-one.svg b/nymea-app/ui/images/media-playlist-repeat-one.svg new file mode 100644 index 00000000..194ffb94 --- /dev/null +++ b/nymea-app/ui/images/media-playlist-repeat-one.svg @@ -0,0 +1,19 @@ + + + + + + + image/svg+xml + + + + + + + + + + 1 + + diff --git a/nymea-app/ui/images/media-playlist-repeat.svg b/nymea-app/ui/images/media-playlist-repeat.svg new file mode 100644 index 00000000..54a2d0e4 --- /dev/null +++ b/nymea-app/ui/images/media-playlist-repeat.svg @@ -0,0 +1,18 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/nymea-app/ui/images/media-playlist-shuffle.svg b/nymea-app/ui/images/media-playlist-shuffle.svg new file mode 100644 index 00000000..d16487d5 --- /dev/null +++ b/nymea-app/ui/images/media-playlist-shuffle.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/media-playlist.svg b/nymea-app/ui/images/media-playlist.svg new file mode 100644 index 00000000..3ab686d4 --- /dev/null +++ b/nymea-app/ui/images/media-playlist.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/stock_music.svg b/nymea-app/ui/images/stock_music.svg new file mode 100644 index 00000000..b43bc37d --- /dev/null +++ b/nymea-app/ui/images/stock_music.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/stock_video.svg b/nymea-app/ui/images/stock_video.svg new file mode 100644 index 00000000..56c5a4f4 --- /dev/null +++ b/nymea-app/ui/images/stock_video.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + +