mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 15:03:09 +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 {
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue