// Discover section carousel — a title, prev/next page controls, and a // clipped, sliding track of album cards. // // Pagination by slice (no scrollbar), mirroring the Svelte // DiscoverySection. Because there is no ScrollView the height is exactly // the card height — nothing injects extra vertical space. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { DiscoverSection } from "../state.slint"; import { AlbumCard } from "AlbumCard.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; component NavButton inherits Rectangle { in property icon; in property enabled: true; callback clicked(); width: 28px; height: 28px; border-radius: 14px; opacity: root.enabled ? 1.0 : 0.4; background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated; QbzIcon { source: root.icon; width: 15px; height: 15px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: root.enabled ? Theme.text-primary : Theme.text-muted; } ta := TouchArea { enabled: root.enabled; mouse-cursor: root.enabled ? pointer : default; clicked => { root.clicked(); } } } component ViewAllLink inherits Rectangle { callback clicked(); width: link-text.preferred-width + 16px; height: 26px; border-radius: 4px; background: ta.has-hover ? Theme.surface-hover : transparent; link-text := Text { // Muted, close to normal text (not the theme accent), with the // trailing arrow — matches Tauri's "See all →". text: @tr("View all →"); color: ta.has-hover ? Theme.text-primary : Theme.text-secondary; font-size: 14px; font-weight: Typography.medium; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } } } export component Carousel inherits VerticalLayout { in property section; in property show-view-all: false; callback album-clicked(string); callback media-action(string, string, string); callback view-all-clicked(); property card-width: 200px; property card-height: 246px; property gap: 32px; property count: root.section.albums.length; property per-page: Math.max( 1, Math.floor((flick.width + root.gap) / (root.card-width + root.gap))); property step: root.per-page * (root.card-width + root.gap); property content-width: root.count * root.card-width + Math.max(0, root.count - 1) * root.gap; // Max leftward scroll (viewport-x is 0 at start, negative when // scrolled right). Zero when everything fits. property max-scroll: Math.max(0px, root.content-width - flick.width); spacing: 12px; // --- Header: title + page controls ----------------------------------- HorizontalLayout { Text { text: root.section.title; color: Theme.text-primary; font-size: Typography.section; font-weight: Typography.semibold; vertical-alignment: center; horizontal-stretch: 1; } HorizontalLayout { spacing: 4px; // "View all" sits to the LEFT of the carousel page chevrons, // matching the Tauri DiscoverySection header. if root.show-view-all: VerticalLayout { alignment: center; ViewAllLink { clicked => { root.view-all-clicked(); } } } VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-left.svg"); enabled: flick.viewport-x < -1px; clicked => { flick.viewport-x = Math.min(0px, flick.viewport-x + root.step); } } } VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-right.svg"); enabled: flick.viewport-x > -root.max-scroll + 1px; clicked => { flick.viewport-x = Math.max(-root.max-scroll, flick.viewport-x - root.step); } } } } } // --- Track: a horizontal Flickable so the row drags by touch / // pointer (kinetic), while card taps still register — Flickable // distinguishes a flick from a tap, unlike nested TouchAreas. The // prev/next buttons nudge viewport-x by a page. viewport-height == // height pins it to horizontal-only so it never fights the home // page's vertical Flickable. viewport := Rectangle { height: root.card-height; clip: true; flick := Flickable { width: 100%; height: root.card-height; viewport-width: root.content-width; viewport-height: self.height; HorizontalLayout { spacing: root.gap; for album in root.section.albums: AlbumCard { album: album; clicked(id) => { root.album-clicked(id); } media-action(kind, id, action) => { root.media-action(kind, id, action); } } } } // Cider-style edge fades — content dissolves into the page // background at the scrolled edges instead of a hard cut. Rectangle { x: 0px; width: 56px; background: @linear-gradient(90deg, Theme.surface-main 0%, transparent 100%); opacity: flick.viewport-x < -1px ? 1.0 : 0.0; animate opacity { duration: 150ms; } } Rectangle { x: parent.width - self.width; width: 56px; background: @linear-gradient(90deg, transparent 0%, Theme.surface-main 100%); opacity: flick.viewport-x > -root.max-scroll + 1px ? 1.0 : 0.0; animate opacity { duration: 150ms; } } } }