feat(slint): NowPlayingBar rebuild, Settings constraints, Discover fixes

NowPlayingBar (start of the playback-bar work):
- height raised to 110px
- SeekBar component: three-line track (total / cached / progress), a
  hover thumb, elapsed/remaining times, and a hover time bubble with a
  downward caret marking the seek position
- song card: background and border dropped, gains the extra height,
  album/artist links and the context icon kept (context icon now
  always shown)
- transport + secondary buttons copied from Tauri (shuffle, prev,
  play, next, repeat, add, favorite; cast, picture-in-picture,
  fullscreen, normalization; volume cluster). Queue/Lyrics stay in the
  header — and the header now orders them lyrics-then-queue.

Settings constraints (1:1 with Tauri):
- Exclusive Mode enabled only on ALSA; DAC Passthrough only on
  PipeWire; Lock Output Device disabled while DAC Passthrough is on;
  Gapless disabled while Streaming Only is on
- backend change cascades: forces off DAC passthrough / force-bit-
  perfect / exclusive / gapless as appropriate; DAC-passthrough and
  streaming-only cascades; initial-buffer slider right-aligned

Discover fixes:
- navigation [<] [>] pair homologated to the same top/left margin
  across every view
- album/artist description moved back into the metadata column (it now
  fills the width there) with uniform secondary text color
- genre filter button hugs its label instead of stretching
- Recently Played Albums and Tracks modules always render, with a
  placeholder until the play history has data
- wider spacing between Discover rows; the Discover gear is no longer
  a Settings shortcut (it is the per-tab sort/show-hide control)

cc
This commit is contained in:
vicrodh committed 2026-05-18 21:22:40 -06:00
1 parent edca636a16
commit 99e3513d68
18 files changed
+589 -150

No files matched your search

+133 -61
View File
@@ -1,7 +1,12 @@
// Bottom now-playing / transport bar.
//
// Controls are laid out and hover-reactive but not yet wired to
// playback. Real playback state arrives in a later milestone.
// 110px tall. A seek bar near the top, then a controls row: transport
// cluster (left), the song card (center, no background/border), and the
// secondary actions + volume cluster (right). Queue and Lyrics are
// intentionally absent — they live in the header.
//
// Playback state is not wired yet; the values are placeholders until the
// playback session lands.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
@@ -9,10 +14,13 @@ import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QbzSlider } from "../primitives/QbzSlider.slint";
import { SeekBar } from "SeekBar.slint";
component IconButton inherits Rectangle {
in property <image> icon;
in property <length> icon-size: 18px;
in property <bool> active: false;
callback clicked();
width: 34px;
@@ -26,7 +34,9 @@ component IconButton inherits Rectangle {
height: root.icon-size;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
tint: root.active
? Theme.accent
: (ta.has-hover ? Theme.text-primary : Theme.text-secondary);
}
ta := TouchArea {
@@ -37,6 +47,51 @@ component IconButton inherits Rectangle {
}
}
// Primary, filled play/pause button.
component PlayButton inherits Rectangle {
in property <image> icon;
callback clicked();
width: 38px;
height: 38px;
border-radius: 19px;
background: ta.has-hover ? #ffffffd6 : #ffffff;
QbzIcon {
source: root.icon;
width: 20px;
height: 20px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: #000000;
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
// Clickable album/artist link text.
component MetaLink inherits TouchArea {
in property <string> label;
callback activated();
width: link-text.preferred-width;
height: link-text.preferred-height;
mouse-cursor: pointer;
clicked => {
root.activated();
}
link-text := Text {
text: root.label;
color: root.has-hover ? Theme.text-primary : Theme.text-muted;
font-size: 12px;
}
}
export component PlayerBar inherits Rectangle {
height: Layout.player-bar-height;
background: Theme.surface-card;
@@ -44,94 +99,111 @@ export component PlayerBar inherits Rectangle {
VerticalLayout {
spacing: 0px;
// Progress line across the top edge.
Rectangle {
height: 2px;
background: Theme.surface-elevated;
Rectangle {
x: 0;
width: parent.width * 0.32;
background: Theme.accent;
}
// Gap from the top edge to the seek bar.
Rectangle { height: 14px; }
// --- Seek bar ----------------------------------------------------
HorizontalLayout {
padding-left: Spacing.md;
padding-right: Spacing.md;
SeekBar { }
}
// --- Controls row ------------------------------------------------
HorizontalLayout {
vertical-stretch: 1;
padding-left: Spacing.md;
padding-right: Spacing.md;
spacing: Spacing.md;
// Transport controls.
// Transport cluster.
VerticalLayout {
alignment: center;
HorizontalLayout {
spacing: 2px;
IconButton { icon: @image-url("../assets/icons/shuffle.svg"); }
IconButton { icon: @image-url("../assets/icons/shuffle.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/skip-back.svg"); }
IconButton {
icon: @image-url("../assets/icons/play.svg");
icon-size: 22px;
}
IconButton { icon: @image-url("../assets/icons/skip-forward.svg"); }
IconButton { icon: @image-url("../assets/icons/repeat.svg"); }
}
}
// Now-playing.
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
HorizontalLayout {
alignment: center;
spacing: Spacing.sm;
Rectangle {
width: 48px;
height: 48px;
border-radius: Radius.sm;
background: Theme.surface-elevated;
}
VerticalLayout {
alignment: center;
Text {
text: "Nothing playing";
color: Theme.text-primary;
font-size: Typography.body;
}
Text {
text: "QBZ MVP";
color: Theme.text-muted;
font-size: 12px;
}
PlayButton { icon: @image-url("../assets/icons/play-fill.svg"); }
}
IconButton { icon: @image-url("../assets/icons/skip-forward.svg"); }
IconButton { icon: @image-url("../assets/icons/repeat.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/plus.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/heart.svg"); icon-size: 16px; }
}
}
// Right-side controls.
VerticalLayout {
// Song card — no background, no border.
HorizontalLayout {
horizontal-stretch: 1;
alignment: center;
HorizontalLayout {
alignment: end;
spacing: Spacing.sm;
IconButton { icon: @image-url("../assets/icons/heart.svg"); }
IconButton { icon: @image-url("../assets/icons/maximize-2.svg"); }
IconButton { icon: @image-url("../assets/icons/volume-2.svg"); }
spacing: 12px;
VerticalLayout {
alignment: center;
Rectangle {
width: 90px;
height: 4px;
border-radius: 2px;
width: 56px;
height: 56px;
border-radius: 6px;
background: Theme.surface-elevated;
Rectangle {
x: 0;
width: parent.width * 0.6;
border-radius: 2px;
background: Theme.accent;
clip: true;
}
}
VerticalLayout {
alignment: center;
spacing: 4px;
Text {
text: @tr("Nothing playing");
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.medium;
overflow: elide;
}
HorizontalLayout {
spacing: 6px;
alignment: start;
MetaLink { label: "QBZ MVP"; }
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/layers.svg");
width: 13px;
height: 13px;
tint: Theme.text-muted;
}
}
}
}
}
}
// Secondary actions + volume cluster.
VerticalLayout {
alignment: center;
HorizontalLayout {
spacing: 2px;
IconButton { icon: @image-url("../assets/icons/cast.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/picture-in-picture-2.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/maximize-2.svg"); icon-size: 16px; }
IconButton { icon: @image-url("../assets/icons/audio-lines.svg"); icon-size: 16px; }
Rectangle { width: 6px; }
IconButton { icon: @image-url("../assets/icons/volume-2.svg"); icon-size: 16px; }
VerticalLayout {
alignment: center;
QbzSlider {
width: 90px;
minimum: 0;
maximum: 100;
value: 70;
}
}
IconButton { icon: @image-url("../assets/icons/minus.svg"); icon-size: 15px; }
IconButton { icon: @image-url("../assets/icons/plus.svg"); icon-size: 15px; }
}
}
}
}
}