Fix 26 bugs, edge cases, and consistency issues from fifth audit pass
Critical: undo toast now trashes only batch output files (not entire dir), JPEG scanline write errors propagated, selective metadata write result returned. High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio rejection, FM integration toggle infinite recursion guard, saturating counter arithmetic in executor. Medium: PNG compression level passed to oxipng, pct mode updates job_config, external file loading updates step indicator, CLI undo removes history entries, watch config write failures reported, fast-copy path reads image dimensions for rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl), CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot fix, generation guards on all preview threads to cancel stale results, default DPI aligned to 0, watermark text width uses char count not byte length. Low: binary path escaped in Nautilus extension, file dialog filter aligned with discovery, reset_wizard clears preset_mode and output_dir.
This commit is contained in:
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::app::AppState;
|
||||
use crate::utils::format_size;
|
||||
|
||||
const THUMB_SIZE: i32 = 120;
|
||||
|
||||
@@ -183,7 +184,16 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
|
||||
|
||||
fn is_image_file(path: &std::path::Path) -> bool {
|
||||
match path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()) {
|
||||
Some(ext) => matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "webp" | "avif" | "gif" | "tiff" | "tif" | "bmp"),
|
||||
Some(ext) => matches!(ext.as_str(),
|
||||
"jpg" | "jpeg" | "png" | "webp" | "avif" | "gif" | "tiff" | "tif" | "bmp"
|
||||
| "ico" | "hdr" | "exr" | "pnm" | "ppm" | "pgm" | "pbm" | "pam"
|
||||
| "tga" | "dds" | "ff" | "farbfeld" | "qoi"
|
||||
| "heic" | "heif" | "jxl"
|
||||
| "svg" | "svgz"
|
||||
| "raw" | "cr2" | "cr3" | "nef" | "nrw" | "arw" | "srf" | "sr2"
|
||||
| "orf" | "rw2" | "raf" | "dng" | "pef" | "srw" | "x3f"
|
||||
| "pcx" | "xpm" | "xbm" | "wbmp" | "jp2" | "j2k" | "jpf" | "jpx"
|
||||
),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -295,7 +305,7 @@ fn refresh_grid(
|
||||
}
|
||||
|
||||
/// Walk the widget tree to find our ListStore and count label, then rebuild
|
||||
fn rebuild_grid_model(
|
||||
pub fn rebuild_grid_model(
|
||||
widget: >k::Widget,
|
||||
loaded_files: &Rc<RefCell<Vec<PathBuf>>>,
|
||||
excluded: &Rc<RefCell<HashSet<PathBuf>>>,
|
||||
@@ -380,18 +390,6 @@ fn update_count_label(
|
||||
update_heading_label(widget, count, included_count, &size_str);
|
||||
}
|
||||
|
||||
fn format_size(bytes: u64) -> String {
|
||||
if bytes < 1024 {
|
||||
format!("{} B", bytes)
|
||||
} else if bytes < 1024 * 1024 {
|
||||
format!("{:.1} KB", bytes as f64 / 1024.0)
|
||||
} else if bytes < 1024 * 1024 * 1024 {
|
||||
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
|
||||
} else {
|
||||
format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// GObject wrapper for list store items
|
||||
// ------------------------------------------------------------------
|
||||
@@ -487,7 +485,7 @@ fn build_empty_state() -> gtk::Box {
|
||||
.build();
|
||||
|
||||
let formats_label = gtk::Label::builder()
|
||||
.label("Supported: JPEG, PNG, WebP, AVIF, GIF, TIFF, BMP")
|
||||
.label("Supports all common image formats including RAW")
|
||||
.css_classes(["dim-label", "caption"])
|
||||
.halign(gtk::Align::Center)
|
||||
.margin_top(8)
|
||||
@@ -573,7 +571,7 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
||||
// Factory: setup
|
||||
{
|
||||
factory.connect_setup(move |_factory, list_item| {
|
||||
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else { return };
|
||||
|
||||
let overlay = gtk::Overlay::builder()
|
||||
.width_request(THUMB_SIZE)
|
||||
@@ -656,13 +654,13 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
||||
let excluded = state.excluded_files.clone();
|
||||
let loaded = state.loaded_files.clone();
|
||||
factory.connect_bind(move |_factory, list_item| {
|
||||
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let item = list_item.item().and_downcast::<ImageItem>().unwrap();
|
||||
let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else { return };
|
||||
let Some(item) = list_item.item().and_downcast::<ImageItem>() else { return };
|
||||
let path = item.path().to_path_buf();
|
||||
|
||||
let vbox = list_item.child().and_downcast::<gtk::Box>().unwrap();
|
||||
let overlay = vbox.first_child().and_downcast::<gtk::Overlay>().unwrap();
|
||||
let name_label = overlay.next_sibling().and_downcast::<gtk::Label>().unwrap();
|
||||
let Some(vbox) = list_item.child().and_downcast::<gtk::Box>() else { return };
|
||||
let Some(overlay) = vbox.first_child().and_downcast::<gtk::Overlay>() else { return };
|
||||
let Some(name_label) = overlay.next_sibling().and_downcast::<gtk::Label>() else { return };
|
||||
|
||||
// Set filename
|
||||
let file_name = path.file_name()
|
||||
@@ -671,19 +669,36 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
||||
name_label.set_label(file_name);
|
||||
|
||||
// Get the frame -> stack -> picture
|
||||
let frame = overlay.child().and_downcast::<gtk::Frame>().unwrap();
|
||||
let thumb_stack = frame.child().and_downcast::<gtk::Stack>().unwrap();
|
||||
let picture = thumb_stack.child_by_name("picture")
|
||||
.and_downcast::<gtk::Picture>().unwrap();
|
||||
let Some(frame) = overlay.child().and_downcast::<gtk::Frame>() else { return };
|
||||
let Some(thumb_stack) = frame.child().and_downcast::<gtk::Stack>() else { return };
|
||||
let Some(picture) = thumb_stack.child_by_name("picture")
|
||||
.and_downcast::<gtk::Picture>() else { return };
|
||||
|
||||
// Reset to placeholder
|
||||
thumb_stack.set_visible_child_name("placeholder");
|
||||
|
||||
// Bump bind generation so stale idle callbacks are ignored
|
||||
let bind_gen: u32 = unsafe {
|
||||
thumb_stack.data::<u32>("bind-gen")
|
||||
.map(|p| *p.as_ref())
|
||||
.unwrap_or(0)
|
||||
.wrapping_add(1)
|
||||
};
|
||||
unsafe { thumb_stack.set_data("bind-gen", bind_gen); }
|
||||
|
||||
// Load thumbnail asynchronously
|
||||
let thumb_stack_c = thumb_stack.clone();
|
||||
let picture_c = picture.clone();
|
||||
let path_c = path.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
let current: u32 = unsafe {
|
||||
thumb_stack_c.data::<u32>("bind-gen")
|
||||
.map(|p| *p.as_ref())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
if current != bind_gen {
|
||||
return; // Item was recycled; skip stale load
|
||||
}
|
||||
load_thumbnail(&path_c, &picture_c, &thumb_stack_c);
|
||||
});
|
||||
|
||||
@@ -725,9 +740,9 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
||||
// Factory: unbind - disconnect signal to avoid stale closures
|
||||
{
|
||||
factory.connect_unbind(move |_factory, list_item| {
|
||||
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let vbox = list_item.child().and_downcast::<gtk::Box>().unwrap();
|
||||
let overlay = vbox.first_child().and_downcast::<gtk::Overlay>().unwrap();
|
||||
let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else { return };
|
||||
let Some(vbox) = list_item.child().and_downcast::<gtk::Box>() else { return };
|
||||
let Some(overlay) = vbox.first_child().and_downcast::<gtk::Overlay>() else { return };
|
||||
|
||||
if let Some(check) = find_check_button(overlay.upcast_ref::<gtk::Widget>()) {
|
||||
let handler: Option<glib::SignalHandlerId> = unsafe {
|
||||
|
||||
Reference in New Issue
Block a user