Files
driftwood/src/ui/update_dialog.rs

304 lines
12 KiB
Rust

use adw::prelude::*;
use gtk::gio;
use std::path::PathBuf;
use std::rc::Rc;
use crate::config::APP_ID;
use crate::core::database::{AppImageRecord, Database};
use crate::core::updater;
use crate::i18n::{i18n, i18n_f};
/// Show an update check + apply dialog for a single AppImage.
pub fn show_update_dialog(
parent: &impl IsA<gtk::Widget>,
record: &AppImageRecord,
db: &Rc<Database>,
) {
let dialog = adw::AlertDialog::builder()
.heading(&i18n("Check for Updates"))
.body(&i18n_f(
"Checking for updates for {name}...",
&[("{name}", record.app_name.as_deref().unwrap_or(&record.filename))],
))
.build();
dialog.add_response("close", &i18n("Close"));
dialog.set_default_response(Some("close"));
dialog.set_close_response("close");
let record_clone = record.clone();
let db_ref = db.clone();
let dialog_ref = dialog.clone();
// Start the update check in the background
let record_id = record.id;
let path = record.path.clone();
let current_version = record.app_version.clone();
glib::spawn_future_local(async move {
let result = gio::spawn_blocking(move || {
let appimage_path = std::path::Path::new(&path);
updater::check_appimage_for_update(
appimage_path,
current_version.as_deref(),
)
})
.await;
match result {
Ok((type_label, raw_info, Some(check_result))) => {
// Store update info in DB
db_ref
.update_update_info(
record_id,
raw_info.as_deref(),
type_label.as_deref(),
)
.ok();
if check_result.update_available {
if let Some(ref version) = check_result.latest_version {
db_ref.set_update_available(record_id, Some(version), check_result.download_url.as_deref()).ok();
}
let mut body = i18n_f(
"{current} -> {latest}",
&[
("{current}", record_clone.app_version.as_deref().unwrap_or("unknown")),
("{latest}", check_result.latest_version.as_deref().unwrap_or("unknown")),
],
);
if let Some(size) = check_result.file_size {
body.push_str(&format!(" ({})", humansize::format_size(size, humansize::BINARY)));
}
body.push_str(&format!("\n\n{}", i18n("A new version is available.")));
if let Some(ref notes) = check_result.release_notes {
if !notes.is_empty() {
// Truncate long release notes for the dialog
let truncated: String = notes.chars().take(300).collect();
let suffix = if truncated.len() < notes.len() { "..." } else { "" };
body.push_str(&format!("\n\n{}{}", truncated, suffix));
}
}
dialog_ref.set_heading(Some(&i18n("Update Available")));
dialog_ref.set_body(&body);
// Add "Update Now" button if we have a download URL
if let Some(download_url) = check_result.download_url {
dialog_ref.add_response("update", &i18n("Update Now"));
dialog_ref.set_response_appearance("update", adw::ResponseAppearance::Suggested);
dialog_ref.set_default_response(Some("update"));
let db_update = db_ref.clone();
let record_path = record_clone.path.clone();
let new_version = check_result.latest_version.clone();
dialog_ref.connect_response(Some("update"), move |dlg, _response| {
start_update(
dlg,
&record_path,
&download_url,
record_id,
new_version.as_deref(),
&db_update,
);
});
}
} else {
dialog_ref.set_heading(Some(&i18n("Up to Date")));
dialog_ref.set_body(&i18n_f(
"{name} is already at the latest version ({version}).",
&[
("{name}", record_clone.app_name.as_deref().unwrap_or(&record_clone.filename)),
("{version}", record_clone.app_version.as_deref().unwrap_or("unknown")),
],
));
db_ref.clear_update_available(record_id).ok();
}
}
Ok((type_label, raw_info, None)) => {
if raw_info.is_some() {
db_ref.update_update_info(record_id, raw_info.as_deref(), type_label.as_deref()).ok();
dialog_ref.set_heading(Some(&i18n("Check Failed")));
dialog_ref.set_body(&i18n("Could not reach the update server. Try again later."));
} else {
dialog_ref.set_heading(Some(&i18n("No Update Info")));
dialog_ref.set_body(
&i18n("This app does not support automatic updates. Check the developer's website for newer versions."),
);
}
}
Err(_) => {
dialog_ref.set_heading(Some(&i18n("Error")));
dialog_ref.set_body(&i18n("An error occurred while checking for updates."));
}
}
});
dialog.present(Some(parent));
}
/// Start the actual update download + apply process.
fn start_update(
dialog: &adw::AlertDialog,
appimage_path: &str,
download_url: &str,
record_id: i64,
new_version: Option<&str>,
db: &Rc<Database>,
) {
dialog.set_heading(Some(&i18n("Updating...")));
dialog.set_body(&i18n("Downloading update. This may take a moment."));
dialog.set_response_enabled("update", false);
let path = appimage_path.to_string();
let url = download_url.to_string();
let version = new_version.map(|s| s.to_string());
let db_ref = db.clone();
let dialog_weak = dialog.downgrade();
glib::spawn_future_local(async move {
let result = gio::spawn_blocking(move || {
let p = std::path::Path::new(&path);
// Always keep old version initially - cleanup decision happens after
updater::perform_update(p, Some(&url), true, None)
})
.await;
let Some(dialog) = dialog_weak.upgrade() else { return };
match result {
Ok(Ok(applied)) => {
// Record the update in history using the actual new version
let actual_version = applied.new_version.as_deref().or(version.as_deref());
if let Some(ver) = actual_version {
db_ref.record_update(record_id, None, Some(ver), Some("download"), None, true).ok();
}
db_ref.clear_update_available(record_id).ok();
let success_body = i18n_f(
"Updated to {version}\nPath: {path}",
&[
("{version}", applied.new_version.as_deref().unwrap_or("latest")),
("{path}", &applied.new_path.display().to_string()),
],
);
dialog.set_heading(Some(&i18n("Update Complete")));
dialog.set_body(&success_body);
dialog.set_response_enabled("update", false);
// Save previous version path for rollback
if let Some(ref old_path) = applied.old_path_backup {
db_ref.set_previous_version(
record_id,
Some(&old_path.to_string_lossy()),
).ok();
}
// Handle old version cleanup
if let Some(old_path) = applied.old_path_backup {
handle_old_version_cleanup(&dialog, old_path);
}
}
Ok(Err(e)) => {
dialog.set_heading(Some(&i18n("Update Failed")));
dialog.set_body(&i18n_f(
"The update could not be applied: {error}",
&[("{error}", &e.to_string())],
));
}
Err(_) => {
dialog.set_heading(Some(&i18n("Update Failed")));
dialog.set_body(&i18n("An unexpected error occurred during the update."));
}
}
});
}
/// After a successful update, handle cleanup of the old version based on user preference.
fn handle_old_version_cleanup(dialog: &adw::AlertDialog, old_path: PathBuf) {
let settings = gio::Settings::new(APP_ID);
let policy = settings.string("update-cleanup");
match policy.as_str() {
"always" => {
// Auto-remove without asking
if old_path.exists() {
if let Err(e) = std::fs::remove_file(&old_path) {
log::warn!("Failed to remove old version {}: {}", old_path.display(), e);
} else {
log::info!("Auto-removed old version: {}", old_path.display());
}
}
}
"never" => {
// Keep the backup, just inform
dialog.set_body(&i18n_f(
"Update complete. The old version is saved at:\n{path}",
&[("{path}", &old_path.display().to_string())],
));
}
_ => {
// "ask" - prompt the user
dialog.set_body(&i18n_f(
"Update complete.\n\nRemove the old version?\n{path}",
&[("{path}", &old_path.display().to_string())],
));
dialog.add_response("remove-old", &i18n("Remove Old Version"));
dialog.set_response_appearance("remove-old", adw::ResponseAppearance::Destructive);
let path = old_path.clone();
dialog.connect_response(Some("remove-old"), move |_dlg, _response| {
if path.exists() {
std::fs::remove_file(&path).ok();
}
});
}
}
}
/// Batch check all AppImages for updates. Returns count of updates found.
pub fn batch_check_updates(db: &Database) -> u32 {
let records = match db.get_all_appimages() {
Ok(r) => r,
Err(e) => {
log::error!("Failed to get appimages for update check: {}", e);
return 0;
}
};
let mut updates_found = 0u32;
for record in &records {
let appimage_path = std::path::Path::new(&record.path);
if !appimage_path.exists() {
continue;
}
let (type_label, raw_info, check_result) = updater::check_appimage_for_update(
appimage_path,
record.app_version.as_deref(),
);
// Store update info
if raw_info.is_some() || type_label.is_some() {
db.update_update_info(
record.id,
raw_info.as_deref(),
type_label.as_deref(),
)
.ok();
}
if let Some(result) = check_result {
if result.update_available {
if let Some(ref version) = result.latest_version {
db.set_update_available(record.id, Some(version), result.download_url.as_deref()).ok();
updates_found += 1;
}
} else {
db.clear_update_available(record.id).ok();
}
}
}
updates_found
}