use adw::prelude::*; use pixstrip_core::preset::Preset; use pixstrip_core::operations::*; use crate::app::{AppState, JobConfig, MetadataMode}; pub fn build_workflow_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(); // Built-in presets section let builtin_group = adw::PreferencesGroup::builder() .title("Built-in Workflows") .description("Select a preset to get started quickly") .build(); let builtin_flow = gtk::FlowBox::builder() .selection_mode(gtk::SelectionMode::Single) .max_children_per_line(4) .min_children_per_line(2) .row_spacing(8) .column_spacing(8) .homogeneous(true) .build(); let builtins = Preset::all_builtins(); for preset in &builtins { let card = build_preset_card(preset); builtin_flow.append(&card); } // When a preset card is activated, apply it to JobConfig and advance { let jc = state.job_config.clone(); builtin_flow.connect_child_activated(move |flow, child| { let idx = child.index() as usize; if let Some(preset) = builtins.get(idx) { apply_preset_to_config(&mut jc.borrow_mut(), preset); } flow.activate_action("win.next-step", None).ok(); }); } builtin_group.add(&builtin_flow); content.append(&builtin_group); // Custom workflow section let custom_group = adw::PreferencesGroup::builder() .title("Custom Workflow") .description("Choose which operations to include, then click Next") .build(); let resize_check = adw::SwitchRow::builder() .title("Resize") .subtitle("Scale images to new dimensions") .active(state.job_config.borrow().resize_enabled) .build(); let adjustments_check = adw::SwitchRow::builder() .title("Adjustments") .subtitle("Rotate, flip, brightness, contrast, effects") .active(state.job_config.borrow().adjustments_enabled) .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(); adjustments_check.connect_active_notify(move |row| { jc.borrow_mut().adjustments_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 let user_group = adw::PreferencesGroup::builder() .title("Your Presets") .description("Import or save your own workflows") .build(); // Show saved user presets let store = pixstrip_core::storage::PresetStore::new(); if let Ok(presets) = store.list() { for preset in &presets { if !preset.is_custom { continue; } let row = adw::ActionRow::builder() .title(&preset.name) .subtitle(&preset.description) .activatable(true) .build(); row.add_prefix(>k::Image::from_icon_name(&preset.icon)); // Export button let export_btn = gtk::Button::builder() .icon_name("document-save-as-symbolic") .tooltip_text("Export preset") .valign(gtk::Align::Center) .build(); export_btn.add_css_class("flat"); let preset_for_export = preset.clone(); export_btn.connect_clicked(move |btn| { let p = preset_for_export.clone(); let dialog = gtk::FileDialog::builder() .title("Export Preset") .initial_name(&format!("{}.pixstrip-preset", p.name)) .modal(true) .build(); if let Some(window) = btn.root().and_then(|r| r.downcast::().ok()) { dialog.save(Some(&window), gtk::gio::Cancellable::NONE, move |result| { if let Ok(file) = result && let Some(path) = file.path() { let store = pixstrip_core::storage::PresetStore::new(); let _ = store.export_to_file(&p, &path); } }); } }); row.add_suffix(&export_btn); // Delete button let delete_btn = gtk::Button::builder() .icon_name("user-trash-symbolic") .tooltip_text("Delete preset") .valign(gtk::Align::Center) .build(); delete_btn.add_css_class("flat"); delete_btn.add_css_class("error"); let pname = preset.name.clone(); let row_ref = row.clone(); let group_ref = user_group.clone(); delete_btn.connect_clicked(move |_| { let store = pixstrip_core::storage::PresetStore::new(); let _ = store.delete(&pname); group_ref.remove(&row_ref); }); row.add_suffix(&delete_btn); row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); let jc = state.job_config.clone(); let p = preset.clone(); row.connect_activated(move |r| { apply_preset_to_config(&mut jc.borrow_mut(), &p); r.activate_action("win.next-step", None).ok(); }); user_group.add(&row); } } let import_button = gtk::Button::builder() .label("Import Preset") .icon_name("document-open-symbolic") .action_name("win.import-preset") .build(); import_button.add_css_class("flat"); user_group.add(&import_button); content.append(&user_group); scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() .maximum_size(800) .child(&scrolled) .build(); // Drop target for .pixstrip-preset files let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY); let jc_drop = state.job_config.clone(); drop_target.connect_drop(move |_target, value, _x, _y| { if let Ok(file) = value.get::() { if let Some(path) = file.path() { if path.extension().and_then(|e| e.to_str()) == Some("pixstrip-preset") { let store = pixstrip_core::storage::PresetStore::new(); if let Ok(preset) = store.import_from_file(&path) { apply_preset_to_config(&mut jc_drop.borrow_mut(), &preset); let _ = store.save(&preset); return true; } } } } false }); clamp.add_controller(drop_target); adw::NavigationPage::builder() .title("Choose a Workflow") .tag("step-workflow") .child(&clamp) .build() } fn apply_preset_to_config(cfg: &mut JobConfig, preset: &Preset) { // Resize match &preset.resize { Some(ResizeConfig::ByWidth(w)) => { cfg.resize_enabled = true; cfg.resize_width = *w; cfg.resize_height = 0; cfg.allow_upscale = false; } Some(ResizeConfig::ByHeight(h)) => { cfg.resize_enabled = true; cfg.resize_width = 0; cfg.resize_height = *h; cfg.allow_upscale = false; } Some(ResizeConfig::FitInBox { max, allow_upscale }) => { cfg.resize_enabled = true; cfg.resize_width = max.width; cfg.resize_height = max.height; cfg.allow_upscale = *allow_upscale; } Some(ResizeConfig::Exact(dims)) => { cfg.resize_enabled = true; cfg.resize_width = dims.width; cfg.resize_height = dims.height; cfg.allow_upscale = true; } None => { cfg.resize_enabled = false; } } // Convert match &preset.convert { Some(ConvertConfig::SingleFormat(fmt)) => { cfg.convert_enabled = true; cfg.convert_format = Some(*fmt); } Some(_) => { cfg.convert_enabled = true; cfg.convert_format = None; } None => { cfg.convert_enabled = false; cfg.convert_format = None; } } // Compress match &preset.compress { Some(CompressConfig::Preset(q)) => { cfg.compress_enabled = true; cfg.quality_preset = *q; } Some(CompressConfig::Custom { jpeg_quality, png_level, webp_quality, .. }) => { cfg.compress_enabled = true; if let Some(jq) = jpeg_quality { cfg.jpeg_quality = *jq; } if let Some(pl) = png_level { cfg.png_level = *pl; } if let Some(wq) = webp_quality { cfg.webp_quality = *wq as u8; } } None => { cfg.compress_enabled = false; } } // Metadata match &preset.metadata { Some(MetadataConfig::StripAll) => { cfg.metadata_enabled = true; cfg.metadata_mode = MetadataMode::StripAll; } Some(MetadataConfig::Privacy) => { cfg.metadata_enabled = true; cfg.metadata_mode = MetadataMode::Privacy; } Some(MetadataConfig::KeepAll) => { cfg.metadata_enabled = true; cfg.metadata_mode = MetadataMode::KeepAll; } Some(MetadataConfig::Custom { strip_gps, strip_camera, strip_software, strip_timestamps, strip_copyright }) => { cfg.metadata_enabled = true; cfg.metadata_mode = MetadataMode::Custom; cfg.strip_gps = *strip_gps; cfg.strip_camera = *strip_camera; cfg.strip_software = *strip_software; cfg.strip_timestamps = *strip_timestamps; cfg.strip_copyright = *strip_copyright; } None => { cfg.metadata_enabled = false; } } } fn build_preset_card(preset: &Preset) -> 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(&preset.icon) .pixel_size(32) .build(); let name_label = gtk::Label::builder() .label(&preset.name) .css_classes(["heading"]) .build(); let desc_label = gtk::Label::builder() .label(&preset.description) .css_classes(["caption", "dim-label"]) .wrap(true) .justify(gtk::Justification::Center) .max_width_chars(20) .build(); inner.append(&icon); inner.append(&name_label); inner.append(&desc_label); card.append(&inner); card }