Fix performance, add screenshots, make banner scrollable
- Make detail view banner scroll with content instead of staying fixed, preventing tall banners from eating screen space - Optimize squashfs offset scanning with buffered 256KB chunk reading instead of loading entire file into memory (critical for 1.5GB+ files) - Add screenshot URL parsing from AppStream XML and async image display with carousel in the overview tab - Fix infinite re-analysis bug: has_appstream check caused every app without AppStream data to be re-analyzed on every startup. Now handled via one-time migration reset in v10 - Database migration v10: add screenshot_urls column, reset analysis status for one-time re-scan with new parser
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use adw::prelude::*;
|
||||
use std::cell::Cell;
|
||||
use std::io::Read as _;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::gio;
|
||||
@@ -42,20 +43,19 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
view_stack.add_titled(&storage_page, Some("storage"), "Storage");
|
||||
view_stack.page(&storage_page).set_icon_name(Some("drive-harddisk-symbolic"));
|
||||
|
||||
// Scrollable view stack
|
||||
// Banner scrolls with content (not sticky) so tall banners don't eat space
|
||||
let scroll_content = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
scroll_content.append(&build_banner(record));
|
||||
scroll_content.append(&view_stack);
|
||||
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.child(&view_stack)
|
||||
.child(&scroll_content)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
// Main vertical layout: banner + scrolled tabs
|
||||
let content = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
content.append(&build_banner(record));
|
||||
content.append(&scrolled);
|
||||
|
||||
toast_overlay.set_child(Some(&content));
|
||||
toast_overlay.set_child(Some(&scrolled));
|
||||
|
||||
// Header bar with ViewSwitcher as title widget (standard GNOME pattern)
|
||||
let header = adw::HeaderBar::new();
|
||||
@@ -366,6 +366,93 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Screenshots section - async image loading from URLs
|
||||
// -----------------------------------------------------------------------
|
||||
if let Some(ref urls_str) = record.screenshot_urls {
|
||||
let urls: Vec<&str> = urls_str.lines().filter(|u| !u.is_empty()).collect();
|
||||
if !urls.is_empty() {
|
||||
let screenshots_group = adw::PreferencesGroup::builder()
|
||||
.title("Screenshots")
|
||||
.build();
|
||||
|
||||
let carousel = adw::Carousel::builder()
|
||||
.hexpand(true)
|
||||
.allow_scroll_wheel(true)
|
||||
.allow_mouse_drag(true)
|
||||
.build();
|
||||
carousel.set_height_request(300);
|
||||
|
||||
let dots = adw::CarouselIndicatorDots::builder()
|
||||
.carousel(&carousel)
|
||||
.build();
|
||||
|
||||
for url in &urls {
|
||||
let picture = gtk::Picture::builder()
|
||||
.content_fit(gtk::ContentFit::Contain)
|
||||
.height_request(300)
|
||||
.build();
|
||||
picture.set_can_shrink(true);
|
||||
|
||||
// Placeholder spinner while loading
|
||||
let overlay = gtk::Overlay::builder().child(&picture).build();
|
||||
let spinner = adw::Spinner::builder()
|
||||
.width_request(32)
|
||||
.height_request(32)
|
||||
.halign(gtk::Align::Center)
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
overlay.add_overlay(&spinner);
|
||||
|
||||
carousel.append(&overlay);
|
||||
|
||||
// Load image asynchronously
|
||||
let url_owned = url.to_string();
|
||||
let picture_ref = picture.clone();
|
||||
let spinner_ref = spinner.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let mut response = ureq::get(&url_owned)
|
||||
.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()?;
|
||||
Some(buf)
|
||||
})
|
||||
.await;
|
||||
|
||||
spinner_ref.set_visible(false);
|
||||
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);
|
||||
picture_ref.set_paintable(Some(&texture));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let carousel_box = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
.margin_top(8)
|
||||
.margin_bottom(8)
|
||||
.build();
|
||||
carousel_box.append(&carousel);
|
||||
if urls.len() > 1 {
|
||||
carousel_box.append(&dots);
|
||||
}
|
||||
screenshots_group.add(&carousel_box);
|
||||
|
||||
inner.append(&screenshots_group);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Links section
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user