From bbe9bbf94cbf77ffef1ef06036e8e80bfbcf46e9 Mon Sep 17 00:00:00 2001 From: Devin Lin Date: Fri, 8 Apr 2022 21:48:42 -0400 Subject: [PATCH] widgets/mediaplayer: Add support for controlling multiple media sources Implement #135 --- .../mediacontrols/MediaControlsSource.qml | 107 +++++--- .../mediacontrols/MediaControlsWidget.qml | 230 +++++++++++------- 2 files changed, 214 insertions(+), 123 deletions(-) diff --git a/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml index 097f19d8..fb785059 100644 --- a/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml +++ b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsSource.qml @@ -12,25 +12,52 @@ import org.kde.plasma.core 2.0 as PlasmaCore PlasmaCore.DataSource { id: mpris2Source - readonly property string source: "@multiplex" - readonly property var playerData: data[source] + engine: "mpris2" + connectedSources: sources + + readonly property string multiplexSource: "@multiplex" + + property var mprisSourcesModel: [] + + readonly property bool hasPlayer: sources.length > 1 - readonly property bool hasPlayer: sources.length > 1 && !!playerData - readonly property string identity: hasPlayer ? playerData.Identity : "" - readonly property bool playing: hasPlayer && playerData.PlaybackStatus === "Playing" - readonly property bool canControl: hasPlayer && playerData.CanControl - readonly property bool canGoBack: hasPlayer && playerData.CanGoPrevious - readonly property bool canGoNext: hasPlayer && playerData.CanGoNext + function startOperation(src, op) { + var service = serviceForSource(src) + var operation = service.operationDescription(op) + return service.startOperationCall(operation) + } - readonly property var currentMetadata: hasPlayer ? playerData.Metadata : ({}) - - readonly property string track: { - const xesamTitle = currentMetadata["xesam:title"] + function goPrevious(source) { + startOperation(source, "Previous"); + } + function goNext(source) { + startOperation(source, "Next"); + } + function playPause(source) { + startOperation(source, "PlayPause"); + } + function isPlaying(source) { + return data[source] ? data[source].PlaybackStatus === "Playing" : false; + } + function canControl(source) { + return data[source] ? data[source].CanControl : false; + } + function canGoBack(source) { + return data[source] ? data[source].CanGoPrevious : false; + } + function canGoNext(source) { + return data[source] ? data[source].CanGoNext : false; + } + function track(source) { + if (!data[source]) { + return ""; + } + const xesamTitle = data[source].Metadata["xesam:title"] if (xesamTitle) { return xesamTitle } // if no track title is given, print out the file name - const xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" + const xesamUrl = data[source].Metadata["xesam:url"] ? data[source].Metadata["xesam:url"].toString() : "" if (!xesamUrl) { return "" } @@ -41,25 +68,43 @@ PlasmaCore.DataSource { const lastUrlPart = xesamUrl.substring(lastSlashPos + 1) return decodeURIComponent(lastUrlPart) } - readonly property string artist: currentMetadata["xesam:artist"] || "" - readonly property string albumArt: currentMetadata["mpris:artUrl"] || "" + function artist(source) { + return data[source] ? data[source].Metadata["xesam:artist"] || "" : ""; + } + function albumArt(source) { + return data[source] ? data[source].Metadata["mpris:artUrl"] || "" : ""; + } + + function updateMprisSourcesModel() { + let model = []; + + let sources = mpris2Source.sources; + for (let i = 0; i < sources.length; ++i) { + let source = sources[i]; + if (source === mpris2Source.multiplexSource) { + continue; + } + + const playerData = mpris2Source.data[source]; + // source data is removed before its name is removed from the list + if (!playerData) { + continue; + } - engine: "mpris2" - connectedSources: [source] - - function startOperation(op) { - var service = serviceForSource(source) - var operation = service.operationDescription(op) - return service.startOperationCall(operation) + model.push({ + 'application': playerData["Identity"], + 'source': source, + }); + } + + mprisSourcesModel = model; } - - function goPrevious() { - startOperation("Previous"); - } - function goNext() { - startOperation("Next"); - } - function playPause(source) { - startOperation("PlayPause"); + + Component.onCompleted: { + mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts() + updateMprisSourcesModel() } + + onSourceAdded: updateMprisSourcesModel() + onSourceRemoved: updateMprisSourcesModel(); } diff --git a/components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml index 9b734c72..ce127869 100644 --- a/components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml +++ b/components/mobileshell/qml/widgets/mediacontrols/MediaControlsWidget.qml @@ -17,110 +17,156 @@ import org.kde.plasma.extras 2.0 as PlasmaExtras import "../../components" as Components -Components.BaseItem { +/** + * Embeddable component that provides MPRIS control. + */ +Item { id: root - visible: mpris2Source.hasPlayer - padding: visible ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0 - implicitHeight: visible ? bottomPadding + topPadding + PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing : 0 - background: BlurredBackground { - imageSource: mpris2Source.albumArt + readonly property real padding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + readonly property real contentHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing + implicitHeight: visible ? padding * 2 + contentHeight : 0 + + MediaControlsSource { + id: mpris2Source } - contentItem: PlasmaCore.ColorScope { - colorGroup: PlasmaCore.Theme.ComplementaryColorGroup - width: root.width - root.leftPadding - root.rightPadding + // page indicator + RowLayout { + z: 1 + visible: view.count > 1 + spacing: Kirigami.Units.smallSpacing + anchors.bottomMargin: Kirigami.Units.smallSpacing * 2 + anchors.bottom: view.bottom + anchors.horizontalCenter: parent.horizontalCenter - MediaControlsSource { - id: mpris2Source + Repeater { + model: view.count + delegate: Rectangle { + width: Kirigami.Units.smallSpacing + height: Kirigami.Units.smallSpacing + radius: width / 2 + color: Qt.rgba(255, 255, 255, view.currentIndex == model.index ? 1 : 0.5) + } } + } + + // list of app media widgets + QQC2.SwipeView { + id: view + clip: true - RowLayout { - id: controlsRow - width: parent.width - height: parent.height - spacing: 0 - - enabled: mpris2Source.canControl - - Image { - id: albumArt - Layout.preferredWidth: height - Layout.fillHeight: true - asynchronous: true - fillMode: Image.PreserveAspectFit - source: mpris2Source.albumArt - sourceSize.height: height - visible: status === Image.Loading || status === Image.Ready - } - - ColumnLayout { - Layout.leftMargin: albumArt.visible ? Kirigami.Units.largeSpacing : 0 - Layout.fillWidth: true - spacing: Kirigami.Units.smallSpacing - - QQC2.Label { - Layout.fillWidth: true - wrapMode: Text.NoWrap - elide: Text.ElideRight - text: mpris2Source.track || i18n("No media playing") - textFormat: Text.PlainText - font.pointSize: PlasmaCore.Theme.defaultFont.pointSize - maximumLineCount: 1 - color: "white" + anchors.fill: parent + + Repeater { + model: mpris2Source.mprisSourcesModel + + delegate: Components.BaseItem { + id: playerItem + + property string source: modelData.source + + padding: root.padding + implicitHeight: root.contentHeight + root.padding * 2 + implicitWidth: root.width + + background: BlurredBackground { + imageSource: mpris2Source.albumArt(playerItem.source) } + + contentItem: PlasmaCore.ColorScope { + colorGroup: PlasmaCore.Theme.ComplementaryColorGroup + width: playerItem.width - playerItem.leftPadding - playerItem.rightPadding + + RowLayout { + id: controlsRow + width: parent.width + height: parent.height + spacing: 0 - QQC2.Label { - Layout.fillWidth: true - wrapMode: Text.NoWrap - elide: Text.ElideRight - // if no artist is given, show player name instead - text: mpris2Source.artist || mpris2Source.identity || "" - textFormat: Text.PlainText - font.pointSize: PlasmaCore.Theme.smallestFont.pointSize - maximumLineCount: 1 - opacity: 0.9 - color: "white" + enabled: mpris2Source.canControl(playerItem.source) + + Image { + id: albumArt + Layout.preferredWidth: height + Layout.fillHeight: true + asynchronous: true + fillMode: Image.PreserveAspectFit + source: mpris2Source.albumArt(playerItem.source) + sourceSize.height: height + visible: status === Image.Loading || status === Image.Ready + } + + ColumnLayout { + Layout.leftMargin: albumArt.visible ? Kirigami.Units.largeSpacing : 0 + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.NoWrap + elide: Text.ElideRight + text: mpris2Source.track(playerItem.source) || i18n("No media playing") + textFormat: Text.PlainText + font.pointSize: PlasmaCore.Theme.defaultFont.pointSize + maximumLineCount: 1 + color: "white" + } + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.NoWrap + elide: Text.ElideRight + // if no artist is given, show player name instead + text: mpris2Source.artist(playerItem.source) || modelData.application || "" + textFormat: Text.PlainText + font.pointSize: PlasmaCore.Theme.smallestFont.pointSize + maximumLineCount: 1 + opacity: 0.9 + color: "white" + } + } + + PlasmaComponents3.ToolButton { + Layout.fillHeight: true + Layout.preferredWidth: height + + enabled: mpris2Source.canGoBack(playerItem.source) + icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" + icon.width: PlasmaCore.Units.iconSizes.small + icon.height: PlasmaCore.Units.iconSizes.small + onClicked: mpris2Source.goPrevious(playerItem.source) + visible: mpris2Source.canGoBack(playerItem.source) || mpris2Source.canGoNext(playerItem.source) + Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Previous track") + } + + PlasmaComponents3.ToolButton { + Layout.fillHeight: true + Layout.preferredWidth: height + + icon.name: mpris2Source.isPlaying(playerItem.source) ? "media-playback-pause" : "media-playback-start" + icon.width: PlasmaCore.Units.iconSizes.small + icon.height: PlasmaCore.Units.iconSizes.small + onClicked: mpris2Source.playPause(playerItem.source) + Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Play or Pause media") + } + + PlasmaComponents3.ToolButton { + Layout.fillHeight: true + Layout.preferredWidth: height + + enabled: mpris2Source.canGoBack(playerItem.source) + icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" + icon.width: PlasmaCore.Units.iconSizes.small + icon.height: PlasmaCore.Units.iconSizes.small + onClicked: mpris2Source.goNext(playerItem.source) + visible: mpris2Source.canGoBack(playerItem.source) || mpris2Source.canGoNext(playerItem.source) + Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Next track") + } + } } } - - PlasmaComponents3.ToolButton { - Layout.fillHeight: true - Layout.preferredWidth: height - - enabled: mpris2Source.canGoBack - icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" - icon.width: PlasmaCore.Units.iconSizes.small - icon.height: PlasmaCore.Units.iconSizes.small - onClicked: mpris2Source.goPrevious() - visible: mpris2Source.canGoBack || mpris2Source.canGoNext - Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Previous track") - } - - PlasmaComponents3.ToolButton { - Layout.fillHeight: true - Layout.preferredWidth: height - - icon.name: mpris2Source.playing ? "media-playback-pause" : "media-playback-start" - icon.width: PlasmaCore.Units.iconSizes.small - icon.height: PlasmaCore.Units.iconSizes.small - onClicked: mpris2Source.playPause() - Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Play or Pause media") - } - - PlasmaComponents3.ToolButton { - Layout.fillHeight: true - Layout.preferredWidth: height - - enabled: mpris2Source.canGoNext - icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" - icon.width: PlasmaCore.Units.iconSizes.small - icon.height: PlasmaCore.Units.iconSizes.small - onClicked: mpris2Source.goNext() - visible: mpris2Source.canGoBack || mpris2Source.canGoNext - Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Next track") - } } } }