diff --git a/frontend/src/pages/artist.rs b/frontend/src/pages/artist.rs index 526dddc..8238763 100644 --- a/frontend/src/pages/artist.rs +++ b/frontend/src/pages/artist.rs @@ -109,37 +109,9 @@ pub fn artist_page(props: &Props) -> Html { let is_watched = d.artist_status == "owned" || d.artist_status == "partial" || d.artist_status == "wanted"; - if is_watched { - // Unwatch All - let artist_id_num = d.artist.id; - let artist_name = d.artist.name.clone(); - let message = message.clone(); - let error = error.clone(); - let fetch = fetch.clone(); - let artist_id = id.clone(); - html! { - { - message.set(Some(format!("Unwatched {artist_name}"))); - fetch.emit(artist_id); - } - Err(e) => error.set(Some(e.0)), - } - }); - })}> - { "Unwatch All" } - - } - } else { - // Watch All — prefer enrichment MBID over DB record (import may not have set it) + let is_fully_watched = d.artist_status == "owned"; + + let watch_btn = if !is_fully_watched { let artist_name = d.artist.name.clone(); let artist_mbid = d .artist_info @@ -175,7 +147,43 @@ pub fn artist_page(props: &Props) -> Html { { "Watch All" } } - } + } else { + html! {} + }; + + let unwatch_btn = if is_watched { + let artist_id_num = d.artist.id; + let artist_name = d.artist.name.clone(); + let message = message.clone(); + let error = error.clone(); + let fetch = fetch.clone(); + let artist_id = id.clone(); + html! { + { + message.set(Some(format!("Unwatched {artist_name}"))); + fetch.emit(artist_id); + } + Err(e) => error.set(Some(e.0)), + } + }); + })}> + { "Unwatch All" } + + } + } else { + html! {} + }; + + html! { <> { watch_btn } { unwatch_btn } > } }; let monitor_btn = { @@ -375,8 +383,11 @@ pub fn artist_page(props: &Props) -> Html { let tc = album.track_count; - // Watch/Unwatch toggle for albums - let watch_btn = if is_unwatched { + // Watch/Unwatch buttons for albums — show both when partially watched + let is_fully_owned = album.status == "owned"; + let is_album_watched = album.status != "unwatched"; + + let album_watch_btn = { let artist_name = d.artist.name.clone(); let album_title = album.title.clone(); let album_mbid = album.mbid.clone(); @@ -385,7 +396,8 @@ pub fn artist_page(props: &Props) -> Html { let fetch = fetch.clone(); let artist_id = id.clone(); html! { - Html { { "Watch" } } - } else { + }; + + let album_unwatch_btn = { let album_title = album.title.clone(); let album_mbid = album.mbid.clone(); let message = message.clone(); @@ -418,7 +432,8 @@ pub fn artist_page(props: &Props) -> Html { let fetch = fetch.clone(); let artist_id = id.clone(); html! { - Html { } }; + let watch_btn = html! { { album_watch_btn } { album_unwatch_btn } }; + html! { diff --git a/frontend/src/pages/library.rs b/frontend/src/pages/library.rs index c6c7c59..04e5246 100644 --- a/frontend/src/pages/library.rs +++ b/frontend/src/pages/library.rs @@ -64,18 +64,76 @@ pub fn library_page() -> Html { { for artists.iter().map(|a| { let artist_id = a.id; - let error = error.clone(); - let fetch = fetch_artists.clone(); - let on_remove = Callback::from(move |_: MouseEvent| { + let artist_name = a.name.clone(); + let artist_mbid = a.musicbrainz_id.clone(); + let is_watched = a.total_watched > 0; + let is_fully_watched = a.enriched && a.total_watched >= a.total_items && a.total_items > 0; + let is_monitored = a.monitored; + + let on_remove = { let error = error.clone(); - let fetch = fetch.clone(); - wasm_bindgen_futures::spawn_local(async move { - match api::delete_artist(artist_id).await { - Ok(_) => fetch.emit(()), - Err(e) => error.set(Some(e.0)), - } - }); - }); + let fetch = fetch_artists.clone(); + Callback::from(move |_: MouseEvent| { + let error = error.clone(); + let fetch = fetch.clone(); + wasm_bindgen_futures::spawn_local(async move { + match api::delete_artist(artist_id).await { + Ok(_) => fetch.emit(()), + Err(e) => error.set(Some(e.0)), + } + }); + }) + }; + + let on_watch = { + let error = error.clone(); + let fetch = fetch_artists.clone(); + let name = artist_name.clone(); + let mbid = artist_mbid.clone(); + Callback::from(move |_: MouseEvent| { + let error = error.clone(); + let fetch = fetch.clone(); + let name = name.clone(); + let mbid = mbid.clone(); + wasm_bindgen_futures::spawn_local(async move { + match api::add_artist(&name, mbid.as_deref()).await { + Ok(_) => fetch.emit(()), + Err(e) => error.set(Some(e.0)), + } + }); + }) + }; + + let on_unwatch = { + let error = error.clone(); + let fetch = fetch_artists.clone(); + Callback::from(move |_: MouseEvent| { + let error = error.clone(); + let fetch = fetch.clone(); + wasm_bindgen_futures::spawn_local(async move { + match api::unwatch_artist(artist_id).await { + Ok(_) => fetch.emit(()), + Err(e) => error.set(Some(e.0)), + } + }); + }) + }; + + let on_monitor_toggle = { + let error = error.clone(); + let fetch = fetch_artists.clone(); + Callback::from(move |_: MouseEvent| { + let error = error.clone(); + let fetch = fetch.clone(); + wasm_bindgen_futures::spawn_local(async move { + match api::set_artist_monitored(artist_id, !is_monitored).await { + Ok(_) => fetch.emit(()), + Err(e) => error.set(Some(e.0)), + } + }); + }) + }; + html! { @@ -116,8 +174,22 @@ pub fn library_page() -> Html { { a.total_items } } - - + + + { "Watch" } + + + { "Unwatch" } + + + { if is_monitored { "Monitored" } else { "Monitor" } } + + { "Remove" } diff --git a/frontend/style.css b/frontend/style.css index 09b6d6e..a17b03c 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -248,6 +248,8 @@ tr:hover { background: var(--bg-card); } .btn-danger { background: var(--danger); color: white; } .btn-secondary { background: var(--bg-card); color: var(--text-primary); border: 1px solid var(--border); } .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.8rem; } +.btn-fixed { min-width: 5.5rem; text-align: center; } +.btn-lib { min-width: 4.5rem; text-align: center; padding-left: 0.25rem; padding-right: 0.25rem; } /* Forms */ input, select { diff --git a/src/routes/artists.rs b/src/routes/artists.rs index c011f81..18443f9 100644 --- a/src/routes/artists.rs +++ b/src/routes/artists.rs @@ -95,9 +95,11 @@ async fn list_artists( auth::require_auth(&session)?; let artists = queries::artists::list(state.db.conn(), query.limit, query.offset).await?; + let wanted = queries::wanted::list(state.db.conn(), None, None).await?; + let mut items: Vec = Vec::new(); for a in &artists { - // Check if we have cached artist-level totals from a prior detail page load + // Get total_items from enrichment cache (total available tracks from MB) let cache_key = format!("artist_totals:{}", a.id); let cached_totals: Option<(u32, u32, u32)> = if let Ok(Some(json)) = queries::cache::get(state.db.conn(), &cache_key).await { @@ -107,12 +109,20 @@ async fn list_artists( }; let enriched = cached_totals.is_some(); - let (total_watched, total_owned, total_items) = - if let Some((avail, watched, owned)) = cached_totals { - (watched as usize, owned as usize, avail as usize) - } else { - (0, 0, 0) - }; + let total_items = cached_totals + .map(|(avail, _, _)| avail as usize) + .unwrap_or(0); + + // Compute watched/owned live from wanted_items (always current) + let artist_wanted: Vec<_> = wanted + .iter() + .filter(|w| w.artist_id == Some(a.id)) + .collect(); + let total_watched = artist_wanted.len(); + let total_owned = artist_wanted + .iter() + .filter(|w| w.status == WantedStatus::Owned) + .count(); items.push(ArtistListItem { id: a.id,