widgets/mediaplayer: Add support for controlling multiple media sources

Implement #135
This commit is contained in:
Devin Lin 2022-04-08 21:48:42 -04:00
parent 79c4dac78a
commit bbe9bbf94c
2 changed files with 214 additions and 123 deletions

View file

@ -12,25 +12,52 @@ import org.kde.plasma.core 2.0 as PlasmaCore
PlasmaCore.DataSource { PlasmaCore.DataSource {
id: mpris2Source id: mpris2Source
readonly property string source: "@multiplex" engine: "mpris2"
readonly property var playerData: data[source] connectedSources: sources
readonly property bool hasPlayer: sources.length > 1 && !!playerData readonly property string multiplexSource: "@multiplex"
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
readonly property var currentMetadata: hasPlayer ? playerData.Metadata : ({}) property var mprisSourcesModel: []
readonly property string track: { readonly property bool hasPlayer: sources.length > 1
const xesamTitle = currentMetadata["xesam:title"]
function startOperation(src, op) {
var service = serviceForSource(src)
var operation = service.operationDescription(op)
return service.startOperationCall(operation)
}
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) { if (xesamTitle) {
return xesamTitle return xesamTitle
} }
// if no track title is given, print out the file name // 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) { if (!xesamUrl) {
return "" return ""
} }
@ -41,25 +68,43 @@ PlasmaCore.DataSource {
const lastUrlPart = xesamUrl.substring(lastSlashPos + 1) const lastUrlPart = xesamUrl.substring(lastSlashPos + 1)
return decodeURIComponent(lastUrlPart) return decodeURIComponent(lastUrlPart)
} }
readonly property string artist: currentMetadata["xesam:artist"] || "" function artist(source) {
readonly property string albumArt: currentMetadata["mpris:artUrl"] || "" return data[source] ? data[source].Metadata["xesam:artist"] || "" : "";
}
engine: "mpris2" function albumArt(source) {
connectedSources: [source] return data[source] ? data[source].Metadata["mpris:artUrl"] || "" : "";
function startOperation(op) {
var service = serviceForSource(source)
var operation = service.operationDescription(op)
return service.startOperationCall(operation)
} }
function goPrevious() { function updateMprisSourcesModel() {
startOperation("Previous"); 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;
}
model.push({
'application': playerData["Identity"],
'source': source,
});
}
mprisSourcesModel = model;
} }
function goNext() {
startOperation("Next"); Component.onCompleted: {
} mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts()
function playPause(source) { updateMprisSourcesModel()
startOperation("PlayPause");
} }
onSourceAdded: updateMprisSourcesModel()
onSourceRemoved: updateMprisSourcesModel();
} }

View file

@ -17,110 +17,156 @@ import org.kde.plasma.extras 2.0 as PlasmaExtras
import "../../components" as Components import "../../components" as Components
Components.BaseItem { /**
* Embeddable component that provides MPRIS control.
*/
Item {
id: root id: root
visible: mpris2Source.hasPlayer 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 { readonly property real padding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
imageSource: mpris2Source.albumArt readonly property real contentHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaCore.Units.smallSpacing
implicitHeight: visible ? padding * 2 + contentHeight : 0
MediaControlsSource {
id: mpris2Source
} }
contentItem: PlasmaCore.ColorScope { // page indicator
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup RowLayout {
width: root.width - root.leftPadding - root.rightPadding 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 { Repeater {
id: mpris2Source 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)
}
} }
}
RowLayout { // list of app media widgets
id: controlsRow QQC2.SwipeView {
width: parent.width id: view
height: parent.height clip: true
spacing: 0
enabled: mpris2Source.canControl anchors.fill: parent
Image { Repeater {
id: albumArt model: mpris2Source.mprisSourcesModel
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 { delegate: Components.BaseItem {
Layout.leftMargin: albumArt.visible ? Kirigami.Units.largeSpacing : 0 id: playerItem
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.Label { property string source: modelData.source
Layout.fillWidth: true
wrapMode: Text.NoWrap padding: root.padding
elide: Text.ElideRight implicitHeight: root.contentHeight + root.padding * 2
text: mpris2Source.track || i18n("No media playing") implicitWidth: root.width
textFormat: Text.PlainText
font.pointSize: PlasmaCore.Theme.defaultFont.pointSize background: BlurredBackground {
maximumLineCount: 1 imageSource: mpris2Source.albumArt(playerItem.source)
color: "white"
} }
QQC2.Label { contentItem: PlasmaCore.ColorScope {
Layout.fillWidth: true colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
wrapMode: Text.NoWrap width: playerItem.width - playerItem.leftPadding - playerItem.rightPadding
elide: Text.ElideRight
// if no artist is given, show player name instead RowLayout {
text: mpris2Source.artist || mpris2Source.identity || "" id: controlsRow
textFormat: Text.PlainText width: parent.width
font.pointSize: PlasmaCore.Theme.smallestFont.pointSize height: parent.height
maximumLineCount: 1 spacing: 0
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")
}
} }
} }
} }