Add WCAG accessible labels to integration dialog list boxes
This commit is contained in:
211
src/ui/integration_dialog.rs
Normal file
211
src/ui/integration_dialog.rs
Normal file
@@ -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<gtk::Widget>,
|
||||||
|
record: &AppImageRecord,
|
||||||
|
db: &Rc<Database>,
|
||||||
|
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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user