From e20759d1cb8c2b4f267e56e56f82c849c55b66c2 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 27 Feb 2026 19:48:59 +0200 Subject: [PATCH] Rewrite detail view copy for beginners, add tab transitions and lightbox fixes --- Cargo.toml | 2 +- data/resources/style.css | 19 ++ src/ui/detail_view.rs | 422 ++++++++++++++++++++++++++------------ src/ui/security_report.rs | 10 +- 4 files changed, 316 insertions(+), 137 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f3b19b..9f61955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "GPL-3.0-or-later" [dependencies] gtk = { version = "0.11", package = "gtk4", features = ["v4_16"] } -adw = { version = "0.9", package = "libadwaita", features = ["v1_6"] } +adw = { version = "0.9", package = "libadwaita", features = ["v1_7"] } glib = "0.22" gio = "0.22" diff --git a/data/resources/style.css b/data/resources/style.css index 6e3e64f..5b8138d 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -221,3 +221,22 @@ button.flat:not(.pill):not(.suggested-action):not(.destructive-action) { min-width: 24px; min-height: 24px; } + +/* ===== Screenshot Lightbox ===== */ +window.lightbox { + background-color: rgba(0, 0, 0, 0.92); + border-radius: 12px; +} + +window.lightbox .lightbox-counter { + background: rgba(0, 0, 0, 0.6); + color: white; + border-radius: 12px; + padding: 4px 12px; + font-size: 0.85em; +} + +window.lightbox .lightbox-nav { + min-width: 48px; + min-height: 48px; +} diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index 20cae58..2ffcd58 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -23,8 +23,10 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc) -> adw::Nav // Toast overlay for copy actions let toast_overlay = adw::ToastOverlay::new(); - // ViewStack for tabbed content + // ViewStack for tabbed content with crossfade transitions let view_stack = adw::ViewStack::new(); + view_stack.set_enable_transitions(true); + view_stack.set_transition_duration(200); // Build tab pages let overview_page = build_overview_tab(record, db); @@ -324,14 +326,14 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { let row = adw::ActionRow::builder() .title("License") .subtitle(lic) - .tooltip_text("SPDX license identifier for this application") + .tooltip_text("The license that governs how this app can be used and shared") .build(); about_group.add(&row); } if let Some(ref pg) = record.project_group { let row = adw::ActionRow::builder() - .title("Project group") + .title("Project") .subtitle(pg) .build(); about_group.add(&row); @@ -411,16 +413,24 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .build(); overlay.add_overlay(&spinner); + // Don't let overlays steal focus (prevents scroll jump on dialog close) + overlay.set_focusable(false); + overlay.set_focus_on_click(false); + // Click handler for lightbox let textures_click = textures.clone(); let click = gtk::GestureClick::new(); click.connect_released(move |gesture, _, _, _| { let textures_ref = textures_click.borrow(); - if let Some(Some(ref texture)) = textures_ref.get(idx) { + if textures_ref.iter().any(|t| t.is_some()) { if let Some(widget) = gesture.widget() { if let Some(root) = gtk::prelude::WidgetExt::root(&widget) { if let Ok(window) = root.downcast::() { - show_screenshot_lightbox(&window, texture); + show_screenshot_lightbox( + &window, + &textures_click, + idx, + ); } } } @@ -497,7 +507,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { let link_entries: &[(&str, &str, &Option)] = &[ ("Homepage", "web-browser-symbolic", &record.homepage_url), - ("Bug tracker", "bug-symbolic", &record.bugtracker_url), + ("Report a problem", "bug-symbolic", &record.bugtracker_url), ("Source code", "code-symbolic", &record.vcs_url), ("Documentation", "help-browser-symbolic", &record.help_url), ("Donate", "emblem-favorite-symbolic", &record.donation_url), @@ -560,9 +570,8 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { display_label )) .tooltip_text( - "AppImages can include built-in update information that tells Driftwood \ - where to check for newer versions. Common methods include GitHub releases, \ - zsync (efficient delta updates), and direct download URLs." + "Driftwood can check for newer versions of this app automatically. \ + The developer has included information about where updates are published." ) .build(); updates_group.add(&row); @@ -723,7 +732,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { if has_capabilities { let cap_group = adw::PreferencesGroup::builder() - .title("Capabilities") + .title("Features") .build(); if let Some(ref kw) = record.keywords { @@ -751,7 +760,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .title("Content rating") .subtitle(cr) .tooltip_text( - "Content rating based on the OARS (Open Age Ratings Service) system", + "An age rating for the app's content, similar to game ratings.", ) .build(); cap_group.add(&row); @@ -761,11 +770,11 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { if let Ok(actions) = serde_json::from_str::>(actions_json) { if !actions.is_empty() { let row = adw::ActionRow::builder() - .title("Desktop actions") + .title("Quick actions") .subtitle(&actions.join(", ")) .tooltip_text( "Additional actions available from the right-click menu \ - when this app is integrated into the desktop", + when this app is added to your app menu.", ) .build(); cap_group.add(&row); @@ -784,29 +793,32 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .build(); let type_str = match record.appimage_type { - Some(1) => "Type 1 (ISO 9660) - older format, still widely supported", - Some(2) => "Type 2 (SquashFS) - modern format, most common today", + Some(1) => "Type 1 - older format, still widely supported", + Some(2) => "Type 2 - modern, compressed format", _ => "Unknown type", }; let type_row = adw::ActionRow::builder() - .title("AppImage format") + .title("Package format") .subtitle(type_str) .tooltip_text( - "AppImages come in two formats. Type 1 uses an ISO 9660 filesystem \ - (older, simpler). Type 2 uses SquashFS (modern, compressed, smaller \ - files). Type 2 is the standard today and is what most AppImage tools \ - produce." + "AppImages come in two formats. Type 1 is the older format. \ + Type 2 is the current standard - it uses compression for smaller \ + files and faster loading." ) .build(); info_group.add(&type_row); let exec_row = adw::ActionRow::builder() - .title("Executable") + .title("Ready to run") .subtitle(if record.is_executable { - "Yes - this file has execute permission" + "Yes - this file is ready to launch" } else { - "No - execute permission is missing. It will be set automatically when launched." + "No - will be fixed automatically when launched" }) + .tooltip_text( + "Whether the file has the permissions needed to run. \ + If not, Driftwood sets this up automatically the first time you launch it." + ) .build(); info_group.add(&exec_row); @@ -814,13 +826,13 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { let sig_row = adw::ActionRow::builder() .title("Digital signature") .subtitle(if record.has_signature { - "This AppImage contains a GPG signature" + "Signed by the developer" } else { "Not signed" }) .tooltip_text( - "AppImages can be digitally signed by their author using GPG. \ - A signature helps verify that the file hasn't been tampered with." + "This app was signed by its developer, which helps verify \ + it hasn't been tampered with since it was published." ) .build(); let sig_badge = if record.has_signature { @@ -839,7 +851,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { info_group.add(&seen_row); let scanned_row = adw::ActionRow::builder() - .title("Last scanned") + .title("Last checked") .subtitle(&record.last_scanned) .build(); info_group.add(&scanned_row); @@ -886,21 +898,21 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & // Desktop Integration group let integration_group = adw::PreferencesGroup::builder() - .title("Desktop Integration") + .title("App Menu") .description( - "Show this app in your Activities menu and app launcher, \ - just like a regular installed application." + "Add this app to your launcher so you can find it \ + like any other installed app." ) .build(); let switch_row = adw::SwitchRow::builder() .title("Add to application menu") - .subtitle("Creates a .desktop entry and installs the app icon") + .subtitle("Creates a shortcut and installs the app icon") .active(record.integrated) .tooltip_text( - "Desktop integration makes this AppImage appear in your Activities menu \ - and app launcher, just like a regular installed app. It creates a .desktop \ - file (a shortcut) and copies the app's icon to your system icon folder." + "This makes the app appear in your Activities menu and app launcher, \ + just like a regular installed app. It creates a shortcut file and \ + copies the app's icon to your system." ) .build(); @@ -940,7 +952,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & if record.integrated { if let Some(ref desktop_file) = record.desktop_file { let row = adw::ActionRow::builder() - .title("Desktop file") + .title("Shortcut file") .subtitle(desktop_file) .subtitle_selectable(true) .build(); @@ -954,8 +966,8 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & let compat_group = adw::PreferencesGroup::builder() .title("Compatibility") .description( - "How well this app works with your display server and filesystem. \ - Most issues here can be resolved with a small package install." + "How well this app works with your system. \ + Most issues can be fixed with a quick install." ) .build(); @@ -966,13 +978,12 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & .unwrap_or(WaylandStatus::Unknown); let wayland_row = adw::ActionRow::builder() - .title("Wayland display") + .title("Display compatibility") .subtitle(wayland_user_explanation(&wayland_status)) .tooltip_text( - "Wayland is the modern display system used by GNOME and most Linux desktops. \ - It replaced the older X11 system. Apps built for X11 still work through \ - a compatibility layer called XWayland, but native Wayland apps look \ - sharper and perform better, especially on high-resolution screens." + "Wayland is the modern display system on Linux. Apps built for the \ + older system (X11) still work, but native Wayland apps look sharper, \ + especially on high-resolution screens." ) .build(); let wayland_badge = widgets::status_badge(wayland_status.label(), wayland_status.badge_class()); @@ -982,14 +993,13 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & // Analyze toolkit button let analyze_row = adw::ActionRow::builder() - .title("Analyze toolkit") - .subtitle("Inspect bundled libraries to detect which UI toolkit this app uses") + .title("Detect app framework") + .subtitle("Check which technology this app is built with") .activatable(true) .tooltip_text( - "UI toolkits (like GTK, Qt, Electron) are the frameworks apps use to \ - draw their windows and buttons. Knowing the toolkit helps predict Wayland \ - compatibility - GTK4 apps are natively Wayland, while older Qt or Electron \ - apps may need XWayland." + "Apps are built with different frameworks (like GTK, Qt, or Electron). \ + Knowing the framework helps predict how well the app works with \ + your display system." ) .build(); let analyze_icon = gtk::Image::from_icon_name("system-search-symbolic"); @@ -1017,12 +1027,12 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & let toolkit_label = analysis.toolkit.label(); let lib_count = analysis.libraries_found.len(); row_clone.set_subtitle(&format!( - "Detected: {} ({} libraries scanned)", - toolkit_label, lib_count, + "Built with: {}", + toolkit_label, )); } Err(_) => { - row_clone.set_subtitle("Analysis failed - the AppImage may not be mountable"); + row_clone.set_subtitle("Analysis failed - could not read the app's contents"); } } }); @@ -1032,11 +1042,14 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & // Runtime Wayland status (from post-launch analysis) if let Some(ref runtime_status) = record.runtime_wayland_status { let runtime_row = adw::ActionRow::builder() - .title("Last observed protocol") + .title("Last display mode") .subtitle(&format!( "When this app was last launched, it used: {}", runtime_status )) + .tooltip_text( + "How the app connected to your display the last time it was launched." + ) .build(); if let Some(ref checked) = record.runtime_wayland_checked { let info = gtk::Label::builder() @@ -1058,13 +1071,11 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & .unwrap_or(fuse_system.status.clone()); let fuse_row = adw::ActionRow::builder() - .title("FUSE (filesystem)") + .title("App mounting") .subtitle(fuse_user_explanation(&fuse_status)) .tooltip_text( - "FUSE (Filesystem in Userspace) lets AppImages mount themselves as \ - virtual drives so they can run directly without extracting. Without it, \ - AppImages still work but need to extract to a temp folder first, which \ - is slower. Most systems have FUSE already, but some need libfuse2 installed." + "FUSE lets apps like AppImages run directly without unpacking first. \ + Without it, apps still work but take a little longer to start." ) .build(); let fuse_badge = widgets::status_badge_with_icon( @@ -1077,7 +1088,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & if let Some(cmd) = fuse_install_command(&fuse_status) { let copy_btn = widgets::copy_button(cmd, Some(toast_overlay)); copy_btn.set_valign(gtk::Align::Center); - copy_btn.set_tooltip_text(Some(&format!("Copy '{}' to clipboard", cmd))); + copy_btn.set_tooltip_text(Some("Copy install command to clipboard")); fuse_row.add_suffix(©_btn); } compat_group.add(&fuse_row); @@ -1086,16 +1097,15 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & let appimage_path = std::path::Path::new(&record.path); let app_fuse_status = fuse::determine_app_fuse_status(&fuse_system, appimage_path); let launch_method_row = adw::ActionRow::builder() - .title("Launch method") + .title("Startup method") .subtitle(&format!( "This app will launch using: {}", app_fuse_status.label() )) .tooltip_text( - "AppImages can launch two ways: 'FUSE mount' mounts the image as a \ - virtual drive (fast, instant startup), or 'extract' unpacks to a temp \ - folder first (slower, but works everywhere). The method is chosen \ - automatically based on your system's FUSE support." + "AppImages can start two ways: mounting (fast, instant startup) or \ + unpacking to a temporary folder first (slower, but works everywhere). \ + The method is chosen automatically based on your system." ) .build(); let launch_badge = widgets::status_badge( @@ -1109,10 +1119,10 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & // Sandboxing group let sandbox_group = adw::PreferencesGroup::builder() - .title("Sandboxing") + .title("App Isolation") .description( - "Isolate this app for extra security. Sandboxing limits what \ - the app can access on your system." + "Restrict what this app can access on your system \ + for extra security." ) .build(); @@ -1125,24 +1135,20 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & let firejail_available = launcher::has_firejail(); let sandbox_subtitle = if firejail_available { - format!( - "Isolate this app using Firejail. Current mode: {}", - current_mode.display_label() - ) + format!("Currently: {}", current_mode.display_label()) } else { - "Firejail is not installed. Use the row below to copy the install command.".to_string() + "Not available yet. Install with one command using the button below.".to_string() }; let firejail_row = adw::SwitchRow::builder() - .title("Firejail sandbox") + .title("Isolate this app") .subtitle(&sandbox_subtitle) .active(current_mode == SandboxMode::Firejail) .sensitive(firejail_available) .tooltip_text( - "Sandboxing restricts what an app can access on your system - files, \ - network, devices, etc. This adds a security layer so that even if an \ - app is compromised, it cannot freely access your personal data. Firejail \ - is a lightweight Linux sandboxing tool." + "Sandboxing restricts what an app can access - files, network, \ + devices, etc. Even if an app has a security issue, it can't \ + freely access your personal data." ) .build(); @@ -1163,15 +1169,15 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & if !firejail_available { let firejail_cmd = "sudo apt install firejail"; let info_row = adw::ActionRow::builder() - .title("Install Firejail") - .subtitle(firejail_cmd) + .title("Install app isolation") + .subtitle("Install with one command") .build(); let badge = widgets::status_badge("Missing", "warning"); badge.set_valign(gtk::Align::Center); info_row.add_suffix(&badge); let copy_btn = widgets::copy_button(firejail_cmd, Some(toast_overlay)); copy_btn.set_valign(gtk::Align::Center); - copy_btn.set_tooltip_text(Some(&format!("Copy '{}' to clipboard", firejail_cmd))); + copy_btn.set_tooltip_text(Some("Copy install command to clipboard")); info_row.add_suffix(©_btn); sandbox_group.add(&info_row); } @@ -1207,10 +1213,9 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .build(); let group = adw::PreferencesGroup::builder() - .title("Vulnerability Scanning") + .title("Security Check") .description( - "Scan the libraries bundled inside this AppImage for known \ - security vulnerabilities (CVEs)." + "Check this app for known security issues." ) .build(); @@ -1231,18 +1236,22 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { group.add(&row); } else { let lib_row = adw::ActionRow::builder() - .title("Bundled libraries") + .title("Included components") .subtitle(&format!( - "{} libraries detected inside this AppImage", + "{} components found inside this app", libs.len() )) + .tooltip_text( + "Apps bundle their own copies of system components (libraries). \ + These can sometimes contain known security issues if they're outdated." + ) .build(); group.add(&lib_row); if summary.total() == 0 { let row = adw::ActionRow::builder() .title("Vulnerabilities") - .subtitle("No known security issues found in the bundled libraries.") + .subtitle("No known security issues found.") .build(); let badge = widgets::status_badge("Clean", "success"); badge.set_valign(gtk::Align::Center); @@ -1266,15 +1275,12 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { // Scan button let scan_row = adw::ActionRow::builder() - .title("Run security scan") - .subtitle("Check bundled libraries against known CVE databases") + .title("Run security check") + .subtitle("Check for known security issues in this app") .activatable(true) .tooltip_text( - "CVE stands for Common Vulnerabilities and Exposures - a public list \ - of known security bugs in software. AppImages bundle their own copies \ - of system libraries, which may contain outdated versions with known \ - vulnerabilities. This scan checks those bundled libraries against the \ - OSV.dev database to find any known issues." + "This checks the components inside this app against a public database \ + of known security issues to see if any are outdated or vulnerable." ) .build(); let scan_icon = gtk::Image::from_icon_name("security-medium-symbolic"); @@ -1313,7 +1319,7 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { } } Err(_) => { - row_clone.set_subtitle("Scan failed - the AppImage may not be mountable"); + row_clone.set_subtitle("Check failed - could not read the app's contents"); } } }); @@ -1330,14 +1336,14 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { if let Some(ref hash) = record.sha256 { let hash_row = adw::ActionRow::builder() - .title("SHA256 checksum") + .title("File fingerprint") .subtitle(hash) .subtitle_selectable(true) .tooltip_text( - "A SHA256 checksum is a unique fingerprint of the file. If even one \ - byte changes, the checksum changes completely. You can compare this \ - against the developer's published checksum to verify the file hasn't \ - been tampered with or corrupted during download." + "A unique code (SHA256 checksum) generated from the file's contents. \ + If the file changes in any way, this code changes too. You can compare \ + it against the developer's published fingerprint to verify nothing \ + was altered." ) .build(); hash_row.add_css_class("property"); @@ -1416,13 +1422,13 @@ fn build_storage_tab( // Data paths group let paths_group = adw::PreferencesGroup::builder() .title("App Data") - .description("Config, cache, and data directories this app may have created.") + .description("Settings, cache, and data this app may have saved to your system.") .build(); // Discover button let discover_row = adw::ActionRow::builder() .title("Find app data") - .subtitle("Search for config, cache, and data directories") + .subtitle("Search for files this app has saved") .activatable(true) .build(); let discover_icon = gtk::Image::from_icon_name("folder-saved-search-symbolic"); @@ -1449,7 +1455,7 @@ fn build_storage_tab( Ok(fp) => { let count = fp.paths.len(); if count == 0 { - row_clone.set_subtitle("No associated data directories found"); + row_clone.set_subtitle("No saved data found"); } else { row_clone.set_subtitle(&format!( "Found {} path{} using {}", @@ -1501,7 +1507,7 @@ fn build_storage_tab( .build(); let path_row = adw::ActionRow::builder() - .title("Path") + .title("File location") .subtitle(&record.path) .subtitle_selectable(true) .build(); @@ -1548,35 +1554,36 @@ fn build_storage_tab( fn wayland_user_explanation(status: &WaylandStatus) -> &'static str { match status { WaylandStatus::Native => - "Runs natively on Wayland - the best experience on modern Linux desktops.", + "Fully compatible - the best experience on your system.", WaylandStatus::XWayland => - "Uses XWayland for display. Works fine, but may appear slightly \ + "Works through a compatibility layer. May appear slightly \ blurry on high-resolution screens.", WaylandStatus::Possible => - "Might work on Wayland with the right settings. Try launching it to find out.", + "Might work well. Try launching it to find out.", WaylandStatus::X11Only => - "Designed for X11 only. It will run through XWayland automatically, \ - but you may notice minor display quirks.", + "Built for an older display system. It will run automatically, \ + but you may notice minor visual quirks.", WaylandStatus::Unknown => - "Not yet determined. Launch the app or use 'Analyze toolkit' below to check.", + "Not yet determined. Launch the app or use 'Detect app framework' to check.", } } fn fuse_user_explanation(status: &FuseStatus) -> &'static str { match status { FuseStatus::FullyFunctional => - "FUSE is working - AppImages mount directly for fast startup.", + "Everything is set up - apps start instantly.", FuseStatus::Fuse3Only => - "Only FUSE 3 found. Some AppImages need FUSE 2. \ - Click the copy button to get the install command.", + "A small system component is missing. Most apps will still work, \ + but some may need it. Copy the install command to fix this.", FuseStatus::NoFusermount => - "FUSE tools not found. The app will still work by extracting to a \ - temporary folder, but startup will be slower.", + "A system component is missing, so apps will take a little longer \ + to start. They'll still work fine.", FuseStatus::NoDevFuse => - "/dev/fuse not available. FUSE may not be configured on your system. \ - Apps will extract to a temp folder instead.", + "Your system doesn't support instant app mounting. Apps will unpack \ + before starting, which takes a bit longer.", FuseStatus::MissingLibfuse2 => - "libfuse2 is missing. Click the copy button to get the install command.", + "A small system component is needed for fast startup. \ + Copy the install command to fix this.", } } @@ -1590,29 +1597,182 @@ fn fuse_install_command(status: &FuseStatus) -> Option<&'static str> { } } -/// Show a screenshot in a fullscreen-ish lightbox dialog. -fn show_screenshot_lightbox(parent: >k::Window, texture: >k::gdk::Texture) { +/// Show a screenshot in a fullscreen lightbox window with prev/next navigation. +/// Uses a separate gtk::Window to avoid parent scroll position interference. +fn show_screenshot_lightbox( + parent: >k::Window, + textures: &Rc>>>, + initial_index: usize, +) { + let current = Rc::new(std::cell::Cell::new(initial_index)); + let textures = textures.clone(); + let count = textures.borrow().len(); + let has_multiple = count > 1; + + // Fullscreen modal window with dark background + let win = gtk::Window::builder() + .transient_for(parent) + .modal(true) + .decorated(false) + .default_width(parent.width()) + .default_height(parent.height()) + .build(); + win.add_css_class("lightbox"); + + // Image with generous margins so there's a dark border to click on let picture = gtk::Picture::builder() - .paintable(texture) .content_fit(gtk::ContentFit::Contain) - .hexpand(true) - .vexpand(true) + .halign(gtk::Align::Center) + .valign(gtk::Align::Center) + .margin_start(72) + .margin_end(72) + .margin_top(56) + .margin_bottom(56) .build(); picture.set_can_shrink(true); - let dialog = adw::Dialog::builder() - .title("Screenshot") - .content_width(900) - .content_height(600) + // Set initial texture + { + let t = textures.borrow(); + if let Some(Some(ref tex)) = t.get(initial_index) { + picture.set_paintable(Some(tex)); + } + } + + // Navigation buttons + let prev_btn = gtk::Button::builder() + .icon_name("go-previous-symbolic") + .css_classes(["circular", "osd", "lightbox-nav"]) + .valign(gtk::Align::Center) + .halign(gtk::Align::Start) + .margin_start(16) .build(); - let toolbar = adw::ToolbarView::new(); - let header = adw::HeaderBar::new(); - toolbar.add_top_bar(&header); - toolbar.set_content(Some(&picture)); - dialog.set_child(Some(&toolbar)); + let next_btn = gtk::Button::builder() + .icon_name("go-next-symbolic") + .css_classes(["circular", "osd", "lightbox-nav"]) + .valign(gtk::Align::Center) + .halign(gtk::Align::End) + .margin_end(16) + .build(); - dialog.present(Some(parent)); + // Counter label (e.g. "2 / 5") + let counter = gtk::Label::builder() + .label(&format!("{} / {}", initial_index + 1, count)) + .css_classes(["lightbox-counter"]) + .halign(gtk::Align::Center) + .valign(gtk::Align::End) + .margin_bottom(16) + .build(); + + // Close button (top-right) + let close_btn = gtk::Button::builder() + .icon_name("window-close-symbolic") + .css_classes(["circular", "osd"]) + .halign(gtk::Align::End) + .valign(gtk::Align::Start) + .margin_top(16) + .margin_end(16) + .build(); + + // Build overlay: picture as child, buttons + counter as overlays + let overlay = gtk::Overlay::builder() + .child(&picture) + .hexpand(true) + .vexpand(true) + .build(); + + overlay.add_overlay(&close_btn); + if has_multiple { + overlay.add_overlay(&prev_btn); + overlay.add_overlay(&next_btn); + overlay.add_overlay(&counter); + } + + win.set_child(Some(&overlay)); + + // --- Click outside image to close --- + // Picture's gesture claims clicks on the image, preventing close. + let pic_gesture = gtk::GestureClick::new(); + pic_gesture.connect_released(|_, _, _, _| {}); + picture.add_controller(pic_gesture); + + // Window gesture fires for clicks on the dark margin area. + { + let win_ref = win.clone(); + let bg_gesture = gtk::GestureClick::new(); + bg_gesture.connect_released(move |_, _, _, _| { + win_ref.close(); + }); + win.add_controller(bg_gesture); + } + + // --- Close button --- + { + let win_ref = win.clone(); + close_btn.connect_clicked(move |_| { + win_ref.close(); + }); + } + + // --- Navigation --- + let make_navigate = |direction: i32| { + let textures_ref = textures.clone(); + let current_ref = current.clone(); + let picture_ref = picture.clone(); + let counter_ref = counter.clone(); + move || { + let idx = current_ref.get(); + let new_idx = if direction < 0 { + if idx == 0 { count - 1 } else { idx - 1 } + } else { + if idx + 1 >= count { 0 } else { idx + 1 } + }; + let t = textures_ref.borrow(); + if let Some(Some(ref tex)) = t.get(new_idx) { + picture_ref.set_paintable(Some(tex)); + current_ref.set(new_idx); + counter_ref.set_label(&format!("{} / {}", new_idx + 1, count)); + } + } + }; + + if has_multiple { + let go_prev = make_navigate(-1); + prev_btn.connect_clicked(move |_| go_prev()); + + let go_next = make_navigate(1); + next_btn.connect_clicked(move |_| go_next()); + } + + // --- Keyboard: Escape to close, Left/Right to navigate --- + { + let win_ref = win.clone(); + let go_prev_key = make_navigate(-1); + let go_next_key = make_navigate(1); + let key_ctl = gtk::EventControllerKey::new(); + key_ctl.connect_key_pressed(move |_, key, _, _| { + match key { + gtk::gdk::Key::Escape => { + win_ref.close(); + return glib::Propagation::Stop; + } + gtk::gdk::Key::Left if has_multiple => { + go_prev_key(); + return glib::Propagation::Stop; + } + gtk::gdk::Key::Right if has_multiple => { + go_next_key(); + return glib::Propagation::Stop; + } + _ => {} + } + glib::Propagation::Proceed + }); + win.add_controller(key_ctl); + } + + win.present(); } /// Fetch a favicon for a URL and set it on an image widget. diff --git a/src/ui/security_report.rs b/src/ui/security_report.rs index 165fa97..6d03de8 100644 --- a/src/ui/security_report.rs +++ b/src/ui/security_report.rs @@ -103,14 +103,14 @@ fn build_report_content(db: &Rc) -> gtk::ScrolledWindow { let empty = adw::StatusPage::builder() .icon_name("security-medium-symbolic") .title("No Security Scans") - .description("Run a security scan to check bundled libraries for known vulnerabilities.") + .description("Run a security check to look for known issues in your apps.") .build(); content.append(&empty); } else { let clean = adw::StatusPage::builder() .icon_name("security-high-symbolic") .title("All Clear") - .description("No known vulnerabilities found in any bundled libraries.") + .description("No known security issues found in any of your apps.") .build(); content.append(&clean); } @@ -144,13 +144,13 @@ fn build_report_content(db: &Rc) -> gtk::ScrolledWindow { fn build_summary_group(summary: &crate::core::database::CveSummary) -> adw::PreferencesGroup { let group = adw::PreferencesGroup::builder() .title("Vulnerability Summary") - .description("Overall security status across all AppImages") + .description("Overall security status across all your apps") .build(); let total_row = adw::ActionRow::builder() .title("Total vulnerabilities") .subtitle(&summary.total().to_string()) - .tooltip_text("Common Vulnerabilities and Exposures found in bundled libraries") + .tooltip_text("Known security issues found in the components bundled inside your apps") .build(); let total_badge = widgets::status_badge(summary.max_severity(), summary.badge_class()); total_badge.set_valign(gtk::Align::Center); @@ -207,7 +207,7 @@ fn build_app_findings_group( summary: &crate::core::database::CveSummary, cve_matches: &[crate::core::database::CveMatchRecord], ) -> adw::PreferencesGroup { - let description = format!("{} CVE (vulnerability) records found", summary.total()); + let description = format!("{} known security issues found", summary.total()); let group = adw::PreferencesGroup::builder() .title(app_name) .description(&description)