use adw::prelude::*; use std::rc::Rc; use crate::core::database::{AppImageRecord, Database}; use crate::core::fuse::FuseStatus; use crate::core::integrator; use crate::core::wayland::WaylandStatus; use crate::i18n::{i18n, i18n_f}; use super::widgets; /// Show a confirmation dialog before integrating an AppImage into the desktop. /// Returns true if the user confirms, false if cancelled. pub fn show_integration_dialog( parent: &impl IsA, record: &AppImageRecord, db: &Rc, on_complete: impl Fn(bool) + 'static, ) { let name = record.app_name.as_deref().unwrap_or(&record.filename); let dialog = adw::AlertDialog::builder() .heading(&i18n_f("Integrate {name}?", &[("{name}", name)])) .body(&i18n("This will add the application to your desktop menu.")) .close_response("cancel") .default_response("integrate") .build(); dialog.add_response("cancel", &i18n("Cancel")); dialog.add_response("integrate", &i18n("Integrate")); dialog.set_response_appearance("integrate", adw::ResponseAppearance::Suggested); // Build extra content with details let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .margin_top(6) .build(); // App identity section let identity_box = gtk::ListBox::new(); identity_box.add_css_class("boxed-list"); identity_box.set_selection_mode(gtk::SelectionMode::None); identity_box.update_property(&[ gtk::accessible::Property::Label(&i18n("Application details")), ]); // Name let name_row = adw::ActionRow::builder() .title(&i18n("Application")) .subtitle(name) .build(); if let Some(ref icon_path) = record.icon_path { let path = std::path::Path::new(icon_path); if path.exists() { if let Ok(texture) = gtk::gdk::Texture::from_filename(path) { let image = gtk::Image::builder() .pixel_size(32) .build(); image.set_paintable(Some(&texture)); name_row.add_prefix(&image); } } } identity_box.append(&name_row); // Version if let Some(ref version) = record.app_version { let row = adw::ActionRow::builder() .title(&i18n("Version")) .subtitle(version) .build(); identity_box.append(&row); } content.append(&identity_box); // What will happen section let actions_box = gtk::ListBox::new(); actions_box.add_css_class("boxed-list"); actions_box.set_selection_mode(gtk::SelectionMode::None); actions_box.update_property(&[ gtk::accessible::Property::Label(&i18n("Integration actions")), ]); let desktop_row = adw::ActionRow::builder() .title(&i18n("Desktop entry")) .subtitle(&i18n("A .desktop file will be created in ~/.local/share/applications")) .build(); let check1 = gtk::Image::from_icon_name("emblem-ok-symbolic"); check1.set_valign(gtk::Align::Center); desktop_row.add_prefix(&check1); actions_box.append(&desktop_row); let icon_row = adw::ActionRow::builder() .title(&i18n("Icon")) .subtitle(&i18n("The app icon will be installed to ~/.local/share/icons")) .build(); let check2 = gtk::Image::from_icon_name("emblem-ok-symbolic"); check2.set_valign(gtk::Align::Center); icon_row.add_prefix(&check2); actions_box.append(&icon_row); content.append(&actions_box); // Runtime compatibility warnings - now with styled banner let wayland_status = record .wayland_status .as_deref() .map(WaylandStatus::from_str) .unwrap_or(WaylandStatus::Unknown); let fuse_status = record .fuse_status .as_deref() .map(FuseStatus::from_str) .unwrap_or(FuseStatus::MissingLibfuse2); let mut warnings: Vec<(String, String, String)> = Vec::new(); if wayland_status == WaylandStatus::X11Only { warnings.push(( i18n("X11 only"), i18n("This app does not support Wayland and will run through XWayland"), "X11".to_string(), )); } else if wayland_status == WaylandStatus::XWayland { warnings.push(( i18n("XWayland"), i18n("This app runs through the XWayland compatibility layer"), "XWayland".to_string(), )); } if !fuse_status.is_functional() { let fuse_msg = match fuse_status { FuseStatus::Fuse3Only => i18n("Only FUSE3 is installed - libfuse2 may be needed"), FuseStatus::NoFusermount => i18n("fusermount not found - AppImage mount may fail"), FuseStatus::NoDevFuse => i18n("/dev/fuse not available - AppImage mount will fail"), FuseStatus::MissingLibfuse2 => i18n("libfuse2 not installed - fallback extraction will be used"), _ => i18n("FUSE issue detected"), }; warnings.push(("FUSE".to_string(), fuse_msg, fuse_status.label().to_string())); } if !warnings.is_empty() { // Styled warning banner let warning_box = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(8) .build(); warning_box.add_css_class("compat-warning-banner"); let warning_header = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); let warning_icon = gtk::Image::from_icon_name("dialog-warning-symbolic"); warning_icon.set_pixel_size(16); warning_header.append(&warning_icon); let warning_title = gtk::Label::builder() .label(&i18n("Compatibility Notes")) .css_classes(["title-4"]) .halign(gtk::Align::Start) .build(); warning_header.append(&warning_title); warning_box.append(&warning_header); let compat_list = gtk::ListBox::new(); compat_list.add_css_class("boxed-list"); compat_list.set_selection_mode(gtk::SelectionMode::None); for (title, subtitle, badge_text) in &warnings { let row = adw::ActionRow::builder() .title(title.as_str()) .subtitle(subtitle.as_str()) .build(); let badge = widgets::status_badge(badge_text, "warning"); badge.set_valign(gtk::Align::Center); row.add_suffix(&badge); compat_list.append(&row); } warning_box.append(&compat_list); content.append(&warning_box); } dialog.set_extra_child(Some(&content)); let record_clone = record.clone(); let db_ref = db.clone(); let record_id = record.id; dialog.connect_response(None, move |_dialog, response| { if response == "integrate" { match integrator::integrate_tracked(&record_clone, &db_ref) { Ok(result) => { if let Some(ref icon_path) = result.icon_install_path { log::info!("Icon installed to: {}", icon_path.display()); } db_ref .set_integrated( record_id, true, Some(&result.desktop_file_path.to_string_lossy()), ) .ok(); on_complete(true); } Err(e) => { log::error!("Integration failed: {}", e); on_complete(false); } } } else { on_complete(false); } }); dialog.present(Some(parent)); }