- New step_adjustments: rotation (5 options) and flip (3 options) - New step_watermark: text/image watermark with position, opacity, font size - New step_rename: prefix/suffix/counter with live preview and template engine - Updated step_metadata: added Custom mode with per-category checkboxes (GPS, camera, software, timestamps, copyright) with show/hide toggle - Expanded JobConfig with all operation fields (watermark, rename, metadata custom) - Updated wizard from 7 to 10 steps in correct pipeline order - Fixed page index references from 6 to 9 for output step - Added MetadataMode::Custom handling in preset builder and output summary
230 lines
7.3 KiB
Rust
230 lines
7.3 KiB
Rust
use adw::prelude::*;
|
|
use crate::app::AppState;
|
|
|
|
pub fn build_rename_page(state: &AppState) -> adw::NavigationPage {
|
|
let scrolled = gtk::ScrolledWindow::builder()
|
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
.vexpand(true)
|
|
.build();
|
|
|
|
let content = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.spacing(12)
|
|
.margin_top(12)
|
|
.margin_bottom(12)
|
|
.margin_start(24)
|
|
.margin_end(24)
|
|
.build();
|
|
|
|
let cfg = state.job_config.borrow();
|
|
|
|
// Enable toggle
|
|
let enable_row = adw::SwitchRow::builder()
|
|
.title("Enable Rename")
|
|
.subtitle("Rename output files with prefix, suffix, or template")
|
|
.active(cfg.rename_enabled)
|
|
.build();
|
|
|
|
let enable_group = adw::PreferencesGroup::new();
|
|
enable_group.add(&enable_row);
|
|
content.append(&enable_group);
|
|
|
|
// Simple mode: prefix + suffix + counter
|
|
let simple_group = adw::PreferencesGroup::builder()
|
|
.title("Simple Rename")
|
|
.description("Add prefix, suffix, and sequential counter")
|
|
.build();
|
|
|
|
let prefix_row = adw::EntryRow::builder()
|
|
.title("Prefix")
|
|
.text(&cfg.rename_prefix)
|
|
.build();
|
|
|
|
let suffix_row = adw::EntryRow::builder()
|
|
.title("Suffix")
|
|
.text(&cfg.rename_suffix)
|
|
.build();
|
|
|
|
let counter_start_row = adw::SpinRow::builder()
|
|
.title("Counter Start")
|
|
.subtitle("First number in sequence")
|
|
.adjustment(>k::Adjustment::new(cfg.rename_counter_start as f64, 0.0, 99999.0, 1.0, 10.0, 0.0))
|
|
.build();
|
|
|
|
let counter_padding_row = adw::SpinRow::builder()
|
|
.title("Counter Padding")
|
|
.subtitle("Minimum digits (e.g., 3 = 001, 002, 003)")
|
|
.adjustment(>k::Adjustment::new(cfg.rename_counter_padding as f64, 1.0, 10.0, 1.0, 1.0, 0.0))
|
|
.build();
|
|
|
|
simple_group.add(&prefix_row);
|
|
simple_group.add(&suffix_row);
|
|
simple_group.add(&counter_start_row);
|
|
simple_group.add(&counter_padding_row);
|
|
content.append(&simple_group);
|
|
|
|
// Live preview
|
|
let preview_group = adw::PreferencesGroup::builder()
|
|
.title("Preview")
|
|
.description("Example of how 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)
|
|
.margin_top(8)
|
|
.margin_bottom(8)
|
|
.margin_start(12)
|
|
.build();
|
|
|
|
preview_group.add(&preview_label);
|
|
content.append(&preview_group);
|
|
|
|
// Advanced: template engine
|
|
let advanced_group = adw::PreferencesGroup::builder()
|
|
.title("Advanced: Template Engine")
|
|
.description("Use variables in curly braces for full control")
|
|
.build();
|
|
|
|
let template_row = adw::EntryRow::builder()
|
|
.title("Template")
|
|
.text(&cfg.rename_template)
|
|
.build();
|
|
|
|
let help_label = gtk::Label::builder()
|
|
.label(
|
|
"Available variables:\n\
|
|
{name} - original filename (no extension)\n\
|
|
{ext} - output extension\n\
|
|
{counter} or {counter:3} - zero-padded counter\n\
|
|
{date} - today's date\n\
|
|
{exif_date} - EXIF date taken\n\
|
|
{camera} - camera model from EXIF\n\
|
|
{width} x {height} - output dimensions\n\
|
|
{original_ext} - original file extension"
|
|
)
|
|
.css_classes(["dim-label", "caption"])
|
|
.halign(gtk::Align::Start)
|
|
.wrap(true)
|
|
.margin_top(4)
|
|
.margin_bottom(8)
|
|
.margin_start(12)
|
|
.build();
|
|
|
|
advanced_group.add(&template_row);
|
|
advanced_group.add(&help_label);
|
|
content.append(&advanced_group);
|
|
|
|
drop(cfg);
|
|
|
|
// Wire signals
|
|
{
|
|
let jc = state.job_config.clone();
|
|
enable_row.connect_active_notify(move |row| {
|
|
jc.borrow_mut().rename_enabled = row.is_active();
|
|
});
|
|
}
|
|
|
|
// Update preview helper
|
|
let update_preview = {
|
|
let files = state.loaded_files.clone();
|
|
let jc = state.job_config.clone();
|
|
let preview = preview_label.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));
|
|
} 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));
|
|
}
|
|
}
|
|
};
|
|
|
|
// Call once to set initial preview
|
|
update_preview();
|
|
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let up = update_preview.clone();
|
|
prefix_row.connect_changed(move |row| {
|
|
jc.borrow_mut().rename_prefix = row.text().to_string();
|
|
up();
|
|
});
|
|
}
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let up = update_preview.clone();
|
|
suffix_row.connect_changed(move |row| {
|
|
jc.borrow_mut().rename_suffix = row.text().to_string();
|
|
up();
|
|
});
|
|
}
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let up = update_preview.clone();
|
|
counter_start_row.connect_value_notify(move |row| {
|
|
jc.borrow_mut().rename_counter_start = row.value() as u32;
|
|
up();
|
|
});
|
|
}
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let up = update_preview.clone();
|
|
counter_padding_row.connect_value_notify(move |row| {
|
|
jc.borrow_mut().rename_counter_padding = row.value() as u32;
|
|
up();
|
|
});
|
|
}
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let up = update_preview;
|
|
template_row.connect_changed(move |row| {
|
|
jc.borrow_mut().rename_template = row.text().to_string();
|
|
up();
|
|
});
|
|
}
|
|
|
|
scrolled.set_child(Some(&content));
|
|
|
|
let clamp = adw::Clamp::builder()
|
|
.maximum_size(600)
|
|
.child(&scrolled)
|
|
.build();
|
|
|
|
adw::NavigationPage::builder()
|
|
.title("Rename")
|
|
.tag("step-rename")
|
|
.child(&clamp)
|
|
.build()
|
|
}
|