From 1a608c010969ea9e52bf990ff249a36ea0241854 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 27 Feb 2026 10:06:33 +0200 Subject: [PATCH] Add WCAG accessible labels to integration dialog list boxes --- src/ui/integration_dialog.rs | 211 +++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/ui/integration_dialog.rs diff --git a/src/ui/integration_dialog.rs b/src/ui/integration_dialog.rs new file mode 100644 index 0000000..ecfb4fa --- /dev/null +++ b/src/ui/integration_dialog.rs @@ -0,0 +1,211 @@ +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 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(&format!("Integrate {}?", name)) + .body("This will add the application to your desktop menu.") + .close_response("cancel") + .default_response("integrate") + .build(); + + dialog.add_response("cancel", "Cancel"); + dialog.add_response("integrate", "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("Application details"), + ]); + + // Name + let name_row = adw::ActionRow::builder() + .title("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("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("Integration actions"), + ]); + + let desktop_row = adw::ActionRow::builder() + .title("Desktop entry") + .subtitle("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("Icon") + .subtitle("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<(&str, &str, &str)> = Vec::new(); + + if wayland_status == WaylandStatus::X11Only { + warnings.push(("X11 only", "This app does not support Wayland and will run through XWayland", "X11")); + } else if wayland_status == WaylandStatus::XWayland { + warnings.push(("XWayland", "This app runs through the XWayland compatibility layer", "XWayland")); + } + + if !fuse_status.is_functional() { + let fuse_msg = match fuse_status { + FuseStatus::Fuse3Only => "Only FUSE3 is installed - libfuse2 may be needed", + FuseStatus::NoFusermount => "fusermount not found - AppImage mount may fail", + FuseStatus::NoDevFuse => "/dev/fuse not available - AppImage mount will fail", + FuseStatus::MissingLibfuse2 => "libfuse2 not installed - fallback extraction will be used", + _ => "FUSE issue detected", + }; + warnings.push(("FUSE", fuse_msg, fuse_status.label())); + } + + 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("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) + .subtitle(*subtitle) + .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(&record_clone) { + 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)); +}