AlbumCard and PlaylistCard are now 200x246 (was 220x266), with the larger texts -2px and the smaller ones -1px. All album/playlist carousels and grids follow (card-width/height 200/246) so nothing reflows: Carousel, PlaylistCarousel, AlbumCollectionView/AlbumGrid, ReleaseGrid, Spotlight, HomeSkeleton, DiscoverBrowseView, SearchResultsView, FavoritesView. Artist / radio cards are untouched. cc
348 lines
13 KiB
Plaintext
348 lines
13 KiB
Plaintext
// 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, GenreFilterActions } 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 <bool> 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 <length> album-w: 200px;
|
|
property <length> album-h: 266px;
|
|
property <length> gap: 24px;
|
|
property <length> list-row-h: 64px;
|
|
property <length> 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 => {
|
|
GenreFilterActions.set-context("discover");
|
|
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;
|
|
}
|
|
}
|