mpris flyout menu with transport controls, progress bar, album art

This commit is contained in:
Damocles 2026-04-12 17:52:38 +02:00
parent 2f31783ead
commit a5cb257891
4 changed files with 234 additions and 2 deletions

View file

@ -95,7 +95,7 @@ PanelWindow {
// Media // Media
M.BarGroup { M.BarGroup {
borderColor: M.Theme.base0E borderColor: M.Theme.base0E
M.Mpris {} M.Mpris { bar: bar }
M.Volume { visible: M.Modules.volume.enable } M.Volume { visible: M.Modules.volume.enable }
} }

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import "." as M import "." as M
@ -37,8 +38,21 @@ M.BarSection {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
required property var bar
TapHandler { TapHandler {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onTapped: root.player?.togglePlaying() onTapped: menuLoader.active = !menuLoader.active
}
Loader {
id: menuLoader
active: false
sourceComponent: M.MprisMenu {
player: root.player
screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onDismissed: menuLoader.active = false
}
} }
} }

217
modules/MprisMenu.qml Normal file
View file

@ -0,0 +1,217 @@
import QtQuick
import Quickshell.Services.Mpris
import "." as M
M.PopupPanel {
id: menuWindow
panelWidth: 280
required property MprisPlayer player
// Album art placeholder (or real art if available)
Rectangle {
width: menuWindow.panelWidth
height: 80
color: M.Theme.base02
radius: M.Theme.radius
Image {
anchors.fill: parent
source: menuWindow.player?.trackArtUrl ?? ""
fillMode: Image.PreserveAspectCrop
visible: status === Image.Ready
// Round top corners to match panel
layer.enabled: true
layer.effect: Item {
// simple clip the panel background behind handles the rounding
}
}
// Fallback icon when no art
Text {
anchors.centerIn: parent
text: "\uF001"
color: M.Theme.base04
font.pixelSize: 28
font.family: M.Theme.iconFontFamily
visible: !menuWindow.player?.trackArtUrl
}
}
// Track info
Item {
width: menuWindow.panelWidth
height: titleCol.implicitHeight + 8
Column {
id: titleCol
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 2
Text {
width: parent.width
text: menuWindow.player?.trackTitle || "No track"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
font.bold: true
elide: Text.ElideRight
}
Text {
width: parent.width
text: {
const p = menuWindow.player;
if (!p) return "";
const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || "");
return [artist, p.trackAlbum].filter(s => s).join(" — ");
}
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
elide: Text.ElideRight
visible: text !== ""
}
}
}
// Progress bar
Item {
width: menuWindow.panelWidth
height: 20
readonly property real pos: menuWindow.player?.position ?? 0
readonly property real dur: menuWindow.player?.length ?? 0
readonly property real frac: dur > 0 ? pos / dur : 0
// Time labels
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: _fmt(parent.pos)
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
function _fmt(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return m + ":" + String(s % 60).padStart(2, "0");
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: _fmt(parent.dur)
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
function _fmt(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return m + ":" + String(s % 60).padStart(2, "0");
}
}
// Bar
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
height: 4
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 2
}
Rectangle {
width: parent.width * Math.min(1, Math.max(0, parent.parent.frac))
height: parent.height
color: M.Theme.base0E
radius: 2
}
}
}
// Transport controls
Item {
width: menuWindow.panelWidth
height: 36
Row {
anchors.centerIn: parent
spacing: 24
// Previous
Text {
text: "\uF048"
color: menuWindow.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize + 4
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: menuWindow.player?.canGoPrevious ?? false
onClicked: menuWindow.player.previous()
}
}
// Play/Pause
Text {
text: menuWindow.player?.playbackState === MprisPlaybackState.Playing ? "\uF04C" : "\uF04B"
color: M.Theme.base0E
font.pixelSize: M.Theme.fontSize + 8
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: menuWindow.player?.togglePlaying()
}
}
// Next
Text {
text: "\uF051"
color: menuWindow.player?.canGoNext ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize + 4
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: menuWindow.player?.canGoNext ?? false
onClicked: menuWindow.player.next()
}
}
}
}
// Player name
Item {
width: menuWindow.panelWidth
height: 20
Text {
anchors.centerIn: parent
text: menuWindow.player?.identity ?? ""
color: M.Theme.base03
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
}
}

View file

@ -20,6 +20,7 @@ Osd 1.0 Osd.qml
ThemedIcon 1.0 ThemedIcon.qml ThemedIcon 1.0 ThemedIcon.qml
Battery 1.0 Battery.qml Battery 1.0 Battery.qml
Mpris 1.0 Mpris.qml Mpris 1.0 Mpris.qml
MprisMenu 1.0 MprisMenu.qml
Network 1.0 Network.qml Network 1.0 Network.qml
Bluetooth 1.0 Bluetooth.qml Bluetooth 1.0 Bluetooth.qml
Backlight 1.0 Backlight.qml Backlight 1.0 Backlight.qml