import QtQuick import Quickshell import Quickshell.Wayland import Quickshell.Services.Mpris import "." as M M.BarSection { id: root spacing: M.Theme.moduleSpacing opacity: M.Modules.mpris.enable && player !== null ? 1 : 0 visible: opacity > 0 tooltip: "" readonly property MprisPlayer player: Mpris.players.values[0] ?? null readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing required property var bar property bool _panelHovered: false property bool _pinned: false readonly property bool _anyHover: root._hovered || _panelHovered readonly property bool _showPanel: _anyHover || _pinned on_AnyHoverChanged: { if (_anyHover) _unpinTimer.stop(); else if (_pinned) _unpinTimer.start(); } Timer { id: _unpinTimer interval: 500 onTriggered: root._pinned = false } M.BarIcon { icon: root.playing ? "\uF04B" : (root.player?.playbackState === MprisPlaybackState.Paused ? "\uDB80\uDFE4" : "\uDB81\uDCDB") color: M.Theme.base0E anchors.verticalCenter: parent.verticalCenter MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root._pinned = !root._pinned } } M.BarLabel { label: root.player?.trackTitle || root.player?.identity || "" color: M.Theme.base0E elide: Text.ElideRight width: Math.min(implicitWidth, 200) anchors.verticalCenter: parent.verticalCenter MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root._pinned = !root._pinned } } PanelWindow { id: panel screen: QsWindow.window?.screen ?? null visible: _winVisible color: "transparent" property bool _winVisible: false WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusiveZone: 0 WlrLayershell.namespace: "nova-mpris" anchors.top: true anchors.left: true margins.top: 0 margins.left: Math.max(0, Math.min( Math.round(root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - implicitWidth / 2), (panel.screen?.width ?? 1920) - implicitWidth )) implicitWidth: panelContent.width implicitHeight: panelContent.height Connections { target: root function on_ShowPanelChanged() { if (root._showPanel) { panel._winVisible = true; hideAnim.stop(); showAnim.start(); } else { showAnim.stop(); hideAnim.start(); } } } ParallelAnimation { id: showAnim NumberAnimation { target: panelContent; property: "opacity"; to: 1; duration: 120; easing.type: Easing.OutCubic } NumberAnimation { target: panelContent; property: "y"; to: 0; duration: 150; easing.type: Easing.OutCubic } } ParallelAnimation { id: hideAnim NumberAnimation { target: panelContent; property: "opacity"; to: 0; duration: 150; easing.type: Easing.InCubic } NumberAnimation { target: panelContent; property: "y"; to: -panelContent.height; duration: 150; easing.type: Easing.InCubic } onFinished: panel._winVisible = false } MouseArea { anchors.fill: parent hoverEnabled: true onContainsMouseChanged: root._panelHovered = containsMouse } Rectangle { x: panelContent.x y: panelContent.y width: panelContent.width height: panelContent.height color: M.Theme.base01 opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) topLeftRadius: 0 topRightRadius: 0 bottomLeftRadius: M.Theme.radius bottomRightRadius: M.Theme.radius } Column { id: panelContent width: 280 opacity: 0 y: -height topPadding: 4 bottomPadding: 4 spacing: 2 // Album art Item { width: parent.width height: _artImg.status === Image.Ready ? 140 : 60 clip: true Rectangle { anchors.fill: parent color: M.Theme.base02 } Image { id: _artImg anchors.fill: parent fillMode: Image.PreserveAspectCrop visible: status === Image.Ready asynchronous: true property string _lastGoodSource: "" property string _lastTrack: "" readonly property string _artUrl: root.player?.trackArtUrl ?? "" readonly property string _track: root.player?.trackTitle ?? "" on_ArtUrlChanged: if (_artUrl) _lastGoodSource = _artUrl on_TrackChanged: if (_track !== _lastTrack) { _lastTrack = _track; _lastGoodSource = _artUrl || "" } source: _lastGoodSource } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 40 visible: _artImg.visible gradient: Gradient { GradientStop { position: 0; color: "transparent" } GradientStop { position: 1; color: M.Theme.base01 } } } Text { anchors.centerIn: parent text: "\uF001" color: M.Theme.base04 font.pixelSize: 28 font.family: M.Theme.iconFontFamily visible: _artImg.status !== Image.Ready } } // Track info Item { width: parent.width 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: root.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 = root.player; if (!p) return ""; const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); } color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily elide: Text.ElideRight visible: text !== "" } } } // Progress Item { width: parent.width height: 20 readonly property real pos: root.player?.position ?? 0 readonly property real dur: root.player?.length ?? 0 readonly property real frac: dur > 0 ? pos / dur : 0 function _fmtTime(ms) { const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); return m + ":" + String(s % 60).padStart(2, "0"); } Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: parent._fmtTime(parent.pos) color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: parent._fmtTime(parent.dur) color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily } 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: parent.width height: 36 Row { anchors.centerIn: parent spacing: 24 Text { text: "\uF048" color: root.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: root.player?.canGoPrevious ?? false onClicked: root.player.previous() } } Text { text: root.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: root.player?.togglePlaying() } } Text { text: "\uF051" color: root.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: root.player?.canGoNext ?? false onClicked: root.player.next() } } } } // Player name Text { width: parent.width height: 20 horizontalAlignment: Text.AlignHCenter text: root.player?.identity ?? "" color: M.Theme.base03 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily } } } }