mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-08-03 18:24:46 +00:00
widgets/mediaplayer: Add support for controlling multiple media sources
Implement #135
This commit is contained in:
parent
79c4dac78a
commit
bbe9bbf94c
2 changed files with 214 additions and 123 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
function goNext() {
|
|
||||||
startOperation("Next");
|
const playerData = mpris2Source.data[source];
|
||||||
|
// source data is removed before its name is removed from the list
|
||||||
|
if (!playerData) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
function playPause(source) {
|
|
||||||
startOperation("PlayPause");
|
model.push({
|
||||||
|
'application': playerData["Identity"],
|
||||||
|
'source': source,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mprisSourcesModel = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts()
|
||||||
|
updateMprisSourcesModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSourceAdded: updateMprisSourcesModel()
|
||||||
|
onSourceRemoved: updateMprisSourcesModel();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,32 +17,75 @@ 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
|
||||||
|
|
||||||
contentItem: PlasmaCore.ColorScope {
|
|
||||||
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
|
|
||||||
width: root.width - root.leftPadding - root.rightPadding
|
|
||||||
|
|
||||||
MediaControlsSource {
|
MediaControlsSource {
|
||||||
id: mpris2Source
|
id: mpris2Source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 {
|
RowLayout {
|
||||||
id: controlsRow
|
id: controlsRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
enabled: mpris2Source.canControl
|
enabled: mpris2Source.canControl(playerItem.source)
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: albumArt
|
id: albumArt
|
||||||
|
|
@ -50,7 +93,7 @@ Components.BaseItem {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
source: mpris2Source.albumArt
|
source: mpris2Source.albumArt(playerItem.source)
|
||||||
sourceSize.height: height
|
sourceSize.height: height
|
||||||
visible: status === Image.Loading || status === Image.Ready
|
visible: status === Image.Loading || status === Image.Ready
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +107,7 @@ Components.BaseItem {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
text: mpris2Source.track || i18n("No media playing")
|
text: mpris2Source.track(playerItem.source) || i18n("No media playing")
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
font.pointSize: PlasmaCore.Theme.defaultFont.pointSize
|
font.pointSize: PlasmaCore.Theme.defaultFont.pointSize
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
|
|
@ -76,7 +119,7 @@ Components.BaseItem {
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
// if no artist is given, show player name instead
|
// if no artist is given, show player name instead
|
||||||
text: mpris2Source.artist || mpris2Source.identity || ""
|
text: mpris2Source.artist(playerItem.source) || modelData.application || ""
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
font.pointSize: PlasmaCore.Theme.smallestFont.pointSize
|
font.pointSize: PlasmaCore.Theme.smallestFont.pointSize
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
|
|
@ -89,12 +132,12 @@ Components.BaseItem {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: height
|
Layout.preferredWidth: height
|
||||||
|
|
||||||
enabled: mpris2Source.canGoBack
|
enabled: mpris2Source.canGoBack(playerItem.source)
|
||||||
icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
|
icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
|
||||||
icon.width: PlasmaCore.Units.iconSizes.small
|
icon.width: PlasmaCore.Units.iconSizes.small
|
||||||
icon.height: PlasmaCore.Units.iconSizes.small
|
icon.height: PlasmaCore.Units.iconSizes.small
|
||||||
onClicked: mpris2Source.goPrevious()
|
onClicked: mpris2Source.goPrevious(playerItem.source)
|
||||||
visible: mpris2Source.canGoBack || mpris2Source.canGoNext
|
visible: mpris2Source.canGoBack(playerItem.source) || mpris2Source.canGoNext(playerItem.source)
|
||||||
Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Previous track")
|
Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Previous track")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,10 +145,10 @@ Components.BaseItem {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: height
|
Layout.preferredWidth: height
|
||||||
|
|
||||||
icon.name: mpris2Source.playing ? "media-playback-pause" : "media-playback-start"
|
icon.name: mpris2Source.isPlaying(playerItem.source) ? "media-playback-pause" : "media-playback-start"
|
||||||
icon.width: PlasmaCore.Units.iconSizes.small
|
icon.width: PlasmaCore.Units.iconSizes.small
|
||||||
icon.height: PlasmaCore.Units.iconSizes.small
|
icon.height: PlasmaCore.Units.iconSizes.small
|
||||||
onClicked: mpris2Source.playPause()
|
onClicked: mpris2Source.playPause(playerItem.source)
|
||||||
Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Play or Pause media")
|
Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Play or Pause media")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,14 +156,17 @@ Components.BaseItem {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: height
|
Layout.preferredWidth: height
|
||||||
|
|
||||||
enabled: mpris2Source.canGoNext
|
enabled: mpris2Source.canGoBack(playerItem.source)
|
||||||
icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
|
icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
|
||||||
icon.width: PlasmaCore.Units.iconSizes.small
|
icon.width: PlasmaCore.Units.iconSizes.small
|
||||||
icon.height: PlasmaCore.Units.iconSizes.small
|
icon.height: PlasmaCore.Units.iconSizes.small
|
||||||
onClicked: mpris2Source.goNext()
|
onClicked: mpris2Source.goNext(playerItem.source)
|
||||||
visible: mpris2Source.canGoBack || mpris2Source.canGoNext
|
visible: mpris2Source.canGoBack(playerItem.source) || mpris2Source.canGoNext(playerItem.source)
|
||||||
Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Next track")
|
Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Next track")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue