// Qobuz mix detail view (DailyQ / WeeklyQ / FavQ / TopQ). Ported to // the AlbumView level: a header (gradient artwork + "Qobuz Mixes" // label + title + description + "N tracks • duration" info line + // circular action buttons) over a track list with a column header // and rich rows (artwork + title/artist + album + duration + quality // + favorite + more). import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { MixState, DragState, NavState } from "../state.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; import { TrackRow } from "../primitives/TrackRow.slint"; import { LoadingSpinner } from "../primitives/LoadingSpinner.slint"; import { ListScrollbar } from "../primitives/ListScrollbar.slint"; import { NavButtons } from "../shell/NavButtons.slint"; // Circular header action button (Play / Shuffle / Save / Refresh). component CircleAction inherits Rectangle { in property icon; in property primary: false; in property diameter: 38px; in property enabled: true; callback clicked(); width: root.diameter; height: root.diameter; border-radius: root.diameter / 2; opacity: root.enabled ? 1.0 : 0.4; background: root.primary ? (ta.has-hover ? #ffffffd6 : Theme.accent) : (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated); QbzIcon { source: root.icon; width: root.primary ? 18px : 16px; height: root.primary ? 18px : 16px; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; tint: root.primary ? #ffffff : Theme.text-secondary; } ta := TouchArea { mouse-cursor: root.enabled ? pointer : default; clicked => { if (root.enabled) { root.clicked(); } } } } export component MixView inherits Rectangle { callback play-track(string); callback play-all(); callback shuffle(); callback refresh(); callback media-action(string, string, string); // Per-mix header gradient, matching the QobuzMixesRow tiles. property mix-gradient: MixState.kind == "daily" ? @linear-gradient(135deg, #1e3a8a 0%, #6366f1 50%, #c084fc 100%) : MixState.kind == "weekly" ? @linear-gradient(135deg, #065f46 0%, #10b981 50%, #fbbf24 100%) : MixState.kind == "fav" ? @linear-gradient(135deg, #7f1d1d 0%, #ef4444 50%, #fb923c 100%) : @linear-gradient(135deg, #1f2937 0%, #4b5563 50%, #fbbf24 100%); background: Theme.surface-main; flick := Flickable { interactive: !DragState.active; viewport-width: self.width; viewport-height: page.preferred-height; // Scroll-position restore on back/forward (see nav::scope_for). property sr-armed: NavState.restore-scope; function sr-restore() { if (NavState.restore-scope == "mix" && self.viewport-height > self.height) { self.viewport-y = NavState.scroll-restore; NavState.restore-scope = ""; } } init => { self.sr-restore(); } changed sr-armed => { self.sr-restore(); } changed viewport-height => { self.sr-restore(); } changed viewport-y => { NavState.report-scroll(self.viewport-y); } page := VerticalLayout { width: parent.width; padding-left: 32px; padding-right: 32px; padding-top: 11px; padding-bottom: 100px; spacing: 0px; alignment: start; HorizontalLayout { alignment: start; NavButtons { } } Rectangle { height: 22px; } // --- Header ------------------------------------------------ HorizontalLayout { spacing: 32px; alignment: start; // Gradient artwork. Rectangle { width: 224px; height: 224px; border-radius: Radius.md; background: root.mix-gradient; Text { text: MixState.title; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; color: #ffffff; font-size: 34px; font-weight: Typography.bold; } } // Metadata. VerticalLayout { alignment: start; padding-top: 4px; horizontal-stretch: 1; Text { text: @tr("Qobuz Mixes"); color: Theme.text-muted; font-size: 11px; font-weight: Typography.semibold; letter-spacing: 1.5px; } Rectangle { height: 6px; } Text { text: MixState.title; color: Theme.text-primary; font-size: Typography.section; font-weight: Typography.bold; } Rectangle { height: 8px; } Text { width: root.width - 320px; text: MixState.subtitle; color: Theme.text-secondary; font-size: Typography.body; wrap: word-wrap; } Rectangle { height: 10px; } if MixState.track-count > 0: Text { text: MixState.track-count + " " + @tr("tracks") + " • " + MixState.total-duration; color: Theme.text-secondary; font-size: Typography.legal; } Rectangle { height: 20px; } // Action buttons. HorizontalLayout { alignment: start; spacing: 12px; CircleAction { icon: @image-url("../assets/icons/play-fill.svg"); primary: true; diameter: 44px; enabled: MixState.tracks.length > 0; clicked => { root.play-all(); } } VerticalLayout { alignment: center; CircleAction { icon: @image-url("../assets/icons/shuffle.svg"); enabled: MixState.tracks.length > 0; clicked => { root.shuffle(); } } } VerticalLayout { alignment: center; CircleAction { icon: @image-url("../assets/icons/list-plus.svg"); enabled: MixState.tracks.length > 0; } } VerticalLayout { alignment: center; CircleAction { icon: @image-url("../assets/icons/home-gear.svg"); clicked => { root.refresh(); } } } } } } Rectangle { height: 28px; } if MixState.loading: HorizontalLayout { alignment: center; LoadingSpinner { size: 36px; } } if !MixState.loading && MixState.tracks.length == 0: Text { text: @tr("No tracks for this mix."); color: Theme.text-muted; font-size: Typography.body; } // --- Track list column header ------------------------------ if MixState.tracks.length > 0: HorizontalLayout { padding-left: 12px; padding-right: 12px; spacing: 16px; Text { text: "#"; width: 40px; color: Theme.text-muted; font-size: Typography.legal; horizontal-alignment: center; } Text { text: @tr("Title"); color: Theme.text-muted; font-size: Typography.legal; horizontal-stretch: 1; } Text { text: @tr("Album"); width: 220px; color: Theme.text-muted; font-size: Typography.legal; } Text { text: @tr("Duration"); width: 64px; color: Theme.text-muted; font-size: Typography.legal; horizontal-alignment: center; } Text { text: @tr("Quality"); width: 92px; color: Theme.text-muted; font-size: Typography.legal; horizontal-alignment: center; } Rectangle { width: 28px; } Rectangle { width: 32px; } } if MixState.tracks.length > 0: Rectangle { height: 3px; } if MixState.tracks.length > 0: Rectangle { height: 1px; background: Theme.border-subtle; } Rectangle { height: 6px; } // Reuses the shared TrackRow (album column + favorite // + track context menu). A row "play" starts the mix from // that track; menu actions bubble up as media-action. for track[i] in MixState.tracks: TrackRow { track: track; index: i; show-artwork: true; show-album: true; show-favorite: true; media-action(kind, id, action) => { if (action == "play") { root.play-track(id); } else { root.media-action(kind, id, action); } } } } } // Shared scrollbar in the right gutter (homologated cross-view). ListScrollbar { x: parent.width - self.width - 4px; width: 14px; height: 100%; viewport-height: flick.viewport-height; visible-height: flick.height; viewport-y <=> flick.viewport-y; } }