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]
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"

View File

@@ -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;
}

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
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<Database>) -> 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<Database>) -> 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::<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>)] = &[
("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<Database>) -> 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<Database>) -> 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<Database>) -> 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<Database>) -> gtk::Box {
if let Ok(actions) = serde_json::from_str::<Vec<String>>(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<Database>) -> 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<Database>) -> 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<Database>) -> 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<Database>, 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<Database>, 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<Database>, 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<Database>, 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<Database>, 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<Database>, 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<Database>, 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<Database>, 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<Database>, 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(&copy_btn);
}
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 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<Database>, 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<Database>, 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<Database>, 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(&copy_btn);
sandbox_group.add(&info_row);
}
@@ -1207,10 +1213,9 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> 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<Database>) -> 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<Database>) -> 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<Database>) -> 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<Database>) -> 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: &gtk::Window, texture: &gtk::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: &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()
.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.

View File

@@ -103,14 +103,14 @@ fn build_report_content(db: &Rc<Database>) -> 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<Database>) -> 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)