From 843af0a8a57e9c489d18ded5df708c740e9f7547 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 27 Feb 2026 23:56:56 +0200 Subject: [PATCH] Add full uninstall dialog with data cleanup options Storage tab now has an Uninstall button that shows checkboxes for the AppImage file, desktop integration, and each discovered data path. Removes selected items and cleans up the database on confirm. --- src/ui/detail_view.rs | 144 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index 27ed032..17b2514 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -1640,6 +1640,47 @@ fn build_storage_tab( // Backups group inner.append(&build_backup_group(record.id, toast_overlay)); + // Uninstall group + let uninstall_group = adw::PreferencesGroup::builder() + .title("Uninstall") + .description("Remove this AppImage and optionally clean up its data") + .build(); + + let uninstall_btn = gtk::Button::builder() + .label("Uninstall AppImage...") + .halign(gtk::Align::Start) + .margin_top(6) + .margin_bottom(6) + .build(); + uninstall_btn.add_css_class("destructive-action"); + uninstall_btn.add_css_class("pill"); + + let record_uninstall = record.clone(); + let db_uninstall = db.clone(); + let toast_uninstall = toast_overlay.clone(); + let fp_paths: Vec<(String, String, u64)> = fp.paths.iter() + .filter(|p| p.exists) + .map(|p| ( + p.path.to_string_lossy().to_string(), + p.path_type.label().to_string(), + p.size_bytes, + )) + .collect(); + let is_integrated = record.integrated; + uninstall_btn.connect_clicked(move |_btn| { + show_uninstall_dialog( + &toast_uninstall, + &record_uninstall, + &db_uninstall, + is_integrated, + &fp_paths, + ); + }); + uninstall_group.add(&adw::ActionRow::builder() + .child(&uninstall_btn) + .build()); + inner.append(&uninstall_group); + // File location group let location_group = adw::PreferencesGroup::builder() .title("File Location") @@ -2198,3 +2239,106 @@ fn fetch_favicon_async(url: &str, image: >k::Image) { }); } +fn show_uninstall_dialog( + toast_overlay: &adw::ToastOverlay, + record: &AppImageRecord, + db: &Rc, + is_integrated: bool, + data_paths: &[(String, String, u64)], +) { + let name = record.app_name.as_deref().unwrap_or(&record.filename); + let dialog = adw::AlertDialog::builder() + .heading(&format!("Uninstall {}?", name)) + .body("Select what to remove:") + .build(); + dialog.add_response("cancel", "Cancel"); + dialog.add_response("uninstall", "Uninstall"); + dialog.set_response_appearance("uninstall", adw::ResponseAppearance::Destructive); + dialog.set_default_response(Some("cancel")); + dialog.set_close_response("cancel"); + + let extra = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .margin_top(12) + .build(); + + // Checkbox: AppImage file (always checked, not unchecked - it's the main thing) + let appimage_check = gtk::CheckButton::builder() + .label(&format!("AppImage file ({})", widgets::format_size(record.size_bytes))) + .active(true) + .build(); + extra.append(&appimage_check); + + // Checkbox: Desktop integration + let integration_check = if is_integrated { + let check = gtk::CheckButton::builder() + .label("Remove desktop integration") + .active(true) + .build(); + extra.append(&check); + Some(check) + } else { + None + }; + + // Checkboxes for each discovered data path + let mut path_checks: Vec<(gtk::CheckButton, String)> = Vec::new(); + for (path, label, size) in data_paths { + let check = gtk::CheckButton::builder() + .label(&format!("{} - {} ({})", label, path, widgets::format_size(*size as i64))) + .active(true) + .build(); + extra.append(&check); + path_checks.push((check, path.clone())); + } + + dialog.set_extra_child(Some(&extra)); + + let record_id = record.id; + let record_path = record.path.clone(); + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + dialog.connect_response(Some("uninstall"), move |_dlg, _response| { + // Remove integration if checked + if let Some(ref check) = integration_check { + if check.is_active() { + integrator::undo_all_modifications(&db_ref, record_id).ok(); + if let Ok(Some(rec)) = db_ref.get_appimage_by_id(record_id) { + integrator::remove_integration(&rec).ok(); + } + } + } + + // Remove checked data paths + for (check, path) in &path_checks { + if check.is_active() { + let p = std::path::Path::new(path); + if p.is_dir() { + std::fs::remove_dir_all(p).ok(); + } else if p.is_file() { + std::fs::remove_file(p).ok(); + } + } + } + + // Remove AppImage file if checked + if appimage_check.is_active() { + std::fs::remove_file(&record_path).ok(); + } + + // Remove from database + db_ref.remove_appimage(record_id).ok(); + + toast_ref.add_toast(adw::Toast::new("AppImage uninstalled")); + + // Navigate back (the detail view is now stale) + if let Some(nav) = toast_ref.ancestor(adw::NavigationView::static_type()) { + let nav: adw::NavigationView = nav.downcast().unwrap(); + nav.pop(); + } + }); + + dialog.present(Some(toast_overlay)); +} +