use adw::prelude::*; use gtk::gio; use std::cell::Cell; use std::rc::Rc; use crate::core::database::Database; use crate::core::updater; use crate::i18n::{i18n, i18n_f}; /// Show a dialog to update all AppImages that have updates available. pub fn show_batch_update_dialog(parent: &impl IsA, db: &Rc) { let records = db.get_all_appimages().unwrap_or_default(); let updatable: Vec<_> = records .into_iter() .filter(|r| { if let (Some(ref latest), Some(ref current)) = (&r.latest_version, &r.app_version) { r.update_url.is_some() && updater::version_is_newer(latest, current) } else { false } }) .collect(); if updatable.is_empty() { let dialog = adw::AlertDialog::builder() .heading(&i18n("No Updates")) .body(&i18n("All AppImages are up to date.")) .build(); dialog.add_response("ok", &i18n("OK")); dialog.present(Some(parent)); return; } let dialog = adw::Dialog::builder() .title(&i18n("Update All")) .content_width(500) .content_height(450) .build(); let toolbar = adw::ToolbarView::new(); let header = adw::HeaderBar::new(); toolbar.add_top_bar(&header); let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .margin_start(18) .margin_end(18) .margin_top(12) .margin_bottom(12) .build(); let status_label = gtk::Label::builder() .label(&i18n_f( "{count} updates available", &[("{count}", &updatable.len().to_string())], )) .xalign(0.0) .build(); status_label.add_css_class("title-4"); content.append(&status_label); let overall_progress = gtk::ProgressBar::builder() .show_text(true) .text(&i18n("Ready")) .build(); content.append(&overall_progress); // List of apps to update let list_box = gtk::ListBox::new(); list_box.add_css_class("boxed-list"); list_box.set_selection_mode(gtk::SelectionMode::None); let mut row_data: Vec<(i64, String, String, String, adw::ActionRow, gtk::Label)> = Vec::new(); for record in &updatable { let name = record.app_name.as_deref().unwrap_or(&record.filename); let current_ver = record.app_version.as_deref().unwrap_or("?"); let latest_ver = record.latest_version.as_deref().unwrap_or("?"); let row = adw::ActionRow::builder() .title(name) .subtitle(&format!("{} -> {}", current_ver, latest_ver)) .build(); let status_badge = gtk::Label::builder() .label(&i18n("Pending")) .valign(gtk::Align::Center) .build(); status_badge.add_css_class("dim-label"); row.add_suffix(&status_badge); list_box.append(&row); row_data.push(( record.id, record.path.clone(), record.update_url.clone().unwrap_or_default(), record.latest_version.clone().unwrap_or_default(), row, status_badge, )); } let scrolled = gtk::ScrolledWindow::builder() .child(&list_box) .vexpand(true) .min_content_height(200) .build(); content.append(&scrolled); // Buttons let button_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(12) .halign(gtk::Align::End) .margin_top(6) .build(); let cancel_btn = gtk::Button::builder() .label(&i18n("Cancel")) .build(); let update_btn = gtk::Button::builder() .label(&i18n("Update All")) .build(); update_btn.add_css_class("suggested-action"); button_box.append(&cancel_btn); button_box.append(&update_btn); content.append(&button_box); toolbar.set_content(Some(&content)); dialog.set_child(Some(&toolbar)); let dialog_weak = dialog.downgrade(); cancel_btn.connect_clicked(move |_| { if let Some(d) = dialog_weak.upgrade() { d.close(); } }); let cancelled = Rc::new(Cell::new(false)); let cancelled_close = cancelled.clone(); // Mark cancelled when dialog is closed during update dialog.connect_closed(move |_| { cancelled_close.set(true); }); let dialog_update_weak = dialog.downgrade(); update_btn.connect_clicked(move |btn| { btn.set_sensitive(false); let total = row_data.len(); let progress_bar = overall_progress.clone(); let status = status_label.clone(); let cancelled = cancelled.clone(); let row_data_clone: Vec<(i64, String, String, String, adw::ActionRow, gtk::Label)> = row_data.iter().map(|(id, path, url, ver, row, badge)| { (*id, path.clone(), url.clone(), ver.clone(), row.clone(), badge.clone()) }).collect(); let dialog_weak = dialog_update_weak.clone(); glib::spawn_future_local(async move { let mut completed = 0usize; let mut succeeded = 0usize; let mut failed = 0usize; for (record_id, path, url, new_version, _row, badge) in &row_data_clone { if cancelled.get() { break; } badge.set_label(&i18n("Updating...")); badge.remove_css_class("dim-label"); badge.add_css_class("accent"); let path = path.clone(); let url = url.clone(); let version = new_version.clone(); let record_id = *record_id; let result = gio::spawn_blocking(move || { let p = std::path::Path::new(&path); let update_result = updater::perform_update( p, Some(&url), true, // keep old for rollback None, ); // On success, record rollback path and clear update flag if let Ok(ref applied) = update_result { let bg_db = Database::open().expect("DB open failed"); if let Some(ref old_path) = applied.old_path_backup { bg_db.set_previous_version( record_id, Some(&old_path.to_string_lossy()), ).ok(); } let actual_ver = applied.new_version.as_deref() .unwrap_or(&version); bg_db.record_update( record_id, None, Some(actual_ver), Some("batch-update"), None, true, ).ok(); bg_db.clear_update_available(record_id).ok(); } update_result }) .await; completed += 1; let fraction = completed as f64 / total as f64; progress_bar.set_fraction(fraction); progress_bar.set_text(Some(&format!("{}/{}", completed, total))); status.set_label(&i18n_f( "Updating {current} of {total}...", &[ ("{current}", &completed.to_string()), ("{total}", &total.to_string()), ], )); match result { Ok(Ok(_)) => { badge.set_label(&i18n("Done")); badge.remove_css_class("accent"); badge.add_css_class("success"); succeeded += 1; } _ => { badge.set_label(&i18n("Failed")); badge.remove_css_class("accent"); badge.add_css_class("error"); failed += 1; } } } // Final status let summary = if failed == 0 { i18n_f( "All {count} updates applied successfully", &[("{count}", &succeeded.to_string())], ) } else { i18n_f( "{ok} succeeded, {fail} failed", &[ ("{ok}", &succeeded.to_string()), ("{fail}", &failed.to_string()), ], ) }; status.set_label(&summary); progress_bar.set_text(Some(&i18n("Complete"))); progress_bar.set_fraction(1.0); // Change cancel to Close if let Some(d) = dialog_weak.upgrade() { // The user can now close via the header bar let _ = d; } }); }); dialog.present(Some(parent)); }