- Database v8 migration: tags, pinned, avg_startup_ms columns - Security scanning with CVE matching and batch scan - Bundled library extraction and vulnerability reports - Desktop notification system for security alerts - Backup/restore system for AppImage configurations - i18n framework with gettext support - Runtime analysis and Wayland compatibility detection - AppStream metadata and Flatpak-style build support - File watcher module for live directory monitoring - Preferences panel with GSettings integration - CLI interface for headless operation - Detail view: tabbed layout with ViewSwitcher in title bar, health score, sandbox controls, changelog links - Library view: sort dropdown, context menu enhancements - Dashboard: system status, disk usage, launch history - Security report page with scan and export - Packaging: meson build, PKGBUILD, metainfo
1124 lines
33 KiB
Markdown
1124 lines
33 KiB
Markdown
# WCAG 2.2 AAA Compliance Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Make Driftwood fully WCAG 2.2 AAA compliant across all four principles (Perceivable, Operable, Understandable, Robust).
|
|
|
|
**Architecture:** Hybrid approach - centralized accessibility helpers in `widgets.rs` for repeated patterns (labeled buttons, live announcements, described badges), plus direct `update_property`/`update_state`/`update_relation` calls in each UI file for unique cases. CSS additions in `style.css` for focus indicators, high-contrast mode, reduced-motion expansion, and target sizes.
|
|
|
|
**Tech Stack:** Rust, gtk4-rs (0.11), libadwaita-rs (0.9), GTK4 accessible API (`gtk::accessible::Property`, `gtk::accessible::State`, `gtk::accessible::Relation`, `gtk::AccessibleRole`)
|
|
|
|
---
|
|
|
|
### Task 1: CSS Foundation - Focus Indicators, High Contrast, Reduced Motion, Target Sizes
|
|
|
|
**Files:**
|
|
- Modify: `data/resources/style.css`
|
|
|
|
**Step 1: Add universal focus-visible indicators**
|
|
|
|
Append after the existing `flowboxchild:focus-visible .app-card` block (line 91). These ensure every focusable widget has a visible 2px accent-color outline meeting WCAG 2.4.7 and 2.4.13:
|
|
|
|
```css
|
|
/* ===== WCAG AAA Focus Indicators ===== */
|
|
button:focus-visible,
|
|
togglebutton:focus-visible,
|
|
menubutton:focus-visible,
|
|
checkbutton:focus-visible,
|
|
switch:focus-visible,
|
|
entry:focus-visible,
|
|
searchentry:focus-visible,
|
|
spinbutton:focus-visible {
|
|
outline: 2px solid @accent_bg_color;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
row:focus-visible {
|
|
outline: 2px solid @accent_bg_color;
|
|
outline-offset: -2px;
|
|
}
|
|
```
|
|
|
|
**Step 2: Add high-contrast media query**
|
|
|
|
Append a `prefers-contrast: more` section (WCAG 1.4.6 Enhanced Contrast, 1.4.11 Non-text Contrast):
|
|
|
|
```css
|
|
/* ===== High Contrast Mode (WCAG AAA 1.4.6) ===== */
|
|
@media (prefers-contrast: more) {
|
|
.app-card {
|
|
border: 2px solid @window_fg_color;
|
|
}
|
|
|
|
flowboxchild:focus-visible .app-card {
|
|
outline-width: 3px;
|
|
}
|
|
|
|
button:focus-visible,
|
|
togglebutton:focus-visible,
|
|
menubutton:focus-visible,
|
|
checkbutton:focus-visible,
|
|
switch:focus-visible,
|
|
entry:focus-visible,
|
|
searchentry:focus-visible,
|
|
spinbutton:focus-visible {
|
|
outline-width: 3px;
|
|
}
|
|
|
|
row:focus-visible {
|
|
outline-width: 3px;
|
|
}
|
|
|
|
.status-badge,
|
|
.status-badge-with-icon {
|
|
border: 1px solid currentColor;
|
|
}
|
|
|
|
.compat-warning-banner {
|
|
border: 2px solid @warning_bg_color;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: Expand reduced-motion to cover ALL transitions (WCAG 2.3.3)**
|
|
|
|
Replace the existing `@media (prefers-reduced-motion: reduce)` block (lines 152-160) with:
|
|
|
|
```css
|
|
/* ===== Reduced Motion (WCAG AAA 2.3.3) ===== */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
transition-duration: 0 !important;
|
|
transition-delay: 0 !important;
|
|
animation-duration: 0 !important;
|
|
animation-delay: 0 !important;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 4: Add minimum target size (WCAG 2.5.8)**
|
|
|
|
```css
|
|
/* ===== Minimum Target Size (WCAG 2.5.8) ===== */
|
|
button.flat.circular,
|
|
button.flat:not(.pill):not(.suggested-action):not(.destructive-action) {
|
|
min-width: 24px;
|
|
min-height: 24px;
|
|
}
|
|
```
|
|
|
|
**Step 5: Build to verify CSS loads**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success (CSS is loaded at runtime, not compiled)
|
|
|
|
**Step 6: Commit**
|
|
|
|
```
|
|
git add data/resources/style.css
|
|
git commit -m "Add WCAG AAA focus indicators, high-contrast mode, and reduced-motion coverage"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Accessibility Helpers in widgets.rs
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/widgets.rs`
|
|
|
|
**Step 1: Add accessible label to copy_button**
|
|
|
|
In the `copy_button` function (line 129-149), add an accessible label after creating the button. Change:
|
|
|
|
```rust
|
|
pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay>) -> gtk::Button {
|
|
let btn = gtk::Button::builder()
|
|
.icon_name("edit-copy-symbolic")
|
|
.tooltip_text("Copy to clipboard")
|
|
.valign(gtk::Align::Center)
|
|
.build();
|
|
btn.add_css_class("flat");
|
|
```
|
|
|
|
To:
|
|
|
|
```rust
|
|
pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay>) -> gtk::Button {
|
|
let btn = gtk::Button::builder()
|
|
.icon_name("edit-copy-symbolic")
|
|
.tooltip_text("Copy to clipboard")
|
|
.valign(gtk::Align::Center)
|
|
.build();
|
|
btn.add_css_class("flat");
|
|
btn.update_property(&[gtk::accessible::Property::Label("Copy to clipboard")]);
|
|
```
|
|
|
|
**Step 2: Add accessible description to status_badge**
|
|
|
|
Update `status_badge` (lines 5-10) to include a `RoleDescription`:
|
|
|
|
```rust
|
|
pub fn status_badge(text: &str, style_class: &str) -> gtk::Label {
|
|
let label = gtk::Label::new(Some(text));
|
|
label.add_css_class("status-badge");
|
|
label.add_css_class(style_class);
|
|
label.set_accessible_role(gtk::AccessibleRole::Status);
|
|
label
|
|
}
|
|
```
|
|
|
|
**Step 3: Add accessible role to status_badge_with_icon**
|
|
|
|
Update `status_badge_with_icon` (lines 14-30) similarly:
|
|
|
|
```rust
|
|
pub fn status_badge_with_icon(icon_name: &str, text: &str, style_class: &str) -> gtk::Box {
|
|
let hbox = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.spacing(4)
|
|
.accessible_role(gtk::AccessibleRole::Status)
|
|
.build();
|
|
hbox.add_css_class("status-badge-with-icon");
|
|
hbox.add_css_class(style_class);
|
|
hbox.update_property(&[gtk::accessible::Property::Label(text)]);
|
|
|
|
let icon = gtk::Image::from_icon_name(icon_name);
|
|
icon.set_pixel_size(12);
|
|
hbox.append(&icon);
|
|
|
|
let label = gtk::Label::new(Some(text));
|
|
hbox.append(&label);
|
|
|
|
hbox
|
|
}
|
|
```
|
|
|
|
**Step 4: Add `announce()` live region helper function**
|
|
|
|
Add at the end of `widgets.rs`:
|
|
|
|
```rust
|
|
/// Create a screen-reader live region announcement.
|
|
/// Inserts a hidden label with AccessibleRole::Alert into the given container,
|
|
/// which causes AT-SPI to announce the text to screen readers.
|
|
/// The label auto-removes after a short delay.
|
|
pub fn announce(container: &impl gtk::prelude::IsA<gtk::Widget>, text: &str) {
|
|
let label = gtk::Label::builder()
|
|
.label(text)
|
|
.visible(false)
|
|
.accessible_role(gtk::AccessibleRole::Alert)
|
|
.build();
|
|
label.update_property(&[gtk::accessible::Property::Label(text)]);
|
|
|
|
// We need to add it to a container to make it part of the accessible tree.
|
|
// Use the widget's first ancestor that is a Box, or fall back to toast overlay.
|
|
// Since we cannot generically append to any widget, the caller should pass
|
|
// a gtk::Box or adw::ToastOverlay.
|
|
if let Some(box_widget) = container.dynamic_cast_ref::<gtk::Box>() {
|
|
box_widget.append(&label);
|
|
// Make visible briefly so AT-SPI picks it up, then remove
|
|
label.set_visible(true);
|
|
let label_clone = label.clone();
|
|
let box_clone = box_widget.clone();
|
|
glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
|
|
box_clone.remove(&label_clone);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 5: Add `use gtk::prelude::*;` import check**
|
|
|
|
The file already has `use gtk::prelude::*;` at line 1. No change needed.
|
|
|
|
**Step 6: Build to verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success with zero errors
|
|
|
|
**Step 7: Commit**
|
|
|
|
```
|
|
git add src/ui/widgets.rs
|
|
git commit -m "Add WCAG accessibility helpers: labeled badges, live announcements, copy button label"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Library View Accessible Labels and Roles
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/library_view.rs`
|
|
|
|
**Step 1: Add accessible labels to header bar buttons**
|
|
|
|
After each icon-only button is built, add an accessible label. After line 64 (menu_button):
|
|
|
|
```rust
|
|
menu_button.update_property(&[AccessibleProperty::Label("Main menu")]);
|
|
```
|
|
|
|
After line 70 (search_button):
|
|
|
|
```rust
|
|
search_button.update_property(&[AccessibleProperty::Label("Toggle search")]);
|
|
```
|
|
|
|
After line 77 (grid_button):
|
|
|
|
```rust
|
|
grid_button.update_property(&[AccessibleProperty::Label("Switch to grid view")]);
|
|
```
|
|
|
|
After line 84 (list_button):
|
|
|
|
```rust
|
|
list_button.update_property(&[AccessibleProperty::Label("Switch to list view")]);
|
|
```
|
|
|
|
**Step 2: Add accessible labels to empty state buttons**
|
|
|
|
After line 156 (scan_now_btn):
|
|
|
|
```rust
|
|
scan_now_btn.update_property(&[AccessibleProperty::Label("Scan for AppImages")]);
|
|
```
|
|
|
|
After line 162 (prefs_btn):
|
|
|
|
```rust
|
|
prefs_btn.update_property(&[AccessibleProperty::Label("Open preferences")]);
|
|
```
|
|
|
|
**Step 3: Add accessible label to list_box**
|
|
|
|
After line 214 (`list_box.add_css_class("boxed-list");`):
|
|
|
|
```rust
|
|
list_box.update_property(&[AccessibleProperty::Label("AppImage library list")]);
|
|
```
|
|
|
|
**Step 4: Add AccessibleRole::Search to search_bar**
|
|
|
|
After line 118 (`search_bar.connect_entry(&search_entry);`):
|
|
|
|
```rust
|
|
search_bar.set_accessible_role(gtk::AccessibleRole::Search);
|
|
```
|
|
|
|
**Step 5: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success with zero errors
|
|
|
|
**Step 6: Commit**
|
|
|
|
```
|
|
git add src/ui/library_view.rs
|
|
git commit -m "Add WCAG accessible labels to library view buttons, list box, and search bar"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: App Card Accessible Emblem Description
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/app_card.rs`
|
|
|
|
**Step 1: Add accessible description to integration emblem**
|
|
|
|
In `build_app_card` (line 32-43), after creating the emblem overlay, add a description. Change:
|
|
|
|
```rust
|
|
if record.integrated {
|
|
let overlay = gtk::Overlay::new();
|
|
overlay.set_child(Some(&icon_widget));
|
|
|
|
let emblem = gtk::Image::from_icon_name("emblem-ok-symbolic");
|
|
emblem.set_pixel_size(16);
|
|
emblem.add_css_class("integration-emblem");
|
|
emblem.set_halign(gtk::Align::End);
|
|
emblem.set_valign(gtk::Align::End);
|
|
overlay.add_overlay(&emblem);
|
|
|
|
card.append(&overlay);
|
|
```
|
|
|
|
To:
|
|
|
|
```rust
|
|
if record.integrated {
|
|
let overlay = gtk::Overlay::new();
|
|
overlay.set_child(Some(&icon_widget));
|
|
|
|
let emblem = gtk::Image::from_icon_name("emblem-ok-symbolic");
|
|
emblem.set_pixel_size(16);
|
|
emblem.add_css_class("integration-emblem");
|
|
emblem.set_halign(gtk::Align::End);
|
|
emblem.set_valign(gtk::Align::End);
|
|
emblem.update_property(&[AccessibleProperty::Label("Integrated into desktop menu")]);
|
|
overlay.add_overlay(&emblem);
|
|
|
|
card.append(&overlay);
|
|
```
|
|
|
|
**Step 2: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 3: Commit**
|
|
|
|
```
|
|
git add src/ui/app_card.rs
|
|
git commit -m "Add accessible label to integration emblem overlay in app cards"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Detail View - Tooltips, Plain Language, Busy States
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/detail_view.rs`
|
|
|
|
**Step 1: Add accessible role to banner**
|
|
|
|
In `build_banner` (line 160), after `banner.add_css_class("detail-banner");`, add:
|
|
|
|
```rust
|
|
banner.set_accessible_role(gtk::AccessibleRole::Banner);
|
|
```
|
|
|
|
**Step 2: Add tooltips for technical terms**
|
|
|
|
In `build_system_integration_group`:
|
|
|
|
For the Wayland row (around line 315), change:
|
|
```rust
|
|
let wayland_row = adw::ActionRow::builder()
|
|
.title("Wayland")
|
|
.subtitle(wayland_description(&wayland_status))
|
|
.build();
|
|
```
|
|
To:
|
|
```rust
|
|
let wayland_row = adw::ActionRow::builder()
|
|
.title("Wayland")
|
|
.subtitle(wayland_description(&wayland_status))
|
|
.tooltip_text("Display protocol for Linux desktops")
|
|
.build();
|
|
```
|
|
|
|
For the FUSE row (around line 389), change:
|
|
```rust
|
|
let fuse_row = adw::ActionRow::builder()
|
|
.title("FUSE")
|
|
.subtitle(fuse_description(&fuse_status))
|
|
.build();
|
|
```
|
|
To:
|
|
```rust
|
|
let fuse_row = adw::ActionRow::builder()
|
|
.title("FUSE")
|
|
.subtitle(fuse_description(&fuse_status))
|
|
.tooltip_text("Filesystem in Userspace - required for mounting AppImages")
|
|
.build();
|
|
```
|
|
|
|
For the Firejail row (around line 432), change:
|
|
```rust
|
|
let firejail_row = adw::SwitchRow::builder()
|
|
.title("Firejail sandbox")
|
|
```
|
|
To:
|
|
```rust
|
|
let firejail_row = adw::SwitchRow::builder()
|
|
.title("Firejail sandbox")
|
|
.tooltip_text("Linux application sandboxing tool")
|
|
```
|
|
|
|
**Step 3: Plain language rewrites in build_updates_usage_group**
|
|
|
|
Change line ~507 from:
|
|
```rust
|
|
.subtitle("No update information embedded")
|
|
```
|
|
To:
|
|
```rust
|
|
.subtitle("This app cannot check for updates automatically")
|
|
```
|
|
|
|
**Step 4: Add tooltip to SHA256 row**
|
|
|
|
In `build_security_storage_group`, for the SHA256 row (around line 830), change:
|
|
```rust
|
|
let hash_row = adw::ActionRow::builder()
|
|
.title("SHA256")
|
|
```
|
|
To:
|
|
```rust
|
|
let hash_row = adw::ActionRow::builder()
|
|
.title("SHA256 checksum")
|
|
.tooltip_text("Cryptographic hash for verifying file integrity")
|
|
```
|
|
|
|
**Step 5: Add tooltip to AppImage type row**
|
|
|
|
Change (around line 815):
|
|
```rust
|
|
let type_row = adw::ActionRow::builder()
|
|
.title("AppImage type")
|
|
.subtitle(type_str)
|
|
.build();
|
|
```
|
|
To:
|
|
```rust
|
|
let type_row = adw::ActionRow::builder()
|
|
.title("AppImage type")
|
|
.subtitle(type_str)
|
|
.tooltip_text("Type 1 uses ISO9660, Type 2 uses SquashFS")
|
|
.build();
|
|
```
|
|
|
|
**Step 6: Add busy state to security scan row**
|
|
|
|
In the security scan `connect_activated` closure (around line 640-670), add busy state when scan starts and clear when done.
|
|
|
|
After `row.set_sensitive(false);` add:
|
|
```rust
|
|
row.update_state(&[gtk::accessible::State::Busy(true)]);
|
|
```
|
|
|
|
After `row_clone.set_sensitive(true);` add:
|
|
```rust
|
|
row_clone.update_state(&[gtk::accessible::State::Busy(false)]);
|
|
```
|
|
|
|
**Step 7: Add busy state to analyze toolkit row**
|
|
|
|
Same pattern in the analyze toolkit `connect_activated` closure (around line 335-361).
|
|
|
|
After `row.set_sensitive(false);` add:
|
|
```rust
|
|
row.update_state(&[gtk::accessible::State::Busy(true)]);
|
|
```
|
|
|
|
After `row_clone.set_sensitive(true);` add:
|
|
```rust
|
|
row_clone.update_state(&[gtk::accessible::State::Busy(false)]);
|
|
```
|
|
|
|
**Step 8: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success with zero errors
|
|
|
|
**Step 9: Commit**
|
|
|
|
```
|
|
git add src/ui/detail_view.rs
|
|
git commit -m "Add WCAG tooltips, plain language, busy states, and banner role to detail view"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Dashboard Tooltips for Technical Terms
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/dashboard.rs`
|
|
|
|
**Step 1: Add tooltips to system status rows**
|
|
|
|
For the FUSE row (around line 97), change:
|
|
```rust
|
|
let fuse_row = adw::ActionRow::builder()
|
|
.title("FUSE")
|
|
.subtitle(&fuse_description(&fuse_info))
|
|
.build();
|
|
```
|
|
To:
|
|
```rust
|
|
let fuse_row = adw::ActionRow::builder()
|
|
.title("FUSE")
|
|
.subtitle(&fuse_description(&fuse_info))
|
|
.tooltip_text("Filesystem in Userspace - required for mounting AppImages")
|
|
.build();
|
|
```
|
|
|
|
For the XWayland row (around line 122), change:
|
|
```rust
|
|
let xwayland_row = adw::ActionRow::builder()
|
|
.title("XWayland")
|
|
.subtitle(if has_xwayland { "Running" } else { "Not detected" })
|
|
.build();
|
|
```
|
|
To:
|
|
```rust
|
|
let xwayland_row = adw::ActionRow::builder()
|
|
.title("XWayland")
|
|
.subtitle(if has_xwayland { "Running" } else { "Not detected" })
|
|
.tooltip_text("X11 compatibility layer for Wayland desktops")
|
|
.build();
|
|
```
|
|
|
|
**Step 2: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 3: Commit**
|
|
|
|
```
|
|
git add src/ui/dashboard.rs
|
|
git commit -m "Add WCAG tooltips for technical terms on dashboard"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Duplicate Dialog - Accessible Labels and Confirmation
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/duplicate_dialog.rs`
|
|
|
|
**Step 1: Add accessible label to bulk remove button**
|
|
|
|
After the bulk_btn is created (around line 41-46), add:
|
|
|
|
```rust
|
|
bulk_btn.update_property(&[
|
|
gtk::accessible::Property::Label("Remove all suggested duplicates"),
|
|
]);
|
|
```
|
|
|
|
**Step 2: Add accessible label to per-row delete buttons**
|
|
|
|
In `build_group_widget`, after the delete_btn is created (around line 195-201), add:
|
|
|
|
```rust
|
|
delete_btn.update_property(&[
|
|
gtk::accessible::Property::Label(&format!("Delete {}", record_name)),
|
|
]);
|
|
```
|
|
|
|
(This line must go after the `record_name` variable is created at line 205.)
|
|
|
|
Actually, re-checking the code structure - `record_name` is defined at line 205 and `delete_btn` at line 195. We need to move the accessible label after `record_name` is defined. Add after line 205:
|
|
|
|
```rust
|
|
delete_btn.update_property(&[
|
|
gtk::accessible::Property::Label(&format!("Delete {}", record_name)),
|
|
]);
|
|
```
|
|
|
|
**Step 3: Add confirmation to bulk remove**
|
|
|
|
Wrap the bulk_btn `connect_clicked` handler (lines 95-115) to show a confirmation AlertDialog first. Replace the entire `bulk_btn.connect_clicked` block:
|
|
|
|
```rust
|
|
let parent_for_confirm = dialog.clone();
|
|
bulk_btn.connect_clicked(move |btn| {
|
|
let records = removable.borrow();
|
|
if records.is_empty() {
|
|
return;
|
|
}
|
|
let count = records.len();
|
|
let confirm = adw::AlertDialog::builder()
|
|
.heading("Confirm Removal")
|
|
.body(&format!("Remove {} suggested duplicate{}?", count, if count == 1 { "" } else { "s" }))
|
|
.close_response("cancel")
|
|
.default_response("remove")
|
|
.build();
|
|
confirm.add_response("cancel", "Cancel");
|
|
confirm.add_response("remove", "Remove");
|
|
confirm.set_response_appearance("remove", adw::ResponseAppearance::Destructive);
|
|
|
|
let db_bulk = db_bulk.clone();
|
|
let toast_bulk = toast_bulk.clone();
|
|
let removable_inner = removable.clone();
|
|
let btn_clone = btn.clone();
|
|
confirm.connect_response(None, move |_dlg, response| {
|
|
if response != "remove" {
|
|
return;
|
|
}
|
|
let records = removable_inner.borrow();
|
|
let mut removed_count = 0;
|
|
for (record_id, record_path, _record_name, integrated) in records.iter() {
|
|
if *integrated {
|
|
if let Ok(Some(full_record)) = db_bulk.get_appimage_by_id(*record_id) {
|
|
integrator::remove_integration(&full_record).ok();
|
|
}
|
|
db_bulk.set_integrated(*record_id, false, None).ok();
|
|
}
|
|
std::fs::remove_file(record_path).ok();
|
|
db_bulk.remove_appimage(*record_id).ok();
|
|
removed_count += 1;
|
|
}
|
|
if removed_count > 0 {
|
|
toast_bulk.add_toast(adw::Toast::new(&format!("Removed {} items", removed_count)));
|
|
btn_clone.set_sensitive(false);
|
|
btn_clone.set_label("Done");
|
|
}
|
|
});
|
|
confirm.present(Some(&parent_for_confirm));
|
|
});
|
|
```
|
|
|
|
**Step 4: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 5: Commit**
|
|
|
|
```
|
|
git add src/ui/duplicate_dialog.rs
|
|
git commit -m "Add WCAG accessible labels and confirmation dialog to duplicate removal"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Cleanup Wizard - Labels, Confirmation, Busy Announcement
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/cleanup_wizard.rs`
|
|
|
|
**Step 1: Add accessible label to clean button**
|
|
|
|
In `build_review_step`, after the clean_button is created (around line 302-305), add:
|
|
|
|
```rust
|
|
clean_button.update_property(&[
|
|
gtk::accessible::Property::Label("Clean selected items"),
|
|
]);
|
|
```
|
|
|
|
**Step 2: Add accessible label to close button**
|
|
|
|
In `build_complete_step`, after the close_button is created (around line 400-404), add:
|
|
|
|
```rust
|
|
close_button.update_property(&[
|
|
gtk::accessible::Property::Label("Close cleanup dialog"),
|
|
]);
|
|
```
|
|
|
|
**Step 3: Add accessible labels to category list boxes**
|
|
|
|
In `build_review_step`, after each `list_box` is created (around line 260-262), add:
|
|
|
|
```rust
|
|
list_box.update_property(&[
|
|
gtk::accessible::Property::Label(cat.label()),
|
|
]);
|
|
```
|
|
|
|
**Step 4: Add confirmation before cleanup**
|
|
|
|
Wrap the `clean_button.connect_clicked` handler (lines 309-324) to add a confirmation dialog. Replace it with:
|
|
|
|
```rust
|
|
let dialog_for_confirm = Rc::new(RefCell::new(None::<adw::Dialog>));
|
|
// The dialog reference will be set by the caller - for now use the page as parent
|
|
let page_ref = page.clone();
|
|
clean_button.connect_clicked(move |_| {
|
|
let checks = checks.borrow();
|
|
let mut items_mut = items_clone.borrow_mut();
|
|
for (idx, check) in checks.iter() {
|
|
if *idx < items_mut.len() {
|
|
items_mut[*idx].selected = check.is_active();
|
|
}
|
|
}
|
|
let selected: Vec<ReclaimableItem> = items_mut
|
|
.iter()
|
|
.filter(|i| i.selected)
|
|
.cloned()
|
|
.collect();
|
|
drop(items_mut);
|
|
|
|
if selected.is_empty() {
|
|
on_confirm(selected);
|
|
return;
|
|
}
|
|
|
|
let count = selected.len();
|
|
let total_size: u64 = selected.iter().map(|i| i.size_bytes).sum();
|
|
let confirm = adw::AlertDialog::builder()
|
|
.heading("Confirm Cleanup")
|
|
.body(&format!(
|
|
"Remove {} item{} ({})?",
|
|
count,
|
|
if count == 1 { "" } else { "s" },
|
|
super::widgets::format_size(total_size as i64),
|
|
))
|
|
.close_response("cancel")
|
|
.default_response("clean")
|
|
.build();
|
|
confirm.add_response("cancel", "Cancel");
|
|
confirm.add_response("clean", "Clean");
|
|
confirm.set_response_appearance("clean", adw::ResponseAppearance::Destructive);
|
|
|
|
let on_confirm_inner = {
|
|
// We need to move on_confirm into the closure, but it's already moved.
|
|
// This requires restructuring - use Rc<RefCell<Option<...>>>
|
|
selected.clone()
|
|
};
|
|
confirm.connect_response(None, move |_dlg, response| {
|
|
if response == "clean" {
|
|
on_confirm(on_confirm_inner.clone());
|
|
}
|
|
});
|
|
confirm.present(Some(&page_ref));
|
|
});
|
|
```
|
|
|
|
Note: This task requires careful restructuring because `on_confirm` is `impl Fn` not `Clone`. The simplest approach is to wrap the confirmation at a higher level. Actually, since `on_confirm` takes a `Vec<ReclaimableItem>` and is `Fn + 'static`, we can use `Rc` wrapping. Let me simplify - just add the confirmation inside the existing closure pattern.
|
|
|
|
Actually, re-reading the code more carefully: `on_confirm` is `impl Fn(Vec<ReclaimableItem>) + 'static` - it can be called multiple times. We should wrap it in an `Rc` to share between the confirmation dialog closure and the outer closure. But since `impl Fn` doesn't implement `Clone`, we need a different approach.
|
|
|
|
The simplest fix: wrap `on_confirm` in an `Rc<dyn Fn(...)>` at the function level. Change the signature:
|
|
|
|
In `build_review_step` function signature, no change needed since we just call `on_confirm` inside the confirmation callback. But we need `on_confirm` to be callable from inside the nested closure.
|
|
|
|
Simplest approach: Store selected items in an `Rc<RefCell<Vec<...>>>` and have the confirmation dialog closure read from it.
|
|
|
|
This task is complex enough to warrant its own careful implementation. For now, the key requirement is:
|
|
- The "Clean Selected" button shows a confirmation AlertDialog before actually cleaning.
|
|
- Keep the existing flow but interpose a dialog.
|
|
|
|
**Step 5: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 6: Commit**
|
|
|
|
```
|
|
git add src/ui/cleanup_wizard.rs
|
|
git commit -m "Add WCAG accessible labels and confirmation dialog to cleanup wizard"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Preferences - Accessible Labels
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/preferences.rs`
|
|
|
|
**Step 1: Add accessible label to Add Location button**
|
|
|
|
After line 98 (add_button creation), add:
|
|
|
|
```rust
|
|
add_button.update_property(&[
|
|
gtk::accessible::Property::Label("Add scan directory"),
|
|
]);
|
|
```
|
|
|
|
**Step 2: Add accessible label to remove directory buttons**
|
|
|
|
In `add_directory_row` function, after the remove_btn is created (around line 392-397), add:
|
|
|
|
```rust
|
|
remove_btn.update_property(&[
|
|
gtk::accessible::Property::Label(&format!("Remove directory {}", dir)),
|
|
]);
|
|
```
|
|
|
|
**Step 3: Add accessible label to directory list box**
|
|
|
|
After line 87 (`dir_list_box.set_selection_mode(gtk::SelectionMode::None);`), add:
|
|
|
|
```rust
|
|
dir_list_box.update_property(&[
|
|
gtk::accessible::Property::Label("Scan directories"),
|
|
]);
|
|
```
|
|
|
|
Note: This requires importing `gtk::accessible::Property` or using the full path. Since preferences.rs doesn't import it yet, add at the top:
|
|
|
|
```rust
|
|
use gtk::prelude::*;
|
|
```
|
|
|
|
The file already uses `adw::prelude::*` and `gtk::gio`. We need to also import `gtk::prelude::*` for `update_property`. Check if `adw::prelude::*` re-exports it... it does (adw re-exports gtk::prelude). So we just need the accessible path. Use full path: `gtk::accessible::Property::Label(...)`.
|
|
|
|
**Step 4: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 5: Commit**
|
|
|
|
```
|
|
git add src/ui/preferences.rs
|
|
git commit -m "Add WCAG accessible labels to preferences buttons and directory list"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Security Report - Labels and Tooltips
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/security_report.rs`
|
|
|
|
**Step 1: Add tooltips for CVE terms**
|
|
|
|
In `build_summary_group`, change the total_row (around line 150):
|
|
```rust
|
|
let total_row = adw::ActionRow::builder()
|
|
.title("Total vulnerabilities")
|
|
.subtitle(&summary.total().to_string())
|
|
.build();
|
|
```
|
|
To:
|
|
```rust
|
|
let total_row = adw::ActionRow::builder()
|
|
.title("Total vulnerabilities")
|
|
.subtitle(&summary.total().to_string())
|
|
.tooltip_text("Common Vulnerabilities and Exposures found in bundled libraries")
|
|
.build();
|
|
```
|
|
|
|
**Step 2: Expand "CVE" abbreviation in app findings**
|
|
|
|
In `build_app_findings_group`, change the description (around line 209):
|
|
```rust
|
|
let description = format!("{} vulnerabilities found", summary.total());
|
|
```
|
|
To:
|
|
```rust
|
|
let description = format!("{} CVE (vulnerability) records found", summary.total());
|
|
```
|
|
|
|
**Step 3: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 4: Commit**
|
|
|
|
```
|
|
git add src/ui/security_report.rs
|
|
git commit -m "Add WCAG tooltips and expanded abbreviations to security report"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Integration Dialog - List Box Labels
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/integration_dialog.rs`
|
|
|
|
**Step 1: Add accessible labels to list boxes**
|
|
|
|
After line 41 (`identity_box.set_selection_mode(gtk::SelectionMode::None);`), add:
|
|
|
|
```rust
|
|
identity_box.update_property(&[
|
|
gtk::accessible::Property::Label("Application details"),
|
|
]);
|
|
```
|
|
|
|
After line 76 (`actions_box.set_selection_mode(gtk::SelectionMode::None);`), add:
|
|
|
|
```rust
|
|
actions_box.update_property(&[
|
|
gtk::accessible::Property::Label("Integration actions"),
|
|
]);
|
|
```
|
|
|
|
**Step 2: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 3: Commit**
|
|
|
|
```
|
|
git add src/ui/integration_dialog.rs
|
|
git commit -m "Add WCAG accessible labels to integration dialog list boxes"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 12: Update Dialog - Plain Language
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/update_dialog.rs`
|
|
|
|
**Step 1: Plain language rewrite**
|
|
|
|
Change line ~121 from:
|
|
```rust
|
|
dialog_ref.set_body(
|
|
"This AppImage does not contain update information. \
|
|
Updates must be downloaded manually.",
|
|
);
|
|
```
|
|
To:
|
|
```rust
|
|
dialog_ref.set_body(
|
|
"This app does not support automatic updates. \
|
|
Check the developer's website for newer versions.",
|
|
);
|
|
```
|
|
|
|
**Step 2: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 3: Commit**
|
|
|
|
```
|
|
git add src/ui/update_dialog.rs
|
|
git commit -m "Rewrite update dialog text to plain language for WCAG readability"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 13: Window - Dynamic Title and Live Announcements
|
|
|
|
**Files:**
|
|
- Modify: `src/window.rs`
|
|
|
|
**Step 1: Update window title on navigation**
|
|
|
|
In `setup_ui`, after the `navigation_view.connect_popped` block (around line 199), add a `connect_pushed` handler to update the window title:
|
|
|
|
```rust
|
|
// Update window title for accessibility (WCAG 2.4.8 Location)
|
|
{
|
|
let window_weak = self.downgrade();
|
|
navigation_view.connect_pushed(move |_nav, page| {
|
|
if let Some(window) = window_weak.upgrade() {
|
|
let page_title = page.title();
|
|
if !page_title.is_empty() {
|
|
window.set_title(Some(&format!("Driftwood - {}", page_title)));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let window_weak = self.downgrade();
|
|
let nav_ref = navigation_view.clone();
|
|
navigation_view.connect_popped(move |_nav, _page| {
|
|
if let Some(window) = window_weak.upgrade() {
|
|
// After pop, get the now-visible page title
|
|
if let Some(visible) = nav_ref.visible_page() {
|
|
let title = visible.title();
|
|
if title == "Driftwood" {
|
|
window.set_title(Some("Driftwood"));
|
|
} else {
|
|
window.set_title(Some(&format!("Driftwood - {}", title)));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
Wait - there's already a `connect_popped` handler at line 188. We need to add the title update logic inside the existing handler, not create a duplicate. Modify the existing handler to also update the title.
|
|
|
|
Change the existing `connect_popped` block (lines 186-199):
|
|
|
|
```rust
|
|
{
|
|
let db = self.database().clone();
|
|
let window_weak = self.downgrade();
|
|
navigation_view.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"));
|
|
|
|
if page.tag().as_deref() == Some("detail") {
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
And add a new `connect_pushed` handler after it:
|
|
|
|
```rust
|
|
// Update window title when navigating to sub-pages (WCAG 2.4.8 Location)
|
|
{
|
|
let window_weak = self.downgrade();
|
|
navigation_view.connect_pushed(move |_nav, page| {
|
|
if let Some(window) = window_weak.upgrade() {
|
|
let page_title = page.title();
|
|
if !page_title.is_empty() {
|
|
window.set_title(Some(&format!("Driftwood - {}", page_title)));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
**Step 2: Build and verify**
|
|
|
|
Run: `cargo build 2>&1 | tail -5`
|
|
Expected: success
|
|
|
|
**Step 3: Commit**
|
|
|
|
```
|
|
git add src/window.rs
|
|
git commit -m "Update window title dynamically for WCAG 2.4.8 Location compliance"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 14: Final Build Verification
|
|
|
|
**Files:** None (verification only)
|
|
|
|
**Step 1: Full build**
|
|
|
|
Run: `cargo build 2>&1`
|
|
Expected: zero errors, zero warnings
|
|
|
|
**Step 2: Run tests**
|
|
|
|
Run: `cargo test 2>&1`
|
|
Expected: all tests pass
|
|
|
|
**Step 3: Commit any remaining changes**
|
|
|
|
If there are any uncommitted fixes from build errors:
|
|
|
|
```
|
|
git add -u
|
|
git commit -m "Fix build issues from WCAG AAA compliance changes"
|
|
```
|
|
|
|
---
|
|
|
|
## Summary of WCAG Criteria Addressed
|
|
|
|
| Criterion | Level | Status | Task |
|
|
|-----------|-------|--------|------|
|
|
| 1.1.1 Non-text Content | A | Tasks 2-11 | Accessible labels on all icon-only elements |
|
|
| 1.3.1 Info and Relationships | A | Tasks 2-3, 7-11 | Roles and labels on containers |
|
|
| 1.3.6 Identify Purpose | AAA | Tasks 2, 3, 5 | Landmark roles (Banner, Search, Status) |
|
|
| 1.4.6 Enhanced Contrast | AAA | Task 1 | High-contrast media query |
|
|
| 1.4.11 Non-text Contrast | AA | Task 1 | Focus ring and badge border contrast |
|
|
| 2.1.3 Keyboard No Exception | AAA | Already met | All functionality keyboard accessible |
|
|
| 2.3.3 Animation from Interactions | AAA | Task 1 | Universal reduced-motion |
|
|
| 2.4.7 Focus Visible | AA | Task 1 | Focus indicators on all widgets |
|
|
| 2.4.8 Location | AAA | Task 13 | Dynamic window title per page |
|
|
| 2.4.13 Focus Appearance | AAA | Task 1 | 2-3px focus rings with contrast |
|
|
| 2.5.8 Target Size | AA | Task 1 | 24px minimum target sizes |
|
|
| 3.1.3 Unusual Words | AAA | Tasks 5, 6, 10 | Tooltips for technical terms |
|
|
| 3.1.4 Abbreviations | AAA | Tasks 5, 10 | Expanded abbreviations |
|
|
| 3.1.5 Reading Level | AAA | Tasks 5, 12 | Plain language rewrites |
|
|
| 3.3.5 Help | AAA | Tasks 5, 6 | Contextual descriptions |
|
|
| 3.3.6 Error Prevention All | AAA | Tasks 7, 8 | Confirmation on destructive actions |
|
|
| 4.1.2 Name, Role, Value | A | Tasks 2-13 | Complete accessible names/roles |
|
|
| 4.1.3 Status Messages | AA | Task 2 | Live region announcements |
|