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, record: &AppImageRecord, db: &Rc, ) { 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(None, move |dlg, response| { if response == "update" { 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, ) { 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); // 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(None, move |_dlg, response| { if response == "remove-old" { 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 }