Fix icon extraction, drop overlay styling, and card padding

- Fix upsert_appimage returning 0 for existing records by falling
  back to a SELECT query when last_insert_rowid is 0
- Replace --appimage-offset execution with binary squashfs magic scan
  to avoid hanging on AppImages with custom AppRun scripts
- Add 5-second timeout fallback if binary scan fails
- Extract desktop files from usr/share/applications/ for reverse-DNS
  named entries that root-level *.desktop glob misses
- Add root-level png/svg fallback in icon search
- Add CSS for drop overlay scrim, drop zone card, and drop zone icon
- Add card padding (24px 20px) so content does not touch card edges
- Always scan on startup to discover new AppImages
This commit is contained in:
lashman
2026-02-27 17:49:04 +02:00
parent 423323d5a9
commit 6526f92a6f
5 changed files with 132 additions and 14 deletions

View File

@@ -32,7 +32,7 @@
<description>Application color scheme: default (follow system), force-light, or force-dark.</description>
</key>
<key name="auto-scan-on-startup" type="b">
<default>false</default>
<default>true</default>
<summary>Auto scan on startup</summary>
<description>Whether to automatically scan for AppImages when the application starts.</description>
</key>

View File

@@ -66,7 +66,29 @@
color: @window_fg_color;
}
/* ===== Drop Overlay ===== */
.drop-overlay-scrim {
background: alpha(@window_bg_color, 0.75);
}
.drop-zone-card {
background: @card_bg_color;
border-radius: 16px;
padding: 48px 40px;
border: 2px dashed alpha(@accent_bg_color, 0.5);
box-shadow: 0 8px 32px alpha(black, 0.15);
}
.drop-zone-icon {
color: @accent_bg_color;
opacity: 0.7;
}
/* ===== Card View (using libadwaita .card) ===== */
.card {
padding: 24px 20px;
}
flowboxchild:focus-visible .card {
outline: 2px solid @accent_bg_color;
outline-offset: 3px;

View File

@@ -703,7 +703,17 @@ impl Database {
last_scanned = datetime('now')",
params![path, filename, appimage_type, size_bytes, is_executable, file_modified],
)?;
Ok(self.conn.last_insert_rowid())
// last_insert_rowid() returns 0 for ON CONFLICT UPDATE, so query the actual id
let id = self.conn.last_insert_rowid();
if id != 0 {
return Ok(id);
}
let id: i64 = self.conn.query_row(
"SELECT id FROM appimages WHERE path = ?1",
params![path],
|row| row.get(0),
)?;
Ok(id)
}
pub fn update_metadata(

View File

@@ -74,13 +74,70 @@ fn has_unsquashfs() -> bool {
.is_ok()
}
/// Find the squashfs offset by scanning for a valid superblock in the binary.
/// This avoids executing the AppImage, which can hang for apps with custom AppRun scripts.
fn find_squashfs_offset(path: &Path) -> Result<u64, InspectorError> {
let data = fs::read(path)?;
let magic = b"hsqs";
// Search for squashfs magic after the ELF header (skip first 4KB to avoid false matches)
let start = 4096.min(data.len());
for i in start..data.len().saturating_sub(96) {
if &data[i..i + 4] == magic {
// Validate: check squashfs superblock version at offset +28 (major) and +30 (minor)
let major = u16::from_le_bytes([data[i + 28], data[i + 29]]);
let minor = u16::from_le_bytes([data[i + 30], data[i + 31]]);
// Valid squashfs 4.0
if major == 4 && minor == 0 {
// Also check block_size at offset +12 is a power of 2 and reasonable (4KB-1MB)
let block_size = u32::from_le_bytes([data[i + 12], data[i + 13], data[i + 14], data[i + 15]]);
if block_size.is_power_of_two() && block_size >= 4096 && block_size <= 1_048_576 {
return Ok(i as u64);
}
}
}
}
Err(InspectorError::NoOffset)
}
/// Get the squashfs offset from the AppImage by running it with --appimage-offset.
/// Falls back to binary scanning if execution times out or fails.
fn get_squashfs_offset(path: &Path) -> Result<u64, InspectorError> {
let output = Command::new(path)
// First try the fast binary scan approach (no execution needed)
if let Ok(offset) = find_squashfs_offset(path) {
return Ok(offset);
}
// Fallback: run the AppImage with a timeout
let child = Command::new(path)
.arg("--appimage-offset")
.env("APPIMAGE_EXTRACT_AND_RUN", "0")
.output()?;
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn();
let mut child = match child {
Ok(c) => c,
Err(e) => return Err(InspectorError::IoError(e)),
};
// Wait up to 5 seconds
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) => {
if start.elapsed() > std::time::Duration::from_secs(5) {
let _ = child.kill();
let _ = child.wait();
return Err(InspectorError::NoOffset);
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => return Err(InspectorError::IoError(e)),
}
}
let output = child.wait_with_output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.trim()
@@ -103,7 +160,10 @@ fn extract_metadata_files(
.arg(dest)
.arg(appimage_path)
.arg("*.desktop")
.arg("usr/share/applications/*.desktop")
.arg(".DirIcon")
.arg("*.png")
.arg("*.svg")
.arg("usr/share/icons/*")
.arg("usr/share/metainfo/*.xml")
.arg("usr/share/appdata/*.xml")
@@ -135,7 +195,10 @@ fn extract_metadata_files_direct(
.arg(dest)
.arg(appimage_path)
.arg("*.desktop")
.arg("usr/share/applications/*.desktop")
.arg(".DirIcon")
.arg("*.png")
.arg("*.svg")
.arg("usr/share/icons/*")
.arg("usr/share/metainfo/*.xml")
.arg("usr/share/appdata/*.xml")
@@ -152,8 +215,10 @@ fn extract_metadata_files_direct(
}
}
/// Find the first .desktop file in a directory.
/// Find the first .desktop file in the extract directory.
/// Checks root level first, then usr/share/applications/.
fn find_desktop_file(dir: &Path) -> Option<PathBuf> {
// Check root of extract dir
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
@@ -162,6 +227,16 @@ fn find_desktop_file(dir: &Path) -> Option<PathBuf> {
}
}
}
// Check usr/share/applications/
let apps_dir = dir.join("usr/share/applications");
if let Ok(entries) = fs::read_dir(&apps_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("desktop") {
return Some(path);
}
}
}
None
}
@@ -266,9 +341,9 @@ fn detect_architecture(path: &Path) -> Option<String> {
/// Find an icon file in the extracted squashfs directory.
fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
// First try .DirIcon
// First try .DirIcon (skip if it's a broken symlink)
let dir_icon = extract_dir.join(".DirIcon");
if dir_icon.exists() {
if dir_icon.exists() && dir_icon.metadata().is_ok() {
return Some(dir_icon);
}
@@ -282,7 +357,7 @@ fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
}
}
// Check usr/share/icons recursively
// Check usr/share/icons recursively (prefer largest resolution)
let icons_dir = extract_dir.join("usr/share/icons");
if icons_dir.exists() {
if let Some(found) = find_icon_recursive(&icons_dir, name) {
@@ -291,6 +366,19 @@ fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
}
}
// Fallback: grab any .png or .svg at the root level
if let Ok(entries) = fs::read_dir(extract_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext == "png" || ext == "svg" {
return Some(path);
}
}
}
}
None
}

View File

@@ -784,11 +784,8 @@ impl DriftwoodWindow {
}
}
// Auto-scan on startup if enabled
let settings = self.settings();
if settings.boolean("auto-scan-on-startup") {
self.trigger_scan();
}
// Always scan on startup to discover new AppImages and complete pending analyses
self.trigger_scan();
// Check for orphaned desktop entries in the background
let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
@@ -878,7 +875,8 @@ impl DriftwoodWindow {
let size_unchanged = ex.size_bytes == d.size_bytes as i64;
let mtime_unchanged = modified.as_deref() == ex.file_modified.as_deref();
let analysis_done = ex.analysis_status.as_deref() == Some("complete");
if size_unchanged && mtime_unchanged && analysis_done {
let has_icon = ex.icon_path.is_some();
if size_unchanged && mtime_unchanged && analysis_done && has_icon {
skipped_count += 1;
continue;
}