From 0234f872bc9faf0341354ed2fc09d53ad4b40a57 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 13:00:52 +0200 Subject: [PATCH] Add custom workflow operation toggles, improve output step summary Workflow step: replace the auto-advancing Custom card with a proper operation checklist using SwitchRow toggles for each operation (Resize, Adjustments, Convert, Compress, Metadata, Watermark, Rename). Wired to job config so selections persist through the wizard. Output step: show actual file size alongside image count. Refresh both count and size dynamically when navigating to the output step. --- pixstrip-gtk/src/app.rs | 12 +- pixstrip-gtk/src/steps/step_output.rs | 9 +- pixstrip-gtk/src/steps/step_workflow.rs | 145 ++++++++++++++---------- 3 files changed, 104 insertions(+), 62 deletions(-) diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index d298994..b80079b 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -527,14 +527,20 @@ fn navigate_to_step(ui: &WizardUi, target: usize) { // Update dynamic content on certain steps if target == 9 { - // Output step - update image count and operation summary - let count = ui.state.loaded_files.borrow().len(); + // Output step - update image count, total size, and operation summary + let files = ui.state.loaded_files.borrow(); + let count = files.len(); + let total_size: u64 = files.iter() + .filter_map(|p| std::fs::metadata(p).ok()) + .map(|m| m.len()) + .sum(); + drop(files); if let Some(page) = ui.pages.get(9) { walk_widgets(&page.child(), &|widget| { if let Some(row) = widget.downcast_ref::() && row.title().as_str() == "Images to process" { - row.set_subtitle(&format!("{} images", count)); + row.set_subtitle(&format!("{} images ({})", count, format_bytes(total_size))); } }); } diff --git a/pixstrip-gtk/src/steps/step_output.rs b/pixstrip-gtk/src/steps/step_output.rs index 5943dd7..598b6d0 100644 --- a/pixstrip-gtk/src/steps/step_output.rs +++ b/pixstrip-gtk/src/steps/step_output.rs @@ -16,7 +16,7 @@ pub fn build_output_page(state: &AppState) -> adw::NavigationPage { .margin_end(24) .build(); - // Operation summary + // Operation summary - dynamically updated when this step is shown let summary_group = adw::PreferencesGroup::builder() .title("Operation Summary") .description("Review your processing settings before starting") @@ -36,9 +36,14 @@ pub fn build_output_page(state: &AppState) -> adw::NavigationPage { .title("Output Directory") .build(); + let default_output = state.output_dir.borrow() + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "processed/ (subfolder next to originals)".to_string()); + let output_row = adw::ActionRow::builder() .title("Output Location") - .subtitle("processed/ (subfolder next to originals)") + .subtitle(&default_output) .activatable(true) .action_name("win.choose-output") .build(); diff --git a/pixstrip-gtk/src/steps/step_workflow.rs b/pixstrip-gtk/src/steps/step_workflow.rs index 5d27b2d..0f8ef36 100644 --- a/pixstrip-gtk/src/steps/step_workflow.rs +++ b/pixstrip-gtk/src/steps/step_workflow.rs @@ -57,20 +57,97 @@ pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage { // Custom workflow section let custom_group = adw::PreferencesGroup::builder() .title("Custom Workflow") - .description("Choose which operations to include") + .description("Choose which operations to include, then click Next") .build(); - let custom_card = build_custom_card(); - let custom_flow = gtk::FlowBox::builder() - .selection_mode(gtk::SelectionMode::None) - .max_children_per_line(4) - .min_children_per_line(2) + let resize_check = adw::SwitchRow::builder() + .title("Resize") + .subtitle("Scale images to new dimensions") + .active(state.job_config.borrow().resize_enabled) .build(); - custom_flow.append(&custom_card); - custom_flow.connect_child_activated(|flow, _child| { - flow.activate_action("win.next-step", None).ok(); - }); - custom_group.add(&custom_flow); + + let adjustments_check = adw::SwitchRow::builder() + .title("Adjustments") + .subtitle("Rotate, flip, brightness, contrast, effects") + .active(false) + .build(); + + let convert_check = adw::SwitchRow::builder() + .title("Convert") + .subtitle("Change image format (JPEG, PNG, WebP, AVIF)") + .active(state.job_config.borrow().convert_enabled) + .build(); + + let compress_check = adw::SwitchRow::builder() + .title("Compress") + .subtitle("Reduce file size with quality control") + .active(state.job_config.borrow().compress_enabled) + .build(); + + let metadata_check = adw::SwitchRow::builder() + .title("Metadata") + .subtitle("Strip or modify EXIF, GPS, camera data") + .active(state.job_config.borrow().metadata_enabled) + .build(); + + let watermark_check = adw::SwitchRow::builder() + .title("Watermark") + .subtitle("Add text or image overlay") + .active(state.job_config.borrow().watermark_enabled) + .build(); + + let rename_check = adw::SwitchRow::builder() + .title("Rename") + .subtitle("Rename files with prefix, suffix, or template") + .active(state.job_config.borrow().rename_enabled) + .build(); + + custom_group.add(&resize_check); + custom_group.add(&adjustments_check); + custom_group.add(&convert_check); + custom_group.add(&compress_check); + custom_group.add(&metadata_check); + custom_group.add(&watermark_check); + custom_group.add(&rename_check); + + // Wire custom operation toggles to job config + { + let jc = state.job_config.clone(); + resize_check.connect_active_notify(move |row| { + jc.borrow_mut().resize_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + convert_check.connect_active_notify(move |row| { + jc.borrow_mut().convert_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + compress_check.connect_active_notify(move |row| { + jc.borrow_mut().compress_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + metadata_check.connect_active_notify(move |row| { + jc.borrow_mut().metadata_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + watermark_check.connect_active_notify(move |row| { + jc.borrow_mut().watermark_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + rename_check.connect_active_notify(move |row| { + jc.borrow_mut().rename_enabled = row.is_active(); + }); + } + content.append(&custom_group); // User presets section @@ -276,49 +353,3 @@ fn build_preset_card(preset: &Preset) -> gtk::Box { card } -fn build_custom_card() -> gtk::Box { - let card = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(8) - .halign(gtk::Align::Center) - .valign(gtk::Align::Start) - .build(); - card.add_css_class("card"); - card.set_size_request(180, 120); - - let inner = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(4) - .margin_top(16) - .margin_bottom(16) - .margin_start(12) - .margin_end(12) - .halign(gtk::Align::Center) - .valign(gtk::Align::Center) - .vexpand(true) - .build(); - - let icon = gtk::Image::builder() - .icon_name("applications-system-symbolic") - .pixel_size(32) - .build(); - - let name_label = gtk::Label::builder() - .label("Custom...") - .css_classes(["heading"]) - .build(); - - let desc_label = gtk::Label::builder() - .label("Pick your own operations") - .css_classes(["caption", "dim-label"]) - .wrap(true) - .justify(gtk::Justification::Center) - .build(); - - inner.append(&icon); - inner.append(&name_label); - inner.append(&desc_label); - card.append(&inner); - - card -}