// Pinned carousel — title, prev/next page controls, and a clipped sliding // track of MIXED cards (albums / artists / playlists), dispatched per-item // on `PinnedItem.kind`. Same simple paging mechanism as PlaylistCarousel; // one fixed 200x246 slot per item (the ArtistCarousel slot pattern) so the // three card types share one row. Used by the Home / For You "pinned" // section arms, fed from the shared PinnedState global. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { PinnedItem, ShellState, AppearanceState } from "../state.slint"; import { AlbumCard } from "AlbumCard.slint"; import { ArtistGridCard } from "ArtistGridCard.slint"; import { PlaylistCard } from "PlaylistCard.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 : (ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : 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(); } } } export component PinnedCarousel inherits VerticalLayout { in property title; in property <[PinnedItem]> items; callback album-clicked(string); callback artist-clicked(string); // Playlist open/play rides media-action ("playlist", id, "open"/"play") // — PlaylistCard has no clicked callback (body-opens: true, Home // convention). callback media-action(string, string, string); property card-width: 200px; property card-height: 246px; property gap: 32px; property count: root.items.length; property per-page: Math.max( 1, Math.floor((viewport.width + root.gap) / (root.card-width + root.gap))); property page-count: Math.max(1, Math.ceil(root.count / root.per-page)); property page: 0; property current-page: Math.min(Math.max(0, root.page), root.page-count - 1); 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; spacing: 12px; HorizontalLayout { spacing: 8px; Text { text: root.title; color: Theme.text-primary; font-size: 18px; font-weight: Typography.semibold; vertical-alignment: center; horizontal-stretch: 1; } HorizontalLayout { spacing: 4px; VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-left.svg"); enabled: root.current-page > 0; clicked => { root.page = root.current-page - 1; } } } VerticalLayout { alignment: center; NavButton { icon: @image-url("../assets/icons/chevron-right.svg"); enabled: root.current-page < root.page-count - 1; clicked => { root.page = root.current-page + 1; } } } } } viewport := Rectangle { height: root.card-height; clip: true; Rectangle { x: -root.current-page * root.step; y: 0px; width: root.content-width; height: root.card-height; animate x { duration: 220ms; easing: ease-in-out; } HorizontalLayout { spacing: root.gap; // One fixed-width slot per item; the slot hosts exactly one // card, selected by the item's kind. for item in root.items: Rectangle { width: root.card-width; height: root.card-height; if item.kind == "album": AlbumCard { album: item.album; clicked(id) => { root.album-clicked(id); } media-action(k, i, a) => { root.media-action(k, i, a); } } if item.kind == "artist": ArtistGridCard { artist: item.artist; clicked(id) => { root.artist-clicked(id); } media-action(k, i, a) => { root.media-action(k, i, a); } } if item.kind == "playlist": PlaylistCard { playlist: item.playlist; body-opens: true; media-action(k, i, a) => { root.media-action(k, i, a); } } } } } Rectangle { x: 0px; width: 56px; background: @linear-gradient(90deg, Theme.surface-main 0%, transparent 100%); opacity: root.current-page > 0 ? 1.0 : 0.0; visible: !ShellState.app-background-active; animate opacity { duration: 150ms; } } Rectangle { x: parent.width - self.width; width: 56px; background: @linear-gradient(90deg, transparent 0%, Theme.surface-main 100%); opacity: root.current-page < root.page-count - 1 ? 1.0 : 0.0; visible: !ShellState.app-background-active; animate opacity { duration: 150ms; } } } }