// Reusable album list-row — the "list view" counterpart to the grid // AlbumCard. Modeled on Tauri's My QBZ > Collections list (and the // DiscographyBuilderView column grid): a thumbnail + title/artist ITEM // column, then TYPE, SOURCE, QUALITY, TRACKS and YEAR columns plus a // trailing overflow ("…") button. // // Intentionally column-for-column with the Tauri reference, not a // reinterpretation. Reused by Discover "View all", Favorites, // LocalLibrary and My QBZ — every column except ITEM is hideable via a // bool prop so a context can show only the columns it cares about. In // the DiscoverBrowse context the SOURCE column is hidden (single-source // catalog). // // QUALITY reuses the shared QualityBadgeFull — the SAME badge AlbumView // shows below the album art — so quality reads identically everywhere. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { AlbumCardItem } from "../state.slint"; import { QbzIcon } from "QbzIcon.slint"; import { QualityBadgeFull } from "QualityBadgeFull.slint"; import { ContextMenu } from "ContextMenu.slint"; import { ContextMenuItem } from "ContextMenuItem.slint"; // Fixed column widths shared by the header and the rows so they align. // (rank · art · type · source · quality · tracks · year · overflow; // the ITEM title/artist stack takes the remaining stretch space.) export global AlbumListCols { out property rank: 36px; out property art: 52px; out property type: 84px; out property source: 72px; out property quality: 150px; out property tracks: 64px; out property year: 64px; out property overflow: 36px; out property gap: 12px; } // Column header row — labels the columns. Mirrors the // DiscographyBuilderView / MixtapeCollectionDetailView header convention. export component AlbumListHeader inherits Rectangle { in property show-rank: false; in property show-source: true; height: 32px; HorizontalLayout { padding-left: 12px; padding-right: 12px; spacing: AlbumListCols.gap; if root.show-rank: cell-rank := Text { width: AlbumListCols.rank; text: "#"; color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; vertical-alignment: center; } // Art column placeholder (no label). Rectangle { width: AlbumListCols.art; } // ITEM stretch column. Text { horizontal-stretch: 1; text: @tr("ITEM"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 0.5px; vertical-alignment: center; overflow: elide; } Text { width: AlbumListCols.type; text: @tr("TYPE"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 0.5px; vertical-alignment: center; } if root.show-source: Text { width: AlbumListCols.source; text: @tr("SOURCE"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 0.5px; vertical-alignment: center; } Text { width: AlbumListCols.quality; text: @tr("QUALITY"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 0.5px; vertical-alignment: center; } Text { width: AlbumListCols.tracks; text: @tr("TRACKS"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 0.5px; horizontal-alignment: center; vertical-alignment: center; } Text { width: AlbumListCols.year; text: @tr("YEAR"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 0.5px; horizontal-alignment: center; vertical-alignment: center; } // Overflow column placeholder (no label). Rectangle { width: AlbumListCols.overflow; } } // Bottom hairline separator. Rectangle { y: parent.height - self.height; height: 1px; background: Theme.border-subtle; } } export component AlbumListRow inherits Rectangle { in property album; // Fade out when the row is being removed (favorites un-favorite). opacity: root.album.removing ? 0.0 : 1.0; animate opacity { duration: 250ms; } // Row index — drives the even/odd zebra striping (coherent with TrackRow). in property index; // Optional leading rank ("#1" etc) for ranked lists (Most Streamed). in property show-rank: false; in property rank; // The SOURCE column is hidden in single-source contexts (Discover). in property show-source: true; callback clicked(string); // kind ("album" | "artist"), id, action ("play" | "queue" | // "play-next" | "favorite" | "open"). callback media-action(string, string, string); // Hover spans the row body + the art / artist / overflow affordances so // the row highlights as one (mirrors TrackRow's row-hovered). property row-hovered: ta.has-hover || art-ta.has-hover || artist-ta.has-hover || more-ta.has-hover; height: 64px; border-radius: Radius.sm; // Even/odd zebra (odd rows faintly tinted), coherent with TrackRow. background: root.row-hovered ? Theme.surface-hover : (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent); ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(root.album.id); } } HorizontalLayout { padding-left: 12px; padding-right: 12px; spacing: AlbumListCols.gap; // --- # --------------------------------------------------------- if root.show-rank: Text { width: AlbumListCols.rank; text: root.rank; color: Theme.text-muted; font-size: 13px; vertical-alignment: center; } // --- Artwork thumb (play on hover) ---------------------------- VerticalLayout { alignment: center; Rectangle { width: AlbumListCols.art; height: AlbumListCols.art; border-radius: 6px; background: Theme.surface-elevated; clip: true; Image { width: 100%; height: 100%; source: root.album.artwork; image-fit: cover; } // Hover scrim + centered play glyph over the cover. Rectangle { width: 100%; height: 100%; background: #000000; opacity: root.row-hovered ? 0.45 : 0.0; animate opacity { duration: 120ms; } } if root.row-hovered: QbzIcon { source: @image-url("../assets/icons/play-fill.svg"); width: 22px; height: 22px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: #ffffff; } art-ta := TouchArea { mouse-cursor: pointer; clicked => { root.media-action("album", root.album.id, "play"); } } } } // --- ITEM: title + artist ------------------------------------- VerticalLayout { horizontal-stretch: 1; alignment: center; spacing: 2px; Text { text: root.album.title; color: Theme.text-primary; font-size: Typography.body; font-weight: Typography.medium; overflow: elide; } // Clickable artist name → artist page when an id is known. artist-ta := TouchArea { height: 18px; mouse-cursor: root.album.artist-id != "" ? pointer : default; clicked => { if (root.album.artist-id != "") { root.media-action("artist", root.album.artist-id, "open"); } } Text { width: 100%; text: root.album.artist; color: root.album.artist-id != "" && artist-ta.has-hover ? Theme.text-primary : Theme.text-muted; font-size: Typography.link; vertical-alignment: center; overflow: elide; } } } // --- TYPE ------------------------------------------------------ Text { width: AlbumListCols.type; text: root.album.release-type; color: Theme.text-secondary; font-size: 12px; vertical-alignment: center; overflow: elide; } // --- SOURCE (hideable) — discreet per-source glyph, NOT text. --- // Mirrors TrackRow's source indicator: local -> tinted hard-drive, // qobuz/offline + plex -> their own mark; small + flat (the bold // colored badge is reserved for the AlbumCard grid). if root.show-source: Rectangle { width: AlbumListCols.source; if root.album.source != "": Image { source: root.album.source == "plex" ? @image-url("../assets/icons/plex-logo.svg") : (root.album.source == "qobuz" || root.album.source == "qobuz_download" ? @image-url("../assets/icons/qobuz-logo-filled.svg") : @image-url("../assets/icons/hard-drive.svg")); colorize: root.album.source == "local" ? Theme.text-muted : transparent; width: root.album.source == "local" ? 15px : 16px; height: self.width; x: 0; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; image-fit: contain; } } // --- QUALITY (shared QualityBadgeFull) ------------------------ Rectangle { width: AlbumListCols.quality; VerticalLayout { alignment: center; QualityBadgeFull { tier: root.album.quality-tier; detail: root.album.quality-detail; } } } // --- TRACKS ---------------------------------------------------- Text { width: AlbumListCols.tracks; text: root.album.track-count; color: Theme.text-secondary; font-size: 13px; horizontal-alignment: center; vertical-alignment: center; } // --- YEAR ------------------------------------------------------ Text { width: AlbumListCols.year; text: root.album.plain-year; color: Theme.text-secondary; font-size: 13px; horizontal-alignment: center; vertical-alignment: center; } // --- Overflow "…" --------------------------------------------- VerticalLayout { alignment: center; Rectangle { width: AlbumListCols.overflow; height: 32px; border-radius: 6px; background: more-ta.has-hover ? Theme.surface-elevated : transparent; QbzIcon { source: @image-url("../assets/icons/ellipsis.svg"); width: 18px; height: 18px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: more-ta.has-hover ? Theme.text-primary : Theme.text-muted; } more-ta := TouchArea { mouse-cursor: pointer; clicked => { row-menu.show(); } } row-menu := PopupWindow { x: -170px; y: 34px; width: 196px; height: 142px; ContextMenu { ContextMenuItem { icon: @image-url("../assets/icons/library-big.svg"); label: @tr("Open album"); clicked => { root.clicked(root.album.id); } } ContextMenuItem { icon: @image-url("../assets/icons/play.svg"); label: @tr("Play"); clicked => { root.media-action("album", root.album.id, "play"); } } ContextMenuItem { icon: @image-url("../assets/icons/list-plus.svg"); label: @tr("Play next"); clicked => { root.media-action("album", root.album.id, "play-next"); } } ContextMenuItem { icon: @image-url("../assets/icons/list-end.svg"); label: @tr("Add to queue"); clicked => { root.media-action("album", root.album.id, "queue"); } } ContextMenuItem { icon: root.album.is-favorite ? @image-url("../assets/icons/heart-filled.svg") : @image-url("../assets/icons/heart.svg"); label: root.album.is-favorite ? @tr("Remove from favorites") : @tr("Add to favorites"); clicked => { root.media-action("album", root.album.id, "favorite"); } } } } } } } }