From af52ce17edb7b1fb85a4e32f39dd349f63d1663f Mon Sep 17 00:00:00 2001 From: Aditya Mehra Date: Fri, 20 Aug 2021 19:37:15 +0930 Subject: [PATCH] add initial support for radio player and color themes --- __init__.py | 13 ++ ui/RadioPlayer.qml | 454 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+) create mode 100644 ui/RadioPlayer.qml diff --git a/__init__.py b/__init__.py index 014bbca..046143c 100644 --- a/__init__.py +++ b/__init__.py @@ -83,7 +83,9 @@ def initialize(self): self.add_event('playback.display.video.type', self.handle_display_video) self.add_event('playback.display.audio.type', self.handle_display_audio) + self.add_event('playback.display.radio.type', self.handle_display_radio) self.add_event('playback.display.remove', self.handle_remove_player) + self.add_event('playback.display.set.player.theme', self.handle_display_player_settheme) self.clear_gui_info() # Handle common audio intents. 'Audio' skills should listen for the @@ -269,12 +271,23 @@ def _play_query_timeout(self, message): if search_phrase in self.query_extensions: del self.query_extensions[search_phrase] + def handle_display_player_settheme(self, message): + theme = message.data.get("theme", "") + self.gui["textColor"] = theme.get("textColor", "") + self.gui["spectrumColor"] = theme.get("spectrumColor", "") + self.gui["seekBackgroundColor"] = theme.get("seekBackgroundColor", "") + self.gui["seekForgroundColor"] = theme.get("seekForgroundColor", "") + self.gui["cardBackgroundColor"] = theme.get("cardBackgroundColor", "") + def handle_display_video(self, message): self.gui.show_page("VideoPlayer.qml", override_idle=True) def handle_display_audio(self, message): self.gui.show_page("AudioPlayer.qml", override_idle=True) + def handle_display_radio(self, message): + self.gui.show_page("RadioPlayer.qml", override_idle=True) + def handle_remove_player(self, message): self.gui.release() diff --git a/ui/RadioPlayer.qml b/ui/RadioPlayer.qml new file mode 100644 index 0000000..be30197 --- /dev/null +++ b/ui/RadioPlayer.qml @@ -0,0 +1,454 @@ +/* + * Copyright 2019 by Aditya Mehra + * Copyright 2019 by Marco Martin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Layouts 1.12 +import QtMultimedia 5.12 +import QtGraphicalEffects 1.0 +import QtQuick.Templates 2.12 as T +import QtQuick.Controls 2.12 as Controls +import org.kde.kirigami 2.11 as Kirigami +import Mycroft 1.0 as Mycroft + +Mycroft.CardDelegate { + id: root + + fillWidth: true + skillBackgroundColorOverlay: "black" + cardBackgroundOverlayColor: sessionData.cardBackgroundColor ? sessionData.cardBackgroundColor : "black" + cardRadius: 10 + + readonly property var audioService: Mycroft.MediaService + + property var source + property string status: "stop" + property var thumbnail + property var title + property var author + property var playerMeta + property var cpsMeta + + //Player Support Vertical / Horizontal Layouts + property int switchWidth: Kirigami.Units.gridUnit * 22 + readonly property bool horizontal: width > switchWidth + + //Individual Components Visibility Properties + property bool progressBar: true + property bool thumbnailVisible: true + property bool titleVisible: true + + //Player Button Control Actions + property var nextAction: "mediaservice.gui.requested.next" + property var previousAction: "mediaservice.gui.requested.previous" + property var currentState: audioService.playbackState + + //Mediaplayer Related Properties To Be Set By Probe MediaPlayer + property var playerDuration + property var playerPosition + + //Spectrum Related Properties + property var spectrum: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + property var soundModelLength: audioService.spectrum.length + property color spectrumColorNormal: sessionData.spectrumColor ? sessionData.spectrumColor : Qt.rgba(1, 1, 1, 0.5) + property color spectrumColorMid: sessionData.spectrumColor ? sessionData.spectrumColor : spectrumColorNormal + property color spectrumColorPeak: sessionData.spectrumColor ? sessionData.spectrumColor : Qt.rgba(1, 0, 0, 0.5) + property real spectrumScale: 1 + property bool spectrumVisible: true + readonly property real spectrumHeight: (rep.parent.height / normalize(spectrumScale)) + + //Support custom colors for text / seekbar background / seekbar forground + property color textColor: sessionData.textColor ? sessionData.textColor : "white" + property color seekBackgroundColor: sessionData.seekBackgroundColor ? sessionData.seekBackgroundColor : "#bdbebf" + property color seekForgroundColor: sessionData.seekForegroundColor + + onSourceChanged: { + console.log(source) + play() + } + + Timer { + id: sampler + running: true + interval: 100 + repeat: true + onTriggered: { + spectrum = audioService.spectrum + } + } + + onActiveFocusChanged: { + if(activeFocus){ + playButton.forceActiveFocus(); + } + } + + function formatedDuration(millis){ + var minutes = Math.floor(millis / 60000); + var seconds = ((millis % 60000) / 1000).toFixed(0); + return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; + } + + function formatedPosition(millis){ + var minutes = Math.floor(millis / 60000); + var seconds = ((millis % 60000) / 1000).toFixed(0); + return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; + } + + function normalize(e){ + switch(e){case.1:return 10;case.2:return 9;case.3:return 8; + case.4:return 7;case.5:return 6;case.6:return 5;case.7:return 4;case.8:return 3; + case.9:return 2;case 1:return 1; default: return 1} + } + + function play(){ + audioService.playURL(source) + } + + function pause(){ + audioService.playerPause() + } + + function stop(){ + audioService.playerStop() + } + + function resume(){ + audioService.playerContinue() + } + + function seek(val){ + audioService.playerSeek(val) + } + + Connections { + target: Mycroft.MediaService + + onDurationChanged: { + playerDuration = dur + } + onPositionChanged: { + playerPosition = pos + } + onPlayRequested: { + source = audioService.getTrack() + } + + onStopRequested: { + source = "" + } + + onMediaStatusChanged: { + console.log(status) + } + + onMetaUpdated: { + console.log("Got Meta Update Signal Here") + + root.playerMeta = audioService.getPlayerMeta() + root.title = root.playerMeta.Title ? root.playerMeta.Title : "" + if(root.playerMeta.hasOwnProperty("Artist")) { + root.author = root.playerMeta.Artist + } else if(root.playerMeta.hasOwnProperty("ContributingArtist")) { + root.author = root.playerMeta.ContributingArtist + } else { + root.author = "" + } + + console.log("From QML Meta Updated Loading Metainfo") + console.log("Author: " + root.author + " Title: " + root.title) + } + + onMetaReceived: { + root.cpsMeta = audioService.getCPSMeta() + root.thumbnail = root.cpsMeta.thumbnail + + console.log("From QML Media Received Loading Metainfo") + console.log("Image: " + root.thumbnail) + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: bottomArea.top + anchors.topMargin: Mycroft.Units.gridUnit * 2 + anchors.leftMargin: Mycroft.Units.gridUnit * 2 + anchors.rightMargin: Mycroft.Units.gridUnit * 2 + z: 2 + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "transparent" + + GridLayout { + id: mainLayout + anchors.fill: parent + columnSpacing: 32 + columns: 2 + + //VideoOutput { + //id: vidOut + //Layout.fillWidth: true + //Layout.fillHeight: true + //source: audioService + //} + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.bottomMargin: 11 + color: "transparent" + + Image { + id: albumimg + visible: root.thumbnail != "" ? 1 : 0 + enabled: root.thumbnail != "" ? 1 : 0 + anchors.fill: parent + anchors.leftMargin: Mycroft.Units.gridUnit / 2 + anchors.topMargin: Mycroft.Units.gridUnit / 2 + anchors.rightMargin: Mycroft.Units.gridUnit / 2 + anchors.bottomMargin: Mycroft.Units.gridUnit / 2 + source: root.thumbnail + z: 100 + } + + RectangularGlow { + id: effect + anchors.fill: albumimg + visible: root.thumbnail != "" ? 1 : 0 + enabled: root.thumbnail != "" ? 1 : 0 + glowRadius: 5 + color: Qt.rgba(0, 0, 0, 0.7) + cornerRadius: 10 + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "transparent" + + ColumnLayout { + anchors.fill: parent + + Controls.Label { + id: authortitle + text: root.author + maximumLineCount: 1 + Layout.fillWidth: true + font.bold: true + font.pixelSize: Math.round(height * 0.765) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + font.capitalization: Font.Capitalize + color: root.textColor + visible: true + enabled: true + } + + Controls.Label { + id: songtitle + text: root.title + maximumLineCount: 1 + Layout.fillWidth: true + font.pixelSize: Math.round(height * 0.805) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + font.capitalization: Font.Capitalize + color: root.textColor + visible: true + enabled: true + } + + + RowLayout { + spacing: Kirigami.Units.largeSpacing * 3 + + Controls.Button { + id: previousButton + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + focus: false + KeyNavigation.right: playButton + onClicked: { + console.log("To Do: Add Replay") + } + + contentItem: Kirigami.Icon { + source: "system-reboot" + color: "white" + } + + background: Rectangle { + color: "transparent" + } + + Keys.onReturnPressed: { + clicked() + } + } + + Controls.Button { + id: playButton + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + onClicked: { + root.currentState === MediaPlayer.PlayingState ? root.pause() : root.currentState === MediaPlayer.PausedState ? root.resume() : root.play() + } + + background: Rectangle { + color: "transparent" + } + + contentItem: Kirigami.Icon { + color: "white" + source: root.currentState === MediaPlayer.PlayingState ? "media-playback-pause" : "media-playback-start" + } + } + } + Item { + Layout.preferredHeight: parent.height * 0.20 + Layout.fillWidth: true + } + } + } + } + } + } + + Rectangle { + id: bottomArea + anchors.bottom: parent.bottom + width: parent.width + height: Mycroft.Units.gridUnit * 6 + color: "transparent" + + RowLayout { + anchors.top: parent.top + anchors.topMargin: Mycroft.Units.gridUnit + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Mycroft.Units.gridUnit * 2 + anchors.rightMargin: Mycroft.Units.gridUnit * 2 + height: Mycroft.Units.gridUnit * 3 + + Controls.Label { + id: playerPosLabelBottom + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + font.pixelSize: height * 0.9 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + text: playerPosition ? formatedPosition(playerPosition) : "" + color: "white" + } + + Controls.Label { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + font.pixelSize: height * 0.9 + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: playerDuration ? formatedDuration(playerDuration) : "" + color: "white" + } + } + + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: Mycroft.Units.gridUnit * 0.5 + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Kirigami.Units.largeSpacing + anchors.rightMargin: Kirigami.Units.largeSpacing + + Row { + id: visualizationRowItemParent + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: parent.width * 0.18 + anchors.rightMargin: parent.width * 0.18 + height: parent.height + spacing: 4 + visible: spectrumVisible + enabled: spectrumVisible + z: -5 + + Repeater { + id: rep + model: root.soundModelLength + + delegate: Rectangle { + width: (visualizationRowItemParent.width * 0.85) / root.soundModelLength + radius: 3 + opacity: root.currentState === MediaPlayer.PlayingState ? 1 : 0 + height: 15 + root.spectrum[modelData] * root.spectrumHeight + anchors.bottom: parent.bottom + + gradient: Gradient { + GradientStop {position: 0.05; color: height > root.spectrumHeight / 1.25 ? spectrumColorPeak : spectrumColorNormal} + GradientStop {position: 0.25; color: spectrumColorMid} + GradientStop {position: 0.50; color: spectrumColorNormal} + GradientStop {position: 0.85; color: spectrumColorMid} + } + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.Linear + } + } + Behavior on opacity { + NumberAnimation{ + duration: 1500 + root.spectrum[modelData] * parent.height + easing.type: Easing.Linear + } + } + } + } + } + } + + Rectangle { + id: seekableslider + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: (Mycroft.Units.gridUnit * 2) - Kirigami.Units.smallSpacing + radius: 10 + color: root.seekBackgroundColor + + Controls.Label { + anchors.centerIn: parent + color: root.textColor + text: "STREAMING" + } + } + } +} +