From fe12316bc4e80ebf0fa106acf18f8566298a06f3 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 15:59:44 +0200 Subject: [PATCH] Add thumbnail selection for compression and watermark previews Users can now click different batch images in a thumbnail strip to switch which image is used for the quality comparison preview and watermark position preview. Shows up to 10 thumbnails with accent highlight on the selected one. --- pixstrip-gtk/src/steps/step_compress.rs | 90 +++++++++++++++++++++++- pixstrip-gtk/src/steps/step_watermark.rs | 54 ++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/pixstrip-gtk/src/steps/step_compress.rs b/pixstrip-gtk/src/steps/step_compress.rs index 826a084..036ff4d 100644 --- a/pixstrip-gtk/src/steps/step_compress.rs +++ b/pixstrip-gtk/src/steps/step_compress.rs @@ -276,6 +276,53 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { preview_group.add(&size_box); preview_group.add(&preview_frame); + // Thumbnail strip for selecting preview image + let thumb_scrolled = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Automatic) + .vscrollbar_policy(gtk::PolicyType::Never) + .max_content_height(60) + .build(); + + let thumb_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(4) + .margin_top(4) + .margin_bottom(4) + .halign(gtk::Align::Center) + .build(); + thumb_scrolled.set_child(Some(&thumb_box)); + + let preview_index: Rc> = Rc::new(RefCell::new(0)); + + // Populate thumbnails from loaded files + { + let files = state.loaded_files.borrow(); + let max_thumbs = files.len().min(10); // Show at most 10 thumbnails + for i in 0..max_thumbs { + let pic = gtk::Picture::builder() + .content_fit(gtk::ContentFit::Cover) + .width_request(50) + .height_request(50) + .build(); + pic.set_filename(Some(&files[i])); + let frame = gtk::Frame::builder() + .child(&pic) + .build(); + if i == 0 { + frame.add_css_class("accent"); + } + + let btn = gtk::Button::builder() + .child(&frame) + .has_frame(false) + .tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image")) + .build(); + + thumb_box.append(&btn); + } + thumb_scrolled.set_visible(max_thumbs > 1); + } + // "No image loaded" placeholder let no_image_label = gtk::Label::builder() .label("Add images first to see compression preview") @@ -284,6 +331,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { .margin_top(4) .build(); preview_group.add(&no_image_label); + preview_group.add(&thumb_scrolled); content.append(&preview_group); @@ -371,6 +419,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { let comp_label = compressed_size_label.clone(); let no_img_label = no_image_label.clone(); let jc = state.job_config.clone(); + let pidx = preview_index.clone(); Rc::new(move || { let loaded = files.borrow(); @@ -380,8 +429,9 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { } no_img_label.set_visible(false); - // Pick the first image as sample - let sample_path = loaded[0].clone(); + // Pick the selected preview image + let idx = *pidx.borrow(); + let sample_path = loaded.get(idx).cloned().unwrap_or_else(|| loaded[0].clone()); let cfg = jc.borrow(); let preset = cfg.quality_preset; drop(cfg); @@ -449,6 +499,42 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { }) }; + // Wire thumbnail buttons to switch preview image + { + let mut child = thumb_box.first_child(); + let mut idx = 0usize; + while let Some(widget) = child { + if let Some(btn) = widget.downcast_ref::() { + let pidx = preview_index.clone(); + let up = update_preview.clone(); + let tb = thumb_box.clone(); + let current_idx = idx; + btn.connect_clicked(move |_| { + *pidx.borrow_mut() = current_idx; + up(); + // Update highlight on thumbnails + let mut c = tb.first_child(); + let mut j = 0usize; + while let Some(w) = c { + if let Some(b) = w.downcast_ref::() { + if let Some(f) = b.child().and_then(|c| c.downcast::().ok()) { + if j == current_idx { + f.add_css_class("accent"); + } else { + f.remove_css_class("accent"); + } + } + } + c = w.next_sibling(); + j += 1; + } + }); + idx += 1; + } + child = widget.next_sibling(); + } + } + // Trigger initial preview load { let up = update_preview.clone(); diff --git a/pixstrip-gtk/src/steps/step_watermark.rs b/pixstrip-gtk/src/steps/step_watermark.rs index 8310936..727d5d7 100644 --- a/pixstrip-gtk/src/steps/step_watermark.rs +++ b/pixstrip-gtk/src/steps/step_watermark.rs @@ -237,6 +237,59 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { preview_picture.set_visible(has_files); } + // Thumbnail strip for selecting preview image + let wm_thumb_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(4) + .halign(gtk::Align::Center) + .margin_top(4) + .build(); + { + let files = state.loaded_files.borrow(); + let max_thumbs = files.len().min(10); + for i in 0..max_thumbs { + let pic = gtk::Picture::builder() + .content_fit(gtk::ContentFit::Cover) + .width_request(40) + .height_request(40) + .build(); + pic.set_filename(Some(&files[i])); + let frame = gtk::Frame::builder() + .child(&pic) + .build(); + if i == 0 { frame.add_css_class("accent"); } + + let btn = gtk::Button::builder() + .child(&frame) + .has_frame(false) + .tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image")) + .build(); + + let pp = preview_picture.clone(); + let path = files[i].clone(); + let tb = wm_thumb_box.clone(); + let current_idx = i; + btn.connect_clicked(move |_| { + pp.set_filename(Some(&path)); + let mut c = tb.first_child(); + let mut j = 0usize; + while let Some(w) = c { + if let Some(b) = w.downcast_ref::() { + if let Some(f) = b.child().and_then(|c| c.downcast::().ok()) { + if j == current_idx { f.add_css_class("accent"); } + else { f.remove_css_class("accent"); } + } + } + c = w.next_sibling(); + j += 1; + } + }); + + wm_thumb_box.append(&btn); + } + wm_thumb_box.set_visible(max_thumbs > 1); + } + let preview_stack = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) @@ -244,6 +297,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .margin_bottom(8) .build(); preview_stack.append(&preview_overlay); + preview_stack.append(&wm_thumb_box); preview_stack.append(&no_preview_label); preview_group.add(&preview_stack);