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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user