// MediaCavaWidget.qml - cava visualiser + media info + volume in one pill import QtQuick import QtQuick.Layouts import Quickshell.Io import Quickshell.Services.Mpris import ".." Popup { id: root popupName: "media" property MprisPlayer player: { var players = Mpris.players.values for (var i = 0; i < players.length; i++) if (players[i].identity.toLowerCase() === "spotify") return players[i] return players.length > 0 ? players[0] : null } property bool isPlaying: player?.playbackState === MprisPlaybackState.Playing ?? false property real displayPosition: 0 Timer { interval: 1000 running: root.visible && root.player !== null repeat: true triggeredOnStart: true onTriggered: root.displayPosition = root.player?.position ?? 0 } function formatTime(secs) { secs = Math.max(0, Math.floor(secs)) return Math.floor(secs / 60) + ":" + String(secs % 60).padStart(2, "0") } component CtrlBtn: Text { property string icon: "" property color baseColor: Theme.pillText property color hoverColor: Theme.pillHover signal activate() text: icon font { family: Theme.fontMono; pixelSize: 20 } color: ma.containsMouse ? hoverColor : baseColor Behavior on color { ColorAnimation { duration: 100 } } MouseArea { id: ma anchors.fill: parent hoverEnabled: true onClicked: parent.activate() } } ColumnLayout { width: 200 spacing: 8 // Album art - hidden when no art is available Rectangle { visible: artImage.status === Image.Ready Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: 180 Layout.preferredHeight: artImage.status === Image.Ready ? 180 : 0 Layout.topMargin: artImage.status === Image.Ready ? 10 : 0 radius: Theme.radius clip: true color: Theme.pill Image { id: artImage anchors.fill: parent source: root.player?.trackArtUrl ?? "" fillMode: Image.PreserveAspectCrop smooth: true } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { if (root.player) Exec.run(["hyprctl", "dispatch", "hl.dsp.focus({ window = 'class:" + root.player.identity + "' })"]) GlobalStates.popups.close() } } } // Track title with marquee scroll Item { id: titleClip Layout.fillWidth: true Layout.leftMargin: 15 Layout.rightMargin: 15 Layout.topMargin: 4 implicitHeight: titleText.implicitHeight clip: true Text { id: titleText text: root.player?.trackTitle ?? "Nothing playing" font { family: Theme.fontSans; pixelSize: Theme.fontSize + 1; bold: true } color: titleMa.containsMouse && root.player ? Qt.lighter(Theme.pillText, 1.3) : Theme.pillText Behavior on color { ColorAnimation { duration: 100 } } MouseArea { id: titleMa anchors.fill: parent hoverEnabled: true cursorShape: root.player ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: if (root.player) { Exec.run(["hyprctl", "dispatch", "hl.dsp.focus({ window = 'class:" + root.player.identity + "' })"]) GlobalStates.popups.close() } } onTextChanged: { marquee.stop() x = 0 Qt.callLater(() => { if (implicitWidth > titleClip.width) marquee.start() }) } } Component.onCompleted: Qt.callLater(() => { if (titleText.implicitWidth > width) marquee.start() }) SequentialAnimation { id: marquee loops: Animation.Infinite PauseAnimation { duration: 1500 } NumberAnimation { target: titleText; property: "x" from: 0 to: -(titleText.implicitWidth - titleClip.width + 10) duration: Math.max(1, titleText.implicitWidth - titleClip.width) * 18 easing.type: Easing.Linear } PauseAnimation { duration: 800 } NumberAnimation { target: titleText; property: "x" to: 0; duration: 400; easing.type: Easing.OutCubic } } } // Artist Text { Layout.fillWidth: true Layout.leftMargin: 15 Layout.rightMargin: 15 horizontalAlignment: Text.AlignHCenter text: root.player?.trackArtists ? Array.from(root.player.trackArtists).join(", ") : "" font { family: Theme.fontSans; pixelSize: Theme.fontSize } color: Theme.pillText elide: Text.ElideRight } // Progress bar Rectangle { Layout.fillWidth: true Layout.leftMargin: 15 Layout.rightMargin: 15 height: 3 radius: 2 color: Theme.pill Rectangle { width: root.player && root.player.length > 0 ? Math.min(1, root.displayPosition / root.player.length) * parent.width : 0 height: parent.height radius: parent.radius color: Theme.color6 } } // Time labels RowLayout { Layout.fillWidth: true Layout.leftMargin: 15 Layout.rightMargin: 15 Text { text: root.formatTime(root.displayPosition) font { family: Theme.fontMono; pixelSize: Theme.fontSize - 1 } color: Theme.pillText } Item { Layout.fillWidth: true } Text { text: root.formatTime(root.player?.length ?? 0) font { family: Theme.fontMono; pixelSize: Theme.fontSize - 1 } color: Theme.pillText } } // Controls RowLayout { Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: 10 spacing: 20 CtrlBtn { text: ""; onActivate: root.player?.previous() } CtrlBtn { text: root.isPlaying ? "" : ""; onActivate: root.player?.togglePlaying() } CtrlBtn { text: ""; onActivate: root.player?.stop() } CtrlBtn { text: ""; onActivate: root.player?.next() } } } }