// Discover — Home view: a fixed 56px toolbar (tab pill + genre filter + // settings gear) over a scrollable column of section carousels. // // Section data is supplied by the Rust home controller through the // HomeState global. The Editor's Picks / For You tabs, the genre filter, // and the settings gear are visual for now; wiring them is a later // increment. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { HomeState, HomeActions, GenreFilterState, NavState, ContentView } from "../state.slint"; import { Carousel } from "Carousel.slint"; import { SlimCarousel } from "SlimCarousel.slint"; import { HomeSkeleton } from "HomeSkeleton.slint"; import { ForYouView } from "ForYouView.slint"; import { GenreFilterPopup } from "GenreFilterPopup.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; import { NavButtons } from "../shell/NavButtons.slint"; // Header + hint shown for a recently-played module while it has no data // yet (the play history fills once the playback session records plays). component RecentPlaceholder inherits VerticalLayout { in property title; in property hint; spacing: 10px; Text { text: root.title; color: Theme.text-primary; font-size: 18px; font-weight: Typography.semibold; } Text { text: root.hint; color: Theme.text-muted; font-size: 13px; } } component Tab inherits Rectangle { in property label; in property active: false; callback clicked(); border-radius: 4px; background: root.active ? Theme.surface-main : ta.has-hover ? Theme.surface-hover : transparent; HorizontalLayout { padding-left: 14px; padding-right: 14px; padding-top: 6px; padding-bottom: 6px; Text { text: root.label; color: root.active ? Theme.text-primary : Theme.text-muted; font-size: 13px; font-weight: Typography.medium; } } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } } } component GenreButton inherits Rectangle { callback clicked(); // Accent-filled + "N genres" label when a filter is active, // matching Tauri. property active: GenreFilterState.selected-count > 0; // Hug the content — the button is only as wide as its label. width: genre-row.preferred-width; height: 32px; 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(); } } } component GearButton inherits Rectangle { callback clicked(); width: 32px; height: 32px; border-radius: 4px; background: ta.has-hover ? Theme.surface-hover : transparent; QbzIcon { source: @image-url("../assets/icons/home-gear.svg"); width: 20px; height: 20px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: ta.has-hover ? Theme.text-primary : Theme.text-muted; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } } } export component HomeView inherits Rectangle { callback album-clicked(string); callback open-artist(string); callback media-action(string, string, string); background: Theme.surface-main; VerticalLayout { spacing: 0px; // --- Toolbar (fixed 56px) ---------------------------------------- // Side controls are anchored to the toolbar edges; the three-tab // pill is absolutely centered on the toolbar so it stays at the // true horizontal center regardless of the side controls' widths. Rectangle { height: 56px; background: Theme.surface-main; // History pair + the three-tab nav bar, grouped at the left // with a gap delimiting the two clusters. Explicit width // (= content) so the layout does not fill the toolbar and // overlap the right controls. left-controls := HorizontalLayout { x: 32px; // Align the toolbar content's vertical center to y=25 — // 11px (the back/forward padding-top used by the album / // artist / settings views) + 14px (half the 28px nav // buttons). Keeps the [<] [>] pair at the same screen // position across every view, with no jump on navigation. y: 25px - self.height / 2; width: self.preferred-width; // Explicit height too — without it the layout fills the // 56px toolbar, so `y` resolves to 0 and the tab pill // sticks to the top instead of centering. height: self.preferred-height; spacing: 16px; VerticalLayout { alignment: center; NavButtons { } } tab-pill := Rectangle { width: tab-row.preferred-width; height: tab-row.preferred-height; background: Theme.surface-elevated; border-radius: 6px; tab-row := HorizontalLayout { padding: 3px; spacing: 4px; Tab { label: @tr("Home"); active: HomeState.active-tab == "home"; clicked => { HomeActions.select-tab("home"); } } Tab { label: @tr("Editor's Picks"); active: HomeState.active-tab == "editorPicks"; clicked => { HomeActions.select-tab("editorPicks"); } } Tab { label: @tr("For You"); active: HomeState.active-tab == "forYou"; clicked => { HomeActions.select-tab("forYou"); } } } } } // Genre filter + sort gear, anchored right. Explicit width // (= content) so `x` resolves correctly — without it the // layout fills the toolbar and the cluster lands far left, // on top of the history arrows. right-controls := HorizontalLayout { x: parent.width - self.width - 32px; // Same y baseline as the left controls (see above). y: 25px - self.height / 2; width: self.preferred-width; height: self.preferred-height; spacing: 6px; GenreButton { clicked => { GenreFilterState.open = !GenreFilterState.open; } } // Per-tab sort / show-hide control for the Discover // sections (not global Settings — that lives in the // header menu). Its menu is a later increment. GearButton { } } } Rectangle { height: 1px; background: Theme.border-subtle; } // --- Scrollable sections ----------------------------------------- // A Flickable (not a ScrollView): wheel scrolling with no scrollbar // gutters at all. viewport-width is pinned to the Flickable width so // the carousels' clipped content can never scroll horizontally. Flickable { vertical-stretch: 1; viewport-width: self.width; viewport-height: home-content.preferred-height; home-content := VerticalLayout { padding: 32px; spacing: 40px; alignment: start; if HomeState.active-tab != "forYou" && HomeState.loading && HomeState.sections.length == 0: HomeSkeleton { } for section in HomeState.sections: Carousel { section: section; album-clicked(id) => { root.album-clicked(id); } media-action(kind, id, action) => { root.media-action(kind, id, action); } } // For You renders its own ordered, heterogeneous // sections from ForYouState. if HomeState.active-tab == "forYou": ForYouView { open-album(id) => { root.album-clicked(id); } open-artist(id) => { root.open-artist(id); } media-action(kind, id, action) => { root.media-action(kind, id, action); } play-track(id) => { root.media-action("track", id, "play"); } play-top-tracks(id) => { root.media-action("artist", id, "play-top"); } start-radio(id) => { root.media-action("artist", id, "radio"); } open-mix(which) => { root.media-action("mix", which, "open"); } } // The Recently Played + Popular sections below belong // to the Home tab only; Editor's Picks / For You show // just their own cached section set above. // Recently Played Albums — always present; shows a // placeholder until the play history has data. if HomeState.active-tab == "home" && HomeState.recent-albums.length > 0: Carousel { section: { title: @tr("Recently Played Albums"), albums: HomeState.recent-albums, }; album-clicked(id) => { root.album-clicked(id); } media-action(kind, id, action) => { root.media-action(kind, id, action); } } if HomeState.active-tab == "home" && HomeState.recent-albums.length == 0: RecentPlaceholder { title: @tr("Recently Played Albums"); hint: @tr("Albums you play will appear here."); } // Recently Played Tracks — paginated 4x3 carousel // (12 per page, up to two pages). if HomeState.active-tab == "home" && HomeState.recent.length > 0: SlimCarousel { title: @tr("Recently Played Tracks"); items: HomeState.recent; item-clicked(id) => { root.media-action("track", id, "play"); } } if HomeState.active-tab == "home" && HomeState.recent.length == 0: RecentPlaceholder { title: @tr("Recently Played Tracks"); hint: @tr("Tracks you play will appear here."); } if HomeState.active-tab == "home" && HomeState.popular.length > 0: SlimCarousel { title: @tr("Popular albums"); items: HomeState.popular; item-clicked(id) => { root.album-clicked(id); } } } } } // --- Genre filter overlay -------------------------------------------- // Backdrop closes the popup on outside click; the panel anchors to // the top-right under the Filter-by-genre button. if GenreFilterState.open: Rectangle { width: 100%; height: 100%; TouchArea { clicked => { GenreFilterState.open = false; } } } if GenreFilterState.open: GenreFilterPopup { x: root.width - self.width - 32px; y: 52px; } }