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:
@@ -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(©_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(©_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: >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<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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user