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.
This commit is contained in:
lashman
2026-02-27 23:56:56 +02:00
parent 1a6eb4ec99
commit 843af0a8a5

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));
}