aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island/MediaPill.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 11:46:56 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 11:46:56 +0200
commit39d3eb69555769d3216171ab7688e3e82bbf48d2 (patch)
treeb04f49bd92f083e6f876a6a8cb36617bfaf4549e /desktop/island/MediaPill.qml
parentf200f7007272129bf59fef3e0b0e0a19f5bd362c (diff)
downloadquickshell-39d3eb69555769d3216171ab7688e3e82bbf48d2.tar.gz
quickshell-39d3eb69555769d3216171ab7688e3e82bbf48d2.zip
feat(island): a media pill left of the clock
Shows album art and a scrolling "artist · album · title" while a player is playing or paused, using the sound module's Player so the browser proxy duplicates are already filtered. Hovering swaps the text for a progress bar, swapped in place rather than growing the pill; MPRIS does not push position, so it is re-read every second while hovered, and a click seeks only when the player reports canSeek and a known length. The wheel steps tracks one per notch, as the layout flag does, so a touchpad does not skip several. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/island/MediaPill.qml')
-rw-r--r--desktop/island/MediaPill.qml157
1 files changed, 157 insertions, 0 deletions
diff --git a/desktop/island/MediaPill.qml b/desktop/island/MediaPill.qml
new file mode 100644
index 0000000..fa4a6ba
--- /dev/null
+++ b/desktop/island/MediaPill.qml
@@ -0,0 +1,157 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// 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
+ }
+ }
+}