feat(mpris): pin preferred media source (#3150)

* feat(mpris): pin preferred media source

Add a minimal "pin media source" option so the bar media widget and OSD
keep showing a preferred player (e.g. a music player) even when another
source (e.g. YouTube in the browser) starts playing.
 - MprisController: new pinnedIdentity property + setPinned(identity);
  _bestPlayingPlayer() prefers a playing pinned player (exact identity
  match, then case-insensitive substring fallback). Existing behavior
  is unchanged when no pin is set.
- Persistence via SessionData.pinnedPlayerIdentity (spec entry added so
  SessionData.set() accepts the key); empty string clears the pin.
- MediaDropdownOverlay (Media Players panel): each player row gets a
  theme-colored circle indicator on the right. Empty circle = no pin
  (initial state). Right-click a row to make it the default source
  (circle fills); right-click another row to move the pin; right-click
  the pinned row again to clear (back to empty). Left-click behavior
  unchanged (selects the player).

* Update quickshell/Services/MprisController.qml

Concur with Claude's suggestion.

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update quickshell/Services/MprisController.qml

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Scott Steelyandclaude[bot] authored and GitHub committed 2026-08-30 14:30:23 -04:00
1 parent 151aedbb68
commit 393a54f7eb
4 files changed
+44 -2

No files matched your search

+1
View File
@@ -219,6 +219,7 @@ Singleton {
property string bluetoothAdapterOverride: ""
property string lastPlayerIdentity: ""
property string pinnedPlayerIdentity: ""
property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: []
@@ -87,6 +87,7 @@ var SPEC = {
bluetoothAdapterOverride: { def: "" },
lastPlayerIdentity: { def: "" },
pinnedPlayerIdentity: { def: "" },
deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
@@ -578,7 +578,7 @@ Item {
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
width: parent.width - Theme.spacingM * 2 - pinIndicator.width - Theme.spacingS
DankIcon {
name: "music_note"
@@ -618,12 +618,36 @@ Item {
}
}
Rectangle {
id: pinIndicator
readonly property bool pinned: !!modelData?.identity && MprisController.pinnedIdentity === modelData.identity
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 12
height: 12
radius: 6
color: pinned ? Theme.primary : "transparent"
border.color: pinned ? Theme.primary : Theme.outlineHeavy
border.width: 2
}
MouseArea {
id: playerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (modelData?.identity) {
if (MprisController.pinnedIdentity === modelData.identity)
MprisController.setPinned("");
else
MprisController.setPinned(modelData.identity);
}
return;
}
if (modelData?.identity) {
root.playerSelected(modelData);
}
+16
View File
@@ -43,6 +43,7 @@ Singleton {
});
}
property MprisPlayer activePlayer: null
property string pinnedIdentity: SessionData.pinnedPlayerIdentity || ""
property real activePlayerStableLength: 0
// Chromium can report blank metadata between tracks
property string stableTitle: ""
@@ -247,6 +248,15 @@ Singleton {
if (playing.length === 0)
return null;
if (pinnedIdentity !== "") {
const pinLower = pinnedIdentity.toLowerCase();
const exact = playing.filter(player => player.identity === pinnedIdentity);
const matches = exact.length > 0 ? exact : playing.filter(player => (player.identity || "").toLowerCase().includes(pinLower));
const pinned = matches.find(player => player.canControl) ?? matches[0];
if (pinned)
return pinned;
}
const controllable = playing.filter(player => player.canControl);
if (activePlayer?.isPlaying) {
if (activePlayer.canControl || controllable.length === 0)
@@ -301,6 +311,12 @@ Singleton {
_persistIdentity(player.identity);
}
function setPinned(identity: string): void {
if (SessionData.pinnedPlayerIdentity !== identity)
SessionData.set("pinnedPlayerIdentity", identity);
_resolveActivePlayer();
}
function _persistIdentity(identity: string): void {
if (identity && SessionData.lastPlayerIdentity !== identity)
SessionData.set("lastPlayerIdentity", identity);