Files
qbz/crates/qbz-slint/ui/shell/SeekBar.slint
T
vicrodh da7758b9d6 feat(slint): new now-playing bar layout
Land the "New" now-playing bar as the first of the four switchable
layouts. A dedicated SongCard component holds the cover (or an idle
sleeping cat), the elidable title, the artist·album links, and the
vertical audio stamp (compact quality badge over the backend/mode LEDs).
A full-width seek bar sits on top with a buffer-driven stream lock.

The controls row uses responsive symmetric column zones (39/30/25% by
window width); the side columns share one fraction so the transport stays
absolutely centred and PLAY lands on the window centre. The song-card /
controls divider is a hard wall: the text column stretches to push the
audio stamp to its right edge, the album elides first (and never collapses
to zero) so the stamp is always visible and never crosses the wall.

Adds QualityBadgeFull compact mode, backend/mode output LED labels from
settings, a global TooltipOverlay so clipped columns keep their tooltips,
and the supporting icons.

cc
2026-06-02 06:48:36 -06:00

146 lines
5.1 KiB
Plaintext

// Player seek bar — elapsed time, a three-line track (total / cached /
// progress) with a hover thumb, and the remaining time. On hover the
// cursor shows a time bubble with a downward caret marking the position
// it would seek to.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
export component SeekBar inherits HorizontalLayout {
in property <float> progress: 0.0; // 0..1 played
in property <float> cache: 0.0; // 0..1 buffered
in property <float> seekable-max: 1.0; // furthest seekable fraction (stream lock)
in property <string> elapsed: "0:00";
in property <string> remaining: "0:00";
in property <int> duration-secs: 0;
callback seek(float); // 0..1
spacing: 12px;
function clamp01(v: float) -> float {
return Math.min(Math.max(v, 0.0), 1.0);
}
function fmt(secs: int) -> string {
return "\{Math.floor(secs / 60)}:\{Math.mod(secs, 60) < 10 ? "0" : ""}\{Math.mod(secs, 60)}";
}
VerticalLayout {
alignment: center;
Text {
text: root.elapsed;
width: 44px;
horizontal-alignment: right;
color: Theme.text-muted;
font-size: 11px;
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
track := Rectangle {
height: 3px;
border-radius: 2px;
background: Theme.surface-elevated;
// Buffered / cache line.
Rectangle {
x: 0;
y: 0;
width: parent.width * root.clamp01(root.cache);
height: parent.height;
border-radius: 2px;
background: Theme.text-muted;
opacity: 0.35;
}
// Playback progress line.
Rectangle {
x: 0;
y: 0;
width: parent.width * root.clamp01(root.progress);
height: parent.height;
border-radius: 2px;
background: Theme.accent;
}
// Hover thumb.
Rectangle {
width: 12px;
height: 12px;
border-radius: 6px;
background: Theme.text-primary;
y: (parent.height - self.height) / 2;
x: parent.width * root.clamp01(root.progress) - self.width / 2;
opacity: ta.has-hover ? 1.0 : 0.0;
animate opacity { duration: 100ms; }
}
// Tall, easy-to-grab hit area over the thin line.
ta := TouchArea {
width: 100%;
height: 18px;
y: (parent.height - self.height) / 2;
// No-drop cursor over the not-yet-downloaded region.
mouse-cursor: (root.clamp01(self.mouse-x / self.width) > root.seekable-max)
? MouseCursor.not-allowed
: MouseCursor.pointer;
clicked => {
// Lock the seek target to what has downloaded while streaming.
root.seek(Math.min(root.clamp01(self.mouse-x / self.width), root.seekable-max));
}
}
// Hover time tooltip with a downward caret.
tip := Rectangle {
property <int> at-secs:
Math.round(root.clamp01(ta.mouse-x / track.width) * root.duration-secs);
x: Math.min(Math.max(ta.mouse-x - self.width / 2, 0px),
track.width - self.width);
y: -self.height - 11px;
width: bubble.preferred-width;
height: 24px;
opacity: ta.has-hover ? 1.0 : 0.0;
animate opacity { duration: 80ms; }
bubble := Rectangle {
background: Theme.surface-elevated;
border-radius: 4px;
drop-shadow-blur: 10px;
drop-shadow-color: #00000099;
HorizontalLayout {
padding-left: 9px;
padding-right: 9px;
Text {
text: root.fmt(tip.at-secs);
color: Theme.text-primary;
font-size: 11px;
font-weight: Typography.medium;
vertical-alignment: center;
}
}
}
// Caret — a small downward triangle under the bubble.
Path {
width: 10px;
height: 6px;
fill: Theme.surface-elevated;
commands: "M 0 0 L 5 6 L 10 0 Z";
x: (parent.width - self.width) / 2;
y: parent.height - 1px;
}
}
}
}
VerticalLayout {
alignment: center;
Text {
text: root.remaining;
width: 44px;
horizontal-alignment: left;
color: Theme.text-muted;
font-size: 11px;
}
}
}