use adw::prelude::*; use gtk::gio; use std::rc::Rc; use crate::config::APP_ID; use crate::core::database::Database; use crate::core::duplicates; use crate::core::fuse; use crate::core::wayland; use crate::i18n::{i18n, ni18n}; use super::widgets; /// Build the dashboard page showing system health and statistics. pub fn build_dashboard_page(db: &Rc) -> adw::NavigationPage { let clamp = adw::Clamp::builder() .maximum_size(800) .tightening_threshold(600) .build(); let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(24) .margin_top(24) .margin_bottom(24) .margin_start(18) .margin_end(18) .build(); // FUSE warning banner if not functional let fuse_info = fuse::detect_system_fuse(); if !fuse_info.status.is_functional() { let banner = adw::Banner::builder() .title(&i18n("FUSE is not working - some AppImages may not launch")) .button_label(&i18n("Fix Now")) .revealed(true) .accessible_role(gtk::AccessibleRole::Alert) .build(); banner.set_action_name(Some("win.fix-fuse")); content.append(&banner); } // Getting Started checklist (shown for new users) let records = db.get_all_appimages().unwrap_or_default(); let total_count = records.len(); let integrated_count = records.iter().filter(|r| r.integrated).count(); if total_count < 3 { let started_group = adw::PreferencesGroup::builder() .title("Getting Started") .description("New to Driftwood? Here are three steps to get you up and running.") .build(); let scan_row = adw::ActionRow::builder() .title("Scan your system for apps") .subtitle("Look for AppImage files in your configured folders") .activatable(true) .build(); scan_row.set_action_name(Some("win.scan")); if total_count > 0 { let check = widgets::status_badge_with_icon("emblem-ok-symbolic", "Done", "success"); check.set_valign(gtk::Align::Center); scan_row.add_suffix(&check); } started_group.add(&scan_row); let catalog_row = adw::ActionRow::builder() .title("Browse the app catalog") .subtitle("Discover and install apps from the AppImage ecosystem") .activatable(true) .build(); catalog_row.set_action_name(Some("win.catalog")); catalog_row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("Open catalog"))); started_group.add(&catalog_row); let menu_row = adw::ActionRow::builder() .title("Add an app to your launcher") .subtitle("Make an app findable in your application menu") .build(); if integrated_count > 0 { let check = widgets::status_badge_with_icon("emblem-ok-symbolic", "Done", "success"); check.set_valign(gtk::Align::Center); menu_row.add_suffix(&check); } started_group.add(&menu_row); content.append(&started_group); } // Section 1: System Status content.append(&build_system_status_group()); // Section 2: Library Statistics content.append(&build_library_stats_group(db)); // Section 3: Updates Summary (actionable) content.append(&build_updates_summary_group(db)); // Section 4: Duplicates Summary (actionable) content.append(&build_duplicates_summary_group(db)); // Section 5: Disk Usage (actionable) content.append(&build_disk_usage_group(db)); // Section 6: Activity content.append(&build_activity_group(db)); // Section 7: Quick Actions content.append(&build_quick_actions_group()); clamp.set_child(Some(&content)); let scrolled = gtk::ScrolledWindow::builder() .child(&clamp) .vexpand(true) .build(); let header = adw::HeaderBar::new(); let toolbar = adw::ToolbarView::new(); toolbar.add_top_bar(&header); toolbar.set_content(Some(&scrolled)); widgets::apply_pointer_cursors(&toolbar); adw::NavigationPage::builder() .title("Dashboard") .tag("dashboard") .child(&toolbar) .build() } fn build_system_status_group() -> adw::PreferencesGroup { let group = adw::PreferencesGroup::builder() .title("System Status") .description("Display server, FUSE, and compatibility information") .build(); // Session type let session = wayland::detect_session_type(); let session_row = adw::ActionRow::builder() .title("Display server") .subtitle(session.label()) .tooltip_text("How your system draws windows on screen") .build(); let session_badge = widgets::status_badge( session.label(), match session { wayland::SessionType::Wayland => "success", wayland::SessionType::X11 => "warning", wayland::SessionType::Unknown => "neutral", }, ); session_badge.set_valign(gtk::Align::Center); session_row.add_suffix(&session_badge); group.add(&session_row); // Desktop environment let de = wayland::detect_desktop_environment(); let de_row = adw::ActionRow::builder() .title("Desktop environment") .subtitle(&de) .tooltip_text("Your desktop interface") .build(); group.add(&de_row); // FUSE status let fuse_info = fuse::detect_system_fuse(); let fuse_row = adw::ActionRow::builder() .title("App compatibility") .subtitle(&fuse_description(&fuse_info)) .tooltip_text("Most AppImages need a system component called FUSE to run. This shows whether it is set up correctly.") .build(); let fuse_badge = widgets::status_badge( fuse_info.status.label(), fuse_info.status.badge_class(), ); fuse_badge.set_valign(gtk::Align::Center); fuse_row.add_suffix(&fuse_badge); group.add(&fuse_row); // Install hint if FUSE not functional if let Some(ref hint) = fuse_info.install_hint { let hint_row = adw::ActionRow::builder() .title("Fix app compatibility") .subtitle(hint) .subtitle_selectable(true) .build(); hint_row.add_css_class("monospace"); group.add(&hint_row); } // XWayland let has_xwayland = wayland::has_xwayland(); let xwayland_row = adw::ActionRow::builder() .title("XWayland") .subtitle(if has_xwayland { "Running" } else { "Not detected" }) .tooltip_text("Compatibility layer that lets older apps run on modern displays") .build(); let xwayland_badge = widgets::status_badge( if has_xwayland { "Available" } else { "Unavailable" }, if has_xwayland { "success" } else { "neutral" }, ); xwayland_badge.set_valign(gtk::Align::Center); xwayland_row.add_suffix(&xwayland_badge); group.add(&xwayland_row); // AppImageLauncher conflict check if let Some(version) = fuse::detect_appimagelauncher() { let ail_row = adw::ActionRow::builder() .title("AppImageLauncher detected") .subtitle(&format!( "Version {} - may conflict with some AppImage runtimes", version )) .build(); let ail_badge = widgets::status_badge("Conflict", "warning"); ail_badge.set_valign(gtk::Align::Center); ail_row.add_suffix(&ail_badge); group.add(&ail_row); } group } fn fuse_description(info: &fuse::FuseSystemInfo) -> String { let mut parts = Vec::new(); if info.has_libfuse2 { parts.push("libfuse2"); } if info.has_libfuse3 { parts.push("libfuse3"); } if info.has_fusermount { parts.push("fusermount"); } if info.has_dev_fuse { parts.push("/dev/fuse"); } if parts.is_empty() { "No FUSE components detected".to_string() } else { format!("Available: {}", parts.join(", ")) } } fn build_library_stats_group(db: &Rc) -> adw::PreferencesGroup { let group = adw::PreferencesGroup::builder() .title("Library") .description("Overview of your AppImage collection") .build(); let records = db.get_all_appimages().unwrap_or_default(); let total = records.len(); let integrated = records.iter().filter(|r| r.integrated).count(); let executable = records.iter().filter(|r| r.is_executable).count(); let total_row = adw::ActionRow::builder() .title(&ni18n("AppImage", "AppImages", total as u32)) .subtitle(&total.to_string()) .activatable(true) .build(); total_row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("View library"))); 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(); group.add(&integrated_row); let exec_row = adw::ActionRow::builder() .title("Executable") .subtitle(&format!("{} of {}", executable, total)) .build(); if executable < total { let badge = widgets::status_badge( &format!("{} not executable", total - executable), "warning", ); badge.set_valign(gtk::Align::Center); exec_row.add_suffix(&badge); } group.add(&exec_row); group } fn build_updates_summary_group(db: &Rc) -> adw::PreferencesGroup { let group = adw::PreferencesGroup::builder() .title("Updates") .description("AppImage update availability") .build(); let records = db.get_all_appimages().unwrap_or_default(); let with_update_info = records .iter() .filter(|r| r.update_info.is_some()) .count(); let with_updates = records .iter() .filter(|r| { if let (Some(ref latest), Some(ref current)) = (&r.latest_version, &r.app_version) { crate::core::updater::version_is_newer(latest, current) } else { false } }) .count(); let info_row = adw::ActionRow::builder() .title("With update info") .subtitle(&format!("{} of {}", with_update_info, records.len())) .build(); 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); } updates_row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("View updates"))); group.add(&updates_row); if with_updates > 0 { let update_all_row = adw::ActionRow::builder() .title("Update All") .subtitle(&format!("Apply {} available updates", with_updates)) .activatable(true) .build(); update_all_row.set_action_name(Some("win.update-all")); let update_badge = widgets::status_badge("Go", "suggested"); update_badge.set_valign(gtk::Align::Center); update_all_row.add_suffix(&update_badge); update_all_row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("Update all apps"))); group.add(&update_all_row); } // Last checked timestamp let settings = gio::Settings::new(APP_ID); let last_check = settings.string("last-update-check"); let last_label = if last_check.is_empty() { "Never".to_string() } else { widgets::relative_time(&last_check) }; let last_row = adw::ActionRow::builder() .title("Last checked") .subtitle(&last_label) .build(); group.add(&last_row); group } fn build_duplicates_summary_group(db: &Rc) -> adw::PreferencesGroup { let group = adw::PreferencesGroup::builder() .title("Duplicates") .description("Duplicate and multi-version detection") .build(); let groups = duplicates::detect_duplicates(db); let summary = duplicates::summarize_duplicates(&groups); if summary.total_groups == 0 { let row = adw::ActionRow::builder() .title("No duplicates found") .subtitle("All AppImages appear unique") .build(); let badge = widgets::status_badge("Clean", "success"); badge.set_valign(gtk::Align::Center); row.add_suffix(&badge); 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(); groups_row.set_action_name(Some("win.find-duplicates")); groups_row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("View duplicates"))); group.add(&groups_row); if summary.total_potential_savings > 0 { let savings_row = adw::ActionRow::builder() .title("Potential savings") .subtitle(&widgets::format_size(summary.total_potential_savings as i64)) .build(); let badge = widgets::status_badge("Reclaimable", "warning"); badge.set_valign(gtk::Align::Center); savings_row.add_suffix(&badge); group.add(&savings_row); } } group } 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 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(); total_row.set_action_name(Some("win.cleanup")); total_row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("Clean up disk"))); group.add(&total_row); // Largest AppImages let mut sorted = records.clone(); sorted.sort_by(|a, b| b.size_bytes.cmp(&a.size_bytes)); for record in sorted.iter().take(3) { let name = record.app_name.as_deref().unwrap_or(&record.filename); let row = adw::ActionRow::builder() .title(name) .subtitle(&widgets::format_size(record.size_bytes)) .build(); group.add(&row); } group } fn build_activity_group(db: &Rc) -> adw::PreferencesGroup { let group = adw::PreferencesGroup::builder() .title("Activity") .description("Launch history and usage statistics") .build(); let total_launches = db.get_total_launch_count().unwrap_or(0); let total_row = adw::ActionRow::builder() .title("Total launches") .subtitle(&total_launches.to_string()) .build(); group.add(&total_row); if let Ok(Some((name, time))) = db.get_last_launch() { let last_row = adw::ActionRow::builder() .title("Last launched") .subtitle(&format!("{} - {}", name, time)) .build(); group.add(&last_row); } if let Ok(top) = db.get_top_launched(5) { for (name, count) in &top { let row = adw::ActionRow::builder() .title(name) .subtitle(&format!("{} launches", count)) .build(); group.add(&row); } } 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::Description("Scan for AppImages in configured directories"), ]); 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::Description("Check all AppImages for available 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::Description("Remove orphaned desktop entries and icons"), ]); 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 }