feat(slint): favorites Artists sidepanel controller (select-artist + view toggle)
Wire the sidepanel backend ahead of its UI: select-artist loads the chosen artist via artist::load_artist (reusing the /artist/page release_type classifier per the user's decision) and apply_selected_artist maps its release sections into selected-artist-sections (DiscoverSections) + total; a FavoriteArtistAlbum artwork target fills the section covers. The artists-set-view handler flips grid<->sidepanel (persisted as artists_view) and clears the selection on the way back to grid. card_to_item is now pub(crate) for reuse. cc
This commit is contained in:
5 files changed
+126
-1
No files matched your search
@@ -440,7 +440,7 @@ fn mmss(secs: u32) -> String {
|
|||||||
// previous artist-local helper produced one long paragraph for
|
// previous artist-local helper produced one long paragraph for
|
||||||
// multi-paragraph biographies — gone with this refactor.
|
// multi-paragraph biographies — gone with this refactor.
|
||||||
|
|
||||||
fn card_to_item(card: CardData) -> AlbumCardItem {
|
pub(crate) fn card_to_item(card: CardData) -> AlbumCardItem {
|
||||||
// On the artist page the card subtitle slot should show the
|
// On the artist page the card subtitle slot should show the
|
||||||
// release year — the artist is redundant since we're already on
|
// release year — the artist is redundant since we're already on
|
||||||
// their page. The AlbumCard reads `artist` for its subtitle line,
|
// their page. The AlbumCard reads `artist` for its subtitle line,
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ pub enum ArtworkTarget {
|
|||||||
/// One collage cover slot (0-3) of a favorites playlist card. `following`
|
/// One collage cover slot (0-3) of a favorites playlist card. `following`
|
||||||
/// picks the sub-tab source model (Following vs Library/favorites).
|
/// picks the sub-tab source model (Following vs Library/favorites).
|
||||||
FavPlaylistCover { following: bool, index: usize, slot: usize },
|
FavPlaylistCover { following: bool, index: usize, slot: usize },
|
||||||
|
/// An album card in the favorites Artists sidepanel — section `section`
|
||||||
|
/// of `FavoritesState.selected-artist-sections`, album `index`.
|
||||||
|
FavoriteArtistAlbum { section: usize, index: usize },
|
||||||
/// A card in ForYouState.release-watch.albums[index].
|
/// A card in ForYouState.release-watch.albums[index].
|
||||||
ForYouReleaseWatch { index: usize },
|
ForYouReleaseWatch { index: usize },
|
||||||
/// A card in ForYouState.recent-albums.albums[index].
|
/// A card in ForYouState.recent-albums.albums[index].
|
||||||
@@ -564,6 +567,17 @@ fn apply_artwork(
|
|||||||
crate::favorites::set_playlist_cover(window, &id, slot, image);
|
crate::favorites::set_playlist_cover(window, &id, slot, image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ArtworkTarget::FavoriteArtistAlbum { section, index } => {
|
||||||
|
let sections = window
|
||||||
|
.global::<crate::FavoritesState>()
|
||||||
|
.get_selected_artist_sections();
|
||||||
|
if let Some(sec) = sections.row_data(section) {
|
||||||
|
if let Some(mut item) = sec.albums.row_data(index) {
|
||||||
|
item.artwork = image;
|
||||||
|
sec.albums.set_row_data(index, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ArtworkTarget::ForYouReleaseWatch { index } => {
|
ArtworkTarget::ForYouReleaseWatch { index } => {
|
||||||
let model = window.global::<crate::ForYouState>().get_release_watch().albums;
|
let model = window.global::<crate::ForYouState>().get_release_watch().albums;
|
||||||
if let Some(mut item) = model.row_data(index) {
|
if let Some(mut item) = model.row_data(index) {
|
||||||
|
|||||||
@@ -694,6 +694,51 @@ pub fn derive_artists(window: &AppWindow) {
|
|||||||
state.set_artists_grouped(ModelRc::new(VecModel::from(sections)));
|
state.set_artists_grouped(ModelRc::new(VecModel::from(sections)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply the selected artist's albums to the sidepanel right column. Reuses
|
||||||
|
/// the standalone artist page's `/artist/page` `release_type` classifier
|
||||||
|
/// (`artist::load_artist` → `ReleaseSection`s), per the user's decision, so
|
||||||
|
/// the sections are server-authoritative (Discography / EPs & Singles / Live
|
||||||
|
/// / Compilations / Others).
|
||||||
|
pub fn apply_selected_artist(window: &AppWindow, sections: Vec<crate::artist::ReleaseSection>) {
|
||||||
|
let st = window.global::<FavoritesState>();
|
||||||
|
let mut total = 0i32;
|
||||||
|
let ds: Vec<DiscoverSection> = sections
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
let items: Vec<AlbumCardItem> =
|
||||||
|
s.cards.into_iter().map(crate::artist::card_to_item).collect();
|
||||||
|
total += items.len() as i32;
|
||||||
|
DiscoverSection {
|
||||||
|
title: s.title.into(),
|
||||||
|
endpoint: "".into(),
|
||||||
|
albums: ModelRc::new(VecModel::from(items)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
st.set_selected_artist_sections(ModelRc::new(VecModel::from(ds)));
|
||||||
|
st.set_selected_albums_total(total);
|
||||||
|
st.set_selected_albums_loading(false);
|
||||||
|
st.set_selected_albums_error("".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Artwork jobs for the selected artist's sidepanel album cards.
|
||||||
|
pub fn selected_artist_artwork_jobs(
|
||||||
|
sections: &[crate::artist::ReleaseSection],
|
||||||
|
) -> Vec<ArtworkJob> {
|
||||||
|
let mut jobs = Vec::new();
|
||||||
|
for (section, sec) in sections.iter().enumerate() {
|
||||||
|
for (index, card) in sec.cards.iter().enumerate() {
|
||||||
|
if !card.artwork_url.is_empty() {
|
||||||
|
jobs.push(ArtworkJob {
|
||||||
|
url: card.artwork_url.clone(),
|
||||||
|
target: ArtworkTarget::FavoriteArtistAlbum { section, index },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs
|
||||||
|
}
|
||||||
|
|
||||||
/// The loaded favorite tracks as a play-ready queue (Play all).
|
/// The loaded favorite tracks as a play-ready queue (Play all).
|
||||||
pub fn play_tracks() -> Vec<Track> {
|
pub fn play_tracks() -> Vec<Track> {
|
||||||
FAV_CURRENT.lock().map(|c| c.clone()).unwrap_or_default()
|
FAV_CURRENT.lock().map(|c| c.clone()).unwrap_or_default()
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ struct Prefs {
|
|||||||
playlists_view: String,
|
playlists_view: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
artists_group: bool,
|
artists_group: bool,
|
||||||
|
#[serde(default = "d_grid")]
|
||||||
|
artists_view: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Prefs {
|
impl Default for Prefs {
|
||||||
@@ -35,6 +37,7 @@ impl Default for Prefs {
|
|||||||
tracks_group: d_off(),
|
tracks_group: d_off(),
|
||||||
playlists_view: d_grid(),
|
playlists_view: d_grid(),
|
||||||
artists_group: false,
|
artists_group: false,
|
||||||
|
artists_view: d_grid(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +76,7 @@ pub fn load(window: &AppWindow) {
|
|||||||
st.set_tracks_group_mode(p.tracks_group.into());
|
st.set_tracks_group_mode(p.tracks_group.into());
|
||||||
st.set_playlists_view_mode(p.playlists_view.into());
|
st.set_playlists_view_mode(p.playlists_view.into());
|
||||||
st.set_artists_group_enabled(p.artists_group);
|
st.set_artists_group_enabled(p.artists_group);
|
||||||
|
st.set_artists_view_mode(p.artists_view.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist the current toolbar choices read from `FavoritesState`.
|
/// Persist the current toolbar choices read from `FavoritesState`.
|
||||||
@@ -88,6 +92,7 @@ pub fn save(window: &AppWindow) {
|
|||||||
tracks_group: st.get_tracks_group_mode().into(),
|
tracks_group: st.get_tracks_group_mode().into(),
|
||||||
playlists_view: st.get_playlists_view_mode().into(),
|
playlists_view: st.get_playlists_view_mode().into(),
|
||||||
artists_group: st.get_artists_group_enabled(),
|
artists_group: st.get_artists_group_enabled(),
|
||||||
|
artists_view: st.get_artists_view_mode().into(),
|
||||||
};
|
};
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
let _ = std::fs::create_dir_all(parent);
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
|||||||
@@ -4390,6 +4390,67 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// Artists grid <-> sidepanel view toggle (persisted). Switching back to
|
||||||
|
// grid clears the sidepanel selection (matches Tauri).
|
||||||
|
let weak = window.as_weak();
|
||||||
|
window
|
||||||
|
.global::<FavoritesActions>()
|
||||||
|
.on_artists_set_view(move |v| {
|
||||||
|
if let Some(w) = weak.upgrade() {
|
||||||
|
let st = w.global::<FavoritesState>();
|
||||||
|
st.set_artists_view_mode(v.clone());
|
||||||
|
if v == "grid" {
|
||||||
|
st.set_selected_artist_id("".into());
|
||||||
|
}
|
||||||
|
favorites_prefs::save(&w);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Sidepanel: load + show the selected artist's albums, reusing the
|
||||||
|
// standalone artist page's /artist/page release_type classifier.
|
||||||
|
let runtime = app_runtime.clone();
|
||||||
|
let weak = window.as_weak();
|
||||||
|
let handle = tokio_rt.handle().clone();
|
||||||
|
let image_cache = image_cache.clone();
|
||||||
|
window
|
||||||
|
.global::<FavoritesActions>()
|
||||||
|
.on_select_artist(move |id, name| {
|
||||||
|
let Some(w) = weak.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let st = w.global::<FavoritesState>();
|
||||||
|
st.set_selected_artist_id(id.clone());
|
||||||
|
st.set_selected_artist_name(name);
|
||||||
|
st.set_selected_albums_loading(true);
|
||||||
|
st.set_selected_albums_error("".into());
|
||||||
|
let runtime = runtime.clone();
|
||||||
|
let weak2 = weak.clone();
|
||||||
|
let image_cache = image_cache.clone();
|
||||||
|
let id_s = id.to_string();
|
||||||
|
handle.spawn(async move {
|
||||||
|
match artist::load_artist(&runtime, &id_s).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let sections = data.release_sections;
|
||||||
|
let jobs = favorites::selected_artist_artwork_jobs(§ions);
|
||||||
|
let _ = weak2.upgrade_in_event_loop(move |w| {
|
||||||
|
favorites::apply_selected_artist(&w, sections);
|
||||||
|
});
|
||||||
|
artwork::spawn_loads(jobs, weak2.clone(), image_cache.clone());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("[qbz-slint] sidepanel artist {id_s} load failed: {e}");
|
||||||
|
let _ = weak2.upgrade_in_event_loop(move |w| {
|
||||||
|
let st = w.global::<FavoritesState>();
|
||||||
|
st.set_selected_albums_loading(false);
|
||||||
|
st.set_selected_albums_error(e.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
{
|
{
|
||||||
// Playlist card actions: play / play-next / queue / share / favorite.
|
// Playlist card actions: play / play-next / queue / share / favorite.
|
||||||
let runtime = app_runtime.clone();
|
let runtime = app_runtime.clone();
|
||||||
|
|||||||
Reference in new issue
Block a user