// KioskNowPlaying — the kiosk Now Playing centerpiece (2.0.2 frente #3). // // A 50/50 full screen: the current track (big cover + title/artist + AudioStamp // + SeekBar + the 7-control TransportControls) on the left, the Up-Next queue on // the right. Reuses the real transport/seek/stamp components and reads the same // NowPlayingState/QueueState the desktop uses (all covers push-only). Must call // QueueState.panel-opened() on init so the paginated queue slice is fresh. import { Theme } from "../foundation/semantic-colors.slint"; import { Radius } from "../foundation/radius.slint"; import { NowPlayingState, QueueState, QueueItem, LyricsState, ShellState } from "../state.slint"; import { LyricsLinesView } from "LyricsLinesView.slint"; component QueueRowLite inherits Rectangle { in property item; callback play; height: 58px; border-radius: Radius.sm; background: qrow.has-hover ? Theme.alpha-6 : transparent; HorizontalLayout { padding-left: 8px; padding-right: 12px; spacing: 12px; Rectangle { width: 44px; Rectangle { width: 44px; height: 44px; y: (parent.height - self.height) / 2; border-radius: 5px; background: Theme.surface-elevated; clip: true; if root.item.artwork.width > 0: Image { width: 100%; height: 100%; source: root.item.artwork; image-fit: cover; } } } VerticalLayout { horizontal-stretch: 1; alignment: center; spacing: 2px; Text { text: root.item.title; color: root.item.playing ? Theme.accent : Theme.text-primary; font-size: 14px; font-weight: 500; overflow: elide; wrap: no-wrap; } Text { text: root.item.artist; color: Theme.text-muted; font-size: 12.5px; overflow: elide; wrap: no-wrap; } } Text { text: root.item.duration; color: Theme.text-muted; font-size: 12.5px; vertical-alignment: center; } } qrow := TouchArea { clicked => { root.play(); } } } component QueueTab inherits Rectangle { in property label; in property active; callback picked; horizontal-stretch: 0; HorizontalLayout { padding-left: 2px; padding-right: 2px; VerticalLayout { alignment: start; spacing: 4px; Text { text: root.label; color: root.active ? Theme.text-primary : Theme.text-muted; font-size: 14px; font-weight: 700; vertical-alignment: center; } Rectangle { height: 2px; background: root.active ? Theme.accent : transparent; border-radius: 2px; } } } ta := TouchArea { clicked => { root.picked(); } } } import { SeekBar } from "SeekBar.slint"; import { TransportControls } from "TransportControls.slint"; import { AudioStamp } from "AudioStamp.slint"; export component KioskNowPlaying inherits Rectangle { callback open-album(string); callback open-artist(string); callback media-action(string, string, string); // Left panel toggles between the big cover and synced lyrics — the kiosk's // lightweight lyrics surface (no immersive visualizer needed, Pi-friendly). property show-lyrics: false; init => { QueueState.panel-opened(); // Mount in Cover mode — make sure a stale follow flag from a prior visit // doesn't keep the sync engine running. ShellState.kiosk-lyrics-follow = false; } HorizontalLayout { spacing: 0px; // LEFT — the current track. Rectangle { horizontal-stretch: 1; VerticalLayout { padding: 18px; spacing: 12px; // Cover / Lyrics toggle — pinned to the TOP so it lines up with // 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 { vertical-stretch: 0; alignment: start; spacing: 18px; QueueTab { label: @tr("Cover"); active: !root.show-lyrics; picked => { root.show-lyrics = false; // Stop the lyrics-sync engine (see ShellState). ShellState.kiosk-lyrics-follow = false; } } QueueTab { label: @tr("Lyrics"); active: root.show-lyrics; picked => { root.show-lyrics = true; // Open the gate so the karaoke follow runs (the // wrapper's panel-opened() kick then lands instantly). ShellState.kiosk-lyrics-follow = true; } } } // Flexible art/lyrics area — takes the slack (pins the toggle // above and the transport below) and CLIPS, so on a short window // (e.g. 1080x600) the cover shrinks instead of pushing the // controls off-screen. art-area := Rectangle { vertical-stretch: 1; clip: true; // Cover: contained so it is as large as fits and scales with // the window WITHOUT reading parent geometry (the old // Math.min(art-area.height) collapsed it to ~55px). Fills the // dominant area — big on tall windows, shrinks on short ones. if !root.show-lyrics: Image { width: 100%; height: 100%; source: NowPlayingState.artwork-large; image-fit: contain; } // Lyrics fill the panel width (no more right-edge clipping) and // the flexible height. Mount the karaoke view ONLY when lyrics // are READY (status 2 + lines): instantiating LyricsLinesView // against empty/loading state during the tap's own relayout // trips its geometry FOOTGUN (properties.rs:583 "Recursion // detected"). status==2 lands async in a settled layout. if root.show-lyrics: Rectangle { width: 100%; height: 100%; init => { LyricsState.panel-opened(); } if LyricsState.status == 2 && LyricsState.lines.length > 0: LyricsLinesView { width: 100%; height: 100%; side-padding: 8px; line-gap: 13px; size-inactive: 19px; size-active: 23px; dimming-mode: LyricsState.dimming-mode; auto-follow: true; uppercase: LyricsState.uppercase; show-attribution: false; font-name: LyricsState.font-index == 1 ? "LINE Seed JP" : LyricsState.font-index == 2 ? "Montserrat" : LyricsState.font-index == 3 ? "Noto Sans" : LyricsState.font-index == 4 ? "Source Sans 3" : ""; active-color: Theme.accent; } if !(LyricsState.status == 2 && LyricsState.lines.length > 0): Text { width: 100%; height: 100%; 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: NowPlayingState.title; color: Theme.text-primary; font-size: 20px; font-weight: 700; horizontal-alignment: center; overflow: elide; wrap: no-wrap; } artist-row := Rectangle { height: 18px; HorizontalLayout { alignment: center; Text { text: NowPlayingState.artist; color: Theme.text-secondary; font-size: 14px; vertical-alignment: center; } } art-ta := TouchArea { clicked => { root.open-artist(NowPlayingState.artist-id); } } } SeekBar { show-times: true; progress: NowPlayingState.progress; cache: NowPlayingState.cache; seekable-max: NowPlayingState.seekable-max; elapsed: NowPlayingState.elapsed; remaining: NowPlayingState.remaining; duration-secs: NowPlayingState.duration-secs; seek(fraction) => { NowPlayingState.seek(fraction); } } // Transport centered; the quality stamp floats at the right of // the same row instead of owning a whole line (saves vertical // space on short windows). Rectangle { height: 46px; HorizontalLayout { 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 { } } } } } } } Rectangle { width: 1px; background: Theme.border-subtle; } // RIGHT — the queue, with Queue / History tabs. ListView windows the // rows so a long history never mounts every cover at once (Pi-safe). Rectangle { width: (parent.width - 1px) * 0.42; background: Theme.surface-card; VerticalLayout { padding: 18px; spacing: 12px; HorizontalLayout { height: 30px; spacing: 20px; alignment: start; QueueTab { label: @tr("Up Next"); active: QueueState.tab == 0; picked => { QueueState.set-tab(0); } } QueueTab { label: @tr("History"); active: QueueState.tab == 1; picked => { QueueState.set-tab(1); } } } // Body: the desktop QueueSidebar pattern — a stretching Rectangle // holds a Flickable whose viewport-height IS the content height, // so the rows top-anchor. (A ListView with height:100% bottom- // anchored a short list — the recurrent Slint distribution trap; // upcoming-page/history are already paginated slices, so mounting // the page is cheap.) Rectangle { vertical-stretch: 1; if QueueState.tab == 0: Flickable { viewport-width: self.width; viewport-height: up-body.preferred-height; up-body := VerticalLayout { spacing: 2px; for item[i] in QueueState.upcoming-page: QueueRowLite { item: item; play => { QueueState.play-upcoming(i); } } } } if QueueState.tab == 1: Flickable { viewport-width: self.width; viewport-height: hist-body.preferred-height; hist-body := VerticalLayout { spacing: 2px; for item[i] in QueueState.history: QueueRowLite { item: item; play => { QueueState.play-history(i); } } } } } } } } }