Add WCAG tooltips for technical terms on dashboard
This commit is contained in:
@@ -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<Database>) -> 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<Database>) -> 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<Database>) -> gtk::Box {
|
||||
let section = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
fn build_library_stats_group(db: &Rc<Database>) -> 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<Database>) -> 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<Database>) -> 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<Database>) -> gtk::Box {
|
||||
let section = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
fn build_updates_summary_group(db: &Rc<Database>) -> 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<Database>) -> 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<Database>) -> gtk::Box {
|
||||
let section = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
fn build_duplicates_summary_group(db: &Rc<Database>) -> 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<Database>) -> 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<Database>) -> 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<Database>) -> gtk::Box {
|
||||
let section = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
fn build_disk_usage_group(db: &Rc<Database>) -> 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<Database>) -> 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user