Add WCAG 2.2 AAA compliance and automated AT-SPI audit tool
- Bring all UI widgets to WCAG 2.2 AAA conformance across all views - Add accessible labels, roles, descriptions, and announcements - Bump focus outlines to 3px, target sizes to 44px AAA minimum - Fix announce()/announce_result() to walk widget tree via parent() - Add AT-SPI accessibility audit script (tools/a11y-audit.py) that checks SC 4.1.2, 1.1.1, 1.3.1, 2.1.1, 2.5.5, 2.5.8, 2.4.8, 2.4.9, 2.4.10, 2.1.3 with JSON report output for CI - Clean up project structure, archive old plan documents
This commit is contained in:
285
src/window.rs
285
src/window.rs
@@ -18,7 +18,7 @@ use crate::core::orphan;
|
||||
use crate::core::security;
|
||||
use crate::core::updater;
|
||||
use crate::core::watcher;
|
||||
use crate::i18n::{i18n, ni18n_f};
|
||||
use crate::i18n::{i18n, i18n_f, ni18n_f};
|
||||
use crate::ui::catalog_view;
|
||||
use crate::ui::cleanup_wizard;
|
||||
use crate::ui::dashboard;
|
||||
@@ -128,6 +128,9 @@ fn shortcut_row(accel: &str, description: &str) -> adw::ActionRow {
|
||||
.css_classes(["monospace", "dimmed"])
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
accel_label.update_property(&[gtk::accessible::Property::Label(
|
||||
&format!("Keyboard shortcut: {}", accel),
|
||||
)]);
|
||||
row.add_suffix(&accel_label);
|
||||
row
|
||||
}
|
||||
@@ -163,6 +166,9 @@ impl DriftwoodWindow {
|
||||
}
|
||||
|
||||
fn setup_ui(&self) {
|
||||
// Set initial window title for screen readers (WCAG 2.4.2)
|
||||
self.set_title(Some("Driftwood"));
|
||||
|
||||
// Build the hamburger menu model (slim - tabs handle catalog/updates/scan)
|
||||
let menu = gio::Menu::new();
|
||||
menu.append(Some(&i18n("Dashboard")), Some("win.dashboard"));
|
||||
@@ -213,11 +219,15 @@ impl DriftwoodWindow {
|
||||
.reveal(true)
|
||||
.build();
|
||||
|
||||
// Main content box: ViewStack + bottom switcher bar
|
||||
// Toast overlay wraps only the ViewStack so toasts appear above the tab bar
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
toast_overlay.set_child(Some(&view_stack));
|
||||
|
||||
// Main content box: toast-wrapped ViewStack + bottom switcher bar
|
||||
let main_box = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
main_box.append(&view_stack);
|
||||
main_box.append(&toast_overlay);
|
||||
main_box.append(&view_switcher_bar);
|
||||
|
||||
// Drop overlay - centered opaque card over a dimmed scrim
|
||||
@@ -225,6 +235,7 @@ impl DriftwoodWindow {
|
||||
.icon_name("document-open-symbolic")
|
||||
.pixel_size(64)
|
||||
.halign(gtk::Align::Center)
|
||||
.accessible_role(gtk::AccessibleRole::Presentation)
|
||||
.build();
|
||||
drop_overlay_icon.add_css_class("drop-zone-icon");
|
||||
|
||||
@@ -248,9 +259,12 @@ impl DriftwoodWindow {
|
||||
.halign(gtk::Align::Center)
|
||||
.valign(gtk::Align::Center)
|
||||
.width_request(320)
|
||||
.focusable(true)
|
||||
.accessible_role(gtk::AccessibleRole::Button)
|
||||
.build();
|
||||
drop_zone_card.add_css_class("drop-zone-card");
|
||||
drop_zone_card.set_cursor_from_name(Some("pointer"));
|
||||
drop_zone_card.update_property(&[gtk::accessible::Property::Label("Browse files to add an AppImage")]);
|
||||
drop_zone_card.append(&drop_overlay_icon);
|
||||
drop_zone_card.append(&drop_overlay_title);
|
||||
drop_zone_card.append(&drop_overlay_subtitle);
|
||||
@@ -267,6 +281,23 @@ impl DriftwoodWindow {
|
||||
drop_zone_card.add_controller(card_click);
|
||||
}
|
||||
|
||||
// Keyboard activation: Enter/Space on the card opens file picker
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
let key_ctrl = gtk::EventControllerKey::new();
|
||||
key_ctrl.connect_key_pressed(move |_ctrl, key, _code, _mods| {
|
||||
if matches!(key, gtk::gdk::Key::Return | gtk::gdk::Key::KP_Enter | gtk::gdk::Key::space) {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
window.open_file_picker();
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
drop_zone_card.add_controller(key_ctrl);
|
||||
}
|
||||
|
||||
// Revealer for crossfade animation
|
||||
let drop_revealer = gtk::Revealer::builder()
|
||||
.transition_type(gtk::RevealerTransitionType::Crossfade)
|
||||
@@ -308,15 +339,41 @@ impl DriftwoodWindow {
|
||||
drop_overlay_content.add_controller(click);
|
||||
}
|
||||
|
||||
// Escape key dismisses the overlay (WCAG 2.1.1 keyboard access)
|
||||
{
|
||||
let overlay_ref = drop_overlay_content.clone();
|
||||
let revealer_ref = drop_revealer.clone();
|
||||
let window_weak = self.downgrade();
|
||||
let key_ctrl = gtk::EventControllerKey::new();
|
||||
key_ctrl.connect_key_pressed(move |_ctrl, key, _code, _mods| {
|
||||
if key == gtk::gdk::Key::Escape && overlay_ref.is_visible() {
|
||||
revealer_ref.set_reveal_child(false);
|
||||
let overlay_hide = overlay_ref.clone();
|
||||
glib::timeout_add_local_once(
|
||||
std::time::Duration::from_millis(200),
|
||||
move || { overlay_hide.set_visible(false); },
|
||||
);
|
||||
// Return focus to main content
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
drop_overlay_content.add_controller(key_ctrl);
|
||||
}
|
||||
|
||||
// Overlay wraps main content so the drop indicator sits on top
|
||||
let overlay = gtk::Overlay::new();
|
||||
overlay.set_child(Some(&main_box));
|
||||
overlay.add_overlay(&drop_overlay_content);
|
||||
|
||||
// Toast overlay wraps the overlay
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
toast_overlay.set_child(Some(&overlay));
|
||||
|
||||
// --- Drag-and-drop support ---
|
||||
let drop_target = gtk::DropTarget::new(gio::File::static_type(), gtk::gdk::DragAction::COPY);
|
||||
|
||||
@@ -371,7 +428,7 @@ impl DriftwoodWindow {
|
||||
|
||||
// Validate it's an AppImage via magic bytes
|
||||
if discovery::detect_appimage(&path).is_none() {
|
||||
toast_ref.add_toast(adw::Toast::new(&i18n("Not a valid AppImage file")));
|
||||
toast_ref.add_toast(widgets::error_toast(&i18n("Not a valid AppImage file")));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -402,9 +459,9 @@ impl DriftwoodWindow {
|
||||
});
|
||||
}
|
||||
|
||||
toast_overlay.add_controller(drop_target);
|
||||
overlay.add_controller(drop_target);
|
||||
|
||||
self.set_content(Some(&toast_overlay));
|
||||
self.set_content(Some(&overlay));
|
||||
|
||||
// Wire up card/row activation to push detail view (or toggle selection)
|
||||
{
|
||||
@@ -449,16 +506,21 @@ impl DriftwoodWindow {
|
||||
{
|
||||
let db = self.database().clone();
|
||||
let window_weak = self.downgrade();
|
||||
installed_nav.connect_popped(move |_nav, page| {
|
||||
if page.tag().as_deref() == Some("detail") {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
// Update window title for accessibility (WCAG 2.4.8)
|
||||
window.set_title(Some("Driftwood"));
|
||||
installed_nav.connect_popped(move |_nav, _page| {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
// Update window title for accessibility (WCAG 2.4.8)
|
||||
window.set_title(Some("Driftwood"));
|
||||
|
||||
let lib_view = window.imp().library_view.get().unwrap();
|
||||
match db.get_all_appimages() {
|
||||
Ok(records) => lib_view.populate(records),
|
||||
Err(_) => lib_view.set_state(LibraryState::Empty),
|
||||
let lib_view = window.imp().library_view.get().unwrap();
|
||||
match db.get_all_appimages() {
|
||||
Ok(records) => lib_view.populate(records),
|
||||
Err(_) => lib_view.set_state(LibraryState::Empty),
|
||||
}
|
||||
|
||||
// Return focus to the visible content (WCAG 2.4.3)
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,6 +538,10 @@ impl DriftwoodWindow {
|
||||
window.set_title(Some(&format!("Driftwood - {}", page_title)));
|
||||
}
|
||||
}
|
||||
// Move focus to the pushed page (WCAG 2.4.3)
|
||||
if let Some(visible) = nav.visible_page() {
|
||||
visible.grab_focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -533,7 +599,7 @@ impl DriftwoodWindow {
|
||||
// Scan action - runs real scan
|
||||
let scan_action = gio::ActionEntry::builder("scan")
|
||||
.activate(|window: &Self, _, _| {
|
||||
window.trigger_scan();
|
||||
window.trigger_scan(None);
|
||||
})
|
||||
.build();
|
||||
|
||||
@@ -558,10 +624,10 @@ impl DriftwoodWindow {
|
||||
summary.icons_removed,
|
||||
i18n("icons"),
|
||||
);
|
||||
toast_ref.add_toast(adw::Toast::new(&msg));
|
||||
toast_ref.add_toast(widgets::info_toast(&msg));
|
||||
}
|
||||
_ => {
|
||||
toast_ref.add_toast(adw::Toast::new(&i18n("Failed to clean orphaned entries")));
|
||||
toast_ref.add_toast(widgets::error_toast(&i18n("Failed to clean orphaned entries")));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -590,14 +656,19 @@ impl DriftwoodWindow {
|
||||
|
||||
match result {
|
||||
Ok(0) => {
|
||||
toast_ref.add_toast(adw::Toast::new(&i18n("All AppImages are up to date")));
|
||||
toast_ref.add_toast(widgets::info_toast(&i18n("All AppImages are up to date")));
|
||||
}
|
||||
Ok(n) => {
|
||||
let msg = format!("{} update{} available", n, if n == 1 { "" } else { "s" });
|
||||
toast_ref.add_toast(adw::Toast::new(&msg));
|
||||
let msg = ni18n_f(
|
||||
"{count} update available",
|
||||
"{count} updates available",
|
||||
n as u32,
|
||||
&[("{count}", &n.to_string())],
|
||||
);
|
||||
toast_ref.add_toast(widgets::info_toast(&msg));
|
||||
}
|
||||
Err(_) => {
|
||||
toast_ref.add_toast(adw::Toast::new(&i18n("Failed to check for updates")));
|
||||
toast_ref.add_toast(widgets::error_toast(&i18n("Failed to check for updates")));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -636,6 +707,9 @@ impl DriftwoodWindow {
|
||||
.activate(|window: &Self, _, _| {
|
||||
let view_stack = window.imp().view_stack.get().unwrap();
|
||||
view_stack.set_visible_child_name("catalog");
|
||||
if let Some(child) = view_stack.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
@@ -653,7 +727,13 @@ impl DriftwoodWindow {
|
||||
overlay.set_visible(true);
|
||||
if let Some(revealer) = window.imp().drop_revealer.get() {
|
||||
revealer.set_reveal_child(true);
|
||||
// Move focus into the drop zone card (WCAG 2.4.3)
|
||||
if let Some(card) = revealer.child() {
|
||||
card.grab_focus();
|
||||
}
|
||||
}
|
||||
// Announce to screen readers
|
||||
widgets::announce(overlay, "Drop zone opened. Drop an AppImage file or press Enter to browse.");
|
||||
}
|
||||
})
|
||||
.build();
|
||||
@@ -703,6 +783,21 @@ impl DriftwoodWindow {
|
||||
_ => crate::ui::library_view::SortMode::NameAsc,
|
||||
};
|
||||
lib_view.set_sort_mode(sort_mode);
|
||||
|
||||
// Announce sort change for screen readers (WCAG 4.1.3)
|
||||
let sort_label = match mode_str.as_str() {
|
||||
"recent" => "Sorted by recently added",
|
||||
"size" => "Sorted by size",
|
||||
_ => "Sorted by name",
|
||||
};
|
||||
if let Some(toast_overlay) = window.imp().toast_overlay.get() {
|
||||
let toast = adw::Toast::builder()
|
||||
.title(sort_label)
|
||||
.timeout(2)
|
||||
.build();
|
||||
toast_overlay.add_toast(toast);
|
||||
}
|
||||
|
||||
let settings_key = match mode_str.as_str() {
|
||||
"recent" => "recently-added",
|
||||
"size" => "size",
|
||||
@@ -739,7 +834,7 @@ impl DriftwoodWindow {
|
||||
}
|
||||
lib_view.exit_selection_mode();
|
||||
if count > 0 {
|
||||
toast_overlay.add_toast(adw::Toast::new(&format!("Integrated {} apps", count)));
|
||||
toast_overlay.add_toast(widgets::info_toast(&ni18n_f("Integrated {} app", "Integrated {} apps", count as u32, &[("{}", &count.to_string())])));
|
||||
if let Ok(records) = db.get_all_appimages() {
|
||||
lib_view.populate(records);
|
||||
}
|
||||
@@ -772,7 +867,7 @@ impl DriftwoodWindow {
|
||||
}
|
||||
lib_view.exit_selection_mode();
|
||||
if count > 0 {
|
||||
toast_overlay.add_toast(adw::Toast::new(&format!("Deleted {} apps", count)));
|
||||
toast_overlay.add_toast(widgets::info_toast(&ni18n_f("Deleted {} app", "Deleted {} apps", count as u32, &[("{}", &count.to_string())])));
|
||||
if let Ok(records) = db.get_all_appimages() {
|
||||
lib_view.populate(records);
|
||||
}
|
||||
@@ -842,11 +937,7 @@ impl DriftwoodWindow {
|
||||
}
|
||||
Ok(launcher::LaunchResult::Failed(msg)) => {
|
||||
log::error!("Failed to launch: {}", msg);
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&format!("Could not launch: {}", msg))
|
||||
.timeout(5)
|
||||
.build();
|
||||
toast_overlay.add_toast(toast);
|
||||
toast_overlay.add_toast(widgets::error_toast(&i18n_f("Could not launch: {error}", &[("{error}", &msg)])));
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Launch task panicked");
|
||||
@@ -897,9 +988,9 @@ impl DriftwoodWindow {
|
||||
false
|
||||
}).await;
|
||||
match result {
|
||||
Ok(true) => toast_overlay.add_toast(adw::Toast::new("Update available!")),
|
||||
Ok(false) => toast_overlay.add_toast(adw::Toast::new("Already up to date")),
|
||||
Err(_) => toast_overlay.add_toast(adw::Toast::new("Update check failed")),
|
||||
Ok(true) => toast_overlay.add_toast(widgets::info_toast(&i18n("Update available!"))),
|
||||
Ok(false) => toast_overlay.add_toast(widgets::info_toast(&i18n("Already up to date"))),
|
||||
Err(_) => toast_overlay.add_toast(widgets::error_toast(&i18n("Update check failed"))),
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -927,10 +1018,10 @@ impl DriftwoodWindow {
|
||||
match result {
|
||||
Ok(Some(total)) => {
|
||||
if total == 0 {
|
||||
toast_overlay.add_toast(adw::Toast::new("No vulnerabilities found"));
|
||||
toast_overlay.add_toast(widgets::info_toast(&i18n("No vulnerabilities found")));
|
||||
} else {
|
||||
let msg = format!("Found {} CVE{}", total, if total == 1 { "" } else { "s" });
|
||||
toast_overlay.add_toast(adw::Toast::new(&msg));
|
||||
let msg = ni18n_f("Found {} CVE", "Found {} CVEs", total as u32, &[("{}", &total.to_string())]);
|
||||
toast_overlay.add_toast(widgets::info_toast(&msg));
|
||||
}
|
||||
|
||||
// Send desktop notifications for new CVE findings if enabled
|
||||
@@ -946,7 +1037,7 @@ impl DriftwoodWindow {
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => toast_overlay.add_toast(adw::Toast::new("Security scan failed")),
|
||||
_ => toast_overlay.add_toast(widgets::error_toast(&i18n("Security scan failed"))),
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -970,8 +1061,9 @@ impl DriftwoodWindow {
|
||||
db.set_integrated(record_id, false, None).ok();
|
||||
|
||||
let undo_toast = adw::Toast::builder()
|
||||
.title("Removed from app menu")
|
||||
.button_label("Undo")
|
||||
.title(i18n("Removed from app menu"))
|
||||
.button_label(i18n("Undo"))
|
||||
.timeout(7)
|
||||
.build();
|
||||
|
||||
let db_undo = db.clone();
|
||||
@@ -998,11 +1090,11 @@ impl DriftwoodWindow {
|
||||
Ok(result) => {
|
||||
let desktop_path = result.desktop_file_path.to_string_lossy().to_string();
|
||||
db.set_integrated(record_id, true, Some(&desktop_path)).ok();
|
||||
toast_overlay.add_toast(adw::Toast::new("Integrated into desktop menu"));
|
||||
toast_overlay.add_toast(widgets::info_toast(&i18n("Integrated into desktop menu")));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Integration failed: {}", e);
|
||||
toast_overlay.add_toast(adw::Toast::new("Integration failed"));
|
||||
toast_overlay.add_toast(widgets::error_toast(&i18n("Integration failed")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1047,7 +1139,7 @@ impl DriftwoodWindow {
|
||||
let display = gtk::prelude::WidgetExt::display(&window);
|
||||
let clipboard = display.clipboard();
|
||||
clipboard.set_text(&record.path);
|
||||
toast_overlay.add_toast(adw::Toast::new("Path copied to clipboard"));
|
||||
toast_overlay.add_toast(widgets::info_toast(&i18n("Path copied to clipboard")));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1103,6 +1195,9 @@ impl DriftwoodWindow {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
vs.set_visible_child_name("installed");
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1115,6 +1210,9 @@ impl DriftwoodWindow {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
vs.set_visible_child_name("catalog");
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1127,6 +1225,9 @@ impl DriftwoodWindow {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
vs.set_visible_child_name("updates");
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1181,15 +1282,17 @@ impl DriftwoodWindow {
|
||||
|
||||
// Scan on startup if enabled in preferences
|
||||
if self.settings().boolean("auto-scan-on-startup") {
|
||||
if let Some(toast_overlay) = self.imp().toast_overlay.get() {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
.title(&i18n("Scanning for apps in your configured folders..."))
|
||||
.timeout(2)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
self.trigger_scan();
|
||||
let scan_toast = if let Some(toast_overlay) = self.imp().toast_overlay.get() {
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&i18n("Scanning for apps in your configured folders..."))
|
||||
.timeout(10)
|
||||
.build();
|
||||
toast_overlay.add_toast(toast.clone());
|
||||
Some(toast)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.trigger_scan(scan_toast);
|
||||
}
|
||||
|
||||
// Start watching scan directories for new AppImage files
|
||||
@@ -1258,15 +1361,16 @@ impl DriftwoodWindow {
|
||||
if count > 0 {
|
||||
if let Some(toast_overlay) = update_toast {
|
||||
let title = if names.len() <= 3 {
|
||||
format!("Updates available: {}", names.join(", "))
|
||||
i18n_f("Updates available: {apps}", &[("{apps}", &names.join(", "))])
|
||||
} else {
|
||||
format!("{} app updates available ({}, ...)",
|
||||
count, names[..2].join(", "))
|
||||
i18n_f("{count} app updates available ({apps}, ...)",
|
||||
&[("{count}", &count.to_string()), ("{apps}", &names[..2].join(", "))])
|
||||
};
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&title)
|
||||
.button_label("View")
|
||||
.button_label(i18n("View"))
|
||||
.action_name("win.show-updates")
|
||||
.timeout(5)
|
||||
.build();
|
||||
toast_overlay.add_toast(toast);
|
||||
}
|
||||
@@ -1437,7 +1541,7 @@ impl DriftwoodWindow {
|
||||
});
|
||||
}
|
||||
|
||||
fn trigger_scan(&self) {
|
||||
fn trigger_scan(&self, scan_toast: Option<adw::Toast>) {
|
||||
let library_view = self.imp().library_view.get().unwrap();
|
||||
library_view.set_state(LibraryState::Loading);
|
||||
|
||||
@@ -1562,6 +1666,11 @@ impl DriftwoodWindow {
|
||||
.await;
|
||||
|
||||
if let Ok((total, new_count, needs_analysis)) = result {
|
||||
// Dismiss the "Scanning..." toast now that Phase 1 is done
|
||||
if let Some(ref toast) = scan_toast {
|
||||
toast.dismiss();
|
||||
}
|
||||
|
||||
// Refresh the library view immediately (apps appear with "Analyzing..." badge)
|
||||
let window_weak2 = window_weak.clone();
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
@@ -1579,7 +1688,7 @@ impl DriftwoodWindow {
|
||||
1 => i18n("Found 1 new AppImage"),
|
||||
n => format!("{} {} {}", i18n("Found"), n, i18n("new AppImages")),
|
||||
};
|
||||
toast_overlay.add_toast(adw::Toast::new(&msg));
|
||||
toast_overlay.add_toast(widgets::info_toast(&msg));
|
||||
|
||||
// Phase 2: Background analysis per file with debounced UI refresh
|
||||
let running = analysis::running_count();
|
||||
@@ -1693,7 +1802,7 @@ impl DriftwoodWindow {
|
||||
return glib::ControlFlow::Break;
|
||||
};
|
||||
if changed.swap(false, std::sync::atomic::Ordering::Relaxed) {
|
||||
window.trigger_scan();
|
||||
window.trigger_scan(None);
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
@@ -1772,8 +1881,7 @@ impl DriftwoodWindow {
|
||||
record.icon_path.as_deref(), name, 32,
|
||||
);
|
||||
row.add_prefix(&icon);
|
||||
let play_icon = gtk::Image::from_icon_name("media-playback-start-symbolic");
|
||||
row.add_suffix(&play_icon);
|
||||
row.add_suffix(&widgets::accessible_suffix_icon("media-playback-start-symbolic", &i18n("Launch")));
|
||||
|
||||
let record_id = record.id;
|
||||
let dialog_c = dialog_ref.clone();
|
||||
@@ -1805,8 +1913,7 @@ impl DriftwoodWindow {
|
||||
.build();
|
||||
let icon = widgets::app_icon(None, &app.name, 32);
|
||||
row.add_prefix(&icon);
|
||||
let nav_icon = gtk::Image::from_icon_name("go-next-symbolic");
|
||||
row.add_suffix(&nav_icon);
|
||||
row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("Open")));
|
||||
|
||||
let app_id = app.id;
|
||||
let dialog_c = dialog_ref.clone();
|
||||
@@ -1862,6 +1969,19 @@ impl DriftwoodWindow {
|
||||
|
||||
toolbar.set_content(Some(&content_box));
|
||||
dialog.set_child(Some(&toolbar));
|
||||
|
||||
// Return focus to main content when command palette closes (WCAG 2.4.3)
|
||||
let window_weak = self.downgrade();
|
||||
dialog.connect_closed(move |_| {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.present(Some(self));
|
||||
|
||||
// Focus the search entry after presenting
|
||||
@@ -1924,6 +2044,19 @@ impl DriftwoodWindow {
|
||||
scrolled.set_child(Some(&content));
|
||||
toolbar.set_content(Some(&scrolled));
|
||||
dialog.set_child(Some(&toolbar));
|
||||
|
||||
// Return focus to main content when dialog closes (WCAG 2.4.3)
|
||||
let window_weak = self.downgrade();
|
||||
dialog.connect_closed(move |_| {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
if let Some(vs) = window.imp().view_stack.get() {
|
||||
if let Some(child) = vs.visible_child() {
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.present(Some(self));
|
||||
}
|
||||
|
||||
@@ -1972,7 +2105,7 @@ impl DriftwoodWindow {
|
||||
// Validate it's an AppImage via magic bytes
|
||||
if discovery::detect_appimage(&path).is_none() {
|
||||
let toast_overlay = window.imp().toast_overlay.get().unwrap();
|
||||
toast_overlay.add_toast(adw::Toast::new(&i18n("Not a valid AppImage file")));
|
||||
toast_overlay.add_toast(widgets::error_toast(&i18n("Not a valid AppImage file")));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2046,12 +2179,12 @@ impl DriftwoodWindow {
|
||||
match backup::export_app_list(&db, &path) {
|
||||
Ok(count) => {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("Exported {} apps", count)),
|
||||
widgets::info_toast(&ni18n_f("Exported {} app", "Exported {} apps", count as u32, &[("{}", &count.to_string())])),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("Export failed: {}", e)),
|
||||
widgets::error_toast(&i18n_f("Export failed: {error}", &[("{error}", &e.to_string())])),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2082,15 +2215,17 @@ impl DriftwoodWindow {
|
||||
match backup::import_app_list(&db, &path) {
|
||||
Ok(result) => {
|
||||
let msg = if result.missing.is_empty() {
|
||||
format!("Imported metadata for {} apps", result.matched)
|
||||
ni18n_f("Imported metadata for {} app", "Imported metadata for {} apps", result.matched as u32, &[("{}", &result.matched.to_string())])
|
||||
} else {
|
||||
format!(
|
||||
"Imported {} apps, {} not found",
|
||||
result.matched,
|
||||
result.missing.len()
|
||||
i18n_f(
|
||||
"Imported {matched} apps, {missing} not found",
|
||||
&[
|
||||
("{matched}", &result.matched.to_string()),
|
||||
("{missing}", &result.missing.len().to_string()),
|
||||
],
|
||||
)
|
||||
};
|
||||
toast_overlay.add_toast(adw::Toast::new(&msg));
|
||||
toast_overlay.add_toast(widgets::info_toast(&msg));
|
||||
|
||||
// Show missing apps dialog if any
|
||||
if !result.missing.is_empty() {
|
||||
@@ -2100,7 +2235,7 @@ impl DriftwoodWindow {
|
||||
}
|
||||
Err(e) => {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("Import failed: {}", e)),
|
||||
widgets::error_toast(&i18n_f("Import failed: {error}", &[("{error}", &e.to_string())])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user