Add full uninstall dialog with data cleanup options
This commit is contained in:
@@ -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<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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user