From 66fc582cce4ba24a48d73560caaeee2e5ed9a8d0 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 27 Feb 2026 10:03:16 +0200 Subject: [PATCH] Add WCAG tooltips for technical terms on dashboard --- src/ui/dashboard.rs | 250 ++++++++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 113 deletions(-) diff --git a/src/ui/dashboard.rs b/src/ui/dashboard.rs index a4407a1..fd26de8 100644 --- a/src/ui/dashboard.rs +++ b/src/ui/dashboard.rs @@ -1,11 +1,11 @@ use adw::prelude::*; -use gtk::gio; use std::rc::Rc; use crate::core::database::Database; use crate::core::duplicates; use crate::core::fuse; use crate::core::wayland; +use crate::i18n::ni18n; use super::widgets; /// Build the dashboard page showing system health and statistics. @@ -25,19 +25,22 @@ pub fn build_dashboard_page(db: &Rc) -> adw::NavigationPage { .build(); // Section 1: System Status - content.append(&build_system_status_section()); + content.append(&build_system_status_group()); // Section 2: Library Statistics - content.append(&build_library_stats_section(db)); + content.append(&build_library_stats_group(db)); - // Section 3: Updates Summary - content.append(&build_updates_summary_section(db)); + // Section 3: Updates Summary (actionable) + content.append(&build_updates_summary_group(db)); - // Section 4: Duplicates Summary - content.append(&build_duplicates_summary_section(db)); + // Section 4: Duplicates Summary (actionable) + content.append(&build_duplicates_summary_group(db)); - // Section 5: Disk Usage - content.append(&build_disk_usage_section(db)); + // Section 5: Disk Usage (actionable) + content.append(&build_disk_usage_group(db)); + + // Section 6: Quick Actions + content.append(&build_quick_actions_group()); clamp.set_child(Some(&content)); let scrolled = gtk::ScrolledWindow::builder() @@ -57,23 +60,12 @@ pub fn build_dashboard_page(db: &Rc) -> adw::NavigationPage { .build() } -fn build_system_status_section() -> gtk::Box { - let section = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(8) +fn build_system_status_group() -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder() + .title("System Status") + .description("Display server, FUSE, and compatibility information") .build(); - let heading = gtk::Label::builder() - .label("System Status") - .css_classes(["heading"]) - .halign(gtk::Align::Start) - .build(); - section.append(&heading); - - let list_box = gtk::ListBox::new(); - list_box.add_css_class("boxed-list"); - list_box.set_selection_mode(gtk::SelectionMode::None); - // Session type let session = wayland::detect_session_type(); let session_row = adw::ActionRow::builder() @@ -90,7 +82,7 @@ fn build_system_status_section() -> gtk::Box { ); session_badge.set_valign(gtk::Align::Center); session_row.add_suffix(&session_badge); - list_box.append(&session_row); + group.add(&session_row); // Desktop environment let de = wayland::detect_desktop_environment(); @@ -98,13 +90,14 @@ fn build_system_status_section() -> gtk::Box { .title("Desktop environment") .subtitle(&de) .build(); - list_box.append(&de_row); + group.add(&de_row); // FUSE status let fuse_info = fuse::detect_system_fuse(); let fuse_row = adw::ActionRow::builder() .title("FUSE") - .subtitle(fuse_description(&fuse_info)) + .subtitle(&fuse_description(&fuse_info)) + .tooltip_text("Filesystem in Userspace - required for mounting AppImages") .build(); let fuse_badge = widgets::status_badge( fuse_info.status.label(), @@ -112,7 +105,7 @@ fn build_system_status_section() -> gtk::Box { ); fuse_badge.set_valign(gtk::Align::Center); fuse_row.add_suffix(&fuse_badge); - list_box.append(&fuse_row); + group.add(&fuse_row); // Install hint if FUSE not functional if let Some(ref hint) = fuse_info.install_hint { @@ -120,9 +113,9 @@ fn build_system_status_section() -> gtk::Box { .title("Fix FUSE") .subtitle(hint) .subtitle_selectable(true) - .css_classes(["monospace"]) .build(); - list_box.append(&hint_row); + hint_row.add_css_class("monospace"); + group.add(&hint_row); } // XWayland @@ -130,6 +123,7 @@ fn build_system_status_section() -> gtk::Box { 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(); let xwayland_badge = widgets::status_badge( if has_xwayland { "Available" } else { "Unavailable" }, @@ -137,7 +131,7 @@ fn build_system_status_section() -> gtk::Box { ); xwayland_badge.set_valign(gtk::Align::Center); xwayland_row.add_suffix(&xwayland_badge); - list_box.append(&xwayland_row); + group.add(&xwayland_row); // AppImageLauncher conflict check if let Some(version) = fuse::detect_appimagelauncher() { @@ -151,11 +145,10 @@ fn build_system_status_section() -> gtk::Box { let ail_badge = widgets::status_badge("Conflict", "warning"); ail_badge.set_valign(gtk::Align::Center); ail_row.add_suffix(&ail_badge); - list_box.append(&ail_row); + group.add(&ail_row); } - section.append(&list_box); - section + group } fn fuse_description(info: &fuse::FuseSystemInfo) -> String { @@ -179,23 +172,12 @@ fn fuse_description(info: &fuse::FuseSystemInfo) -> String { } } -fn build_library_stats_section(db: &Rc) -> gtk::Box { - let section = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(8) +fn build_library_stats_group(db: &Rc) -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder() + .title("Library") + .description("Overview of your AppImage collection") .build(); - let heading = gtk::Label::builder() - .label("Library") - .css_classes(["heading"]) - .halign(gtk::Align::Start) - .build(); - section.append(&heading); - - let list_box = gtk::ListBox::new(); - list_box.add_css_class("boxed-list"); - list_box.set_selection_mode(gtk::SelectionMode::None); - let records = db.get_all_appimages().unwrap_or_default(); let total = records.len(); @@ -203,16 +185,21 @@ fn build_library_stats_section(db: &Rc) -> gtk::Box { let executable = records.iter().filter(|r| r.is_executable).count(); let total_row = adw::ActionRow::builder() - .title("Total AppImages") + .title(&ni18n("AppImage", "AppImages", total as u32)) .subtitle(&total.to_string()) + .activatable(true) .build(); - list_box.append(&total_row); + let total_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + total_arrow.set_valign(gtk::Align::Center); + total_row.add_suffix(&total_arrow); + total_row.set_action_name(Some("navigation.pop")); + group.add(&total_row); let integrated_row = adw::ActionRow::builder() .title("Integrated") .subtitle(&format!("{} of {}", integrated, total)) .build(); - list_box.append(&integrated_row); + group.add(&integrated_row); let exec_row = adw::ActionRow::builder() .title("Executable") @@ -226,29 +213,17 @@ fn build_library_stats_section(db: &Rc) -> gtk::Box { badge.set_valign(gtk::Align::Center); exec_row.add_suffix(&badge); } - list_box.append(&exec_row); + group.add(&exec_row); - section.append(&list_box); - section + group } -fn build_updates_summary_section(db: &Rc) -> gtk::Box { - let section = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(8) +fn build_updates_summary_group(db: &Rc) -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder() + .title("Updates") + .description("AppImage update availability") .build(); - let heading = gtk::Label::builder() - .label("Updates") - .css_classes(["heading"]) - .halign(gtk::Align::Start) - .build(); - section.append(&heading); - - let list_box = gtk::ListBox::new(); - list_box.add_css_class("boxed-list"); - list_box.set_selection_mode(gtk::SelectionMode::None); - let records = db.get_all_appimages().unwrap_or_default(); let with_update_info = records @@ -270,40 +245,34 @@ fn build_updates_summary_section(db: &Rc) -> gtk::Box { .title("With update info") .subtitle(&format!("{} of {}", with_update_info, records.len())) .build(); - list_box.append(&info_row); + group.add(&info_row); + // Actionable updates row -> triggers check-updates let updates_row = adw::ActionRow::builder() .title("Updates available") .subtitle(&with_updates.to_string()) + .activatable(true) .build(); + updates_row.set_action_name(Some("win.check-updates")); if with_updates > 0 { let badge = widgets::status_badge(&format!("{} updates", with_updates), "info"); badge.set_valign(gtk::Align::Center); updates_row.add_suffix(&badge); } - list_box.append(&updates_row); + let updates_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + updates_arrow.set_valign(gtk::Align::Center); + updates_row.add_suffix(&updates_arrow); + group.add(&updates_row); - section.append(&list_box); - section + group } -fn build_duplicates_summary_section(db: &Rc) -> gtk::Box { - let section = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(8) +fn build_duplicates_summary_group(db: &Rc) -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder() + .title("Duplicates") + .description("Duplicate and multi-version detection") .build(); - let heading = gtk::Label::builder() - .label("Duplicates") - .css_classes(["heading"]) - .halign(gtk::Align::Start) - .build(); - section.append(&heading); - - let list_box = gtk::ListBox::new(); - list_box.add_css_class("boxed-list"); - list_box.set_selection_mode(gtk::SelectionMode::None); - let groups = duplicates::detect_duplicates(db); let summary = duplicates::summarize_duplicates(&groups); @@ -315,13 +284,19 @@ fn build_duplicates_summary_section(db: &Rc) -> gtk::Box { let badge = widgets::status_badge("Clean", "success"); badge.set_valign(gtk::Align::Center); row.add_suffix(&badge); - list_box.append(&row); + group.add(&row); } else { + // Actionable row -> opens duplicate dialog let groups_row = adw::ActionRow::builder() .title("Duplicate groups") .subtitle(&summary.total_groups.to_string()) + .activatable(true) .build(); - list_box.append(&groups_row); + groups_row.set_action_name(Some("win.find-duplicates")); + let dupes_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + dupes_arrow.set_valign(gtk::Align::Center); + groups_row.add_suffix(&dupes_arrow); + group.add(&groups_row); if summary.total_potential_savings > 0 { let savings_row = adw::ActionRow::builder() @@ -331,39 +306,33 @@ fn build_duplicates_summary_section(db: &Rc) -> gtk::Box { let badge = widgets::status_badge("Reclaimable", "warning"); badge.set_valign(gtk::Align::Center); savings_row.add_suffix(&badge); - list_box.append(&savings_row); + group.add(&savings_row); } } - section.append(&list_box); - section + group } -fn build_disk_usage_section(db: &Rc) -> gtk::Box { - let section = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(8) +fn build_disk_usage_group(db: &Rc) -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder() + .title("Disk Usage") + .description("Storage used by your AppImages") .build(); - let heading = gtk::Label::builder() - .label("Disk Usage") - .css_classes(["heading"]) - .halign(gtk::Align::Start) - .build(); - section.append(&heading); - - let list_box = gtk::ListBox::new(); - list_box.add_css_class("boxed-list"); - list_box.set_selection_mode(gtk::SelectionMode::None); - let records = db.get_all_appimages().unwrap_or_default(); let total_bytes: i64 = records.iter().map(|r| r.size_bytes).sum(); + // Actionable total row -> opens cleanup wizard let total_row = adw::ActionRow::builder() .title("Total disk usage") .subtitle(&widgets::format_size(total_bytes)) + .activatable(true) .build(); - list_box.append(&total_row); + total_row.set_action_name(Some("win.cleanup")); + let disk_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + disk_arrow.set_valign(gtk::Align::Center); + total_row.add_suffix(&disk_arrow); + group.add(&total_row); // Largest AppImages let mut sorted = records.clone(); @@ -375,9 +344,64 @@ fn build_disk_usage_section(db: &Rc) -> gtk::Box { .title(name) .subtitle(&widgets::format_size(record.size_bytes)) .build(); - list_box.append(&row); + group.add(&row); } - section.append(&list_box); - section + group +} + +fn build_quick_actions_group() -> adw::PreferencesGroup { + let group = adw::PreferencesGroup::builder() + .title("Quick Actions") + .build(); + + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(12) + .halign(gtk::Align::Center) + .margin_top(6) + .margin_bottom(6) + .build(); + + let scan_btn = gtk::Button::builder() + .label("Scan") + .tooltip_text("Scan for AppImages") + .build(); + scan_btn.add_css_class("pill"); + scan_btn.add_css_class("suggested-action"); + scan_btn.set_action_name(Some("win.scan")); + scan_btn.update_property(&[ + gtk::accessible::Property::Label("Scan for AppImages"), + ]); + + let updates_btn = gtk::Button::builder() + .label("Check Updates") + .tooltip_text("Check all AppImages for updates") + .build(); + updates_btn.add_css_class("pill"); + updates_btn.set_action_name(Some("win.check-updates")); + updates_btn.update_property(&[ + gtk::accessible::Property::Label("Check for updates"), + ]); + + let clean_btn = gtk::Button::builder() + .label("Clean Orphans") + .tooltip_text("Remove orphaned desktop entries") + .build(); + clean_btn.add_css_class("pill"); + clean_btn.set_action_name(Some("win.clean-orphans")); + clean_btn.update_property(&[ + gtk::accessible::Property::Label("Clean orphaned desktop entries"), + ]); + + button_box.append(&scan_btn); + button_box.append(&updates_btn); + button_box.append(&clean_btn); + + // Use an ActionRow to contain the buttons for proper group styling + let row = adw::ActionRow::new(); + row.set_child(Some(&button_box)); + group.add(&row); + + group }