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
258 lines
11 KiB
Plaintext
258 lines
11 KiB
Plaintext
// 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;
|
||
}
|
||
}
|