feat(slint): For You Spotlight section
Ports Tauri's SpotlightLite — one favorite artist highlighted with a
hero and a content row. Backed by get_artist_page.
- Spotlight component: header (title + "Shine a light…" subtitle),
hero (round portrait + ARTIST label + name + play-top-tracks /
open-artist circle buttons), and a draggable Flickable row
mixing a TOP TRACKS card, a RADIO card, and the artist's albums.
- New RadioCard (port of RadioCardLite): a 180px tile with the seed
artwork centered on an elevated panel, a wordmark label over it,
and a title/subtitle below; hover reveals play. (The Tauri
palette-derived panel colour is approximated with a neutral
elevated panel.)
- foryou::load_spotlight rotates among the top-5 favorites by
wall-clock seconds, fetches get_artist_page, and extracts up to 6
albums (album > live > ep-single > compilation, deduped) plus
whether the artist has top tracks. ForYouState gains the
spotlight-* fields; two new artwork targets feed the portrait +
album covers.
- Wiring: ForYouView renders Spotlight after Artists to Follow.
play-top-tracks routes to the existing artist play-top action;
"start radio" has no artist-radio backend yet so it falls back to
playing top tracks (logged).
Remaining For You: Radio Stations section (old card design), Qobuz
Mixes row, and the reco-DB sections (More from library, Rediscover).
cc
This commit is contained in:
8 files changed
+524
No files matched your search
@@ -76,6 +76,10 @@ pub enum ArtworkTarget {
|
||||
ForYouTopArtist { index: usize },
|
||||
/// A tile in ForYouState.artists-to-follow[index].
|
||||
ForYouToFollow { index: usize },
|
||||
/// The Spotlight artist portrait.
|
||||
ForYouSpotlightArtist,
|
||||
/// A card in ForYouState.spotlight-albums[index].
|
||||
ForYouSpotlightAlbum { index: usize },
|
||||
}
|
||||
|
||||
/// An artwork download job: which card, and the image URL.
|
||||
@@ -448,5 +452,17 @@ fn apply_artwork(
|
||||
model.set_row_data(index, item);
|
||||
}
|
||||
}
|
||||
ArtworkTarget::ForYouSpotlightArtist => {
|
||||
window
|
||||
.global::<crate::ForYouState>()
|
||||
.set_spotlight_image(image);
|
||||
}
|
||||
ArtworkTarget::ForYouSpotlightAlbum { index } => {
|
||||
let model = window.global::<crate::ForYouState>().get_spotlight_albums();
|
||||
if let Some(mut item) = model.row_data(index) {
|
||||
item.artwork = image;
|
||||
model.set_row_data(index, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,16 @@ pub struct ForYouData {
|
||||
pub recent_tracks: Vec<TrackSlim>,
|
||||
pub top_artists: Vec<ArtistSlim>,
|
||||
pub artists_to_follow: Vec<ArtistSlim>,
|
||||
pub spotlight: Option<SpotlightData>,
|
||||
}
|
||||
|
||||
pub struct SpotlightData {
|
||||
pub artist_id: String,
|
||||
pub artist_name: String,
|
||||
pub category: String,
|
||||
pub image_url: String,
|
||||
pub has_top_tracks: bool,
|
||||
pub albums: Vec<AlbumCard>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -182,15 +192,117 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Spotlight — highlight one favorite artist (rotated by time) with
|
||||
// their page (albums + whether they have top tracks).
|
||||
let spotlight = load_spotlight(runtime, &fav_artists).await;
|
||||
|
||||
ForYouData {
|
||||
release_watch,
|
||||
recent_albums,
|
||||
recent_tracks,
|
||||
top_artists,
|
||||
artists_to_follow: to_follow,
|
||||
spotlight,
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_spotlight<A>(
|
||||
runtime: &Arc<AppRuntime<A>>,
|
||||
favorites: &[Artist],
|
||||
) -> Option<SpotlightData>
|
||||
where
|
||||
A: FrontendAdapter + Send + Sync + 'static,
|
||||
{
|
||||
if favorites.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Rotate among the top 5 favorites by wall-clock seconds.
|
||||
let pool = favorites.len().min(5);
|
||||
let idx = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as usize % pool)
|
||||
.unwrap_or(0);
|
||||
let seed = &favorites[idx];
|
||||
|
||||
let page = runtime.core().get_artist_page(seed.id, None).await.ok()?;
|
||||
let image_url = page
|
||||
.images
|
||||
.as_ref()
|
||||
.and_then(|i| i.portrait.as_ref())
|
||||
.map(|p| {
|
||||
format!(
|
||||
"https://static.qobuz.com/images/artists/covers/medium/{}.{}",
|
||||
p.hash, p.format
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Up to 6 albums, preferring full albums then live/ep/compilation.
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut albums: Vec<AlbumCard> = Vec::new();
|
||||
for want in ["album", "live", "ep-single", "compilation"] {
|
||||
if albums.len() >= 6 {
|
||||
break;
|
||||
}
|
||||
let Some(groups) = page.releases.as_ref() else {
|
||||
break;
|
||||
};
|
||||
let Some(group) = groups.iter().find(|g| g.release_type == want) else {
|
||||
continue;
|
||||
};
|
||||
for rel in &group.items {
|
||||
if !seen.insert(rel.id.clone()) {
|
||||
continue;
|
||||
}
|
||||
let year = rel
|
||||
.dates
|
||||
.as_ref()
|
||||
.and_then(|d| d.original.as_deref())
|
||||
.and_then(|s| s.get(..4).map(|y| y.to_string()))
|
||||
.unwrap_or_default();
|
||||
let bd = rel.audio_info.as_ref().and_then(|a| a.maximum_bit_depth);
|
||||
let sr = rel.audio_info.as_ref().and_then(|a| a.maximum_sampling_rate);
|
||||
albums.push(AlbumCard {
|
||||
id: rel.id.clone(),
|
||||
title: rel.title.clone(),
|
||||
artist: rel
|
||||
.artist
|
||||
.as_ref()
|
||||
.map(|a| a.name.display.clone())
|
||||
.unwrap_or_else(|| page.name.display.clone()),
|
||||
year,
|
||||
quality_tier: match bd {
|
||||
Some(d) if d >= 24 => "hires",
|
||||
Some(_) => "cd",
|
||||
None => "",
|
||||
}
|
||||
.to_string(),
|
||||
quality_label: match (bd, sr) {
|
||||
(Some(b), Some(r)) => format!("{}-bit / {} kHz", b, r),
|
||||
_ => String::new(),
|
||||
},
|
||||
artwork_url: rel
|
||||
.image
|
||||
.as_ref()
|
||||
.and_then(|img| img.best().cloned())
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
if albums.len() >= 6 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(SpotlightData {
|
||||
artist_id: seed.id.to_string(),
|
||||
artist_name: page.name.display.clone(),
|
||||
category: page.artist_category.clone().unwrap_or_default(),
|
||||
image_url,
|
||||
has_top_tracks: page.top_tracks.as_ref().map(|t| !t.is_empty()).unwrap_or(false),
|
||||
albums,
|
||||
})
|
||||
}
|
||||
|
||||
fn album_items(cards: &[AlbumCard]) -> Vec<AlbumCardItem> {
|
||||
cards
|
||||
.iter()
|
||||
@@ -254,6 +366,19 @@ pub fn apply_for_you(window: &AppWindow, data: &ForYouData) {
|
||||
state.set_artists_to_follow(ModelRc::new(VecModel::from(artist_items(
|
||||
&data.artists_to_follow,
|
||||
))));
|
||||
|
||||
if let Some(sp) = &data.spotlight {
|
||||
state.set_spotlight_visible(true);
|
||||
state.set_spotlight_artist_id(sp.artist_id.clone().into());
|
||||
state.set_spotlight_name(sp.artist_name.clone().into());
|
||||
state.set_spotlight_category(sp.category.clone().into());
|
||||
state.set_spotlight_image_url(sp.image_url.clone().into());
|
||||
state.set_spotlight_has_top_tracks(sp.has_top_tracks);
|
||||
state.set_spotlight_albums(ModelRc::new(VecModel::from(album_items(&sp.albums))));
|
||||
} else {
|
||||
state.set_spotlight_visible(false);
|
||||
}
|
||||
|
||||
state.set_loading(false);
|
||||
state.set_loaded(true);
|
||||
}
|
||||
@@ -304,5 +429,21 @@ pub fn artwork_jobs(data: &ForYouData) -> Vec<ArtworkJob> {
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(sp) = &data.spotlight {
|
||||
if !sp.image_url.is_empty() {
|
||||
jobs.push(ArtworkJob {
|
||||
url: sp.image_url.clone(),
|
||||
target: ArtworkTarget::ForYouSpotlightArtist,
|
||||
});
|
||||
}
|
||||
for (i, c) in sp.albums.iter().enumerate() {
|
||||
if !c.artwork_url.is_empty() {
|
||||
jobs.push(ArtworkJob {
|
||||
url: c.artwork_url.clone(),
|
||||
target: ArtworkTarget::ForYouSpotlightAlbum { index: i },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
jobs
|
||||
}
|
||||
@@ -1227,6 +1227,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
handle.clone(),
|
||||
id.clone(),
|
||||
),
|
||||
("artist", "radio") => {
|
||||
// Spotlight "start radio" — no artist-radio backend
|
||||
// in the Slint MVP yet; play top tracks as the
|
||||
// closest available behavior.
|
||||
log::info!("[qbz-slint] artist radio not wired; playing top tracks for {id}");
|
||||
playback::play_artist_top_tracks(
|
||||
runtime.clone(),
|
||||
weak.clone(),
|
||||
handle.clone(),
|
||||
id.clone(),
|
||||
);
|
||||
}
|
||||
("artist", "follow") => {
|
||||
let runtime = runtime.clone();
|
||||
let weak = weak.clone();
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ForYouState } from "../state.slint";
|
||||
import { Carousel } from "Carousel.slint";
|
||||
import { SlimCarousel } from "SlimCarousel.slint";
|
||||
import { ArtistCarousel } from "ArtistCarousel.slint";
|
||||
import { Spotlight } from "Spotlight.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
|
||||
export component ForYouView inherits VerticalLayout {
|
||||
@@ -20,6 +21,8 @@ export component ForYouView inherits VerticalLayout {
|
||||
callback open-artist(string);
|
||||
callback media-action(string, string, string);
|
||||
callback play-track(string);
|
||||
callback play-top-tracks(string);
|
||||
callback start-radio(string);
|
||||
|
||||
spacing: 40px;
|
||||
alignment: start;
|
||||
@@ -85,4 +88,23 @@ export component ForYouView inherits VerticalLayout {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Spotlight.
|
||||
if ForYouState.spotlight-visible: Spotlight {
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
play-top-tracks(id) => {
|
||||
root.play-top-tracks(id);
|
||||
}
|
||||
start-radio(id) => {
|
||||
root.start-radio(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,6 +277,12 @@ export component HomeView inherits Rectangle {
|
||||
play-track(id) => {
|
||||
root.media-action("track", id, "play");
|
||||
}
|
||||
play-top-tracks(id) => {
|
||||
root.media-action("artist", id, "play-top");
|
||||
}
|
||||
start-radio(id) => {
|
||||
root.media-action("artist", id, "radio");
|
||||
}
|
||||
}
|
||||
|
||||
// The Recently Played + Popular sections below belong
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Radio / Top-Tracks card — port of Tauri's RadioCardLite. A 180px
|
||||
// tile: a square "display" with the seed artwork centered and a
|
||||
// wordmark label ("RADIO" / "TOP TRACKS") over it, then a title +
|
||||
// subtitle below. Hover reveals a play button.
|
||||
//
|
||||
// The Tauri version derives the panel colour from the artwork's
|
||||
// dominant palette; that palette extraction is omitted here (a
|
||||
// neutral elevated panel stands in), the rest matches.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
export component RadioCard inherits Rectangle {
|
||||
in property <string> seed-title;
|
||||
in property <string> seed-subtitle;
|
||||
in property <string> label: "RADIO";
|
||||
in property <image> artwork;
|
||||
callback clicked();
|
||||
callback play();
|
||||
|
||||
width: 180px;
|
||||
height: 234px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 8px;
|
||||
|
||||
// Display panel.
|
||||
panel := Rectangle {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: Radius.sm;
|
||||
background: Theme.surface-elevated;
|
||||
clip: true;
|
||||
|
||||
// Centered artwork "display".
|
||||
Rectangle {
|
||||
width: 114px;
|
||||
height: 114px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2 - 8px;
|
||||
border-radius: 4px;
|
||||
background: Theme.surface-card;
|
||||
clip: true;
|
||||
Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
source: root.artwork;
|
||||
image-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
// Wordmark over the display, bottom.
|
||||
Text {
|
||||
text: root.label;
|
||||
color: #ffffffe6;
|
||||
font-size: 15px;
|
||||
font-weight: Typography.bold;
|
||||
letter-spacing: 2px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: parent.height - self.height - 16px;
|
||||
}
|
||||
|
||||
// Hover overlay + play.
|
||||
Rectangle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000000;
|
||||
opacity: ta.has-hover || play-ta.has-hover ? 0.45 : 0.0;
|
||||
animate opacity { duration: 150ms; }
|
||||
}
|
||||
play-circle := Rectangle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: #ffffff;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
opacity: ta.has-hover || play-ta.has-hover ? 1.0 : 0.0;
|
||||
animate opacity { duration: 150ms; }
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/play-fill.svg");
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: #000000;
|
||||
}
|
||||
play-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Meta.
|
||||
Text {
|
||||
text: root.seed-title;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.medium;
|
||||
overflow: elide;
|
||||
width: 180px;
|
||||
}
|
||||
if root.seed-subtitle != "": Text {
|
||||
text: root.seed-subtitle;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
overflow: elide;
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Discover > For You "Spotlight" — port of Tauri's SpotlightLite.
|
||||
// One favorite artist highlighted: a header, a hero (portrait +
|
||||
// ARTIST + name + play-top-tracks / open-artist circle buttons), and
|
||||
// a draggable row mixing a TOP TRACKS card, a RADIO card, and the
|
||||
// artist's albums.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { ForYouState, AlbumCardItem } from "../state.slint";
|
||||
import { AlbumCard } from "AlbumCard.slint";
|
||||
import { RadioCard } from "RadioCard.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
component CircleButton inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <bool> primary: false;
|
||||
callback clicked();
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
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: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component Spotlight inherits VerticalLayout {
|
||||
callback open-artist(string);
|
||||
callback open-album(string);
|
||||
callback play-top-tracks(string);
|
||||
callback start-radio(string);
|
||||
callback media-action(string, string, string);
|
||||
|
||||
property <length> gap: 24px;
|
||||
// Row content width: TopTracks(180) + Radio(180) + albums(220 each)
|
||||
// + gaps. has-top-tracks toggles the first card.
|
||||
property <length> radio-cards-width:
|
||||
(ForYouState.spotlight-has-top-tracks ? 180px + root.gap : 0px) + 180px + root.gap;
|
||||
property <int> album-count: ForYouState.spotlight-albums.length;
|
||||
property <length> content-width:
|
||||
root.radio-cards-width
|
||||
+ root.album-count * (220px + root.gap);
|
||||
|
||||
spacing: 12px;
|
||||
|
||||
// Header.
|
||||
VerticalLayout {
|
||||
spacing: 2px;
|
||||
Text {
|
||||
text: @tr("Spotlight");
|
||||
color: Theme.text-primary;
|
||||
font-size: 18px;
|
||||
font-weight: Typography.semibold;
|
||||
}
|
||||
Text {
|
||||
text: @tr("Shine a light on one of your favourite artists.");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
}
|
||||
}
|
||||
|
||||
// Hero.
|
||||
HorizontalLayout {
|
||||
spacing: 16px;
|
||||
alignment: start;
|
||||
Rectangle {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 48px;
|
||||
background: Theme.surface-elevated;
|
||||
clip: true;
|
||||
Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
source: ForYouState.spotlight-image;
|
||||
image-fit: cover;
|
||||
}
|
||||
if ForYouState.spotlight-image-url == "": QbzIcon {
|
||||
source: @image-url("../assets/icons/user.svg");
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: Theme.text-muted;
|
||||
}
|
||||
portrait-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.open-artist(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 8px;
|
||||
if ForYouState.spotlight-category != "": Text {
|
||||
text: @tr("ARTIST");
|
||||
color: Theme.text-muted;
|
||||
font-size: 10px;
|
||||
font-weight: Typography.semibold;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
Text {
|
||||
text: ForYouState.spotlight-name;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.title;
|
||||
font-weight: Typography.bold;
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 10px;
|
||||
alignment: start;
|
||||
if ForYouState.spotlight-has-top-tracks: CircleButton {
|
||||
icon: @image-url("../assets/icons/play-fill.svg");
|
||||
primary: true;
|
||||
clicked => {
|
||||
root.play-top-tracks(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
}
|
||||
CircleButton {
|
||||
icon: @image-url("../assets/icons/user.svg");
|
||||
clicked => {
|
||||
root.open-artist(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 4px; }
|
||||
|
||||
// Content row — draggable (Flickable), like the other carousels.
|
||||
Rectangle {
|
||||
height: 270px;
|
||||
clip: true;
|
||||
Flickable {
|
||||
width: 100%;
|
||||
height: parent.height;
|
||||
viewport-width: root.content-width;
|
||||
viewport-height: self.height;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: root.gap;
|
||||
alignment: start;
|
||||
|
||||
if ForYouState.spotlight-has-top-tracks: RadioCard {
|
||||
seed-title: @tr("Top tracks");
|
||||
seed-subtitle: @tr("By {}", ForYouState.spotlight-name);
|
||||
label: "TOP TRACKS";
|
||||
artwork: ForYouState.spotlight-image;
|
||||
clicked => {
|
||||
root.play-top-tracks(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
play => {
|
||||
root.play-top-tracks(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
}
|
||||
RadioCard {
|
||||
seed-title: ForYouState.spotlight-name;
|
||||
seed-subtitle: @tr("Qobuz Radio Station");
|
||||
label: "RADIO";
|
||||
artwork: ForYouState.spotlight-image;
|
||||
clicked => {
|
||||
root.start-radio(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
play => {
|
||||
root.start-radio(ForYouState.spotlight-artist-id);
|
||||
}
|
||||
}
|
||||
for album in ForYouState.spotlight-albums: AlbumCard {
|
||||
album: album;
|
||||
clicked(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,16 @@ export global ForYouState {
|
||||
in property <[SlimItem]> recent-tracks: [];
|
||||
in property <[SlimItem]> top-artists: [];
|
||||
in property <[SlimItem]> artists-to-follow: [];
|
||||
// Spotlight — one favorite artist highlighted with a hero + a row
|
||||
// of their content (top tracks card, radio card, albums).
|
||||
in property <bool> spotlight-visible: false;
|
||||
in property <string> spotlight-artist-id;
|
||||
in property <string> spotlight-name;
|
||||
in property <string> spotlight-category;
|
||||
in property <string> spotlight-image-url;
|
||||
in property <image> spotlight-image;
|
||||
in property <bool> spotlight-has-top-tracks: false;
|
||||
in property <[AlbumCardItem]> spotlight-albums: [];
|
||||
in property <bool> loading: false;
|
||||
// True once a load has completed, so the tab does not refetch on
|
||||
// every visit.
|
||||
|
||||
Reference in new issue
Block a user