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() }