Stop executing AppImages during analysis, add screenshot lightbox and favicons
- fuse.rs: Replace Command::new(appimage_path) with 256KB binary scan for runtime detection - prevents apps like Affinity from launching on tile click - fuse.rs: Read only 12 bytes for Type 2 magic check instead of entire file - security.rs: Use find_squashfs_offset_for() instead of executing AppImages with --appimage-offset flag - updater.rs: Read only first 1MB for update info instead of entire file - detail_view.rs: Click screenshots to open in lightbox dialog - detail_view.rs: Fetch favicons from Google favicon service for link rows
This commit is contained in:
@@ -367,7 +367,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Screenshots section - async image loading from URLs
|
||||
// Screenshots section - async image loading from URLs, click to lightbox
|
||||
// -----------------------------------------------------------------------
|
||||
if let Some(ref urls_str) = record.screenshot_urls {
|
||||
let urls: Vec<&str> = urls_str.lines().filter(|u| !u.is_empty()).collect();
|
||||
@@ -387,11 +387,18 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.carousel(&carousel)
|
||||
.build();
|
||||
|
||||
for url in &urls {
|
||||
// Store textures for lightbox access
|
||||
let textures: Rc<std::cell::RefCell<Vec<Option<gtk::gdk::Texture>>>> =
|
||||
Rc::new(std::cell::RefCell::new(vec![None; urls.len()]));
|
||||
|
||||
for (idx, url) in urls.iter().enumerate() {
|
||||
let picture = gtk::Picture::builder()
|
||||
.content_fit(gtk::ContentFit::Contain)
|
||||
.height_request(300)
|
||||
.build();
|
||||
if let Some(cursor) = gtk::gdk::Cursor::from_name("pointer", None) {
|
||||
picture.set_cursor(Some(&cursor));
|
||||
}
|
||||
picture.set_can_shrink(true);
|
||||
|
||||
// Placeholder spinner while loading
|
||||
@@ -404,12 +411,30 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.build();
|
||||
overlay.add_overlay(&spinner);
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
overlay.add_controller(click);
|
||||
|
||||
carousel.append(&overlay);
|
||||
|
||||
// Load image asynchronously
|
||||
let url_owned = url.to_string();
|
||||
let picture_ref = picture.clone();
|
||||
let spinner_ref = spinner.clone();
|
||||
let textures_load = textures.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let mut response = ureq::get(&url_owned)
|
||||
@@ -432,6 +457,9 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
) {
|
||||
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
||||
picture_ref.set_paintable(Some(&texture));
|
||||
if let Some(slot) = textures_load.borrow_mut().get_mut(idx) {
|
||||
*slot = Some(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -487,9 +515,12 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
icon.set_valign(gtk::Align::Center);
|
||||
row.add_suffix(&icon);
|
||||
|
||||
// Start with the fallback icon, then try to load favicon
|
||||
let prefix_icon = gtk::Image::from_icon_name(*icon_name);
|
||||
prefix_icon.set_valign(gtk::Align::Center);
|
||||
prefix_icon.set_pixel_size(16);
|
||||
row.add_prefix(&prefix_icon);
|
||||
fetch_favicon_async(url, &prefix_icon);
|
||||
|
||||
let url_clone = url.clone();
|
||||
row.connect_activated(move |row| {
|
||||
@@ -1558,3 +1589,78 @@ fn fuse_install_command(status: &FuseStatus) -> Option<&'static str> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a screenshot in a fullscreen-ish lightbox dialog.
|
||||
fn show_screenshot_lightbox(parent: >k::Window, texture: >k::gdk::Texture) {
|
||||
let picture = gtk::Picture::builder()
|
||||
.paintable(texture)
|
||||
.content_fit(gtk::ContentFit::Contain)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
picture.set_can_shrink(true);
|
||||
|
||||
let dialog = adw::Dialog::builder()
|
||||
.title("Screenshot")
|
||||
.content_width(900)
|
||||
.content_height(600)
|
||||
.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));
|
||||
|
||||
dialog.present(Some(parent));
|
||||
}
|
||||
|
||||
/// Fetch a favicon for a URL and set it on an image widget.
|
||||
fn fetch_favicon_async(url: &str, image: >k::Image) {
|
||||
// Extract domain from URL for favicon service
|
||||
let domain = url
|
||||
.trim_start_matches("https://")
|
||||
.trim_start_matches("http://")
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if domain.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let image_ref = image.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let favicon_url = format!(
|
||||
"https://www.google.com/s2/favicons?domain={}&sz=32",
|
||||
domain
|
||||
);
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let mut response = ureq::get(&favicon_url)
|
||||
.header("User-Agent", "Driftwood-AppImage-Manager/0.1")
|
||||
.call()
|
||||
.ok()?;
|
||||
let mut buf = Vec::new();
|
||||
response.body_mut().as_reader().read_to_end(&mut buf).ok()?;
|
||||
if buf.len() > 100 {
|
||||
Some(buf)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Ok(Some(data)) = result {
|
||||
let gbytes = glib::Bytes::from(&data);
|
||||
let stream = gio::MemoryInputStream::from_bytes(&gbytes);
|
||||
if let Ok(pixbuf) = gtk::gdk_pixbuf::Pixbuf::from_stream(
|
||||
&stream,
|
||||
None::<&gio::Cancellable>,
|
||||
) {
|
||||
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
||||
image_ref.set_paintable(Some(&texture));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user