// Playlist carousel — title, "View all" link, prev/next page controls, // and a clipped sliding track of playlist cards. Same paging mechanism as // Carousel. Used by the search results All tab. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { SearchPlaylistItem, ShellState, AppearanceState } from "../state.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(); } } } 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 with the trailing arrow — 1:1 with the standard Carousel's // "View all →" (not the theme accent) so every Home row's link matches. 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 PlaylistCarousel inherits VerticalLayout { in property title; in property <[SearchPlaylistItem]> playlists; in property show-view-all: false; // When true, a card body click opens the playlist and the overlay button // plays it (1:1 with Tauri Discover). Default false (body=play). in property body-opens: false; callback view-all-clicked(); callback media-action(string, string, string); property card-width: 200px; property card-height: 246px; property gap: 32px; property count: root.playlists.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; } // "View all →" sits to the LEFT of the page chevrons, 4px control // grouping — identical to the standard Carousel header. HorizontalLayout { spacing: 4px; 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: 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; for playlist in root.playlists: PlaylistCard { playlist: playlist; body-opens: root.body-opens; 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; } } } }