From 88074ccb4731927027312bbb866eca8c286456a0 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Sat, 6 Jun 2026 22:41:39 +0000 Subject: [PATCH] feat(settings): add source priority selection --- package/contents/config/main.xml | 4 + package/contents/ui/Player.qml | 153 +++++++++++++++++++------- package/contents/ui/configGeneral.qml | 55 +++++++++ package/contents/ui/main.qml | 4 + 4 files changed, 175 insertions(+), 41 deletions(-) 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 {