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") .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) .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); 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)); 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(); 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 } 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 }