Add full uninstall dialog with data cleanup options

This commit is contained in:
2026-02-27 23:56:56 +02:00
parent 84e33d06c8
commit 619b3cb447

View File

@@ -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: &gtk::Image) {
});
}
fn show_uninstall_dialog(
toast_overlay: &adw::ToastOverlay,
record: &AppImageRecord,
db: &Rc<Database>,
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));
}