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.
This commit is contained in:
2026-03-06 15:59:44 +02:00
parent fcb4b0e727
commit fe12316bc4
2 changed files with 142 additions and 2 deletions

View File

@@ -276,6 +276,53 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
preview_group.add(&size_box); preview_group.add(&size_box);
preview_group.add(&preview_frame); 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<RefCell<usize>> = 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 // "No image loaded" placeholder
let no_image_label = gtk::Label::builder() let no_image_label = gtk::Label::builder()
.label("Add images first to see compression preview") .label("Add images first to see compression preview")
@@ -284,6 +331,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
.margin_top(4) .margin_top(4)
.build(); .build();
preview_group.add(&no_image_label); preview_group.add(&no_image_label);
preview_group.add(&thumb_scrolled);
content.append(&preview_group); 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 comp_label = compressed_size_label.clone();
let no_img_label = no_image_label.clone(); let no_img_label = no_image_label.clone();
let jc = state.job_config.clone(); let jc = state.job_config.clone();
let pidx = preview_index.clone();
Rc::new(move || { Rc::new(move || {
let loaded = files.borrow(); let loaded = files.borrow();
@@ -380,8 +429,9 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
} }
no_img_label.set_visible(false); no_img_label.set_visible(false);
// Pick the first image as sample // Pick the selected preview image
let sample_path = loaded[0].clone(); let idx = *pidx.borrow();
let sample_path = loaded.get(idx).cloned().unwrap_or_else(|| loaded[0].clone());
let cfg = jc.borrow(); let cfg = jc.borrow();
let preset = cfg.quality_preset; let preset = cfg.quality_preset;
drop(cfg); 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::<gtk::Button>() {
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::<gtk::Button>() {
if let Some(f) = b.child().and_then(|c| c.downcast::<gtk::Frame>().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 // Trigger initial preview load
{ {
let up = update_preview.clone(); let up = update_preview.clone();

View File

@@ -237,6 +237,59 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
preview_picture.set_visible(has_files); 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::<gtk::Button>() {
if let Some(f) = b.child().and_then(|c| c.downcast::<gtk::Frame>().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() let preview_stack = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.spacing(4) .spacing(4)
@@ -244,6 +297,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.margin_bottom(8) .margin_bottom(8)
.build(); .build();
preview_stack.append(&preview_overlay); preview_stack.append(&preview_overlay);
preview_stack.append(&wm_thumb_box);
preview_stack.append(&no_preview_label); preview_stack.append(&no_preview_label);
preview_group.add(&preview_stack); preview_group.add(&preview_stack);