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