// Most-popular hero card for a TRACK top result. // // Mirrors the Tauri SearchView `.popular-card` track case: a vertical // card with a square artwork on top (hover scrim + favorite / play / // more overlay buttons), then a tight title / artist stack alongside an // icon-only quality badge. Sized to match the search-dashboard ArtistCard // (160x220) so the Most-popular slot sits proportionate next to the // Artists carousel cards. Tauri's `.most-popular-card` is 160x220 with a // 142x142 artwork (`.popular-card-artwork`), padding 6px — replicated here. // // Reusable: takes a TrackItem and bubbles every interaction up through one // media-action(kind, id, action) — "play" | "favorite" | the context-menu // actions — keyed by "track". import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { Spacing } from "../foundation/spacing.slint"; import { TrackItem } from "../state.slint"; import { QbzIcon } from "QbzIcon.slint"; import { TrackContextMenu } from "TrackContextMenu.slint"; // Circular hover-overlay action button — identical visual to AlbumCard's // OverlayButton so the hero cards share one look. component OverlayButton inherits Rectangle { in property icon; in property primary: false; in property diameter: 36px; in property icon-size: 16px; out property hovered: ta.has-hover; callback clicked(); width: root.diameter; height: root.diameter; border-radius: root.diameter / 2; background: root.primary ? (ta.has-hover ? #ffffffd6 : #ffffff) : (ta.has-hover ? #ffffff3d : #ffffff24); border-width: root.primary ? 0px : 1.5px; border-color: #ffffffcc; QbzIcon { source: root.icon; width: root.icon-size; height: root.icon-size; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: root.primary ? #000000 : #ffffff; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } } } export component SearchTrackHero inherits Rectangle { in property track; // Detailed quality string, e.g. "Hi-Res 24-bit / 192 kHz". Rendered as // plain text below the artist (the icon badge was dropped — it clipped // against the card edge). "" hides the line. in property quality-label; // kind ("track"), track id, action ("play" | "favorite" | menu actions). callback media-action(string, string, string); // Match the search-dashboard ArtistCard footprint (160x220) so the // Most-popular hero is proportionate next to the artist cards. Tauri's // `.popular-card`: 160 wide, 6px padding, 142x142 artwork. width: 160px; height: 220px; border-radius: 12px; background: hover.has-hover || root.overlay-on ? Theme.surface-elevated : Theme.surface-card; animate background { duration: 150ms; } clip: true; // Overlay stays up while the pointer is over the artwork OR a button. property overlay-on: hover.has-hover || fav-btn.hovered || play-btn.hovered || more-btn.hovered; VerticalLayout { padding: 6px; alignment: start; spacing: 0px; // --- Artwork + hover overlay ------------------------------------- // Shrunk to 128px (from 142px) so the title / artist / quality-text // stack fits cleanly below it without growing the 160x220 card. // Centered horizontally within the inner width. HorizontalLayout { alignment: center; Rectangle { width: 128px; height: 128px; border-radius: Radius.sm; background: Theme.surface-elevated; clip: true; // Cover art (track's album cover). Empty until the artwork // pipeline resolves it, leaving the placeholder visible. Image { width: 100%; height: 100%; source: root.track.artwork; image-fit: cover; } // Placeholder music glyph behind the cover. if root.track.artwork.width == 0: QbzIcon { source: @image-url("../assets/icons/music.svg"); width: 30px; height: 30px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: Theme.text-muted; } // Hover scrim. Rectangle { width: 100%; height: 100%; background: #000000; opacity: root.overlay-on ? 0.6 : 0.0; animate opacity { duration: 150ms; } } // Card-open (play) + hover detector. Declared before the action // buttons so those, when interactive, win the pointer. hover := TouchArea { width: 100%; height: 100%; mouse-cursor: pointer; clicked => { root.media-action("track", root.track.id, "play"); } } // Hover action buttons — favorite / play / more. Centered in // the 128px artwork. HorizontalLayout { x: 0px; y: (128px - self.height) / 2; width: 128px; height: 38px; alignment: center; spacing: 8px; opacity: root.overlay-on ? 1.0 : 0.0; animate opacity { duration: 150ms; } VerticalLayout { alignment: center; fav-btn := OverlayButton { icon: @image-url("../assets/icons/heart.svg"); diameter: 30px; icon-size: 14px; clicked => { root.media-action("track", root.track.id, "favorite"); } } } play-btn := OverlayButton { icon: @image-url("../assets/icons/play-fill.svg"); primary: true; diameter: 38px; icon-size: 16px; clicked => { root.media-action("track", root.track.id, "play"); } } VerticalLayout { alignment: center; more-btn := OverlayButton { icon: @image-url("../assets/icons/ellipsis.svg"); diameter: 30px; icon-size: 14px; clicked => { track-menu.show(); } } } } // Context menu for the "more" button — the shared track menu. track-menu := TrackContextMenu { x: 20px; y: 48px; track-id: root.track.id; media-action(k, i, a) => { root.media-action(k, i, a); } } } } Rectangle { height: 8px; } // --- Title / artist / quality text ------------------------------- // The icon quality badge was dropped (it clipped against the card // edge). Quality now reads as a plain text line below the artist, // matching the album hero's quality string. Full inner width, elided. VerticalLayout { alignment: start; spacing: 1px; horizontal-stretch: 1; Text { text: root.track.title; width: 100%; horizontal-alignment: center; height: 18px; color: Theme.text-primary; font-size: 13px; font-weight: Typography.medium; vertical-alignment: center; overflow: elide; } if root.track.artist != "": Text { text: root.track.artist; width: 100%; horizontal-alignment: center; height: 16px; color: Theme.text-muted; font-size: 11px; vertical-alignment: center; overflow: elide; } if root.quality-label != "": Text { text: root.quality-label; width: 100%; horizontal-alignment: center; height: 15px; color: Theme.accent; font-size: 11px; vertical-alignment: center; overflow: elide; } } } }