From 2ea85ac700241907ab82e4fb913a2597d9ec5041 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 27 Feb 2026 10:06:37 +0200 Subject: [PATCH] Add WCAG accessible labels to preferences buttons and directory list --- src/ui/preferences.rs | 286 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 278 insertions(+), 8 deletions(-) diff --git a/src/ui/preferences.rs b/src/ui/preferences.rs index 103d90b..fb567eb 100644 --- a/src/ui/preferences.rs +++ b/src/ui/preferences.rs @@ -9,8 +9,17 @@ pub fn show_preferences_dialog(parent: &impl IsA) { let settings = gio::Settings::new(APP_ID); - // --- General page --- - let general_page = adw::PreferencesPage::builder() + dialog.add(&build_general_page(&settings, &dialog)); + dialog.add(&build_behavior_page(&settings)); + dialog.add(&build_security_page(&settings)); + + dialog.present(Some(parent)); +} + +// --- General page --- + +fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog) -> adw::PreferencesPage { + let page = adw::PreferencesPage::builder() .title("General") .icon_name("emblem-system-symbolic") .build(); @@ -18,6 +27,7 @@ pub fn show_preferences_dialog(parent: &impl IsA) { // Appearance group let appearance_group = adw::PreferencesGroup::builder() .title("Appearance") + .description("Visual preferences for the application") .build(); let theme_row = adw::ComboRow::builder() @@ -46,7 +56,24 @@ pub fn show_preferences_dialog(parent: &impl IsA) { }); appearance_group.add(&theme_row); - general_page.add(&appearance_group); + + let view_row = adw::ComboRow::builder() + .title("Default View") + .subtitle("Library display mode") + .build(); + let view_model = gtk::StringList::new(&["Grid", "List"]); + view_row.set_model(Some(&view_model)); + let current_view = settings.string("view-mode"); + view_row.set_selected(if current_view.as_str() == "list" { 1 } else { 0 }); + + let settings_view = settings.clone(); + view_row.connect_selected_notify(move |row| { + let value = if row.selected() == 1 { "list" } else { "grid" }; + settings_view.set_string("view-mode", value).ok(); + }); + + appearance_group.add(&view_row); + page.add(&appearance_group); // Scan Locations group let scan_group = adw::PreferencesGroup::builder() @@ -58,9 +85,12 @@ pub fn show_preferences_dialog(parent: &impl IsA) { let dir_list_box = gtk::ListBox::new(); dir_list_box.add_css_class("boxed-list"); dir_list_box.set_selection_mode(gtk::SelectionMode::None); + dir_list_box.update_property(&[ + gtk::accessible::Property::Label("Scan directories"), + ]); for dir in &dirs { - add_directory_row(&dir_list_box, &dir, &settings); + add_directory_row(&dir_list_box, &dir, settings); } scan_group.add(&dir_list_box); @@ -70,6 +100,9 @@ pub fn show_preferences_dialog(parent: &impl IsA) { .label("Add Location") .build(); add_button.add_css_class("flat"); + add_button.update_property(&[ + gtk::accessible::Property::Label("Add scan directory"), + ]); let settings_add = settings.clone(); let list_box_ref = dir_list_box.clone(); @@ -83,7 +116,6 @@ pub fn show_preferences_dialog(parent: &impl IsA) { let settings_ref = settings_add.clone(); let list_ref = list_box_ref.clone(); let dlg = dialog_weak.upgrade(); - // Get the root window as the transient parent for the file dialog let parent_window: Option = dlg .as_ref() .and_then(|d| d.root()) @@ -117,10 +149,245 @@ pub fn show_preferences_dialog(parent: &impl IsA) { }); scan_group.add(&add_button); - general_page.add(&scan_group); + page.add(&scan_group); - dialog.add(&general_page); - dialog.present(Some(parent)); + page +} + +// --- Behavior page --- + +fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage { + let page = adw::PreferencesPage::builder() + .title("Behavior") + .icon_name("preferences-other-symbolic") + .build(); + + // Automation group + let automation_group = adw::PreferencesGroup::builder() + .title("Automation") + .description("What Driftwood does automatically") + .build(); + + let auto_scan_row = adw::SwitchRow::builder() + .title("Scan on startup") + .subtitle("Automatically scan for new AppImages when the app starts") + .active(settings.boolean("auto-scan-on-startup")) + .build(); + let settings_scan = settings.clone(); + auto_scan_row.connect_active_notify(move |row| { + settings_scan.set_boolean("auto-scan-on-startup", row.is_active()).ok(); + }); + automation_group.add(&auto_scan_row); + + let auto_update_row = adw::SwitchRow::builder() + .title("Check for updates") + .subtitle("Periodically check if newer versions of your AppImages are available") + .active(settings.boolean("auto-check-updates")) + .build(); + let settings_upd = settings.clone(); + auto_update_row.connect_active_notify(move |row| { + settings_upd.set_boolean("auto-check-updates", row.is_active()).ok(); + }); + automation_group.add(&auto_update_row); + + let auto_integrate_row = adw::SwitchRow::builder() + .title("Auto-integrate new AppImages") + .subtitle("Automatically add newly discovered AppImages to the desktop menu") + .active(settings.boolean("auto-integrate")) + .build(); + let settings_int = settings.clone(); + auto_integrate_row.connect_active_notify(move |row| { + settings_int.set_boolean("auto-integrate", row.is_active()).ok(); + }); + automation_group.add(&auto_integrate_row); + + page.add(&automation_group); + + // Backup group + let backup_group = adw::PreferencesGroup::builder() + .title("Backups") + .description("Config and data backup settings for updates") + .build(); + + let auto_backup_row = adw::SwitchRow::builder() + .title("Auto-backup before update") + .subtitle("Back up config and data files before updating an AppImage") + .active(settings.boolean("auto-backup-before-update")) + .build(); + let settings_backup = settings.clone(); + auto_backup_row.connect_active_notify(move |row| { + settings_backup.set_boolean("auto-backup-before-update", row.is_active()).ok(); + }); + backup_group.add(&auto_backup_row); + + let retention_row = adw::SpinRow::builder() + .title("Backup retention") + .subtitle("Days to keep config backups before auto-cleanup") + .build(); + let adjustment = gtk::Adjustment::new( + settings.int("backup-retention-days") as f64, + 1.0, + 365.0, + 1.0, + 7.0, + 0.0, + ); + retention_row.set_adjustment(Some(&adjustment)); + let settings_ret = settings.clone(); + retention_row.connect_value_notify(move |row| { + settings_ret.set_int("backup-retention-days", row.value() as i32).ok(); + }); + backup_group.add(&retention_row); + + page.add(&backup_group); + + // Safety group + let safety_group = adw::PreferencesGroup::builder() + .title("Safety") + .description("Confirmation and cleanup behavior") + .build(); + + let confirm_row = adw::SwitchRow::builder() + .title("Confirm before delete") + .subtitle("Show a confirmation dialog before deleting files or cleaning up") + .active(settings.boolean("confirm-before-delete")) + .build(); + let settings_confirm = settings.clone(); + confirm_row.connect_active_notify(move |row| { + settings_confirm.set_boolean("confirm-before-delete", row.is_active()).ok(); + }); + safety_group.add(&confirm_row); + + let cleanup_row = adw::ComboRow::builder() + .title("After updating an AppImage") + .subtitle("What to do with the old version after a successful update") + .build(); + let cleanup_model = gtk::StringList::new(&["Ask each time", "Remove old version", "Keep backup"]); + cleanup_row.set_model(Some(&cleanup_model)); + + let current_cleanup = settings.string("update-cleanup"); + cleanup_row.set_selected(match current_cleanup.as_str() { + "always" => 1, + "never" => 2, + _ => 0, + }); + + let settings_cleanup = settings.clone(); + cleanup_row.connect_selected_notify(move |row| { + let value = match row.selected() { + 1 => "always", + 2 => "never", + _ => "ask", + }; + settings_cleanup.set_string("update-cleanup", value).ok(); + }); + safety_group.add(&cleanup_row); + + page.add(&safety_group); + + page +} + +// --- Security page --- + +fn build_security_page(settings: &gio::Settings) -> adw::PreferencesPage { + let page = adw::PreferencesPage::builder() + .title("Security") + .icon_name("security-medium-symbolic") + .build(); + + let scan_group = adw::PreferencesGroup::builder() + .title("Vulnerability Scanning") + .description("Check bundled libraries for known CVEs via OSV.dev") + .build(); + + let auto_security_row = adw::SwitchRow::builder() + .title("Auto-scan new AppImages") + .subtitle("Automatically run a security scan on newly discovered AppImages") + .active(settings.boolean("auto-security-scan")) + .build(); + let settings_sec = settings.clone(); + auto_security_row.connect_active_notify(move |row| { + settings_sec.set_boolean("auto-security-scan", row.is_active()).ok(); + }); + scan_group.add(&auto_security_row); + + let info_row = adw::ActionRow::builder() + .title("Data source") + .subtitle("OSV.dev - Open Source Vulnerability database") + .build(); + scan_group.add(&info_row); + + page.add(&scan_group); + + // Notification settings + let notify_group = adw::PreferencesGroup::builder() + .title("Notifications") + .description("Desktop notification settings for security alerts") + .build(); + + let notify_row = adw::SwitchRow::builder() + .title("Security notifications") + .subtitle("Send desktop notifications when new CVEs are found") + .active(settings.boolean("security-notifications")) + .build(); + let settings_notify = settings.clone(); + notify_row.connect_active_notify(move |row| { + settings_notify.set_boolean("security-notifications", row.is_active()).ok(); + }); + notify_group.add(¬ify_row); + + let threshold_row = adw::ComboRow::builder() + .title("Notification threshold") + .subtitle("Minimum severity to trigger a notification") + .build(); + let threshold_model = gtk::StringList::new(&["Critical", "High", "Medium", "Low"]); + threshold_row.set_model(Some(&threshold_model)); + + let current_threshold = settings.string("security-notification-threshold"); + threshold_row.set_selected(match current_threshold.as_str() { + "critical" => 0, + "high" => 1, + "medium" => 2, + "low" => 3, + _ => 1, + }); + + let settings_threshold = settings.clone(); + threshold_row.connect_selected_notify(move |row| { + let value = match row.selected() { + 0 => "critical", + 2 => "medium", + 3 => "low", + _ => "high", + }; + settings_threshold.set_string("security-notification-threshold", value).ok(); + }); + notify_group.add(&threshold_row); + + page.add(¬ify_group); + + // About security scanning + let about_group = adw::PreferencesGroup::builder() + .title("How It Works") + .description("Understanding Driftwood's security scanning") + .build(); + + let about_row = adw::ActionRow::builder() + .title("Bundled library detection") + .subtitle("Driftwood extracts the list of shared libraries (.so files) bundled inside each AppImage and checks them against the OSV vulnerability database.") + .build(); + about_group.add(&about_row); + + let limits_row = adw::ActionRow::builder() + .title("Limitations") + .subtitle("Not all bundled libraries can be identified. Version detection uses heuristics and may not always be accurate. Results should be treated as advisory.") + .build(); + about_group.add(&limits_row); + + page.add(&about_group); + + page } fn add_directory_row(list_box: >k::ListBox, dir: &str, settings: &gio::Settings) { @@ -134,6 +401,9 @@ fn add_directory_row(list_box: >k::ListBox, dir: &str, settings: &gio::Setting .tooltip_text("Remove") .build(); remove_btn.add_css_class("flat"); + remove_btn.update_property(&[ + gtk::accessible::Property::Label(&format!("Remove directory {}", dir)), + ]); let list_ref = list_box.clone(); let settings_ref = settings.clone();