From 4b8592801b325021a60a78ea67b445b728f029c2 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 27 Feb 2026 10:09:31 +0200 Subject: [PATCH] Update window title dynamically for WCAG 2.4.8 Location compliance --- src/window.rs | 272 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 248 insertions(+), 24 deletions(-) diff --git a/src/window.rs b/src/window.rs index cb3943a..09d2475 100644 --- a/src/window.rs +++ b/src/window.rs @@ -8,13 +8,18 @@ use std::time::Instant; use crate::config::APP_ID; use crate::core::database::Database; use crate::core::discovery; +use crate::core::fuse; use crate::core::inspector; use crate::core::orphan; +use crate::core::wayland; +use crate::i18n::{i18n, ni18n_f}; +use crate::ui::cleanup_wizard; use crate::ui::dashboard; use crate::ui::detail_view; use crate::ui::duplicate_dialog; use crate::ui::library_view::{LibraryState, LibraryView}; use crate::ui::preferences; +use crate::ui::security_report; use crate::ui::update_dialog; mod imp { @@ -78,6 +83,19 @@ glib::wrapper! { gtk::Native, gtk::Root, gtk::ShortcutManager; } +fn shortcut_row(accel: &str, description: &str) -> adw::ActionRow { + let row = adw::ActionRow::builder() + .title(description) + .build(); + let accel_label = gtk::Label::builder() + .label(accel) + .css_classes(["monospace", "dimmed"]) + .valign(gtk::Align::Center) + .build(); + row.add_suffix(&accel_label); + row +} + impl DriftwoodWindow { pub fn new(app: &crate::application::DriftwoodApplication) -> Self { glib::Object::builder() @@ -111,17 +129,20 @@ impl DriftwoodWindow { fn setup_ui(&self) { // Build the hamburger menu model let menu = gio::Menu::new(); - menu.append(Some("Dashboard"), Some("win.dashboard")); - menu.append(Some("Preferences"), Some("win.preferences")); + menu.append(Some(&i18n("Dashboard")), Some("win.dashboard")); + menu.append(Some(&i18n("Preferences")), Some("win.preferences")); let section2 = gio::Menu::new(); - section2.append(Some("Scan for AppImages"), Some("win.scan")); - section2.append(Some("Check for Updates"), Some("win.check-updates")); - section2.append(Some("Find Duplicates"), Some("win.find-duplicates")); + section2.append(Some(&i18n("Scan for AppImages")), Some("win.scan")); + section2.append(Some(&i18n("Check for Updates")), Some("win.check-updates")); + section2.append(Some(&i18n("Find Duplicates")), Some("win.find-duplicates")); + section2.append(Some(&i18n("Security Report")), Some("win.security-report")); + section2.append(Some(&i18n("Disk Cleanup")), Some("win.cleanup")); menu.append_section(None, §ion2); let section3 = gio::Menu::new(); - section3.append(Some("About Driftwood"), Some("app.about")); + section3.append(Some(&i18n("Keyboard Shortcuts")), Some("win.show-shortcuts")); + section3.append(Some(&i18n("About Driftwood")), Some("app.about")); menu.append_section(None, §ion3); // Library view (contains header bar, search, grid/list, empty state) @@ -159,6 +180,42 @@ impl DriftwoodWindow { }); } + // Refresh library view when navigating back from detail + // (in case integration or other state changed) + { + let db = self.database().clone(); + let window_weak = self.downgrade(); + navigation_view.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")); + + 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), + } + } + } + }); + } + + // Update window title when navigating to sub-pages (WCAG 2.4.8 Location) + { + let window_weak = self.downgrade(); + navigation_view.connect_pushed(move |nav| { + if let Some(window) = window_weak.upgrade() { + if let Some(page) = nav.visible_page() { + let page_title = page.title(); + if !page_title.is_empty() { + window.set_title(Some(&format!("Driftwood - {}", page_title))); + } + } + } + }); + } + // Store references self.imp() .toast_overlay @@ -214,14 +271,17 @@ impl DriftwoodWindow { match result { Ok(Ok(summary)) => { let msg = format!( - "Cleaned {} desktop entries, {} icons", + "{} {} {}, {} {}", + i18n("Cleaned"), summary.entries_removed, + i18n("desktop entries"), summary.icons_removed, + i18n("icons"), ); toast_ref.add_toast(adw::Toast::new(&msg)); } _ => { - toast_ref.add_toast(adw::Toast::new("Failed to clean orphaned entries")); + toast_ref.add_toast(adw::Toast::new(&i18n("Failed to clean orphaned entries"))); } } }); @@ -239,9 +299,8 @@ impl DriftwoodWindow { // Check for updates action let updates_toast = self.imp().toast_overlay.get().unwrap().clone(); let check_updates_action = gio::ActionEntry::builder("check-updates") - .activate(move |window: &Self, _, _| { + .activate(move |_window: &Self, _, _| { let toast_ref = updates_toast.clone(); - let db = window.database().clone(); glib::spawn_future_local(async move { let result = gio::spawn_blocking(move || { let bg_db = Database::open().expect("Failed to open database"); @@ -251,14 +310,14 @@ impl DriftwoodWindow { match result { Ok(0) => { - toast_ref.add_toast(adw::Toast::new("All AppImages are up to date")); + toast_ref.add_toast(adw::Toast::new(&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)); } Err(_) => { - toast_ref.add_toast(adw::Toast::new("Failed to check for updates")); + toast_ref.add_toast(adw::Toast::new(&i18n("Failed to check for updates"))); } } }); @@ -274,6 +333,31 @@ impl DriftwoodWindow { }) .build(); + // Security report action + let security_report_action = gio::ActionEntry::builder("security-report") + .activate(|window: &Self, _, _| { + let db = window.database().clone(); + let nav = window.imp().navigation_view.get().unwrap(); + let page = security_report::build_security_report_page(&db); + nav.push(&page); + }) + .build(); + + // Disk cleanup wizard action + let cleanup_action = gio::ActionEntry::builder("cleanup") + .activate(|window: &Self, _, _| { + let db = window.database().clone(); + cleanup_wizard::show_cleanup_wizard(window, &db); + }) + .build(); + + // Show keyboard shortcuts dialog + let shortcuts_action = gio::ActionEntry::builder("show-shortcuts") + .activate(|window: &Self, _, _| { + window.show_shortcuts_dialog(); + }) + .build(); + self.add_action_entries([ dashboard_action, preferences_action, @@ -282,6 +366,9 @@ impl DriftwoodWindow { search_action, check_updates_action, find_duplicates_action, + security_report_action, + cleanup_action, + shortcuts_action, ]); // Keyboard shortcuts @@ -290,6 +377,9 @@ impl DriftwoodWindow { gtk_app.set_accels_for_action("win.scan", &["r", "F5"]); gtk_app.set_accels_for_action("win.search", &["f"]); gtk_app.set_accels_for_action("win.preferences", &["comma"]); + gtk_app.set_accels_for_action("win.dashboard", &["d"]); + gtk_app.set_accels_for_action("win.check-updates", &["u"]); + gtk_app.set_accels_for_action("win.show-shortcuts", &["question"]); } } @@ -307,6 +397,12 @@ impl DriftwoodWindow { } } + // Auto-scan on startup if enabled + let settings = self.settings(); + if settings.boolean("auto-scan-on-startup") { + self.trigger_scan(); + } + // Check for orphaned desktop entries in the background let toast_overlay = self.imp().toast_overlay.get().unwrap().clone(); glib::spawn_future_local(async move { @@ -317,15 +413,16 @@ impl DriftwoodWindow { if let Ok(count) = result { if count > 0 { - let msg = if count == 1 { - "1 orphaned desktop entry found. Use 'Clean' to remove it.".to_string() - } else { - format!("{} orphaned desktop entries found. Use 'Clean' to remove them.", count) - }; + let msg = ni18n_f( + "{} orphaned desktop entry found. Use 'Clean' to remove it.", + "{} orphaned desktop entries found. Use 'Clean' to remove them.", + count as u32, + &[("{}", &count.to_string())], + ); let toast = adw::Toast::builder() .title(&msg) .timeout(5) - .button_label("Clean") + .button_label(&i18n("Clean")) .action_name("win.clean-orphans") .build(); toast_overlay.add_toast(toast); @@ -339,12 +436,18 @@ impl DriftwoodWindow { library_view.set_state(LibraryState::Loading); let settings = self.settings(); - let dirs: Vec = settings + let mut dirs: Vec = settings .strv("scan-directories") .iter() .map(|s| s.to_string()) .collect(); + // Include system-wide AppImage directory if it exists + let system_dir = crate::config::SYSTEM_APPIMAGE_DIR; + if std::path::Path::new(system_dir).is_dir() && !dirs.iter().any(|d| d == system_dir) { + dirs.push(system_dir.to_string()); + } + let toast_overlay = self.imp().toast_overlay.get().unwrap().clone(); let window_weak = self.downgrade(); @@ -356,9 +459,18 @@ impl DriftwoodWindow { let start = Instant::now(); let discovered = discovery::scan_directories(&dirs); + // Detect system FUSE status once for all AppImages + let fuse_info = fuse::detect_system_fuse(); + let mut new_count = 0i32; let total = discovered.len() as i32; + // Clean stale DB records for files that no longer exist + let removed = bg_db.remove_missing_appimages().unwrap_or_default(); + let removed_count = removed.len() as i32; + + let mut skipped_count = 0i32; + for d in &discovered { let existing = bg_db .get_appimage_by_path(&d.path.to_string_lossy()) @@ -372,6 +484,20 @@ impl DriftwoodWindow { .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) }); + // Skip re-processing unchanged files (same size + mtime, and all analysis done) + if let Some(ref ex) = existing { + let size_unchanged = ex.size_bytes == d.size_bytes as i64; + let mtime_unchanged = modified.as_deref() == ex.file_modified.as_deref(); + let fully_analyzed = ex.app_name.is_some() + && ex.fuse_status.is_some() + && ex.wayland_status.is_some() + && ex.sha256.is_some(); + if size_unchanged && mtime_unchanged && fully_analyzed { + skipped_count += 1; + continue; + } + } + let id = bg_db.upsert_appimage( &d.path.to_string_lossy(), &d.filename, @@ -410,15 +536,58 @@ impl DriftwoodWindow { ).ok(); } } + + // Per-AppImage FUSE status + let needs_fuse = existing + .as_ref() + .map(|r| r.fuse_status.is_none()) + .unwrap_or(true); + if needs_fuse { + let app_fuse = fuse::determine_app_fuse_status(&fuse_info, &d.path); + bg_db.update_fuse_status(id, app_fuse.as_str()).ok(); + } + + // Wayland compatibility analysis (slower - only for new/unanalyzed) + let needs_wayland = existing + .as_ref() + .map(|r| r.wayland_status.is_none()) + .unwrap_or(true); + if needs_wayland { + let analysis = wayland::analyze_appimage(&d.path); + bg_db.update_wayland_status(id, analysis.status.as_str()).ok(); + } + + // SHA256 hash (for duplicate detection) + let needs_hash = existing + .as_ref() + .map(|r| r.sha256.is_none()) + .unwrap_or(true); + if needs_hash { + if let Ok(hash) = crate::core::discovery::compute_sha256(&d.path) { + bg_db.update_sha256(id, &hash).ok(); + } + } + + // Discover config/data paths (only for new AppImages) + if existing.is_none() { + if let Ok(Some(rec)) = bg_db.get_appimage_by_id(id) { + crate::core::footprint::discover_and_store(&bg_db, id, &rec); + } + } } + log::info!( + "Scan complete: {} files, {} new, {} removed, {} skipped (unchanged), took {}ms", + total, new_count, removed_count, skipped_count, start.elapsed().as_millis() + ); + let duration = start.elapsed().as_millis() as i64; bg_db.log_scan( "manual", &dirs.iter().map(|s| s.to_string()).collect::>(), total, new_count, - 0, + removed_count, duration, ).ok(); @@ -438,16 +607,71 @@ impl DriftwoodWindow { } let msg = match new_count { - 0 if total == 0 => "No AppImages found".to_string(), - 0 => format!("{} AppImages up to date", total), - 1 => "Found 1 new AppImage".to_string(), - n => format!("Found {} new AppImages", n), + 0 if total == 0 => i18n("No AppImages found"), + 0 => format!("{} {}", total, i18n("AppImages up to date")), + 1 => i18n("Found 1 new AppImage"), + n => format!("{} {} {}", i18n("Found"), n, i18n("new AppImages")), }; toast_overlay.add_toast(adw::Toast::new(&msg)); } }); } + fn show_shortcuts_dialog(&self) { + let dialog = adw::Dialog::builder() + .title("Keyboard Shortcuts") + .content_width(400) + .content_height(420) + .build(); + + let toolbar = adw::ToolbarView::new(); + let header = adw::HeaderBar::new(); + toolbar.add_top_bar(&header); + + let scrolled = gtk::ScrolledWindow::builder() + .vexpand(true) + .build(); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(18) + .margin_top(18) + .margin_bottom(18) + .margin_start(18) + .margin_end(18) + .build(); + + // Navigation group + let nav_group = adw::PreferencesGroup::builder() + .title("Navigation") + .build(); + nav_group.add(&shortcut_row("Ctrl+F", "Search")); + nav_group.add(&shortcut_row("Ctrl+D", "Dashboard")); + nav_group.add(&shortcut_row("Ctrl+,", "Preferences")); + content.append(&nav_group); + + // Actions group + let actions_group = adw::PreferencesGroup::builder() + .title("Actions") + .build(); + actions_group.add(&shortcut_row("Ctrl+R / F5", "Scan for AppImages")); + actions_group.add(&shortcut_row("Ctrl+U", "Check for updates")); + content.append(&actions_group); + + // Application group + let app_group = adw::PreferencesGroup::builder() + .title("Application") + .build(); + app_group.add(&shortcut_row("Ctrl+?", "Keyboard shortcuts")); + app_group.add(&shortcut_row("Ctrl+Q", "Quit")); + content.append(&app_group); + + scrolled.set_child(Some(&content)); + toolbar.set_content(Some(&scrolled)); + dialog.set_child(Some(&toolbar)); + dialog.present(Some(self)); + } + fn save_window_state(&self) { let settings = self.settings(); let (width, height) = self.default_size();