Update to artist credit handling
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
use std::rc::Rc;
|
||||
use std::cell::Cell;
|
||||
use gloo_timers::callback::Interval;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::api;
|
||||
use crate::components::watch_indicator::WatchIndicator;
|
||||
use crate::pages::Route;
|
||||
use crate::types::FullArtistDetail;
|
||||
|
||||
@@ -18,11 +20,16 @@ pub fn artist_page(props: &Props) -> Html {
|
||||
let message = use_state(|| None::<String>);
|
||||
let id = props.id.clone();
|
||||
|
||||
// Flag to prevent the background enrichment from overwriting a user-triggered refresh
|
||||
let user_acted: Rc<Cell<bool>> = use_memo((), |_| Cell::new(false));
|
||||
|
||||
// Full fetch (with track counts) — used for refresh after actions
|
||||
let fetch = {
|
||||
let detail = detail.clone();
|
||||
let error = error.clone();
|
||||
let user_acted = user_acted.clone();
|
||||
Callback::from(move |id: String| {
|
||||
user_acted.set(true);
|
||||
let detail = detail.clone();
|
||||
let error = error.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
@@ -39,7 +46,9 @@ pub fn artist_page(props: &Props) -> Html {
|
||||
let detail = detail.clone();
|
||||
let error = error.clone();
|
||||
let id = id.clone();
|
||||
let user_acted = user_acted.clone();
|
||||
use_effect_with(id.clone(), move |_| {
|
||||
user_acted.set(false);
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
// Phase 1: quick load (instant for browsing artists)
|
||||
match api::get_artist_full_quick(&id).await {
|
||||
@@ -48,9 +57,14 @@ pub fn artist_page(props: &Props) -> Html {
|
||||
detail.set(Some(d));
|
||||
|
||||
// Phase 2: if not enriched, fetch full data in background
|
||||
if needs_enrich {
|
||||
if needs_enrich && !user_acted.get() {
|
||||
match api::get_artist_full(&id).await {
|
||||
Ok(full) => detail.set(Some(full)),
|
||||
Ok(full) => {
|
||||
// Only apply if user hasn't triggered a refresh
|
||||
if !user_acted.get() {
|
||||
detail.set(Some(full));
|
||||
}
|
||||
}
|
||||
Err(_) => {} // quick data is still showing, don't overwrite with error
|
||||
}
|
||||
}
|
||||
@@ -61,6 +75,31 @@ pub fn artist_page(props: &Props) -> Html {
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-refresh when background tasks complete (downloads, organize, etc.)
|
||||
{
|
||||
let fetch = fetch.clone();
|
||||
let id = id.clone();
|
||||
let had_tasks: Rc<Cell<bool>> = use_memo((), |_| Cell::new(false));
|
||||
use_effect_with((), move |_| {
|
||||
let interval = Interval::new(3_000, move || {
|
||||
let fetch = fetch.clone();
|
||||
let id = id.clone();
|
||||
let had_tasks = had_tasks.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
if let Ok(status) = api::get_status().await {
|
||||
let has_running = status.tasks.iter().any(|t| t.status == "Running");
|
||||
if had_tasks.get() && !has_running {
|
||||
// Tasks just finished — refresh artist data
|
||||
fetch.emit(id);
|
||||
}
|
||||
had_tasks.set(has_running);
|
||||
}
|
||||
});
|
||||
});
|
||||
move || drop(interval)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref err) = *error {
|
||||
return html! { <div class="error">{ format!("Error: {err}") }</div> };
|
||||
}
|
||||
@@ -69,10 +108,53 @@ pub fn artist_page(props: &Props) -> Html {
|
||||
return html! { <p class="loading">{ "Loading discography from MusicBrainz..." }</p> };
|
||||
};
|
||||
|
||||
let watch_all_btn = {
|
||||
let artist_status = d.artist_status.clone();
|
||||
let show = artist_status != "owned";
|
||||
if show {
|
||||
let artist_name = d.artist.name.clone();
|
||||
let artist_mbid = d.artist.musicbrainz_id.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
let fetch = fetch.clone();
|
||||
let artist_id = id.clone();
|
||||
html! {
|
||||
<button class="btn btn-sm btn-success"
|
||||
onclick={Callback::from(move |_: MouseEvent| {
|
||||
let artist_name = artist_name.clone();
|
||||
let artist_mbid = artist_mbid.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
let fetch = fetch.clone();
|
||||
let artist_id = artist_id.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::add_artist(&artist_name, artist_mbid.as_deref()).await {
|
||||
Ok(s) => {
|
||||
message.set(Some(format!(
|
||||
"Watching {}: added {} tracks ({} already owned)",
|
||||
artist_name, s.tracks_added, s.tracks_already_owned
|
||||
)));
|
||||
fetch.emit(artist_id);
|
||||
}
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
});
|
||||
})}>
|
||||
{ "Watch All" }
|
||||
</button>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>{ &d.artist.name }</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2>{ &d.artist.name }</h2>
|
||||
{ watch_all_btn }
|
||||
</div>
|
||||
if d.enriched {
|
||||
<p class="text-sm">
|
||||
<span style="color: var(--accent);">
|
||||
|
||||
@@ -198,7 +198,7 @@ pub fn dashboard() -> Html {
|
||||
if !s.tasks.is_empty() {
|
||||
<div class="card">
|
||||
<h3>{ "Background Tasks" }</h3>
|
||||
<table>
|
||||
<table class="tasks-table">
|
||||
<thead>
|
||||
<tr><th>{ "Type" }</th><th>{ "Status" }</th><th>{ "Progress" }</th><th>{ "Result" }</th></tr>
|
||||
</thead>
|
||||
|
||||
@@ -46,7 +46,9 @@ pub fn library_page() -> Html {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{ "Name" }</th>
|
||||
<th>{ "Status" }</th>
|
||||
<th>{ "Owned" }</th>
|
||||
<th>{ "Watched" }</th>
|
||||
<th>{ "Tracks" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -60,14 +62,30 @@ pub fn library_page() -> Html {
|
||||
<td>
|
||||
if a.total_items > 0 {
|
||||
<span class="text-sm" style={
|
||||
if a.total_owned == a.total_items { "color: var(--success);" }
|
||||
if a.total_owned >= a.total_watched && a.total_watched > 0 { "color: var(--success);" }
|
||||
else if a.total_owned > 0 { "color: var(--warning);" }
|
||||
else { "color: var(--accent);" }
|
||||
else { "color: var(--text-muted);" }
|
||||
}>
|
||||
{ format!("{}/{} owned", a.total_owned, a.total_items) }
|
||||
{ format!("{}/{}", a.total_owned, a.total_watched) }
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
if a.total_items > 0 {
|
||||
<span class="text-sm" style={
|
||||
if a.total_watched >= a.total_items { "color: var(--accent);" }
|
||||
else if a.total_watched > 0 { "color: var(--accent);" }
|
||||
else { "color: var(--text-muted);" }
|
||||
}>
|
||||
{ format!("{}/{}", a.total_watched, a.total_items) }
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-muted text-sm">
|
||||
if a.total_items > 0 {
|
||||
{ a.total_items }
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use web_sys::HtmlInputElement;
|
||||
use web_sys::HtmlSelectElement;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::api;
|
||||
use crate::pages::Route;
|
||||
use crate::types::*;
|
||||
|
||||
enum SearchResults {
|
||||
@@ -34,15 +36,15 @@ pub fn search_page() -> Html {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
error.set(None);
|
||||
match st.as_str() {
|
||||
"artist" => match api::search_artist(&q, 10).await {
|
||||
"artist" => match api::search_artist(&q, 50).await {
|
||||
Ok(r) => results.set(SearchResults::Artists(r)),
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
},
|
||||
"album" => match api::search_album(&q, None, 10).await {
|
||||
"album" => match api::search_album(&q, None, 50).await {
|
||||
Ok(r) => results.set(SearchResults::Albums(r)),
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
},
|
||||
"track" => match api::search_track(&q, None, 10).await {
|
||||
"track" => match api::search_track(&q, None, 50).await {
|
||||
Ok(r) => results.set(SearchResults::Tracks(r)),
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
},
|
||||
@@ -52,24 +54,6 @@ pub fn search_page() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_add_artist = {
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
move |name: String, mbid: String| {
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::add_artist(&name, Some(&mbid)).await {
|
||||
Ok(s) => message.set(Some(format!(
|
||||
"Added {} tracks ({} already owned)",
|
||||
s.tracks_added, s.tracks_already_owned
|
||||
))),
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let on_add_album = {
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
@@ -132,17 +116,16 @@ pub fn search_page() -> Html {
|
||||
SearchResults::Artists(items) => html! {
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{ "Name" }</th><th>{ "Country" }</th><th>{ "Type" }</th><th>{ "Score" }</th><th></th></tr>
|
||||
<tr><th>{ "Name" }</th><th>{ "Country" }</th><th>{ "Type" }</th><th>{ "Score" }</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ for items.iter().map(|a| {
|
||||
let name = a.name.clone();
|
||||
let mbid = a.id.clone();
|
||||
let on_add = on_add_artist.clone();
|
||||
html! {
|
||||
<tr>
|
||||
<td>
|
||||
{ &a.name }
|
||||
<Link<Route> to={Route::Artist { id: a.id.clone() }}>
|
||||
{ &a.name }
|
||||
</Link<Route>>
|
||||
if let Some(ref d) = a.disambiguation {
|
||||
<span class="text-muted text-sm">{ format!(" ({d})") }</span>
|
||||
}
|
||||
@@ -150,12 +133,6 @@ pub fn search_page() -> Html {
|
||||
<td>{ a.country.as_deref().unwrap_or("") }</td>
|
||||
<td>{ a.artist_type.as_deref().unwrap_or("") }</td>
|
||||
<td>{ a.score }</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-success"
|
||||
onclick={Callback::from(move |_| on_add(name.clone(), mbid.clone()))}>
|
||||
{ "Add" }
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
})}
|
||||
@@ -175,7 +152,11 @@ pub fn search_page() -> Html {
|
||||
let on_add = on_add_album.clone();
|
||||
html! {
|
||||
<tr>
|
||||
<td>{ &a.title }</td>
|
||||
<td>
|
||||
<Link<Route> to={Route::Album { mbid: a.id.clone() }}>
|
||||
{ &a.title }
|
||||
</Link<Route>>
|
||||
</td>
|
||||
<td>{ &a.artist }</td>
|
||||
<td>{ a.year.as_deref().unwrap_or("") }</td>
|
||||
<td>{ a.score }</td>
|
||||
|
||||
Reference in New Issue
Block a user