quickshell: split media cards per MPRIS source, add per-stream volume
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6977568bf2
commit
2f51d2b4f1
1 changed files with 151 additions and 83 deletions
|
|
@ -2214,13 +2214,27 @@ in
|
|||
return "thunderstorm";
|
||||
}
|
||||
|
||||
// --- Media: prefer the actively playing MPRIS player ---
|
||||
property var player: {
|
||||
let ps = Mpris.players.values;
|
||||
for (let i = 0; i < ps.length; i++) {
|
||||
if (ps[i].playbackState === MprisPlaybackState.Playing) return ps[i];
|
||||
// --- Media: one card per MPRIS player so Spotify, a
|
||||
// browser tab, etc. stay separate instead of collapsing
|
||||
// into a single combined player. ---
|
||||
property var players: Mpris.players.values.filter(
|
||||
p => p.trackTitle || p.playbackState === MprisPlaybackState.Playing)
|
||||
|
||||
// Best-effort link from an MPRIS player to its Pipewire
|
||||
// audio stream (matched by app name), so a card can carry a
|
||||
// volume slider via the same per-app path as the volume
|
||||
// widget. null when no stream matches.
|
||||
function streamFor(player) {
|
||||
if (!player) return null;
|
||||
let id = (player.identity || "").toLowerCase();
|
||||
let ns = Pipewire.nodes.values;
|
||||
for (let i = 0; i < ns.length; i++) {
|
||||
let n = ns[i];
|
||||
if (!n.isStream || !n.audio) continue;
|
||||
let an = (n.properties["application.name"] || "").toLowerCase();
|
||||
if (an && (an === id || an.includes(id) || id.includes(an))) return n;
|
||||
}
|
||||
return ps.length > 0 ? ps[0] : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
Row {
|
||||
|
|
@ -2405,97 +2419,151 @@ in
|
|||
width: 300
|
||||
spacing: 8
|
||||
|
||||
// Media player card
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 64
|
||||
radius: Theme.radius
|
||||
color: Theme.cardBg
|
||||
visible: calPopup.player !== null
|
||||
// Media player cards — one per active MPRIS source,
|
||||
// so Spotify and a browser tab stay separate.
|
||||
Repeater {
|
||||
model: calPopup.players
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 10
|
||||
Rectangle {
|
||||
id: mediaCard
|
||||
required property var modelData
|
||||
property var pwNode: calPopup.streamFor(modelData)
|
||||
width: parent.width
|
||||
height: mediaCol.height + 16
|
||||
radius: Theme.radius
|
||||
color: Theme.cardBg
|
||||
|
||||
Rectangle {
|
||||
width: 48; height: 48
|
||||
radius: Theme.radiusSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.base02
|
||||
clip: true
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: albumArt.status !== Image.Ready
|
||||
text: "music_note"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 22
|
||||
}
|
||||
Image {
|
||||
id: albumArt
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: calPopup.player ? calPopup.player.trackArtUrl : ""
|
||||
}
|
||||
}
|
||||
PwObjectTracker { objects: mediaCard.pwNode ? [mediaCard.pwNode] : [] }
|
||||
|
||||
Column {
|
||||
width: parent.width - 48 - 10 - 88 - 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
SText {
|
||||
width: parent.width
|
||||
text: calPopup.player ? calPopup.player.trackTitle : ""
|
||||
color: Theme.base05
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
SText {
|
||||
width: parent.width
|
||||
text: calPopup.player ? calPopup.player.trackArtist : ""
|
||||
color: Theme.base04
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
id: mediaCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 48
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: [
|
||||
{ glyph: "skip_previous", act: "prev" },
|
||||
{ glyph: calPopup.player && calPopup.player.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow", act: "toggle" },
|
||||
{ glyph: "skip_next", act: "next" }
|
||||
]
|
||||
Rectangle {
|
||||
id: mediaBtn
|
||||
required property var modelData
|
||||
width: 28; height: 28; radius: 14
|
||||
color: mediaBtnMa.containsMouse ? Theme.base02 : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFade } }
|
||||
width: 48; height: 48
|
||||
radius: Theme.radiusSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.base02
|
||||
clip: true
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
text: mediaBtn.modelData.glyph
|
||||
color: Theme.base05
|
||||
font.pixelSize: 18
|
||||
visible: albumArt.status !== Image.Ready
|
||||
text: "music_note"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 22
|
||||
}
|
||||
MouseArea {
|
||||
id: mediaBtnMa
|
||||
Image {
|
||||
id: albumArt
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let p = calPopup.player;
|
||||
if (!p) return;
|
||||
if (mediaBtn.modelData.act === "prev") p.previous();
|
||||
else if (mediaBtn.modelData.act === "next") p.next();
|
||||
else p.togglePlaying();
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: mediaCard.modelData.trackArtUrl
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - 48 - 10 - 88 - 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
SText {
|
||||
width: parent.width
|
||||
text: mediaCard.modelData.trackTitle
|
||||
color: Theme.base05
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
SText {
|
||||
width: parent.width
|
||||
text: mediaCard.modelData.trackArtist
|
||||
color: Theme.base04
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: [
|
||||
{ glyph: "skip_previous", act: "prev" },
|
||||
{ glyph: mediaCard.modelData.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow", act: "toggle" },
|
||||
{ glyph: "skip_next", act: "next" }
|
||||
]
|
||||
Rectangle {
|
||||
id: mediaBtn
|
||||
required property var modelData
|
||||
width: 28; height: 28; radius: 14
|
||||
color: mediaBtnMa.containsMouse ? Theme.base02 : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: Theme.animFade } }
|
||||
SIcon {
|
||||
anchors.centerIn: parent
|
||||
text: mediaBtn.modelData.glyph
|
||||
color: Theme.base05
|
||||
font.pixelSize: 18
|
||||
}
|
||||
MouseArea {
|
||||
id: mediaBtnMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let p = mediaCard.modelData;
|
||||
if (!p) return;
|
||||
if (mediaBtn.modelData.act === "prev") p.previous();
|
||||
else if (mediaBtn.modelData.act === "next") p.next();
|
||||
else p.togglePlaying();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-source volume — same per-app path
|
||||
// as the volume widget. Shown when the
|
||||
// player's Pipewire stream is matched.
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
visible: mediaCard.pwNode !== null && mediaCard.pwNode.audio !== null
|
||||
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "volume_up"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 15
|
||||
}
|
||||
|
||||
PillSlider {
|
||||
width: parent.width - 22 - mediaVolLabel.width - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 16
|
||||
trackH: 4
|
||||
value: mediaCard.pwNode && mediaCard.pwNode.audio ? Math.min(1, mediaCard.pwNode.audio.volume) : 0
|
||||
fillColor: Theme.base0C
|
||||
onMoved: (v) => { if (mediaCard.pwNode && mediaCard.pwNode.audio) mediaCard.pwNode.audio.volume = v; }
|
||||
}
|
||||
|
||||
SText {
|
||||
id: mediaVolLabel
|
||||
width: 36
|
||||
text: mediaCard.pwNode && mediaCard.pwNode.audio ? Math.round(mediaCard.pwNode.audio.volume * 100) + "%" : "0%"
|
||||
color: Theme.base04
|
||||
font.pixelSize: 10
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue