Files
qbz/crates/qbz-slint/ui/discover/Carousel.slint
T
vicrodh 6b772b41b0 feat(qbz-slint): album detail page
Clicking an album card opens the album detail view: a back button,
the album header (224px artwork, title, artist, and info line — year,
label, genre, track count, duration — plus play / shuffle / favorite /
more action buttons), and the track list with a column header (#,
title, artist, duration, quality).

- album.rs: fetches the album via QbzCore::get_album, maps it (info
  line, per-track quality tier, durations) onto the AlbumState global,
  and loads the header artwork through the shared image cache.
- AlbumPageView.slint, with NavState driving the shell content region
  between Home and the album page.
- Album clicks bubble AlbumCard -> Carousel -> HomeView -> AppShell ->
  AppWindow -> main; the back button returns to Home.

cc
2026-05-18 14:38:45 -06:00

124 lines
3.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Discover section carousel — a title, prev/next page controls, and a
// clipped, sliding track of album cards.
//
// Pagination by slice (no scrollbar), mirroring the Svelte
// DiscoverySection. Because there is no ScrollView the height is exactly
// the card height — nothing injects extra vertical space.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
component NavButton inherits Rectangle {
in property <string> glyph;
in property <bool> enabled: true;
callback clicked();
width: 28px;
height: 28px;
border-radius: 14px;
opacity: root.enabled ? 1.0 : 0.4;
background: ta.has-hover && root.enabled
? Theme.surface-hover
: Theme.surface-elevated;
Text {
text: root.glyph;
color: root.enabled ? Theme.text-primary : Theme.text-muted;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
}
ta := TouchArea {
enabled: root.enabled;
mouse-cursor: root.enabled ? pointer : default;
clicked => {
root.clicked();
}
}
}
export component Carousel inherits VerticalLayout {
in property <DiscoverSection> section;
callback album-clicked(string);
property <length> card-width: 220px;
property <length> card-height: 266px;
property <length> gap: 32px;
property <int> count: root.section.albums.length;
property <int> per-page: Math.max(
1,
Math.floor((viewport.width + root.gap) / (root.card-width + root.gap)));
property <int> page-count: Math.max(1, Math.ceil(root.count / root.per-page));
property <int> page: 0;
// Self-correcting page index — stays valid when the window resizes.
property <int> current-page: Math.min(Math.max(0, root.page), root.page-count - 1);
property <length> step: root.per-page * (root.card-width + root.gap);
property <length> content-width:
root.count * root.card-width + Math.max(0, root.count - 1) * root.gap;
spacing: 12px;
// --- Header: title + page controls -----------------------------------
HorizontalLayout {
Text {
text: root.section.title;
color: Theme.text-primary;
font-size: 18px;
font-weight: Typography.semibold;
vertical-alignment: center;
horizontal-stretch: 1;
}
HorizontalLayout {
spacing: 4px;
VerticalLayout {
alignment: center;
NavButton {
glyph: "‹";
enabled: root.current-page > 0;
clicked => {
root.page = root.current-page - 1;
}
}
}
VerticalLayout {
alignment: center;
NavButton {
glyph: "›";
enabled: root.current-page < root.page-count - 1;
clicked => {
root.page = root.current-page + 1;
}
}
}
}
}
// --- Track: clipped viewport, slides by page -------------------------
viewport := Rectangle {
height: root.card-height;
clip: true;
Rectangle {
x: -root.current-page * root.step;
y: 0px;
width: root.content-width;
height: root.card-height;
animate x { duration: 220ms; easing: ease-in-out; }
HorizontalLayout {
spacing: root.gap;
for album in root.section.albums: AlbumCard {
album: album;
clicked(id) => {
root.album-clicked(id);
}
}
}
}
}
}