import QtQuick import QtQuick.Layouts import Quickshell.Services.Mpris import "../../../config" as Cfg Item { id: root implicitHeight: contentCol.implicitHeight readonly property var t: Cfg.Config.theme property var activePlayer: null readonly property bool canChangeVolume: root.activePlayer && root.activePlayer.volumeSupported && root.activePlayer.canControl readonly property var _identityMap: ({ "firefox": "Firefox", "spotify": "Spotify", "chromium": "Chromium", "google-chrome": "Chrome", "brave": "Brave", "vlc": "VLC", "mpv": "mpv" }) function selectPlayer() { var players = Mpris.players.values var fallback = null for (var i = 0; i < players.length; i++) { var p = players[i] if (!p || (p.trackTitle === "" && p.identity === "")) continue if (p.playbackState === MprisPlaybackState.Playing) { activePlayer = p; return } if (!fallback) fallback = p } if (activePlayer && activePlayer.playbackState !== MprisPlaybackState.Stopped) return activePlayer = fallback } Instantiator { model: Mpris.players Connections { required property MprisPlayer modelData target: modelData Component.onCompleted: { if (root.activePlayer == null || modelData.playbackState === MprisPlaybackState.Playing) root.selectPlayer() } Component.onDestruction: { if (root.activePlayer === modelData) { root.activePlayer = null root.selectPlayer() } } function onPlaybackStateChanged() { root.selectPlayer() } } } function formatDuration(s) { if (isNaN(s) || s < 0) return "0:00" var m = Math.floor(s / 60) var sec = Math.floor(s % 60) return m + ":" + (sec < 10 ? "0" : "") + sec } function formatIdentity(name) { if (!name) return "Media" var lower = name.toLowerCase() for (var key in root._identityMap) { if (lower.indexOf(key) !== -1) return root._identityMap[key] } return name } function playbackStatusText() { if (!root.activePlayer) return "No players" switch (root.activePlayer.playbackState) { case MprisPlaybackState.Playing: return "Now playing" case MprisPlaybackState.Paused: return "Paused" default: return "Stopped" } } function volumeIcon() { if (!root.activePlayer) return "󰕾" var v = root.activePlayer.volume if (v === 0) return "󰖁" if (v < 0.33) return "󰕿" if (v < 0.66) return "󰖀" return "󰕾" } Timer { id: positionTimer running: root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing interval: 1000 repeat: true onTriggered: { if (root.activePlayer) root.activePlayer.positionChanged() } } ColumnLayout { id: contentCol anchors { left: parent.left; right: parent.right; top: parent.top } spacing: 0 Rectangle { Layout.fillWidth: true height: 32 color: "transparent" RowLayout { anchors.fill: parent spacing: 10 Rectangle { width: 32; height: 32 radius: 8 color: Qt.alpha(root.t.mprisAccent, 0.12) border.color: Qt.alpha(root.t.mprisAccent, 0.22) border.width: 1 Layout.alignment: Qt.AlignVCenter Text { anchors.centerIn: parent text: "󰎆" color: root.t.mprisAccent font.pixelSize: 18 } } ColumnLayout { spacing: 2 Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter Text { text: root.activePlayer ? root.formatIdentity(root.activePlayer.identity) : "No media" color: root.t.mprisTextMain font.pixelSize: 13 font.weight: Font.DemiBold elide: Text.ElideRight Layout.fillWidth: true } Text { text: root.playbackStatusText() color: root.t.mprisTextDim font.pixelSize: 10 opacity: 0.75 } } } } Item { Layout.preferredHeight: 12 } Rectangle { Layout.fillWidth: true height: 1 gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0; color: Qt.alpha(root.t.mprisAccent, 0.5) } GradientStop { position: 1.0; color: "transparent" } } } Item { Layout.preferredHeight: root.activePlayer ? 10 : 0 } ColumnLayout { id: trackArea Layout.fillWidth: true visible: root.activePlayer !== null spacing: 0 RowLayout { Layout.fillWidth: true spacing: 12 Rectangle { width: 64; height: 64 radius: 8 color: root.t.mprisAlbumBg border.color: root.t.mprisAlbumBorder border.width: 1 Layout.alignment: Qt.AlignTop Image { id: albumArt anchors.fill: parent anchors.margins: 2 source: root.activePlayer ? root.activePlayer.trackArtUrl : "" fillMode: Image.PreserveAspectCrop visible: root.activePlayer && root.activePlayer.trackArtUrl !== "" && status === Image.Ready } Text { anchors.centerIn: parent text: "󰝚" color: root.t.mprisAccent font.pixelSize: 28 visible: root.activePlayer && (root.activePlayer.trackArtUrl === "" || !albumArt.visible) } } ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: 2 Text { text: root.activePlayer ? (root.activePlayer.trackTitle || "Unknown Title") : "" color: root.t.mprisTextMain font.pixelSize: 13 font.weight: Font.Medium elide: Text.ElideRight maximumLineCount: 2 wrapMode: Text.WordWrap Layout.fillWidth: true } Text { text: root.activePlayer ? (root.activePlayer.trackArtist || "Unknown Artist") : "" color: root.t.mprisTextDim font.pixelSize: 11 elide: Text.ElideRight Layout.fillWidth: true } Text { text: root.activePlayer ? (root.activePlayer.trackAlbum || "") : "" color: root.t.mprisTextDim font.pixelSize: 10 opacity: 0.7 elide: Text.ElideRight visible: root.activePlayer && root.activePlayer.trackAlbum !== "" Layout.fillWidth: true } } } Item { Layout.preferredHeight: 12 } ColumnLayout { Layout.fillWidth: true spacing: 2 visible: root.activePlayer && root.activePlayer.positionSupported MediaSlider { Layout.fillWidth: true interactive: root.activePlayer && root.activePlayer.canSeek fraction: { var p = root.activePlayer if (!p) return 0 var len = p.length if (len > 0) return Math.max(0, Math.min(1, p.position / len)) return 0 } onSeeked: frac => { var p = root.activePlayer if (p && p.canSeek && p.length > 0) { p.position = frac * p.length } } } RowLayout { Layout.fillWidth: true spacing: 0 Text { text: root.activePlayer ? root.formatDuration(root.activePlayer.position) : "0:00" color: root.t.mprisTextDim font.pixelSize: 9 Layout.alignment: Qt.AlignLeft } Item { Layout.fillWidth: true } Text { text: root.activePlayer ? root.formatDuration(root.activePlayer.length) : "0:00" color: root.t.mprisTextDim font.pixelSize: 9 Layout.alignment: Qt.AlignRight } } } Item { Layout.preferredHeight: 12 } RowLayout { Layout.fillWidth: true spacing: 8 visible: root.canChangeVolume Text { text: root.volumeIcon() color: root.activePlayer && root.activePlayer.volume > 0 ? root.t.mprisAccent : root.t.mprisTextDim font.pixelSize: 16 Layout.alignment: Qt.AlignVCenter } MediaSlider { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter interactive: root.canChangeVolume fraction: { var p = root.activePlayer if (!p) return 1 return p.volume } onSeeked: frac => { var p = root.activePlayer if (p && root.canChangeVolume) { p.volume = frac } } } } Item { Layout.preferredHeight: root.canChangeVolume ? 12 : 0 } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 16 MediaButton { icon: "󰒮" enabled: root.activePlayer && root.activePlayer.canGoPrevious onClicked: root.activePlayer.previous() } MediaButton { primary: true icon: root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing ? "󰏤" : "󰐊" enabled: root.activePlayer && root.activePlayer.canTogglePlaying onClicked: root.activePlayer.togglePlaying() } MediaButton { icon: "󰒭" enabled: root.activePlayer && root.activePlayer.canGoNext onClicked: root.activePlayer.next() } } } Item { Layout.preferredHeight: 6 } } }