// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell.Widgets import QtQuick import ".." import "../modules/sound" // Album art and a scrolling `artist · album · title`, for the player the // sound module's Player picks. Hovering swaps the text for a progress bar, // which seeks on click when the player allows it. Capsule { id: pill readonly property var player: Player.current readonly property string line: [Player.artist, player?.trackAlbum ?? "", Player.title].filter(s => s).join(" · ") readonly property bool canSeek: !!player && player.canSeek && player.positionSupported && player.lengthSupported && player.length > 0 restWidth: art.width + textBox.width + 34 // MPRIS does not push position; positionChanged() makes it re-read. Timer { interval: 1000 repeat: true running: pill.hovered && Player.playing triggeredOnStart: true onTriggered: pill.player?.positionChanged() } ClippingRectangle { id: art x: 8 y: (pill.restHeight - height) / 2 width: 22; height: 22; radius: 4 color: Qt.alpha(Theme.surface, 0.8) Image { anchors.fill: parent source: Player.artUrl cache: false asynchronous: true fillMode: Image.PreserveAspectCrop } } // Marquee: two copies side by side, scrolled by one copy's width, so the // loop point is invisible. Only scrolls when the line does not fit. Item { id: textBox x: art.x + art.width + 10 y: (pill.restHeight - height) / 2 // At least 100, so a short title still leaves room for the bar. width: Math.max(100, Math.min(220, first.implicitWidth)) height: first.implicitHeight clip: true opacity: pill.hovered ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 120 } } readonly property bool scroll: first.implicitWidth > 220 Row { id: marquee spacing: 40 Text { id: first text: pill.line font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 } color: Theme.text } Text { visible: textBox.scroll text: pill.line font: first.font color: first.color } } NumberAnimation { id: scroller target: marquee property: "x" from: 0 to: -(first.implicitWidth + marquee.spacing) duration: (first.implicitWidth + marquee.spacing) * 30 loops: Animation.Infinite running: textBox.scroll && pill.visible onRunningChanged: if (!running) marquee.x = 0 } Connections { target: first function onTextChanged() { marquee.x = 0; if (scroller.running) scroller.restart(); } } } // Wheel up for the previous track, down for the next, one per notch // (120) so a touchpad does not skip several. Takes no buttons, so the // seek bar's clicks still reach it. MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton property real acc: 0 onWheel: wheel => { acc += wheel.angleDelta.y; if (Math.abs(acc) >= 120) { const p = pill.player; if (acc > 0 && p?.canGoPrevious) p.previous(); else if (acc < 0 && p?.canGoNext) p.next(); acc = 0; } wheel.accepted = true; } } // Progress bar, over the text while hovered. Item { x: textBox.x width: textBox.width height: 12 anchors.verticalCenter: parent.verticalCenter opacity: 1 - textBox.opacity visible: opacity > 0 Rectangle { anchors.verticalCenter: parent.verticalCenter width: parent.width height: 4 radius: 2 color: Qt.alpha(Theme.text, 0.15) Rectangle { width: pill.player && pill.player.length > 0 ? parent.width * Math.min(1, pill.player.position / pill.player.length) : 0 height: parent.height radius: 2 color: Theme.accent } } MouseArea { anchors.fill: parent enabled: pill.canSeek cursorShape: Qt.PointingHandCursor onClicked: mouse => pill.player.position = pill.player.length * mouse.x / width } } }