feat(slint): rebuild mix views to AlbumView-level rendering
The mix detail pages were a thin header + flat track list. Rebuilt to
mirror Tauri's playlist-detail layout at the AlbumView port quality.
- Header: a per-mix gradient artwork (224px, same gradients as the
QobuzMixesRow tiles) + "Qobuz Mixes" label + title + description +
"N tracks • duration" info line + circular action buttons (Play
all, Shuffle, Save-as-playlist, Refresh) — matching AlbumView's
header anatomy.
- Track list with a real column header (#, Title, Album, Duration,
Quality) over rich MixTrackRows: artwork thumbnail + number/play
cell (TrackPlayCell, artwork-on), title + explicit badge, artist,
album column, duration, quality, favorite + more controls.
- MixTrackItem gains an album column; MixState carries track-count +
total-duration for the info line.
- Play all / per-track play (from that track), Shuffle (plays a
random order without mutating the list), and Refresh (re-fetches)
wired through media-action.
Fixes a wiring bug: the tile click sends id=kind/action="open", but
the handler matched on the action and passed "open" as the mix kind —
now it reads the kind from id.
cc
This commit is contained in:
5 files changed
+424
-64
No files matched your search
@@ -1311,6 +1311,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The mix tile sends id = mix kind, action = "open".
|
||||
("mix", "open") => {
|
||||
nav::record(nav::NavEntry::Mix { kind: id.clone() });
|
||||
navigate_mix(
|
||||
runtime.clone(),
|
||||
weak.clone(),
|
||||
&handle,
|
||||
image_cache.clone(),
|
||||
id.clone(),
|
||||
);
|
||||
if let Some(w) = weak.upgrade() {
|
||||
update_nav_flags(&w);
|
||||
}
|
||||
}
|
||||
("mix", "play-all") => {
|
||||
let runtime = runtime.clone();
|
||||
let weak = weak.clone();
|
||||
@@ -1320,6 +1334,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
playback::play_tracks(runtime, weak, handle, tracks, 0);
|
||||
});
|
||||
}
|
||||
("mix", "shuffle") => {
|
||||
let runtime = runtime.clone();
|
||||
let weak = weak.clone();
|
||||
let handle = handle.clone();
|
||||
handle.clone().spawn(async move {
|
||||
let tracks = mix::shuffled_tracks();
|
||||
playback::play_tracks(runtime, weak, handle, tracks, 0);
|
||||
});
|
||||
}
|
||||
("mix", "refresh") => {
|
||||
// Re-load the current mix (re-fetch its tracks).
|
||||
if let Some(w) = weak.upgrade() {
|
||||
let kind = w.global::<MixState>().get_kind().to_string();
|
||||
if !kind.is_empty() {
|
||||
navigate_mix(
|
||||
runtime.clone(),
|
||||
weak.clone(),
|
||||
&handle,
|
||||
image_cache.clone(),
|
||||
kind,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
("mix-track", track_id) => {
|
||||
let idx = mix::index_of(track_id);
|
||||
let runtime = runtime.clone();
|
||||
@@ -1330,20 +1368,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
playback::play_tracks(runtime, weak, handle, tracks, idx);
|
||||
});
|
||||
}
|
||||
("mix", which) => {
|
||||
// A Qobuz Mixes tile — open its detail view.
|
||||
nav::record(nav::NavEntry::Mix { kind: which.to_string() });
|
||||
navigate_mix(
|
||||
runtime.clone(),
|
||||
weak.clone(),
|
||||
&handle,
|
||||
image_cache.clone(),
|
||||
which.to_string(),
|
||||
);
|
||||
if let Some(w) = weak.upgrade() {
|
||||
update_nav_flags(&w);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ use qbz_models::Track;
|
||||
use slint::{ComponentHandle, ModelRc, VecModel};
|
||||
|
||||
use crate::artwork::{ArtworkJob, ArtworkTarget};
|
||||
use crate::{AppWindow, MixState, SearchTrackItem};
|
||||
use crate::{AppWindow, MixState, MixTrackItem};
|
||||
|
||||
/// The currently-loaded mix track list, kept so play-all / per-track
|
||||
/// play can build the queue without re-fetching.
|
||||
@@ -124,13 +124,14 @@ fn mmss(secs: u32) -> String {
|
||||
format!("{}:{:02}", secs / 60, secs % 60)
|
||||
}
|
||||
|
||||
fn to_item(track: &Track) -> SearchTrackItem {
|
||||
fn to_item(index: usize, track: &Track) -> MixTrackItem {
|
||||
let mut title = track.title.clone();
|
||||
if let Some(v) = track.version.as_ref().filter(|v| !v.is_empty()) {
|
||||
title = format!("{title} ({v})");
|
||||
}
|
||||
SearchTrackItem {
|
||||
MixTrackItem {
|
||||
id: track.id.to_string().into(),
|
||||
number: (index + 1).to_string().into(),
|
||||
title: title.into(),
|
||||
artist: track
|
||||
.performer
|
||||
@@ -138,6 +139,12 @@ fn to_item(track: &Track) -> SearchTrackItem {
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
album: track
|
||||
.album
|
||||
.as_ref()
|
||||
.map(|a| a.title.clone())
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
duration: mmss(track.duration).into(),
|
||||
quality_tier: match track.maximum_bit_depth {
|
||||
Some(d) if d >= 24 => "hires",
|
||||
@@ -156,9 +163,26 @@ fn to_item(track: &Track) -> SearchTrackItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Human total duration: "1 h 23 min" or "23 min".
|
||||
fn total_duration(tracks: &[Track]) -> String {
|
||||
let secs: u64 = tracks.iter().map(|t| t.duration as u64).sum();
|
||||
let mins = secs / 60;
|
||||
if mins >= 60 {
|
||||
format!("{} h {} min", mins / 60, mins % 60)
|
||||
} else {
|
||||
format!("{} min", mins)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_mix(window: &AppWindow, kind: &str, tracks: Vec<Track>) {
|
||||
let (title, subtitle) = mix_meta(kind);
|
||||
let items: Vec<SearchTrackItem> = tracks.iter().map(to_item).collect();
|
||||
let items: Vec<MixTrackItem> = tracks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, t)| to_item(i, t))
|
||||
.collect();
|
||||
let count = tracks.len() as i32;
|
||||
let duration = total_duration(&tracks);
|
||||
if let Ok(mut cur) = CURRENT_MIX.lock() {
|
||||
*cur = tracks;
|
||||
}
|
||||
@@ -167,6 +191,8 @@ pub fn apply_mix(window: &AppWindow, kind: &str, tracks: Vec<Track>) {
|
||||
state.set_title(title.into());
|
||||
state.set_subtitle(subtitle.into());
|
||||
state.set_tracks(ModelRc::new(VecModel::from(items)));
|
||||
state.set_track_count(count);
|
||||
state.set_total_duration(duration.into());
|
||||
state.set_loading(false);
|
||||
}
|
||||
|
||||
@@ -176,7 +202,9 @@ pub fn reset_mix(window: &AppWindow, kind: &str) {
|
||||
state.set_kind(kind.into());
|
||||
state.set_title(title.into());
|
||||
state.set_subtitle(subtitle.into());
|
||||
state.set_tracks(ModelRc::new(VecModel::from(Vec::<SearchTrackItem>::new())));
|
||||
state.set_tracks(ModelRc::new(VecModel::from(Vec::<MixTrackItem>::new())));
|
||||
state.set_track_count(0);
|
||||
state.set_total_duration("".into());
|
||||
state.set_loading(true);
|
||||
}
|
||||
|
||||
@@ -185,6 +213,14 @@ pub fn current_tracks() -> Vec<Track> {
|
||||
CURRENT_MIX.lock().map(|c| c.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The current mix tracks in a fresh random order (for the Shuffle
|
||||
/// action) — does not mutate the displayed list.
|
||||
pub fn shuffled_tracks() -> Vec<Track> {
|
||||
let mut tracks = current_tracks();
|
||||
shuffle(&mut tracks);
|
||||
tracks
|
||||
}
|
||||
|
||||
/// Index of `track_id` within the current mix (for play-from-here).
|
||||
pub fn index_of(track_id: &str) -> usize {
|
||||
CURRENT_MIX
|
||||
|
||||
@@ -1,18 +1,220 @@
|
||||
// Qobuz mix detail view (DailyQ / WeeklyQ / FavQ / TopQ). Header with
|
||||
// the mix name + description + a Play all button, over the mix's
|
||||
// track list (reusing the shared SearchTrackRow).
|
||||
// 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 { MixState } from "../state.slint";
|
||||
import { SearchTrackRow } from "../search/SearchTrackRow.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { MixState, MixTrackItem } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
|
||||
// Circular header action button (Play / Shuffle / Save / Refresh).
|
||||
component CircleAction inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <bool> primary: false;
|
||||
in property <length> diameter: 38px;
|
||||
in property <bool> 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: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: root.primary ? #ffffff : Theme.text-secondary;
|
||||
}
|
||||
ta := TouchArea {
|
||||
mouse-cursor: root.enabled ? pointer : default;
|
||||
clicked => {
|
||||
if (root.enabled) {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component MixTrackRow inherits Rectangle {
|
||||
in property <MixTrackItem> track;
|
||||
in property <int> index;
|
||||
callback play(string);
|
||||
|
||||
property <bool> row-hovered: ta.has-hover || play-cell.hovered || fav-ta.has-hover || more-ta.has-hover;
|
||||
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
background: root.row-hovered
|
||||
? #ffffff16
|
||||
: (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: default;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
spacing: 16px;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
play-cell := TrackPlayCell {
|
||||
track-id: root.track.id;
|
||||
number: root.track.number;
|
||||
artwork: root.track.artwork;
|
||||
show-artwork: true;
|
||||
row-hovered: root.row-hovered;
|
||||
play => {
|
||||
root.play(root.track.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Title + artist.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
spacing: 2px;
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
spacing: 6px;
|
||||
Text {
|
||||
text: root.track.title;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.medium;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
if root.track.explicit: VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background: Theme.surface-elevated;
|
||||
Text {
|
||||
text: "E";
|
||||
color: Theme.text-muted;
|
||||
font-size: 9px;
|
||||
font-weight: Typography.semibold;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: root.track.artist;
|
||||
visible: root.track.artist != "";
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.link;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
// Album column.
|
||||
Text {
|
||||
text: root.track.album;
|
||||
width: 220px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
// Duration.
|
||||
Text {
|
||||
text: root.track.duration;
|
||||
width: 64px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
// Quality.
|
||||
Text {
|
||||
text: root.track.quality-tier == "hires"
|
||||
? "Hi-Res"
|
||||
: (root.track.quality-tier == "cd" ? "CD" : "");
|
||||
width: 64px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
// Favorite.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: Radius.sm;
|
||||
background: fav-ta.has-hover ? Theme.surface-elevated : transparent;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/heart.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: fav-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
||||
}
|
||||
fav-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
// More.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: Radius.sm;
|
||||
background: more-ta.has-hover ? Theme.surface-elevated : transparent;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/ellipsis.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: more-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
||||
}
|
||||
more-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component MixView inherits Rectangle {
|
||||
callback play-track(string);
|
||||
callback play-all();
|
||||
callback shuffle();
|
||||
callback refresh();
|
||||
|
||||
// Per-mix header gradient, matching the QobuzMixesRow tiles.
|
||||
property <brush> 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;
|
||||
|
||||
@@ -38,67 +240,96 @@ export component MixView inherits Rectangle {
|
||||
|
||||
// --- Header ------------------------------------------------
|
||||
HorizontalLayout {
|
||||
spacing: 20px;
|
||||
spacing: 32px;
|
||||
alignment: start;
|
||||
// Gradient artwork.
|
||||
Rectangle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
background: @linear-gradient(135deg, #1e3a8a 0%, #6366f1 50%, #c084fc 100%);
|
||||
width: 224px;
|
||||
height: 224px;
|
||||
border-radius: Radius.md;
|
||||
background: root.mix-gradient;
|
||||
Text {
|
||||
text: MixState.title;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-size: 34px;
|
||||
font-weight: Typography.bold;
|
||||
}
|
||||
}
|
||||
// Metadata.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 8px;
|
||||
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.title;
|
||||
font-weight: Typography.bold;
|
||||
}
|
||||
Rectangle { height: 8px; }
|
||||
Text {
|
||||
width: root.width - 320px;
|
||||
text: MixState.subtitle;
|
||||
color: Theme.text-muted;
|
||||
color: Theme.text-secondary;
|
||||
font-size: Typography.body;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
if MixState.tracks.length > 0: Rectangle {
|
||||
width: play-row.preferred-width + 28px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background: play-ta.has-hover ? #ffffffd6 : Theme.accent;
|
||||
play-row := HorizontalLayout {
|
||||
x: (parent.width - self.preferred-width) / 2;
|
||||
spacing: 8px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/play-fill.svg");
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
tint: #ffffff;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: @tr("Play all");
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
play-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,11 +346,57 @@ export component MixView inherits Rectangle {
|
||||
font-size: Typography.body;
|
||||
}
|
||||
|
||||
for track[i] in MixState.tracks: SearchTrackRow {
|
||||
// --- 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: 64px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
Rectangle { width: 28px; }
|
||||
Rectangle { width: 32px; }
|
||||
}
|
||||
if MixState.tracks.length > 0: Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
Rectangle { height: 6px; }
|
||||
|
||||
for track[i] in MixState.tracks: MixTrackRow {
|
||||
track: track;
|
||||
index: i;
|
||||
show-artwork: true;
|
||||
media-action(kind, id, action) => {
|
||||
play(id) => {
|
||||
root.play-track(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,12 @@ export component AppShell inherits Rectangle {
|
||||
play-all => {
|
||||
root.media-action("mix", "", "play-all");
|
||||
}
|
||||
shuffle => {
|
||||
root.media-action("mix", "", "shuffle");
|
||||
}
|
||||
refresh => {
|
||||
root.media-action("mix", "", "refresh");
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.settings: SettingsView {
|
||||
settings-bool(k, v) => {
|
||||
|
||||
@@ -791,13 +791,30 @@ export global FavoritesActions {
|
||||
callback track-action(string /* track id */, string /* action */);
|
||||
}
|
||||
|
||||
// One track row in a Qobuz mix detail page — carries an album column
|
||||
// (mix tracks span many albums, unlike an album view).
|
||||
export struct MixTrackItem {
|
||||
id: string,
|
||||
number: string,
|
||||
title: string,
|
||||
artist: string,
|
||||
album: string,
|
||||
duration: string,
|
||||
quality-tier: string,
|
||||
explicit: bool,
|
||||
artwork-url: string,
|
||||
artwork: image,
|
||||
}
|
||||
|
||||
// A Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ), opened
|
||||
// from the For You Qobuz Mixes tiles. Populated by crate::mix.
|
||||
export global MixState {
|
||||
in property <string> kind; // daily | weekly | fav | top
|
||||
in property <string> title;
|
||||
in property <string> subtitle;
|
||||
in property <[SearchTrackItem]> tracks: [];
|
||||
in property <[MixTrackItem]> tracks: [];
|
||||
in property <int> track-count: 0;
|
||||
in property <string> total-duration;
|
||||
in property <bool> loading: false;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user