304 lines
12 KiB
Rust
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
|
|
}
|