274 lines
9.0 KiB
Rust
274 lines
9.0 KiB
Rust
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<gtk::Widget>, db: &Rc<Database>) {
|
|
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));
|
|
}
|