1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
}
}
}
|