Files
qbz/crates/qbz-ui/ui/artist/ArtistReleasesView.slint
T
vicrodh 059d34cda9 perf(ui): element windowing for Discover/Label/Artist/Award grids
Flips windowed on the four AlbumCollectionView consumers with their
scroll wiring and documented content-offset approximations. Mount-only:
artwork stays per-page full dispatch (index-keyed, append-only models),
so covers are already on the model when a card mounts. Load-more
triggers untouched (grid height formula is windowing-invariant).

cc
2026-07-06 19:55:07 -06:00

258 lines
11 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.
// Dedicated artist discography page — the full, paginated grid for one
// release bucket, reached via "See discography" on the artist page.
// Structure homologated with AwardAlbumsView: a fixed header (NavButtons +
// artist kicker + bucket title on the LEFT, a sort select on the RIGHT) then
// an infinite-scroll Flickable driving ArtistReleasesActions.load-more().
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { ArtistReleasesState, ArtistReleasesActions, NavState, ShellState } from "../state.slint";
import { AlbumCollectionView } from "../discover/AlbumCollectionView.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { NavButtons } from "../shell/NavButtons.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
export component ArtistReleasesView inherits Rectangle {
callback open-album(string);
callback open-artist(string);
callback media-action(string, string, string);
property <length> album-w: 200px;
property <length> album-h: 266px;
property <length> gap: 24px;
property <int> sort-index:
ArtistReleasesState.sort-by == "newest" ? 1 :
ArtistReleasesState.sort-by == "oldest" ? 2 :
ArtistReleasesState.sort-by == "title-asc" ? 3 :
ArtistReleasesState.sort-by == "title-desc" ? 4 : 0;
background: Theme.surface-main;
VerticalLayout {
spacing: 0px;
// --- Fixed header ------------------------------------------------
Rectangle {
height: 56px;
background: Theme.surface-main;
// Left cluster — nav buttons then the artist kicker + bucket title.
HorizontalLayout {
x: 32px;
y: 25px - self.height / 2;
width: self.preferred-width;
height: self.preferred-height;
spacing: 16px;
VerticalLayout {
alignment: center;
NavButtons { }
}
VerticalLayout {
alignment: center;
spacing: 2px;
// Kicker — the artist name, server text (NEVER @tr).
Text {
text: ArtistReleasesState.name;
color: Theme.text-muted;
font-size: 11px;
font-weight: Typography.semibold;
letter-spacing: 0.6px;
vertical-alignment: center;
}
Text {
text: ArtistReleasesState.title;
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
vertical-alignment: center;
}
}
}
// Right cluster — the sort select, anchored right.
HorizontalLayout {
x: parent.width - self.preferred-width - 32px;
y: 25px - self.height / 2;
width: self.preferred-width;
height: self.preferred-height;
VerticalLayout {
alignment: center;
QbzSelect {
sm: true;
menu-width: 150px;
current-index: root.sort-index;
options: [
@tr("Default"),
@tr("Newest"),
@tr("Oldest"),
@tr("A–Z"),
@tr("Z–A"),
];
selected(idx) => {
ArtistReleasesActions.set-sort(
idx == 1 ? "newest"
: idx == 2 ? "oldest"
: idx == 3 ? "title-asc"
: idx == 4 ? "title-desc"
: "default");
}
}
}
}
}
// --- Scrolling grid ----------------------------------------------
flick := Flickable {
vertical-stretch: 1;
viewport-width: self.width;
viewport-height: page.preferred-height;
property <string> sr-armed: NavState.restore-scope;
function sr-restore() {
if (NavState.restore-scope == "artist-releases" && self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
NavState.restore-scope = "";
}
}
init => { self.sr-restore(); }
changed sr-armed => { self.sr-restore(); }
changed viewport-height => { self.sr-restore(); }
changed viewport-y => {
NavState.report-scroll(self.viewport-y);
if (-self.viewport-y + self.height >= self.viewport-height - 600px
&& ArtistReleasesState.has-more
&& !ArtistReleasesState.load-more-loading
&& !ArtistReleasesState.loading) {
ArtistReleasesActions.load-more();
}
}
page := VerticalLayout {
width: parent.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 8px;
padding-bottom: 100px;
spacing: 0px;
alignment: start;
if ArtistReleasesState.loading: HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
Text {
text: @tr("Loading…");
color: Theme.text-muted;
font-size: 14px;
horizontal-alignment: center;
}
}
}
if !ArtistReleasesState.loading && ArtistReleasesState.load-error: HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
spacing: 12px;
Text {
text: @tr("Failed to load Library");
color: Theme.text-secondary;
font-size: 14px;
horizontal-alignment: center;
}
Rectangle {
width: retry-row.preferred-width;
height: 34px;
border-radius: 6px;
background: retry-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
retry-row := HorizontalLayout {
padding-left: 16px;
padding-right: 16px;
Text {
text: @tr("Retry");
color: Theme.text-primary;
font-size: 13px;
font-weight: Typography.medium;
vertical-alignment: center;
}
}
retry-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
ArtistReleasesActions.retry();
}
}
}
}
}
if !ArtistReleasesState.loading && !ArtistReleasesState.load-error
&& ArtistReleasesState.albums.length == 0: HorizontalLayout {
alignment: center;
Text {
text: @tr("No releases found.");
color: Theme.text-muted;
font-size: 14px;
}
}
if !ArtistReleasesState.loading && !ArtistReleasesState.load-error: AlbumCollectionView {
albums: ArtistReleasesState.albums;
view-mode: "grid";
is-grouped: false;
show-source: false;
card-width: root.album-w;
card-height: root.album-h;
card-gap: root.gap;
// Viewport windowing (Phase 1): only cards near the
// visible band mount. The offset is the page's
// padding-top (8px) — the loading / error branches are
// mutually exclusive with the grid, so nothing else
// sits above it; the grid's one-viewport margin absorbs
// any small error.
windowed: true;
visible-top: -flick.viewport-y;
visible-height: flick.height;
content-offset: 8px;
// Element windowing only; artwork stays per-page —
// pages arrive index-keyed with covers already on the
// model (id-keyed banding is a future migration).
open-album(id) => { root.open-album(id); }
open-artist(id) => { root.open-artist(id); }
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
if ArtistReleasesState.load-more-loading: HorizontalLayout {
alignment: center;
Rectangle { height: 24px; }
LoadingSpinner { size: 28px; }
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
y: 56px;
width: 14px;
height: parent.height - 56px;
viewport-height: flick.viewport-height;
visible-height: flick.height;
viewport-y <=> flick.viewport-y;
auto-hide: true;
hover-active: ShellState.pointer-in-window
&& ShellState.pointer-x >= flick.absolute-position.x
&& ShellState.pointer-x <= flick.absolute-position.x + flick.width
&& ShellState.pointer-y >= flick.absolute-position.y
&& ShellState.pointer-y <= flick.absolute-position.y + flick.height;
}
}