// LabelReleasesView — the "See all releases" sub-view reached from the // label landing's Releases carousel. A circular label header over the // full album catalog, with a toolbar (search / group-by-artist / Hi-Res // filter / sort) and a grid/list toggle, plus load-more pagination. // // The rendered set (`visible` / `grouped`) is derived in Rust from the // loaded catalog (crate::label::derive_releases). Search is a local // filter over the loaded catalog. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { LabelState, LabelActions, NavState } from "../state.slint"; import { AlbumCollectionView } from "../discover/AlbumCollectionView.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; import { QbzSelect } from "../primitives/QbzSelect.slint"; import { ExpandableSearch } from "../primitives/ExpandableSearch.slint"; import { ListScrollbar } from "../primitives/ListScrollbar.slint"; import { LoadingSpinner } from "../primitives/LoadingSpinner.slint"; import { ToggleButton } from "../primitives/ToggleButton.slint"; import { NavButtons } from "../shell/NavButtons.slint"; export component LabelReleasesView inherits Rectangle { callback open-album(string); callback open-artist(string); callback media-action(string, string, string); property list-row-gap: 4px; background: Theme.surface-main; flick := Flickable { viewport-width: self.width; viewport-height: page.preferred-height; // Scroll-position restore on back/forward (see nav::scope_for). property sr-armed: NavState.restore-scope; function sr-restore() { if (NavState.restore-scope == "label-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); } page := VerticalLayout { width: parent.width; padding-left: 32px; padding-right: 32px; padding-top: 11px; padding-bottom: 100px; spacing: 0px; alignment: start; HorizontalLayout { alignment: start; NavButtons { } } Rectangle { height: 22px; } // --- Label header (circular 180px portrait) ------------------ HorizontalLayout { spacing: 24px; alignment: start; Rectangle { width: 180px; height: 180px; border-radius: 90px; background: Theme.surface-elevated; drop-shadow-blur: 32px; drop-shadow-color: #00000080; drop-shadow-offset-y: 8px; clip: true; if LabelState.image-url == "": QbzIcon { source: @image-url("../assets/icons/disc-3.svg"); width: 60px; height: 60px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: Theme.text-muted; } Image { width: 100%; height: 100%; source: LabelState.image; image-fit: cover; } } VerticalLayout { alignment: center; spacing: 6px; horizontal-stretch: 1; Text { text: @tr("LABEL"); color: Theme.text-muted; font-size: 10px; font-weight: Typography.semibold; letter-spacing: 1.5px; } Text { text: LabelState.name; color: Theme.text-primary; font-size: Typography.title; font-weight: Typography.bold; } if LabelState.total > 0: Text { text: LabelState.total + " " + @tr("albums"); color: Theme.text-muted; font-size: Typography.body; } } } Rectangle { height: 24px; } // --- Toolbar: title + count, then search / group / Hi-Res / // sort / grid-list (floated right) --------------------------- HorizontalLayout { spacing: 12px; alignment: stretch; VerticalLayout { alignment: center; horizontal-stretch: 1; Text { text: @tr("Releases"); color: Theme.text-primary; font-size: Typography.heading; font-weight: Typography.semibold; } Text { text: LabelState.shown + " " + @tr("albums") + (LabelState.hires-count > 0 ? " · " + LabelState.hires-count + " Hi-Res" : ""); color: Theme.text-muted; font-size: Typography.legal; } } HorizontalLayout { horizontal-stretch: 0; spacing: 8px; VerticalLayout { alignment: center; ExpandableSearch { placeholder: @tr("Search this label…"); query <=> LabelState.search-query; edited(q) => { LabelActions.search(q); } } } VerticalLayout { alignment: center; ToggleButton { icon: @image-url("../assets/icons/user.svg"); active: LabelState.group-by-artist; clicked => { LabelState.group-by-artist = !LabelState.group-by-artist; LabelActions.set-group(LabelState.group-by-artist); } } } VerticalLayout { alignment: center; ToggleButton { icon: @image-url("../assets/icons/list-filter.svg"); active: LabelState.filter-hires; clicked => { LabelState.filter-hires = !LabelState.filter-hires; LabelActions.set-hires(LabelState.filter-hires); } } } VerticalLayout { alignment: center; QbzSelect { menu-width: 168px; options: [ @tr("Newest"), @tr("Oldest"), @tr("Title A-Z"), @tr("Title Z-A"), @tr("Artist A-Z"), ]; current-index: LabelState.sort-by == "newest" ? 0 : LabelState.sort-by == "oldest" ? 1 : LabelState.sort-by == "title-asc" ? 2 : LabelState.sort-by == "title-desc" ? 3 : 4; selected(i) => { LabelState.sort-by = i == 0 ? "newest" : i == 1 ? "oldest" : i == 2 ? "title-asc" : i == 3 ? "title-desc" : "artist-asc"; LabelActions.set-sort(LabelState.sort-by); } } } // Grid / list segmented toggle. VerticalLayout { alignment: center; Rectangle { width: 68px; height: 34px; border-radius: 6px; background: Theme.surface-elevated; clip: true; HorizontalLayout { ToggleButton { icon: @image-url("../assets/icons/layout-grid.svg"); active: LabelState.view-mode == "grid"; clicked => { LabelState.view-mode = "grid"; } } ToggleButton { icon: @image-url("../assets/icons/list.svg"); active: LabelState.view-mode == "list"; clicked => { LabelState.view-mode = "list"; } } } } } } } Rectangle { height: 24px; } // --- States -------------------------------------------------- if LabelState.loading && LabelState.albums.length == 0: HorizontalLayout { alignment: center; LoadingSpinner { size: 36px; } } // No search/filter results. if !LabelState.loading && LabelState.albums.length > 0 && LabelState.shown == 0: VerticalLayout { alignment: center; spacing: 10px; HorizontalLayout { alignment: center; QbzIcon { source: @image-url("../assets/icons/search.svg"); width: 32px; height: 32px; tint: Theme.text-muted; } } Text { text: @tr("No results"); color: Theme.text-muted; font-size: 14px; horizontal-alignment: center; } } // --- Album collection (grouped / flat grid / flat list) ------ AlbumCollectionView { is-grouped: LabelState.group-by-artist; albums: LabelState.visible; grouped: LabelState.grouped; view-mode: LabelState.view-mode; show-source: false; list-row-gap: root.list-row-gap; 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); } } // --- Load more ----------------------------------------------- // Driven by `has-more` — /label/getAlbums caps each page well // below the catalog, so `total` is an unreliable gate. if LabelState.albums.length > 0 && LabelState.has-more: HorizontalLayout { alignment: center; Rectangle { width: load-more-text.preferred-width + 24px; height: 32px; load-more-text := Text { text: LabelState.load-more-loading ? @tr("Loading…") : @tr("Load more"); color: load-more-ta.has-hover ? Theme.text-primary : Theme.text-secondary; font-size: 13px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; } load-more-ta := TouchArea { mouse-cursor: pointer; enabled: !LabelState.load-more-loading; clicked => { LabelActions.load-more(); } } } } } } // Shared scrollbar in the right gutter (homologated cross-view). ListScrollbar { x: parent.width - self.width - 4px; width: 14px; height: 100%; viewport-height: flick.viewport-height; visible-height: flick.height; viewport-y <=> flick.viewport-y; } }