Rewrite detail view copy for beginners, add tab transitions and lightbox fixes

- Replace technical jargon with plain language across all 4 detail tabs
- Friendly titles/subtitles with technical details available in tooltips
- Soften terminal commands ("Install with one command" instead of raw commands)
- Rename sections: Desktop Integration -> App Menu, Sandboxing -> App Isolation,
  Vulnerability Scanning -> Security Check, Capabilities -> Features
- Rewrite Wayland/FUSE explanations to avoid acronyms and dev terminology
- Update security report page with beginner-friendly descriptions
- Enable libadwaita 1.7 ViewStack crossfade transitions between detail tabs
- Rewrite screenshot lightbox as separate gtk::Window (fixes scroll jump on
  close, adds click-outside-to-close, rounded corners via CSS)
- Add prev/next navigation arrows and keyboard support to lightbox
This commit is contained in:
lashman
2026-02-27 19:48:59 +02:00
parent 65a1ea78fe
commit c9c9c0341b
5 changed files with 443 additions and 137 deletions

View File

@@ -6,7 +6,7 @@ license = "GPL-3.0-or-later"
[dependencies] [dependencies]
gtk = { version = "0.11", package = "gtk4", features = ["v4_16"] } 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" glib = "0.22"
gio = "0.22" gio = "0.22"

View File

@@ -221,3 +221,22 @@ button.flat:not(.pill):not(.suggested-action):not(.destructive-action) {
min-width: 24px; min-width: 24px;
min-height: 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;
}

View File

@@ -0,0 +1,127 @@
# Beginner-Friendly Copy Overhaul - Design
**Goal:** Rewrite all technical jargon in the detail view into plain language that newcomers can understand, while keeping technical details available in tooltips.
**Approach:** Friendly titles/subtitles for everyone, technical details on hover. Terminal commands stay copyable but with softer framing ("Install with one command" instead of showing the raw command as a subtitle).
**Scope:** detail_view.rs (all 4 tabs), security_report.rs, fuse_user_explanation(), wayland_user_explanation()
---
## Overview Tab
### About section
- "SPDX license identifier for this application" tooltip -> "The license that governs how this app can be used and shared."
- "Project group" title -> "Project"
- "Bug tracker" link label -> "Report a problem"
### File Information section
- "AppImage format" title -> "Package format"
- "Type 1 (ISO 9660) - older format, still widely supported" -> "Type 1 - older format, still widely supported"
- "Type 2 (SquashFS) - modern format, most common today" -> "Type 2 - modern, compressed format"
- Tooltip rewrite: "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."
- "Executable" title -> "Ready to run"
- "Yes - this file has execute permission" -> "Yes - this file is ready to launch"
- "No - execute permission is missing. It will be set automatically when launched." -> "No - will be fixed automatically when launched"
- Tooltip: "Whether the file has the permissions needed to run. If not, Driftwood sets this up automatically the first time you launch it."
- "This AppImage contains a GPG signature" subtitle -> "Signed by the developer"
- Signature tooltip: "This app was signed by its developer, which helps verify it hasn't been tampered with since it was published."
- "Last scanned" -> "Last checked"
### Capabilities section
- Section title: "Capabilities" -> "Features"
- "Desktop actions" -> "Quick actions"
- Tooltip: "Additional actions available from the right-click menu when this app is added to your app menu."
- Content rating tooltip: "An age rating for the app's content, similar to game ratings."
### Updates section
- Tooltip about zsync/delta updates -> "Driftwood can check for newer versions of this app automatically. The developer has included information about where updates are published."
---
## System Tab
### Desktop Integration section
- Section title: "Desktop Integration" -> "App Menu"
- Description -> "Add this app to your launcher so you can find it like any other installed app."
- Switch subtitle: "Creates a .desktop entry and installs the app icon" -> "Creates a shortcut and installs the app icon"
- Switch tooltip: "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."
- "Desktop file" row title -> "Shortcut file"
### Compatibility section
- Description -> "How well this app works with your system. Most issues can be fixed with a quick install."
- "Wayland display" -> "Display compatibility"
- Wayland tooltip: "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."
- "Analyze toolkit" -> "Detect app framework"
- Subtitle: "Inspect bundled libraries to detect which UI toolkit this app uses" -> "Check which technology this app is built with"
- Tooltip: "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."
- Post-analysis subtitle: "Detected: {toolkit} ({count} libraries scanned)" -> "Built with: {toolkit}"
- Error subtitle: "Analysis failed - the AppImage may not be mountable" -> "Analysis failed - could not read the app's contents"
- "Last observed protocol" -> "Last display mode"
- Tooltip: "How the app connected to your display the last time it was launched."
- "FUSE (filesystem)" -> "App mounting"
- FUSE tooltip: "FUSE lets apps like AppImages run directly without unpacking first. Without it, apps still work but take a little longer to start."
- "Launch method" -> "Startup method"
- Launch tooltip: "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."
### Wayland explanations (wayland_user_explanation)
- Native: "Fully compatible - the best experience on your system."
- XWayland: "Works through a compatibility layer. May appear slightly blurry on high-resolution screens."
- Possible: "Might work well. Try launching it to find out."
- X11Only: "Built for an older display system. It will run automatically, but you may notice minor visual quirks."
- Unknown: "Not yet determined. Launch the app or use 'Detect app framework' to check."
### FUSE explanations (fuse_user_explanation)
- FullyFunctional: "Everything is set up - apps start instantly."
- Fuse3Only: "A small system component is missing. Most apps will still work, but some may need it. Copy the install command to fix this."
- NoFusermount: "A system component is missing, so apps will take a little longer to start. They'll still work fine."
- NoDevFuse: "Your system doesn't support instant app mounting. Apps will unpack before starting, which takes a bit longer."
- MissingLibfuse2: "A small system component is needed for fast startup. Copy the install command to fix this."
### Sandboxing section
- Section title: "Sandboxing" -> "App Isolation"
- Description -> "Restrict what this app can access on your system for extra security."
- Switch title: "Firejail sandbox" -> "Isolate this app"
- Switch tooltip: "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."
- Subtitle when available: "Isolate this app using Firejail. Current mode: {mode}" -> "Currently: {mode}"
- Subtitle when missing: "Firejail is not installed. Use the row below to copy the install command." -> "Not available yet. Install with one command using the button below."
- Install row: "Install Firejail" / "sudo apt install firejail" -> "Install app isolation" / "Install with one command"
---
## Security Tab
### Vulnerability Scanning section
- Section title: "Vulnerability Scanning" -> "Security Check"
- Description -> "Check this app for known security issues."
- "Bundled libraries" -> "Included components"
- Subtitle: "{N} libraries detected inside this AppImage" -> "{N} components found inside this app"
- Tooltip: "Apps bundle their own copies of system components (libraries). These can sometimes contain known security issues if they're outdated."
- Clean subtitle: "No known security issues found in the bundled libraries." -> "No known security issues found."
- Scan row: "Run security scan" / "Check bundled libraries against known CVE databases" -> "Run security check" / "Check for known security issues in this app"
- Scan tooltip: "This checks the components inside this app against a public database of known security issues to see if any are outdated or vulnerable."
- Error: "Scan failed - the AppImage may not be mountable" -> "Check failed - could not read the app's contents"
### Integrity section
- "SHA256 checksum" -> "File fingerprint"
- Tooltip: "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."
---
## Storage Tab
- Group description: "Config, cache, and data directories this app may have created." -> "Settings, cache, and data this app may have saved to your system."
- Search subtitle: "Search for config, cache, and data directories" -> "Search for files this app has saved"
- Empty result: "No associated data directories found" -> "No saved data found"
- "Path" -> "File location"
---
## Security Report page (security_report.rs)
- "Run a security scan to check bundled libraries for known vulnerabilities." -> "Run a security check to look for known issues in your apps."
- "No known vulnerabilities found in any bundled libraries." -> "No known security issues found in any of your apps."
- "Overall security status across all AppImages" -> "Overall security status across all your apps"
- Tooltip: "Common Vulnerabilities and Exposures found in bundled libraries" -> "Known security issues found in the components bundled inside your apps."
- Per-app description: "{N} CVE (vulnerability) records found" -> "{N} known security issues found"
- Individual CVE/library expander titles: keep as-is (technical detail layer)

View File

@@ -23,8 +23,10 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
// Toast overlay for copy actions // Toast overlay for copy actions
let toast_overlay = adw::ToastOverlay::new(); let toast_overlay = adw::ToastOverlay::new();
// ViewStack for tabbed content // ViewStack for tabbed content with crossfade transitions
let view_stack = adw::ViewStack::new(); let view_stack = adw::ViewStack::new();
view_stack.set_enable_transitions(true);
view_stack.set_transition_duration(200);
// Build tab pages // Build tab pages
let overview_page = build_overview_tab(record, db); let overview_page = build_overview_tab(record, db);
@@ -324,14 +326,14 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title("License") .title("License")
.subtitle(lic) .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(); .build();
about_group.add(&row); about_group.add(&row);
} }
if let Some(ref pg) = record.project_group { if let Some(ref pg) = record.project_group {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title("Project group") .title("Project")
.subtitle(pg) .subtitle(pg)
.build(); .build();
about_group.add(&row); about_group.add(&row);
@@ -411,16 +413,24 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
.build(); .build();
overlay.add_overlay(&spinner); 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 // Click handler for lightbox
let textures_click = textures.clone(); let textures_click = textures.clone();
let click = gtk::GestureClick::new(); let click = gtk::GestureClick::new();
click.connect_released(move |gesture, _, _, _| { click.connect_released(move |gesture, _, _, _| {
let textures_ref = textures_click.borrow(); 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(widget) = gesture.widget() {
if let Some(root) = gtk::prelude::WidgetExt::root(&widget) { if let Some(root) = gtk::prelude::WidgetExt::root(&widget) {
if let Ok(window) = root.downcast::<gtk::Window>() { if let Ok(window) = root.downcast::<gtk::Window>() {
show_screenshot_lightbox(&window, texture); show_screenshot_lightbox(
&window,
&textures_click,
idx,
);
} }
} }
} }
@@ -497,7 +507,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
let link_entries: &[(&str, &str, &Option<String>)] = &[ let link_entries: &[(&str, &str, &Option<String>)] = &[
("Homepage", "web-browser-symbolic", &record.homepage_url), ("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), ("Source code", "code-symbolic", &record.vcs_url),
("Documentation", "help-browser-symbolic", &record.help_url), ("Documentation", "help-browser-symbolic", &record.help_url),
("Donate", "emblem-favorite-symbolic", &record.donation_url), ("Donate", "emblem-favorite-symbolic", &record.donation_url),
@@ -560,9 +570,8 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
display_label display_label
)) ))
.tooltip_text( .tooltip_text(
"AppImages can include built-in update information that tells Driftwood \ "Driftwood can check for newer versions of this app automatically. \
where to check for newer versions. Common methods include GitHub releases, \ The developer has included information about where updates are published."
zsync (efficient delta updates), and direct download URLs."
) )
.build(); .build();
updates_group.add(&row); updates_group.add(&row);
@@ -723,7 +732,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
if has_capabilities { if has_capabilities {
let cap_group = adw::PreferencesGroup::builder() let cap_group = adw::PreferencesGroup::builder()
.title("Capabilities") .title("Features")
.build(); .build();
if let Some(ref kw) = record.keywords { if let Some(ref kw) = record.keywords {
@@ -751,7 +760,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
.title("Content rating") .title("Content rating")
.subtitle(cr) .subtitle(cr)
.tooltip_text( .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(); .build();
cap_group.add(&row); cap_group.add(&row);
@@ -761,11 +770,11 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
if let Ok(actions) = serde_json::from_str::<Vec<String>>(actions_json) { if let Ok(actions) = serde_json::from_str::<Vec<String>>(actions_json) {
if !actions.is_empty() { if !actions.is_empty() {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title("Desktop actions") .title("Quick actions")
.subtitle(&actions.join(", ")) .subtitle(&actions.join(", "))
.tooltip_text( .tooltip_text(
"Additional actions available from the right-click menu \ "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(); .build();
cap_group.add(&row); cap_group.add(&row);
@@ -784,29 +793,32 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
.build(); .build();
let type_str = match record.appimage_type { let type_str = match record.appimage_type {
Some(1) => "Type 1 (ISO 9660) - older format, still widely supported", Some(1) => "Type 1 - older format, still widely supported",
Some(2) => "Type 2 (SquashFS) - modern format, most common today", Some(2) => "Type 2 - modern, compressed format",
_ => "Unknown type", _ => "Unknown type",
}; };
let type_row = adw::ActionRow::builder() let type_row = adw::ActionRow::builder()
.title("AppImage format") .title("Package format")
.subtitle(type_str) .subtitle(type_str)
.tooltip_text( .tooltip_text(
"AppImages come in two formats. Type 1 uses an ISO 9660 filesystem \ "AppImages come in two formats. Type 1 is the older format. \
(older, simpler). Type 2 uses SquashFS (modern, compressed, smaller \ Type 2 is the current standard - it uses compression for smaller \
files). Type 2 is the standard today and is what most AppImage tools \ files and faster loading."
produce."
) )
.build(); .build();
info_group.add(&type_row); info_group.add(&type_row);
let exec_row = adw::ActionRow::builder() let exec_row = adw::ActionRow::builder()
.title("Executable") .title("Ready to run")
.subtitle(if record.is_executable { .subtitle(if record.is_executable {
"Yes - this file has execute permission" "Yes - this file is ready to launch"
} else { } 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(); .build();
info_group.add(&exec_row); info_group.add(&exec_row);
@@ -814,13 +826,13 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
let sig_row = adw::ActionRow::builder() let sig_row = adw::ActionRow::builder()
.title("Digital signature") .title("Digital signature")
.subtitle(if record.has_signature { .subtitle(if record.has_signature {
"This AppImage contains a GPG signature" "Signed by the developer"
} else { } else {
"Not signed" "Not signed"
}) })
.tooltip_text( .tooltip_text(
"AppImages can be digitally signed by their author using GPG. \ "This app was signed by its developer, which helps verify \
A signature helps verify that the file hasn't been tampered with." it hasn't been tampered with since it was published."
) )
.build(); .build();
let sig_badge = if record.has_signature { let sig_badge = if record.has_signature {
@@ -839,7 +851,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
info_group.add(&seen_row); info_group.add(&seen_row);
let scanned_row = adw::ActionRow::builder() let scanned_row = adw::ActionRow::builder()
.title("Last scanned") .title("Last checked")
.subtitle(&record.last_scanned) .subtitle(&record.last_scanned)
.build(); .build();
info_group.add(&scanned_row); info_group.add(&scanned_row);
@@ -886,21 +898,21 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
// Desktop Integration group // Desktop Integration group
let integration_group = adw::PreferencesGroup::builder() let integration_group = adw::PreferencesGroup::builder()
.title("Desktop Integration") .title("App Menu")
.description( .description(
"Show this app in your Activities menu and app launcher, \ "Add this app to your launcher so you can find it \
just like a regular installed application." like any other installed app."
) )
.build(); .build();
let switch_row = adw::SwitchRow::builder() let switch_row = adw::SwitchRow::builder()
.title("Add to application menu") .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) .active(record.integrated)
.tooltip_text( .tooltip_text(
"Desktop integration makes this AppImage appear in your Activities menu \ "This makes the app appear in your Activities menu and app launcher, \
and app launcher, just like a regular installed app. It creates a .desktop \ just like a regular installed app. It creates a shortcut file and \
file (a shortcut) and copies the app's icon to your system icon folder." copies the app's icon to your system."
) )
.build(); .build();
@@ -940,7 +952,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
if record.integrated { if record.integrated {
if let Some(ref desktop_file) = record.desktop_file { if let Some(ref desktop_file) = record.desktop_file {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title("Desktop file") .title("Shortcut file")
.subtitle(desktop_file) .subtitle(desktop_file)
.subtitle_selectable(true) .subtitle_selectable(true)
.build(); .build();
@@ -954,8 +966,8 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
let compat_group = adw::PreferencesGroup::builder() let compat_group = adw::PreferencesGroup::builder()
.title("Compatibility") .title("Compatibility")
.description( .description(
"How well this app works with your display server and filesystem. \ "How well this app works with your system. \
Most issues here can be resolved with a small package install." Most issues can be fixed with a quick install."
) )
.build(); .build();
@@ -966,13 +978,12 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
.unwrap_or(WaylandStatus::Unknown); .unwrap_or(WaylandStatus::Unknown);
let wayland_row = adw::ActionRow::builder() let wayland_row = adw::ActionRow::builder()
.title("Wayland display") .title("Display compatibility")
.subtitle(wayland_user_explanation(&wayland_status)) .subtitle(wayland_user_explanation(&wayland_status))
.tooltip_text( .tooltip_text(
"Wayland is the modern display system used by GNOME and most Linux desktops. \ "Wayland is the modern display system on Linux. Apps built for the \
It replaced the older X11 system. Apps built for X11 still work through \ older system (X11) still work, but native Wayland apps look sharper, \
a compatibility layer called XWayland, but native Wayland apps look \ especially on high-resolution screens."
sharper and perform better, especially on high-resolution screens."
) )
.build(); .build();
let wayland_badge = widgets::status_badge(wayland_status.label(), wayland_status.badge_class()); 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<Database>, toast_overlay: &
// Analyze toolkit button // Analyze toolkit button
let analyze_row = adw::ActionRow::builder() let analyze_row = adw::ActionRow::builder()
.title("Analyze toolkit") .title("Detect app framework")
.subtitle("Inspect bundled libraries to detect which UI toolkit this app uses") .subtitle("Check which technology this app is built with")
.activatable(true) .activatable(true)
.tooltip_text( .tooltip_text(
"UI toolkits (like GTK, Qt, Electron) are the frameworks apps use to \ "Apps are built with different frameworks (like GTK, Qt, or Electron). \
draw their windows and buttons. Knowing the toolkit helps predict Wayland \ Knowing the framework helps predict how well the app works with \
compatibility - GTK4 apps are natively Wayland, while older Qt or Electron \ your display system."
apps may need XWayland."
) )
.build(); .build();
let analyze_icon = gtk::Image::from_icon_name("system-search-symbolic"); let analyze_icon = gtk::Image::from_icon_name("system-search-symbolic");
@@ -1017,12 +1027,12 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
let toolkit_label = analysis.toolkit.label(); let toolkit_label = analysis.toolkit.label();
let lib_count = analysis.libraries_found.len(); let lib_count = analysis.libraries_found.len();
row_clone.set_subtitle(&format!( row_clone.set_subtitle(&format!(
"Detected: {} ({} libraries scanned)", "Built with: {}",
toolkit_label, lib_count, toolkit_label,
)); ));
} }
Err(_) => { 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<Database>, toast_overlay: &
// Runtime Wayland status (from post-launch analysis) // Runtime Wayland status (from post-launch analysis)
if let Some(ref runtime_status) = record.runtime_wayland_status { if let Some(ref runtime_status) = record.runtime_wayland_status {
let runtime_row = adw::ActionRow::builder() let runtime_row = adw::ActionRow::builder()
.title("Last observed protocol") .title("Last display mode")
.subtitle(&format!( .subtitle(&format!(
"When this app was last launched, it used: {}", "When this app was last launched, it used: {}",
runtime_status runtime_status
)) ))
.tooltip_text(
"How the app connected to your display the last time it was launched."
)
.build(); .build();
if let Some(ref checked) = record.runtime_wayland_checked { if let Some(ref checked) = record.runtime_wayland_checked {
let info = gtk::Label::builder() let info = gtk::Label::builder()
@@ -1058,13 +1071,11 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
.unwrap_or(fuse_system.status.clone()); .unwrap_or(fuse_system.status.clone());
let fuse_row = adw::ActionRow::builder() let fuse_row = adw::ActionRow::builder()
.title("FUSE (filesystem)") .title("App mounting")
.subtitle(fuse_user_explanation(&fuse_status)) .subtitle(fuse_user_explanation(&fuse_status))
.tooltip_text( .tooltip_text(
"FUSE (Filesystem in Userspace) lets AppImages mount themselves as \ "FUSE lets apps like AppImages run directly without unpacking first. \
virtual drives so they can run directly without extracting. Without it, \ Without it, apps still work but take a little longer to start."
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."
) )
.build(); .build();
let fuse_badge = widgets::status_badge_with_icon( let fuse_badge = widgets::status_badge_with_icon(
@@ -1077,7 +1088,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
if let Some(cmd) = fuse_install_command(&fuse_status) { if let Some(cmd) = fuse_install_command(&fuse_status) {
let copy_btn = widgets::copy_button(cmd, Some(toast_overlay)); let copy_btn = widgets::copy_button(cmd, Some(toast_overlay));
copy_btn.set_valign(gtk::Align::Center); 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(&copy_btn); fuse_row.add_suffix(&copy_btn);
} }
compat_group.add(&fuse_row); compat_group.add(&fuse_row);
@@ -1086,16 +1097,15 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
let appimage_path = std::path::Path::new(&record.path); let appimage_path = std::path::Path::new(&record.path);
let app_fuse_status = fuse::determine_app_fuse_status(&fuse_system, appimage_path); let app_fuse_status = fuse::determine_app_fuse_status(&fuse_system, appimage_path);
let launch_method_row = adw::ActionRow::builder() let launch_method_row = adw::ActionRow::builder()
.title("Launch method") .title("Startup method")
.subtitle(&format!( .subtitle(&format!(
"This app will launch using: {}", "This app will launch using: {}",
app_fuse_status.label() app_fuse_status.label()
)) ))
.tooltip_text( .tooltip_text(
"AppImages can launch two ways: 'FUSE mount' mounts the image as a \ "AppImages can start two ways: mounting (fast, instant startup) or \
virtual drive (fast, instant startup), or 'extract' unpacks to a temp \ unpacking to a temporary folder first (slower, but works everywhere). \
folder first (slower, but works everywhere). The method is chosen \ The method is chosen automatically based on your system."
automatically based on your system's FUSE support."
) )
.build(); .build();
let launch_badge = widgets::status_badge( let launch_badge = widgets::status_badge(
@@ -1109,10 +1119,10 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
// Sandboxing group // Sandboxing group
let sandbox_group = adw::PreferencesGroup::builder() let sandbox_group = adw::PreferencesGroup::builder()
.title("Sandboxing") .title("App Isolation")
.description( .description(
"Isolate this app for extra security. Sandboxing limits what \ "Restrict what this app can access on your system \
the app can access on your system." for extra security."
) )
.build(); .build();
@@ -1125,24 +1135,20 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
let firejail_available = launcher::has_firejail(); let firejail_available = launcher::has_firejail();
let sandbox_subtitle = if firejail_available { let sandbox_subtitle = if firejail_available {
format!( format!("Currently: {}", current_mode.display_label())
"Isolate this app using Firejail. Current mode: {}",
current_mode.display_label()
)
} else { } 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() let firejail_row = adw::SwitchRow::builder()
.title("Firejail sandbox") .title("Isolate this app")
.subtitle(&sandbox_subtitle) .subtitle(&sandbox_subtitle)
.active(current_mode == SandboxMode::Firejail) .active(current_mode == SandboxMode::Firejail)
.sensitive(firejail_available) .sensitive(firejail_available)
.tooltip_text( .tooltip_text(
"Sandboxing restricts what an app can access on your system - files, \ "Sandboxing restricts what an app can access - files, network, \
network, devices, etc. This adds a security layer so that even if an \ devices, etc. Even if an app has a security issue, it can't \
app is compromised, it cannot freely access your personal data. Firejail \ freely access your personal data."
is a lightweight Linux sandboxing tool."
) )
.build(); .build();
@@ -1163,15 +1169,15 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
if !firejail_available { if !firejail_available {
let firejail_cmd = "sudo apt install firejail"; let firejail_cmd = "sudo apt install firejail";
let info_row = adw::ActionRow::builder() let info_row = adw::ActionRow::builder()
.title("Install Firejail") .title("Install app isolation")
.subtitle(firejail_cmd) .subtitle("Install with one command")
.build(); .build();
let badge = widgets::status_badge("Missing", "warning"); let badge = widgets::status_badge("Missing", "warning");
badge.set_valign(gtk::Align::Center); badge.set_valign(gtk::Align::Center);
info_row.add_suffix(&badge); info_row.add_suffix(&badge);
let copy_btn = widgets::copy_button(firejail_cmd, Some(toast_overlay)); let copy_btn = widgets::copy_button(firejail_cmd, Some(toast_overlay));
copy_btn.set_valign(gtk::Align::Center); 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(&copy_btn); info_row.add_suffix(&copy_btn);
sandbox_group.add(&info_row); sandbox_group.add(&info_row);
} }
@@ -1207,10 +1213,9 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
.build(); .build();
let group = adw::PreferencesGroup::builder() let group = adw::PreferencesGroup::builder()
.title("Vulnerability Scanning") .title("Security Check")
.description( .description(
"Scan the libraries bundled inside this AppImage for known \ "Check this app for known security issues."
security vulnerabilities (CVEs)."
) )
.build(); .build();
@@ -1231,18 +1236,22 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
group.add(&row); group.add(&row);
} else { } else {
let lib_row = adw::ActionRow::builder() let lib_row = adw::ActionRow::builder()
.title("Bundled libraries") .title("Included components")
.subtitle(&format!( .subtitle(&format!(
"{} libraries detected inside this AppImage", "{} components found inside this app",
libs.len() 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(); .build();
group.add(&lib_row); group.add(&lib_row);
if summary.total() == 0 { if summary.total() == 0 {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title("Vulnerabilities") .title("Vulnerabilities")
.subtitle("No known security issues found in the bundled libraries.") .subtitle("No known security issues found.")
.build(); .build();
let badge = widgets::status_badge("Clean", "success"); let badge = widgets::status_badge("Clean", "success");
badge.set_valign(gtk::Align::Center); badge.set_valign(gtk::Align::Center);
@@ -1266,15 +1275,12 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
// Scan button // Scan button
let scan_row = adw::ActionRow::builder() let scan_row = adw::ActionRow::builder()
.title("Run security scan") .title("Run security check")
.subtitle("Check bundled libraries against known CVE databases") .subtitle("Check for known security issues in this app")
.activatable(true) .activatable(true)
.tooltip_text( .tooltip_text(
"CVE stands for Common Vulnerabilities and Exposures - a public list \ "This checks the components inside this app against a public database \
of known security bugs in software. AppImages bundle their own copies \ of known security issues to see if any are outdated or vulnerable."
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."
) )
.build(); .build();
let scan_icon = gtk::Image::from_icon_name("security-medium-symbolic"); let scan_icon = gtk::Image::from_icon_name("security-medium-symbolic");
@@ -1313,7 +1319,7 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
} }
} }
Err(_) => { 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<Database>) -> gtk::Box {
if let Some(ref hash) = record.sha256 { if let Some(ref hash) = record.sha256 {
let hash_row = adw::ActionRow::builder() let hash_row = adw::ActionRow::builder()
.title("SHA256 checksum") .title("File fingerprint")
.subtitle(hash) .subtitle(hash)
.subtitle_selectable(true) .subtitle_selectable(true)
.tooltip_text( .tooltip_text(
"A SHA256 checksum is a unique fingerprint of the file. If even one \ "A unique code (SHA256 checksum) generated from the file's contents. \
byte changes, the checksum changes completely. You can compare this \ If the file changes in any way, this code changes too. You can compare \
against the developer's published checksum to verify the file hasn't \ it against the developer's published fingerprint to verify nothing \
been tampered with or corrupted during download." was altered."
) )
.build(); .build();
hash_row.add_css_class("property"); hash_row.add_css_class("property");
@@ -1416,13 +1422,13 @@ fn build_storage_tab(
// Data paths group // Data paths group
let paths_group = adw::PreferencesGroup::builder() let paths_group = adw::PreferencesGroup::builder()
.title("App Data") .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(); .build();
// Discover button // Discover button
let discover_row = adw::ActionRow::builder() let discover_row = adw::ActionRow::builder()
.title("Find app data") .title("Find app data")
.subtitle("Search for config, cache, and data directories") .subtitle("Search for files this app has saved")
.activatable(true) .activatable(true)
.build(); .build();
let discover_icon = gtk::Image::from_icon_name("folder-saved-search-symbolic"); let discover_icon = gtk::Image::from_icon_name("folder-saved-search-symbolic");
@@ -1449,7 +1455,7 @@ fn build_storage_tab(
Ok(fp) => { Ok(fp) => {
let count = fp.paths.len(); let count = fp.paths.len();
if count == 0 { if count == 0 {
row_clone.set_subtitle("No associated data directories found"); row_clone.set_subtitle("No saved data found");
} else { } else {
row_clone.set_subtitle(&format!( row_clone.set_subtitle(&format!(
"Found {} path{} using {}", "Found {} path{} using {}",
@@ -1501,7 +1507,7 @@ fn build_storage_tab(
.build(); .build();
let path_row = adw::ActionRow::builder() let path_row = adw::ActionRow::builder()
.title("Path") .title("File location")
.subtitle(&record.path) .subtitle(&record.path)
.subtitle_selectable(true) .subtitle_selectable(true)
.build(); .build();
@@ -1548,35 +1554,36 @@ fn build_storage_tab(
fn wayland_user_explanation(status: &WaylandStatus) -> &'static str { fn wayland_user_explanation(status: &WaylandStatus) -> &'static str {
match status { match status {
WaylandStatus::Native => WaylandStatus::Native =>
"Runs natively on Wayland - the best experience on modern Linux desktops.", "Fully compatible - the best experience on your system.",
WaylandStatus::XWayland => 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.", blurry on high-resolution screens.",
WaylandStatus::Possible => 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 => WaylandStatus::X11Only =>
"Designed for X11 only. It will run through XWayland automatically, \ "Built for an older display system. It will run automatically, \
but you may notice minor display quirks.", but you may notice minor visual quirks.",
WaylandStatus::Unknown => 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 { fn fuse_user_explanation(status: &FuseStatus) -> &'static str {
match status { match status {
FuseStatus::FullyFunctional => FuseStatus::FullyFunctional =>
"FUSE is working - AppImages mount directly for fast startup.", "Everything is set up - apps start instantly.",
FuseStatus::Fuse3Only => FuseStatus::Fuse3Only =>
"Only FUSE 3 found. Some AppImages need FUSE 2. \ "A small system component is missing. Most apps will still work, \
Click the copy button to get the install command.", but some may need it. Copy the install command to fix this.",
FuseStatus::NoFusermount => FuseStatus::NoFusermount =>
"FUSE tools not found. The app will still work by extracting to a \ "A system component is missing, so apps will take a little longer \
temporary folder, but startup will be slower.", to start. They'll still work fine.",
FuseStatus::NoDevFuse => FuseStatus::NoDevFuse =>
"/dev/fuse not available. FUSE may not be configured on your system. \ "Your system doesn't support instant app mounting. Apps will unpack \
Apps will extract to a temp folder instead.", before starting, which takes a bit longer.",
FuseStatus::MissingLibfuse2 => 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. /// Show a screenshot in a fullscreen lightbox window with prev/next navigation.
fn show_screenshot_lightbox(parent: &gtk::Window, texture: &gtk::gdk::Texture) { /// Uses a separate gtk::Window to avoid parent scroll position interference.
fn show_screenshot_lightbox(
parent: &gtk::Window,
textures: &Rc<std::cell::RefCell<Vec<Option<gtk::gdk::Texture>>>>,
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() let picture = gtk::Picture::builder()
.paintable(texture)
.content_fit(gtk::ContentFit::Contain) .content_fit(gtk::ContentFit::Contain)
.hexpand(true) .halign(gtk::Align::Center)
.vexpand(true) .valign(gtk::Align::Center)
.margin_start(72)
.margin_end(72)
.margin_top(56)
.margin_bottom(56)
.build(); .build();
picture.set_can_shrink(true); picture.set_can_shrink(true);
let dialog = adw::Dialog::builder() // Set initial texture
.title("Screenshot") {
.content_width(900) let t = textures.borrow();
.content_height(600) 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(); .build();
let toolbar = adw::ToolbarView::new(); let next_btn = gtk::Button::builder()
let header = adw::HeaderBar::new(); .icon_name("go-next-symbolic")
toolbar.add_top_bar(&header); .css_classes(["circular", "osd", "lightbox-nav"])
toolbar.set_content(Some(&picture)); .valign(gtk::Align::Center)
dialog.set_child(Some(&toolbar)); .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. /// Fetch a favicon for a URL and set it on an image widget.

View File

@@ -103,14 +103,14 @@ fn build_report_content(db: &Rc<Database>) -> gtk::ScrolledWindow {
let empty = adw::StatusPage::builder() let empty = adw::StatusPage::builder()
.icon_name("security-medium-symbolic") .icon_name("security-medium-symbolic")
.title("No Security Scans") .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(); .build();
content.append(&empty); content.append(&empty);
} else { } else {
let clean = adw::StatusPage::builder() let clean = adw::StatusPage::builder()
.icon_name("security-high-symbolic") .icon_name("security-high-symbolic")
.title("All Clear") .title("All Clear")
.description("No known vulnerabilities found in any bundled libraries.") .description("No known security issues found in any of your apps.")
.build(); .build();
content.append(&clean); content.append(&clean);
} }
@@ -144,13 +144,13 @@ fn build_report_content(db: &Rc<Database>) -> gtk::ScrolledWindow {
fn build_summary_group(summary: &crate::core::database::CveSummary) -> adw::PreferencesGroup { fn build_summary_group(summary: &crate::core::database::CveSummary) -> adw::PreferencesGroup {
let group = adw::PreferencesGroup::builder() let group = adw::PreferencesGroup::builder()
.title("Vulnerability Summary") .title("Vulnerability Summary")
.description("Overall security status across all AppImages") .description("Overall security status across all your apps")
.build(); .build();
let total_row = adw::ActionRow::builder() let total_row = adw::ActionRow::builder()
.title("Total vulnerabilities") .title("Total vulnerabilities")
.subtitle(&summary.total().to_string()) .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(); .build();
let total_badge = widgets::status_badge(summary.max_severity(), summary.badge_class()); let total_badge = widgets::status_badge(summary.max_severity(), summary.badge_class());
total_badge.set_valign(gtk::Align::Center); total_badge.set_valign(gtk::Align::Center);
@@ -207,7 +207,7 @@ fn build_app_findings_group(
summary: &crate::core::database::CveSummary, summary: &crate::core::database::CveSummary,
cve_matches: &[crate::core::database::CveMatchRecord], cve_matches: &[crate::core::database::CveMatchRecord],
) -> adw::PreferencesGroup { ) -> 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() let group = adw::PreferencesGroup::builder()
.title(app_name) .title(app_name)
.description(&description) .description(&description)