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.
This commit is contained in:
2026-03-06 13:00:52 +02:00
parent 8f6e4382c4
commit 0234f872bc
3 changed files with 104 additions and 62 deletions

View File

@@ -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::<adw::ActionRow>()
&& row.title().as_str() == "Images to process"
{
row.set_subtitle(&format!("{} images", count));
row.set_subtitle(&format!("{} images ({})", count, format_bytes(total_size)));
}
});
}

View File

@@ -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();

View File

@@ -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
}