feat(kiosk): redesign Now Playing panel — dominant contain cover, compact meta, floating quality stamp

The cover is image-fit:contain filling the flex art area (no fragile parent-geometry math), so it is large and scales with the window. Toggle and meta rows get vertical-stretch:0 so only the art area takes the slack (nested layouts stretch greedily) — kills the dead space and the ~55px cover. Title toned to 20px, quality stamp floats on the transport row instead of owning a line. Cover<->Lyrics toggle; lyrics fill the panel width.

cc
This commit is contained in:
vicrodh committed 2026-07-12 19:38:43 -06:00
1 parent 5be0e2233a
commit 5fa4e8bdd5
1 file changed
+97 -88
+97 -88
View File
@@ -129,12 +129,16 @@ export component KioskNowPlaying inherits Rectangle {
Rectangle { Rectangle {
horizontal-stretch: 1; horizontal-stretch: 1;
VerticalLayout { VerticalLayout {
padding: 26px; padding: 18px;
spacing: 16px; spacing: 12px;
// Cover / Lyrics toggle — pinned to the TOP so it lines up with // Cover / Lyrics toggle — pinned to the TOP so it lines up with
// the Up Next / History tabs on the right panel. // the Up Next / History tabs on the right panel. vertical-stretch:0
// is REQUIRED — a nested layout defaults to greedy stretch in
// Slint, so without it the tabs row eats vertical space and pushes
// the cover down (dead space above it).
HorizontalLayout { HorizontalLayout {
vertical-stretch: 0;
alignment: start; alignment: start;
spacing: 18px; spacing: 18px;
QueueTab { QueueTab {
@@ -158,98 +162,94 @@ export component KioskNowPlaying inherits Rectangle {
} }
} }
// Cover/lyrics + metadata + transport fill the rest, vertically // Flexible art/lyrics area — takes the slack (pins the toggle
// centered — the stretch absorbs the slack so the toggle above // above and the transport below) and CLIPS, so on a short window
// stays pinned to the top (no dead space over it). // (e.g. 1080x600) the cover shrinks instead of pushing the
VerticalLayout { // controls off-screen.
art-area := Rectangle {
vertical-stretch: 1; vertical-stretch: 1;
alignment: center; clip: true;
spacing: 16px;
if !root.show-lyrics: HorizontalLayout { // Cover: contained so it is as large as fits and scales with
alignment: center; // the window WITHOUT reading parent geometry (the old
Rectangle { // Math.min(art-area.height) collapsed it to ~55px). Fills the
width: 240px; // dominant area — big on tall windows, shrinks on short ones.
height: 240px; if !root.show-lyrics: Image {
border-radius: Radius.lg; width: 100%;
background: Theme.surface-elevated; height: 100%;
clip: true; source: NowPlayingState.artwork-large;
if NowPlayingState.artwork-large.width > 0: Image { image-fit: contain;
width: 100%;
height: 100%;
source: NowPlayingState.artwork-large;
image-fit: cover;
}
}
} }
if root.show-lyrics: HorizontalLayout { // Lyrics fill the panel width (no more right-edge clipping) and
alignment: center; // the flexible height. Mount the karaoke view ONLY when lyrics
Rectangle { // are READY (status 2 + lines): instantiating LyricsLinesView
width: 420px; // against empty/loading state during the tap's own relayout
height: 320px; // trips its geometry FOOTGUN (properties.rs:583 "Recursion
// Ask Rust for this track's lyrics when the surface // detected"). status==2 lands async in a settled layout.
// appears (global callback — no geometry read, safe). if root.show-lyrics: Rectangle {
// Mount the karaoke view ONLY when lyrics are READY width: 100%;
// (status 2 + lines) and inside a FIXED-size box: height: 100%;
// instantiating LyricsLinesView against empty/loading init => {
// state or a still-resolving stretch width trips its LyricsState.panel-opened();
// geometry FOOTGUN (properties.rs:583 "Recursion }
// detected" — crash on tap 2026-07-12). status==2 lands if LyricsState.status == 2 && LyricsState.lines.length > 0: LyricsLinesView {
// async in a clean turn; the fixed width keeps width: 100%;
// `changed width` reading a constant. Mirrors the height: 100%;
// desktop LyricsSidebar gate. side-padding: 8px;
init => { line-gap: 13px;
LyricsState.panel-opened(); size-inactive: 19px;
} size-active: 23px;
if LyricsState.status == 2 && LyricsState.lines.length > 0: LyricsLinesView { dimming-mode: LyricsState.dimming-mode;
width: 100%; auto-follow: true;
height: 100%; uppercase: LyricsState.uppercase;
side-padding: 10px; show-attribution: false;
line-gap: 14px; font-name: LyricsState.font-index == 1 ? "LINE Seed JP"
size-inactive: 20px; : LyricsState.font-index == 2 ? "Montserrat"
size-active: 25px; : LyricsState.font-index == 3 ? "Noto Sans"
dimming-mode: LyricsState.dimming-mode; : LyricsState.font-index == 4 ? "Source Sans 3"
auto-follow: true; : "";
uppercase: LyricsState.uppercase; active-color: Theme.accent;
show-attribution: false; }
font-name: LyricsState.font-index == 1 ? "LINE Seed JP" if !(LyricsState.status == 2 && LyricsState.lines.length > 0): Text {
: LyricsState.font-index == 2 ? "Montserrat" width: 100%;
: LyricsState.font-index == 3 ? "Noto Sans" height: 100%;
: LyricsState.font-index == 4 ? "Source Sans 3" text: LyricsState.status == 5 ? @tr("Lyrics unavailable offline")
: ""; : (LyricsState.status == 3 || LyricsState.status == 4) ? @tr("No lyrics available")
active-color: Theme.accent; : @tr("Fetching lyrics...");
} color: Theme.text-muted;
if !(LyricsState.status == 2 && LyricsState.lines.length > 0): Text { font-size: 14px;
width: 100%; horizontal-alignment: center;
height: 100%; vertical-alignment: center;
text: LyricsState.status == 5 ? @tr("Lyrics unavailable offline")
: (LyricsState.status == 3 || LyricsState.status == 4) ? @tr("No lyrics available")
: @tr("Fetching lyrics...");
color: Theme.text-muted;
font-size: 14px;
horizontal-alignment: center;
vertical-alignment: center;
}
} }
} }
}
// Pinned metadata + transport — always on screen even when the
// art area is squeezed on a short window. vertical-stretch:0 keeps
// it natural-height at the bottom (nested layouts stretch greedily
// by default) so ALL slack goes to the art area above.
VerticalLayout {
vertical-stretch: 0;
spacing: 8px;
Text { Text {
text: NowPlayingState.title; text: NowPlayingState.title;
color: Theme.text-primary; color: Theme.text-primary;
font-size: 24px; font-size: 20px;
font-weight: 700; font-weight: 700;
horizontal-alignment: center; horizontal-alignment: center;
wrap: word-wrap; overflow: elide;
wrap: no-wrap;
} }
artist-row := Rectangle { artist-row := Rectangle {
height: 24px; height: 18px;
HorizontalLayout { HorizontalLayout {
alignment: center; alignment: center;
Text { Text {
text: NowPlayingState.artist; text: NowPlayingState.artist;
color: Theme.text-secondary; color: Theme.text-secondary;
font-size: 16px; font-size: 14px;
vertical-alignment: center; vertical-alignment: center;
} }
} }
@@ -260,11 +260,6 @@ export component KioskNowPlaying inherits Rectangle {
} }
} }
HorizontalLayout {
alignment: center;
AudioStamp { }
}
SeekBar { SeekBar {
show-times: true; show-times: true;
progress: NowPlayingState.progress; progress: NowPlayingState.progress;
@@ -278,13 +273,27 @@ export component KioskNowPlaying inherits Rectangle {
} }
} }
HorizontalLayout { // Transport centered; the quality stamp floats at the right of
alignment: center; // the same row instead of owning a whole line (saves vertical
TransportControls { // space on short windows).
play-circle: true; Rectangle {
classic-actions: true; height: 46px;
media-action(k, i, a) => { HorizontalLayout {
root.media-action(k, i, a); alignment: center;
TransportControls {
play-circle: true;
classic-actions: true;
media-action(k, i, a) => {
root.media-action(k, i, a);
}
}
}
HorizontalLayout {
alignment: end;
padding-right: 2px;
VerticalLayout {
alignment: center;
AudioStamp { }
} }
} }
} }