// Discover "View all" full-list page — one album module (New Releases, // Press Accolades, Ideal Discography, Qobuzissimes, Albums of the Week, // Most Streamed) shown as a paginated full grid or list. Opened from a // Carousel's "View all" link. Mirrors Tauri's DiscoverBrowseView: the // back/forward NavButtons + title sit on the LEFT, the header tools // (search, genre filter, grid/list toggle) float RIGHT on the same row. // Pagination is infinite-scroll, driven by DiscoverBrowseState.has-more // off the backend (no total); load-more is disabled while a search is // active (Tauri does the same). import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { DiscoverBrowseState, DiscoverBrowseActions, GenreFilterState } from "../state.slint"; import { AlbumCollectionView } from "AlbumCollectionView.slint"; import { GenreFilterPopup } from "GenreFilterPopup.slint"; import { LoadingSpinner } from "../primitives/LoadingSpinner.slint"; import { ListScrollbar } from "../primitives/ListScrollbar.slint"; import { NavButtons } from "../shell/NavButtons.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; import { ToggleButton } from "../primitives/ToggleButton.slint"; // Search field — a leading magnifier, the input, and a trailing X that // clears (mirrors Tauri's .search-wrapper). component BrowseSearch inherits Rectangle { height: 34px; width: 200px; border-radius: 6px; background: Theme.surface-elevated; border-width: 1px; border-color: input.has-focus ? Theme.accent : Theme.border-subtle; HorizontalLayout { padding-left: 10px; padding-right: 6px; spacing: 8px; VerticalLayout { alignment: center; QbzIcon { source: @image-url("../assets/icons/search.svg"); width: 15px; height: 15px; tint: Theme.text-muted; } } Rectangle { horizontal-stretch: 1; clip: true; input := TextInput { width: 100%; height: 100%; color: Theme.text-primary; font-size: 13px; vertical-alignment: center; single-line: true; text <=> DiscoverBrowseState.search-query; edited => { DiscoverBrowseActions.search-changed(); } } Text { visible: input.text == ""; width: 100%; height: 100%; text: @tr("Search…"); color: Theme.text-muted; font-size: 13px; vertical-alignment: center; overflow: elide; } } // Trailing X — clears the query. VerticalLayout { alignment: center; Rectangle { visible: DiscoverBrowseState.search-query != ""; width: 22px; height: 22px; border-radius: 4px; background: clear-ta.has-hover ? Theme.surface-hover : transparent; QbzIcon { source: @image-url("../assets/icons/x.svg"); width: 12px; height: 12px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: clear-ta.has-hover ? Theme.text-primary : Theme.text-muted; } clear-ta := TouchArea { mouse-cursor: pointer; clicked => { DiscoverBrowseState.search-query = ""; DiscoverBrowseActions.search-changed(); } } } } } } // Genre-filter trigger — same visual as HomeView's GenreButton: accent // fill + "N genres" when a selection is active. component GenreButton inherits Rectangle { callback clicked(); property active: GenreFilterState.selected-count > 0; width: genre-row.preferred-width; height: 34px; border-radius: 6px; background: root.active ? Theme.accent : (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated); genre-row := HorizontalLayout { padding-left: 12px; padding-right: 14px; spacing: 7px; VerticalLayout { alignment: center; QbzIcon { source: @image-url("../assets/icons/list-filter.svg"); width: 14px; height: 14px; tint: root.active ? #ffffff : Theme.text-secondary; } } Text { text: root.active ? (GenreFilterState.selected-count == 1 ? @tr("1 genre") : @tr("{} genres", GenreFilterState.selected-count)) : @tr("Filter by genre"); color: root.active ? #ffffff : Theme.text-secondary; font-size: 13px; font-weight: root.active ? Typography.medium : Typography.regular; vertical-alignment: center; } } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } } } export component DiscoverBrowseView inherits Rectangle { callback open-album(string); callback open-artist(string); callback media-action(string, string, string); property album-w: 220px; property album-h: 266px; property gap: 24px; property list-row-h: 64px; property list-row-gap: 4px; background: Theme.surface-main; VerticalLayout { spacing: 0px; // --- Fixed header (always visible) ------------------------------- // NavButtons + section title on the LEFT (title inline, right after // the nav buttons); search / genre / grid-list tools as one group // floating RIGHT. Like the Discover toolbar: a fixed bar whose // back/forward buttons keep the same screen position as every page // (center at y=25 = 11px top + 14px half-button). Only the list // below scrolls. Rectangle { height: 56px; background: Theme.surface-main; // Left cluster — nav buttons then the title, inline. 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; Text { text: DiscoverBrowseState.title; color: Theme.text-primary; font-size: Typography.section; font-weight: Typography.bold; vertical-alignment: center; } } } // Right cluster — the header tools as one group, anchored right. HorizontalLayout { x: parent.width - self.preferred-width - 32px; y: 25px - self.height / 2; width: self.preferred-width; height: self.preferred-height; spacing: 8px; VerticalLayout { alignment: center; BrowseSearch { } } VerticalLayout { alignment: center; GenreButton { clicked => { GenreFilterState.open = !GenreFilterState.open; } } } 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: DiscoverBrowseState.view-mode == "grid"; clicked => { DiscoverBrowseState.view-mode = "grid"; } } ToggleButton { icon: @image-url("../assets/icons/list.svg"); active: DiscoverBrowseState.view-mode == "list"; clicked => { DiscoverBrowseState.view-mode = "list"; } } } } } } } // --- Scrolling list (only the listing scrolls) ------------------ flick := Flickable { vertical-stretch: 1; viewport-width: self.width; viewport-height: page.preferred-height; // Infinite scroll — when the viewport nears the bottom of the // content (within 600px), pull the next page. Guarded by // has-more + the loading flags AND an empty search query, so a // client-side search filter does not keep paging in the catalog. changed viewport-y => { if (-self.viewport-y + self.height >= self.viewport-height - 600px && DiscoverBrowseState.has-more && !DiscoverBrowseState.loading-more && !DiscoverBrowseState.loading && DiscoverBrowseState.search-query == "") { DiscoverBrowseActions.load-more(); } } page := VerticalLayout { width: parent.width; padding-left: 32px; padding-right: 32px; padding-top: 8px; padding-bottom: 100px; spacing: 0px; alignment: start; if DiscoverBrowseState.loading: HorizontalLayout { alignment: center; LoadingSpinner { size: 36px; } } // Empty-state when a search filters everything out. if !DiscoverBrowseState.loading && DiscoverBrowseState.visible.length == 0 && DiscoverBrowseState.search-query != "": HorizontalLayout { alignment: center; Text { text: @tr("No results found."); color: Theme.text-muted; font-size: 14px; } } // --- Album collection (grid / list) -------------------- if !DiscoverBrowseState.loading: AlbumCollectionView { albums: DiscoverBrowseState.visible; view-mode: DiscoverBrowseState.view-mode; is-grouped: false; show-source: false; card-width: root.album-w; card-height: root.album-h; card-gap: root.gap; 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); } } // Bottom load-more spinner while the next page is in flight. if DiscoverBrowseState.loading-more: HorizontalLayout { alignment: center; Rectangle { height: 24px; } LoadingSpinner { size: 28px; } } } } } // Shared scrollbar in the right gutter (homologated cross-view). Sits // below the fixed 56px header so it tracks only the scrolling list. 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; } // --- Genre filter overlay -------------------------------------------- // Backdrop closes the popup on outside click; the panel anchors to // the top-right under the Filter-by-genre button (mirrors HomeView). if GenreFilterState.open: Rectangle { width: 100%; height: 100%; TouchArea { clicked => { GenreFilterState.open = false; } } } if GenreFilterState.open: GenreFilterPopup { x: root.width - self.width - 32px; y: 56px; } }