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:
@@ -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