diff --git a/package/contents/config/main.xml b/package/contents/config/main.xml
index 5b88d8e..ac567bd 100644
--- a/package/contents/config/main.xml
+++ b/package/contents/config/main.xml
@@ -70,6 +70,10 @@ PLAYING]]>
true
+
+
+
+
5
diff --git a/package/contents/ui/Player.qml b/package/contents/ui/Player.qml
index 3cc174c..2064321 100644
--- a/package/contents/ui/Player.qml
+++ b/package/contents/ui/Player.qml
@@ -11,82 +11,155 @@ import org.kde.plasma.private.mpris as Mpris
QtObject {
id: root
+ readonly property int containerRole: Qt.UserRole + 1
property string sourceName: "any"
+ property string sourcePriority: ""
property var mpris2Model
+ property int modelRevision: 0
+ readonly property var normalizedSourcePriority: parseSourcePriority(sourcePriority)
+ readonly property var resolvedPlayer: resolvePlayer()
readonly property bool ready: {
- if (!mpris2Model.currentPlayer)
+ if (!resolvedPlayer)
return false;
- return mpris2Model.currentPlayer.desktopEntry === sourceName || sourceName === "any";
+ return sourceName === "any" || entriesMatch(resolvedPlayer.desktopEntry, sourceName);
}
- readonly property string artists: ready ? mpris2Model.currentPlayer.artist : ""
- readonly property string title: ready ? mpris2Model.currentPlayer.track : ""
- readonly property int playbackStatus: ready ? mpris2Model.currentPlayer.playbackStatus : Mpris.PlaybackStatus.Unknown
- readonly property int shuffle: ready ? mpris2Model.currentPlayer.shuffle : Mpris.ShuffleStatus.Unknown
- readonly property string artUrl: ready ? mpris2Model.currentPlayer.artUrl : ""
- readonly property int loopStatus: ready ? mpris2Model.currentPlayer.loopStatus : Mpris.LoopStatus.Unknown
- readonly property double songPosition: ready ? mpris2Model.currentPlayer.position : 0
- readonly property double songLength: ready ? mpris2Model.currentPlayer.length : 0
- readonly property real volume: ready ? mpris2Model.currentPlayer.volume : 0
- readonly property string identity: ready ? mpris2Model.currentPlayer.identity : ""
- readonly property bool canGoNext: ready ? mpris2Model.currentPlayer.canGoNext : false
- readonly property bool canGoPrevious: ready ? mpris2Model.currentPlayer.canGoPrevious : false
- readonly property bool canPlay: ready ? mpris2Model.currentPlayer.canPlay : false
- readonly property bool canPause: ready ? mpris2Model.currentPlayer.canPause : false
- readonly property bool canSeek: ready ? mpris2Model.currentPlayer.canSeek : false
- readonly property bool canRaise: ready ? mpris2Model.currentPlayer.canRaise : false
+ readonly property string artists: ready ? resolvedPlayer.artist : ""
+ readonly property string title: ready ? resolvedPlayer.track : ""
+ readonly property int playbackStatus: ready ? resolvedPlayer.playbackStatus : Mpris.PlaybackStatus.Unknown
+ readonly property int shuffle: ready ? resolvedPlayer.shuffle : Mpris.ShuffleStatus.Unknown
+ readonly property string artUrl: ready ? resolvedPlayer.artUrl : ""
+ readonly property int loopStatus: ready ? resolvedPlayer.loopStatus : Mpris.LoopStatus.Unknown
+ readonly property double songPosition: ready ? resolvedPlayer.position : 0
+ readonly property double songLength: ready ? resolvedPlayer.length : 0
+ readonly property real volume: ready ? resolvedPlayer.volume : 0
+ readonly property string identity: ready ? resolvedPlayer.identity : ""
+ readonly property bool canGoNext: ready ? resolvedPlayer.canGoNext : false
+ readonly property bool canGoPrevious: ready ? resolvedPlayer.canGoPrevious : false
+ readonly property bool canPlay: ready ? resolvedPlayer.canPlay : false
+ readonly property bool canPause: ready ? resolvedPlayer.canPause : false
+ readonly property bool canSeek: ready ? resolvedPlayer.canSeek : false
+ readonly property bool canRaise: ready ? resolvedPlayer.canRaise : false
// To know whether Shuffle and Loop can be changed we have to check if the property is defined,
// unlike the other commands, LoopStatus and Shuffle do not have specific properties such as
// CanPause, CanSeek, etc.
- readonly property bool canChangeShuffle: ready ? mpris2Model.currentPlayer.shuffle !== undefined : false
- readonly property bool canChangeLoopStatus: ready ? mpris2Model.currentPlayer.loopStatus !== undefined : false
+ readonly property bool canChangeShuffle: ready ? resolvedPlayer.shuffle !== undefined : false
+ readonly property bool canChangeLoopStatus: ready ? resolvedPlayer.loopStatus !== undefined : false
+
+ function normalizedEntry(value) {
+ return (value || "").toString().trim().toLowerCase();
+ }
+
+ function entriesMatch(left, right) {
+ const normalizedLeft = normalizedEntry(left);
+ const normalizedRight = normalizedEntry(right);
+ if (!normalizedLeft || !normalizedRight)
+ return false;
+
+ return normalizedLeft === normalizedRight;
+ }
+
+ function parseSourcePriority(value) {
+ return (value || "").split(",").map((entry) => {
+ return normalizedEntry(entry);
+ }).filter((entry) => {
+ return entry.length > 0;
+ });
+ }
+
+ function playerAt(row) {
+ if (!mpris2Model || row < 0 || row >= mpris2Model.rowCount())
+ return null;
+
+ return mpris2Model.data(mpris2Model.index(row, 0), containerRole);
+ }
+
+ function findPlayerByEntry(targetEntry) {
+ if (!targetEntry || !mpris2Model)
+ return null;
+
+ const playerCount = mpris2Model.rowCount();
+ for (let row = 0; row < playerCount; ++row) {
+ const player = playerAt(row);
+ if (player && entriesMatch(player.desktopEntry, targetEntry))
+ return player;
+
+ }
+ return null;
+ }
+
+ function resolvePlayer() {
+ modelRevision;
+ if (!mpris2Model)
+ return null;
+
+ for (const targetEntry of normalizedSourcePriority) {
+ const matchedPlayer = findPlayerByEntry(targetEntry);
+ if (matchedPlayer)
+ return matchedPlayer;
+
+ }
+ return mpris2Model.currentPlayer || null;
+ }
function togglePlayPause() {
- if (mpris2Model.currentPlayer)
- mpris2Model.currentPlayer.PlayPause();
+ if (resolvedPlayer)
+ resolvedPlayer.PlayPause();
}
function setPosition(position) {
- mpris2Model.currentPlayer.position = position;
+ if (resolvedPlayer)
+ resolvedPlayer.position = position;
+
}
function next() {
- if (mpris2Model.currentPlayer)
- mpris2Model.currentPlayer.Next();
+ if (resolvedPlayer)
+ resolvedPlayer.Next();
}
function previous() {
- if (mpris2Model.currentPlayer)
- mpris2Model.currentPlayer.Previous();
+ if (resolvedPlayer)
+ resolvedPlayer.Previous();
}
function updatePosition() {
- if (mpris2Model.currentPlayer)
- mpris2Model.currentPlayer.updatePosition();
+ if (resolvedPlayer)
+ resolvedPlayer.updatePosition();
}
function setVolume(volume) {
- mpris2Model.currentPlayer.volume = volume;
+ if (resolvedPlayer)
+ resolvedPlayer.volume = volume;
+
}
function changeVolume(delta, showOSD) {
- mpris2Model.currentPlayer.changeVolume(delta, showOSD);
+ if (resolvedPlayer)
+ resolvedPlayer.changeVolume(delta, showOSD);
+
}
function setShuffle(shuffle) {
- mpris2Model.currentPlayer.shuffle = shuffle;
+ if (resolvedPlayer)
+ resolvedPlayer.shuffle = shuffle;
+
}
function setLoopStatus(loopStatus) {
- mpris2Model.currentPlayer.loopStatus = loopStatus;
+ if (resolvedPlayer)
+ resolvedPlayer.loopStatus = loopStatus;
+
}
function raise() {
- mpris2Model.currentPlayer.Raise();
+ if (resolvedPlayer)
+ resolvedPlayer.Raise();
+
}
function formatTrackTime(s) {
@@ -99,13 +172,11 @@ QtObject {
}
mpris2Model: Mpris.Mpris2Model {
- onRowsInserted: (_, rowIndex) => {
- const CONTAINER_ROLE = Qt.UserRole + 1;
- const player = this.data(this.index(rowIndex, 0), CONTAINER_ROLE);
- if (player.desktopEntry === root.sourceName)
- this.currentIndex = rowIndex;
-
- }
+ onRowsInserted: root.modelRevision += 1
+ onRowsRemoved: root.modelRevision += 1
+ onModelReset: root.modelRevision += 1
+ onDataChanged: root.modelRevision += 1
+ onCurrentPlayerChanged: root.modelRevision += 1
}
}
diff --git a/package/contents/ui/configGeneral.qml b/package/contents/ui/configGeneral.qml
index e10ffaf..a26bedb 100644
--- a/package/contents/ui/configGeneral.qml
+++ b/package/contents/ui/configGeneral.qml
@@ -4,6 +4,7 @@ import QtQuick.Dialogs as Dialogs
import QtQuick.Layouts
import org.kde.kcmutils as KCM
import org.kde.kirigami as Kirigami
+import org.kde.plasma.private.mpris as Mpris
KCM.SimpleKCM {
id: configRoot
@@ -24,13 +25,16 @@ KCM.SimpleKCM {
property alias cfg_foregroundColor: foregroundColorField.text
property alias cfg_textShadowEnabled: textShadowCheckBox.checked
property alias cfg_showMediaControls: showMediaControlsCheckBox.checked
+ property alias cfg_sourcePriority: sourcePriorityField.text
property alias cfg_trackTextVerticalSpacing: trackTextVerticalSpacingSpinBox.value
property alias cfg_labelVerticalSpacing: labelVerticalSpacingSpinBox.value
property alias cfg_separatorGapLabel: separatorGapLabelSpinBox.value
property alias cfg_separatorGapTrack: separatorGapTrackSpinBox.value
property alias cfg_separatorHeight: separatorHeightSpinBox.value
property alias cfg_hideSeparator: hideSeparatorCheckBox.checked
+ readonly property int containerRole: Qt.UserRole + 1
readonly property var availableFonts: Qt.fontFamilies()
+ property var detectedSources: []
readonly property var labelVisibilityOptions: [{
"text": i18n("Auto hide when idle"),
"value": "auto"
@@ -71,6 +75,28 @@ KCM.SimpleKCM {
return options[resolvedIndex].value;
}
+ function normalizeDesktopEntry(value) {
+ return (value || "").toString().trim();
+ }
+
+ function refreshDetectedSources() {
+ const sources = [];
+ const seen = {
+ };
+ const playerCount = detectedSourcesModel.rowCount();
+ for (let row = 0; row < playerCount; ++row) {
+ const player = detectedSourcesModel.data(detectedSourcesModel.index(row, 0), containerRole);
+ const desktopEntry = normalizeDesktopEntry(player ? player.desktopEntry : "");
+ const desktopEntryKey = desktopEntry.toLowerCase();
+ if (!desktopEntry || seen[desktopEntryKey])
+ continue;
+
+ seen[desktopEntryKey] = true;
+ sources.push(desktopEntry);
+ }
+ detectedSources = sources;
+ }
+
onCfg_fontFamilyChanged: {
const index = availableFonts.indexOf(cfg_fontFamily);
if (index >= 0 && fontFamilyComboBox.currentIndex !== index)
@@ -191,6 +217,25 @@ KCM.SimpleKCM {
text: i18n("Show media controls")
}
+ QQC2.TextField {
+ id: sourcePriorityField
+
+ Kirigami.FormData.label: i18n("Source priority:")
+ Layout.fillWidth: true
+ placeholderText: i18n("spotify, firefox")
+ }
+
+ QQC2.Label {
+ id: detectedSourcesField
+
+ Kirigami.FormData.label: i18n("Current sources being detected:")
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.max(implicitHeight, Kirigami.Units.gridUnit * 2)
+ textFormat: TextEdit.PlainText
+ wrapMode: TextEdit.WrapAnywhere
+ text: configRoot.detectedSources.length > 0 ? configRoot.detectedSources.join("\n") : i18n("No active MPRIS desktopEntry values detected")
+ }
+
QQC2.SpinBox {
id: separatorHeightSpinBox
@@ -333,6 +378,16 @@ KCM.SimpleKCM {
onAccepted: configRoot.cfg_foregroundColor = selectedColor.toString()
}
+ Mpris.Mpris2Model {
+ id: detectedSourcesModel
+
+ onRowsInserted: configRoot.refreshDetectedSources()
+ onRowsRemoved: configRoot.refreshDetectedSources()
+ onModelReset: configRoot.refreshDetectedSources()
+ onDataChanged: configRoot.refreshDetectedSources()
+ Component.onCompleted: configRoot.refreshDetectedSources()
+ }
+
component SectionHeader: Item {
required property string title
diff --git a/package/contents/ui/main.qml b/package/contents/ui/main.qml
index 7975088..5c8a7c4 100644
--- a/package/contents/ui/main.qml
+++ b/package/contents/ui/main.qml
@@ -43,6 +43,8 @@ PlasmoidItem {
// qmllint disable missing-property
property bool configuredShowMediaControls: plasmoid.configuration.showMediaControls
// qmllint disable missing-property
+ property string configuredSourcePriority: plasmoid.configuration.sourcePriority
+ // qmllint disable missing-property
property int configuredTrackTextVerticalSpacing: plasmoid.configuration.trackTextVerticalSpacing
// qmllint disable missing-property
property int configuredLabelVerticalSpacing: plasmoid.configuration.labelVerticalSpacing
@@ -73,6 +75,8 @@ PlasmoidItem {
Player {
id: player
+
+ sourcePriority: root.configuredSourcePriority
}
fullRepresentation: Representation {