From 29770be8b5a91719245375d199497c03bfebb966 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 12:54:35 +0200 Subject: [PATCH] Enhance rename preview to show 5 files, add watermark advanced expander Rename step now shows preview for first 5 loaded files (or fallback examples) with incrementing counters instead of a single line. Watermark step gains an advanced expander with rotation, tiling, margin, and scale options alongside the existing opacity control. --- pixstrip-gtk/src/steps/step_rename.rs | 126 ++++++++++++++++------- pixstrip-gtk/src/steps/step_watermark.rs | 39 ++++++- 2 files changed, 125 insertions(+), 40 deletions(-) diff --git a/pixstrip-gtk/src/steps/step_rename.rs b/pixstrip-gtk/src/steps/step_rename.rs index b7c2b04..73563e1 100644 --- a/pixstrip-gtk/src/steps/step_rename.rs +++ b/pixstrip-gtk/src/steps/step_rename.rs @@ -63,22 +63,31 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { simple_group.add(&counter_padding_row); content.append(&simple_group); - // Live preview + // Live preview showing first 5 filenames let preview_group = adw::PreferencesGroup::builder() .title("Preview") - .description("Example of how files will be renamed") + .description("How your files will be renamed") .build(); - let preview_label = gtk::Label::builder() - .label("photo.jpg -> photo_001.jpg") - .css_classes(["monospace", "dim-label"]) - .halign(gtk::Align::Start) + let preview_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(2) .margin_top(8) .margin_bottom(8) .margin_start(12) + .margin_end(12) .build(); - preview_group.add(&preview_label); + // Create 5 preview labels + for _ in 0..5 { + let label = gtk::Label::builder() + .css_classes(["monospace", "dim-label", "caption"]) + .halign(gtk::Align::Start) + .build(); + preview_box.append(&label); + } + + preview_group.add(&preview_box); content.append(&preview_group); // Advanced: template engine @@ -126,46 +135,85 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { }); } - // Update preview helper + // Update preview helper - shows first 5 filenames from the batch let update_preview = { let files = state.loaded_files.clone(); let jc = state.job_config.clone(); - let preview = preview_label.clone(); + let preview = preview_box.clone(); move || { let cfg = jc.borrow(); let loaded = files.borrow(); - let sample_name = loaded - .first() - .and_then(|p| p.file_stem()) - .and_then(|s| s.to_str()) - .unwrap_or("photo"); - let sample_ext = loaded - .first() - .and_then(|p| p.extension()) - .and_then(|e| e.to_str()) - .unwrap_or("jpg"); - if !cfg.rename_template.is_empty() { - // Template mode preview - let result = cfg.rename_template - .replace("{name}", sample_name) - .replace("{ext}", sample_ext) - .replace("{counter}", &format!("{:0>width$}", cfg.rename_counter_start, width = cfg.rename_counter_padding as usize)) - .replace("{date}", "2026-03-06") - .replace("{width}", "1200") - .replace("{height}", "800"); - preview.set_label(&format!("{}.{} -> {}", sample_name, sample_ext, result)); + // Sample filenames: use actual loaded files, or fallback examples + let samples: Vec<(&str, &str)> = if loaded.is_empty() { + vec![ + ("photo", "jpg"), + ("sunset", "png"), + ("beach", "jpg"), + ("portrait", "webp"), + ("landscape", "jpg"), + ] } else { - // Simple mode preview - let rename_cfg = pixstrip_core::operations::RenameConfig { - prefix: cfg.rename_prefix.clone(), - suffix: cfg.rename_suffix.clone(), - counter_start: cfg.rename_counter_start, - counter_padding: cfg.rename_counter_padding, - template: None, - }; - let result = rename_cfg.apply_simple(sample_name, sample_ext, 1); - preview.set_label(&format!("{}.{} -> {}", sample_name, sample_ext, result)); + loaded + .iter() + .take(5) + .map(|p| { + let name = p + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("photo"); + let ext = p + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("jpg"); + (name, ext) + }) + .collect() + }; + + let mut child = preview.first_child(); + for (i, (name, ext)) in samples.iter().enumerate() { + let Some(widget) = child.clone() else { break }; + if let Some(label) = widget.downcast_ref::() { + let counter = cfg.rename_counter_start + i as u32; + if !cfg.rename_template.is_empty() { + let result = cfg + .rename_template + .replace("{name}", name) + .replace("{ext}", ext) + .replace( + "{counter}", + &format!( + "{:0>width$}", + counter, + width = cfg.rename_counter_padding as usize + ), + ) + .replace("{date}", "2026-03-06") + .replace("{width}", "1200") + .replace("{height}", "800"); + label.set_label(&format!("{}.{} -> {}", name, ext, result)); + } else { + let rename_cfg = pixstrip_core::operations::RenameConfig { + prefix: cfg.rename_prefix.clone(), + suffix: cfg.rename_suffix.clone(), + counter_start: cfg.rename_counter_start, + counter_padding: cfg.rename_counter_padding, + template: None, + }; + let result = + rename_cfg.apply_simple(name, ext, (i + 1) as u32); + label.set_label(&format!("{}.{} -> {}", name, ext, result)); + } + label.set_visible(true); + } + child = widget.next_sibling(); + } + + // Hide unused labels + while let Some(widget) = child.clone() { + widget.set_visible(false); + child = widget.next_sibling(); } } }; diff --git a/pixstrip-gtk/src/steps/step_watermark.rs b/pixstrip-gtk/src/steps/step_watermark.rs index c9b6902..64c2bb3 100644 --- a/pixstrip-gtk/src/steps/step_watermark.rs +++ b/pixstrip-gtk/src/steps/step_watermark.rs @@ -118,7 +118,13 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { // Advanced options let advanced_group = adw::PreferencesGroup::builder() + .title("Advanced") + .build(); + + let advanced_expander = adw::ExpanderRow::builder() .title("Advanced Options") + .subtitle("Opacity, rotation, tiling, margin") + .show_enable_switch(false) .build(); let opacity_row = adw::SpinRow::builder() @@ -128,7 +134,38 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .digits(2) .build(); - advanced_group.add(&opacity_row); + let rotation_row = adw::ComboRow::builder() + .title("Rotation") + .subtitle("Rotate the watermark") + .build(); + let rotation_model = gtk::StringList::new(&["None", "45 degrees", "-45 degrees", "90 degrees"]); + rotation_row.set_model(Some(&rotation_model)); + + let tiled_row = adw::SwitchRow::builder() + .title("Tiled / Repeated") + .subtitle("Repeat watermark across the entire image") + .active(false) + .build(); + + let margin_row = adw::SpinRow::builder() + .title("Margin from Edges") + .subtitle("Padding in pixels from image edges") + .adjustment(>k::Adjustment::new(10.0, 0.0, 200.0, 1.0, 10.0, 0.0)) + .build(); + + let scale_row = adw::SpinRow::builder() + .title("Scale (% of image)") + .subtitle("Watermark size relative to image") + .adjustment(>k::Adjustment::new(20.0, 1.0, 100.0, 1.0, 5.0, 0.0)) + .build(); + + advanced_expander.add_row(&opacity_row); + advanced_expander.add_row(&rotation_row); + advanced_expander.add_row(&tiled_row); + advanced_expander.add_row(&margin_row); + advanced_expander.add_row(&scale_row); + + advanced_group.add(&advanced_expander); content.append(&advanced_group); drop(cfg);