diff --git a/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po index c2703d9c7..1fce9e59a 100644 --- a/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po @@ -4864,3 +4864,6 @@ msgstr "Als Nächstes" msgid "Removed {} tracks from favorites" msgstr "{} Titel aus Favoriten entfernt" + +msgid "Removing {} tracks from favorites…" +msgstr "{} Titel werden aus Favoriten entfernt…" diff --git a/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po index c7a85ff7e..9ab520f11 100644 --- a/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po @@ -660,3 +660,6 @@ msgstr "Next up" msgid "Removed {} tracks from favorites" msgstr "Removed {} tracks from favorites" + +msgid "Removing {} tracks from favorites…" +msgstr "Removing {} tracks from favorites…" diff --git a/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po index d615a8d55..d551b7f17 100644 --- a/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po @@ -4894,3 +4894,6 @@ msgstr "A continuación" msgid "Removed {} tracks from favorites" msgstr "Se quitaron {} tracks de favoritos" + +msgid "Removing {} tracks from favorites…" +msgstr "Quitando {} tracks de favoritos…" diff --git a/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po index b48d20326..ebe90f00d 100644 --- a/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po @@ -4864,3 +4864,6 @@ msgstr "À suivre" msgid "Removed {} tracks from favorites" msgstr "{} pistes retirées des favoris" + +msgid "Removing {} tracks from favorites…" +msgstr "Suppression de {} pistes des favoris…" diff --git a/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po index d5190543f..fad638f33 100644 --- a/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po @@ -4852,3 +4852,6 @@ msgstr "次に再生" msgid "Removed {} tracks from favorites" msgstr "{} トラックをお気に入りから削除しました" + +msgid "Removing {} tracks from favorites…" +msgstr "{} トラックをお気に入りから削除中…" diff --git a/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po index e24ff3a0c..e412a71b8 100644 --- a/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po @@ -4876,3 +4876,6 @@ msgstr "Hierna" msgid "Removed {} tracks from favorites" msgstr "{} nummers verwijderd uit favorieten" + +msgid "Removing {} tracks from favorites…" +msgstr "{} nummers worden verwijderd uit favorieten…" diff --git a/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po index 156e9ddbb..98ca5dd0e 100644 --- a/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po @@ -4864,3 +4864,6 @@ msgstr "A seguir" msgid "Removed {} tracks from favorites" msgstr "{} faixas removidas dos favoritos" + +msgid "Removing {} tracks from favorites…" +msgstr "Removendo {} faixas dos favoritos…" diff --git a/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po index 5c2e80f2e..c55dfcdb9 100644 --- a/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po @@ -4878,3 +4878,6 @@ msgstr "Далее" msgid "Removed {} tracks from favorites" msgstr "{} треков удалено из избранного" + +msgid "Removing {} tracks from favorites…" +msgstr "Удаление {} треков из избранного…" diff --git a/crates/qbz/src/main.rs b/crates/qbz/src/main.rs index f9d3c0182..5bdf1c895 100644 --- a/crates/qbz/src/main.rs +++ b/crates/qbz/src/main.rs @@ -19165,35 +19165,64 @@ fn main() -> Result<(), Box> { local_library::clear_tracks_selection(&w); } "remove-favorites" => { - // Bulk local un-favorite (owner 2026-07-26): drop every - // selected row from the local-favorites store (the - // All-feed reads it back), flip the row hearts live. + // Bulk local un-favorite: ONE model pass (the first + // cut looped the whole model per selected row and + // froze the UI thread on big libraries — owner + // 2026-07-26). Hearts hollow instantly; the store + // mutations ride a blocking thread with progress + // toasts so the user always knows it's working. let rows = local_library::selected_local_tracks(&w); - let model = w.global::().get_tracks(); let n = rows.len(); + if n == 0 { + return; + } + let ids: std::collections::HashSet = + rows.iter().map(|t| t.id.to_string()).collect(); + let mut keys = Vec::with_capacity(n); for t in &rows { - let key = if t.source.as_deref() == Some("plex") { + keys.push(if t.source.as_deref() == Some("plex") { format!("plex:{}", t.file_path) } else { t.file_path.clone() - }; - let _ = crate::local_favorites::unfavorite("track", &key); - let row_id = t.id.to_string(); - for i in 0..model.row_count() { - if let Some(mut item) = model.row_data(i) { - if item.id.as_str() == row_id { - item.is_favorite = false; - model.set_row_data(i, item); - break; - } + }); + } + let model = w.global::().get_tracks(); + for i in 0..model.row_count() { + if let Some(mut item) = model.row_data(i) { + if item.is_favorite && ids.contains(item.id.as_str()) { + item.is_favorite = false; + model.set_row_data(i, item); } } } local_library::clear_tracks_selection(&w); - crate::toast::success( + crate::toast::info( &w, - qbz_i18n::t_args("Removed {} tracks from favorites", &[&n.to_string()]), + qbz_i18n::t_args( + "Removing {} tracks from favorites…", + &[&n.to_string()], + ), ); + let weak2 = weak.clone(); + handle.spawn(async move { + let keys2 = keys.clone(); + tokio::task::spawn_blocking(move || { + for k in &keys2 { + let _ = crate::local_favorites::unfavorite("track", k); + } + }) + .await + .ok(); + let _ = weak2.upgrade_in_event_loop(|w| { + crate::toast::success( + &w, + qbz_i18n::t_args( + "Removed {} tracks from favorites", + &[&n.to_string()], + ), + ); + }); + }); } "add-to-playlist" => { // Source-aware refs: Plex rows ride as "plex:", @@ -22794,10 +22823,20 @@ fn main() -> Result<(), Box> { if ids.is_empty() { return; } + // Progress feedback (owner 2026-07-26): an up-front + // toast, and a 150 ms courtesy delay between API + // calls so a big selection isn't a burst at Qobuz. let runtime = runtime.clone(); let weak = weak.clone(); let handle = handle.clone(); let image_cache = image_cache.clone(); + crate::toast::info( + &w, + qbz_i18n::t_args( + "Removing {} tracks from favorites…", + &[&ids.len().to_string()], + ), + ); handle.clone().spawn(async move { for id in &ids { if let Err(e) = @@ -22810,6 +22849,10 @@ fn main() -> Result<(), Box> { if let Ok(tid) = id.parse::() { crate::fav_cache::set(tid, false); } + tokio::time::sleep( + std::time::Duration::from_millis(150), + ) + .await; } let _ = weak.upgrade_in_event_loop(|w| { favorites::set_multi_select(&w, false);