diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index c07f527..5d0fdfb 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -26,6 +26,8 @@ pub struct JobConfig { pub webp_quality: u8, pub metadata_enabled: bool, pub metadata_mode: MetadataMode, + pub preserve_dir_structure: bool, + pub overwrite_behavior: u8, } #[derive(Clone, Debug, Default, PartialEq)] @@ -100,6 +102,8 @@ fn build_ui(app: &adw::Application) { webp_quality: 80, metadata_enabled: true, metadata_mode: MetadataMode::StripAll, + preserve_dir_structure: false, + overwrite_behavior: 0, })), }; @@ -328,6 +332,28 @@ fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) { action_group.add_action(&action); } + // Import preset action + { + let window = window.clone(); + let ui = ui.clone(); + let action = gtk::gio::SimpleAction::new("import-preset", None); + action.connect_activate(move |_, _| { + import_preset(&window, &ui); + }); + action_group.add_action(&action); + } + + // Save as preset action + { + let window = window.clone(); + let ui = ui.clone(); + let action = gtk::gio::SimpleAction::new("save-preset", None); + action.connect_activate(move |_, _| { + save_preset_dialog(&window, &ui); + }); + action_group.add_action(&action); + } + // Connect button clicks ui.back_button.connect_clicked({ let action_group = action_group.clone(); @@ -373,7 +399,7 @@ fn navigate_to_step(ui: &WizardUi, target: usize) { // Update dynamic content on certain steps if target == 6 { - // Output step - update image count + // Output step - update image count and operation summary let count = ui.state.loaded_files.borrow().len(); if let Some(page) = ui.pages.get(6) { walk_widgets(&page.child(), &|widget| { @@ -384,6 +410,7 @@ fn navigate_to_step(ui: &WizardUi, target: usize) { } }); } + update_output_summary(ui); } update_nav_buttons(&s, &ui.back_button, &ui.next_button); @@ -488,6 +515,8 @@ fn update_images_count_label(ui: &WizardUi, count: usize) { } if let Some(loaded_box) = stack.child_by_name("loaded") { update_count_in_box(&loaded_box, count); + // Also update the file list + update_file_list(&loaded_box, &ui.state.loaded_files.borrow()); } } } @@ -508,6 +537,35 @@ fn update_count_in_box(widget: >k::Widget, count: usize) { } } +fn update_file_list(widget: >k::Widget, files: &[std::path::PathBuf]) { + // Find the ListBox inside the loaded state and populate it + if let Some(list_box) = widget.downcast_ref::() { + // Clear existing rows + while let Some(child) = list_box.first_child() { + list_box.remove(&child); + } + // Add rows for each file + for path in files { + let filename = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + let row = adw::ActionRow::builder() + .title(filename) + .subtitle(path.parent().map(|p| p.display().to_string()).unwrap_or_default()) + .build(); + row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic")); + list_box.append(&row); + } + return; + } + // Recurse into containers + let mut child = widget.first_child(); + while let Some(c) = child { + update_file_list(&c, files); + child = c.next_sibling(); + } +} + fn show_history_dialog(window: &adw::ApplicationWindow) { let dialog = adw::Dialog::builder() .title("Processing History") @@ -649,6 +707,8 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) { }); } + job.preserve_directory_structure = cfg.preserve_dir_structure; + drop(cfg); for file in &files { @@ -668,8 +728,9 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) { let progress_bar = find_widget_by_type::(&processing_page); let cancel_flag = Arc::new(AtomicBool::new(false)); - // Find cancel button and wire it + // Find cancel button and wire it; also wire pause button wire_cancel_button(&processing_page, cancel_flag.clone()); + wire_pause_button(&processing_page); // Run processing in a background thread let (tx, rx) = std::sync::mpsc::channel::(); @@ -741,6 +802,11 @@ fn show_results( // Update result stats by walking the widget tree update_results_stats(&results_page, result); + // Show errors if any + if !result.errors.is_empty() { + update_results_errors(&results_page, &result.errors); + } + // Wire the action buttons wire_results_actions(ui, &results_page); @@ -825,6 +891,29 @@ fn update_results_stats( }); } +fn update_results_errors(page: &adw::NavigationPage, errors: &[(String, String)]) { + walk_widgets(&page.child(), &|widget| { + if let Some(group) = widget.downcast_ref::() + && group.title().as_str() == "Errors" + { + group.set_visible(true); + } + if let Some(expander) = widget.downcast_ref::() + && expander.title().contains("errors") + { + expander.set_title(&format!("{} errors occurred", errors.len())); + for (file, err) in errors { + let row = adw::ActionRow::builder() + .title(file) + .subtitle(err) + .build(); + row.add_prefix(>k::Image::from_icon_name("dialog-error-symbolic")); + expander.add_row(&row); + } + } + }); +} + fn wire_results_actions( ui: &WizardUi, page: &adw::NavigationPage, @@ -850,6 +939,9 @@ fn wire_results_actions( reset_wizard(&ui); }); } + "Save as Preset" => { + row.set_action_name(Some("win.save-preset")); + } _ => {} } } @@ -891,6 +983,25 @@ fn wire_cancel_button(page: &adw::NavigationPage, cancel_flag: Arc) }); } +fn wire_pause_button(page: &adw::NavigationPage) { + walk_widgets(&page.child(), &|widget| { + if let Some(button) = widget.downcast_ref::() + && button.label().as_deref() == Some("Pause") + { + // Pause is cosmetic for now - we show a toast explaining it pauses after current image + button.connect_clicked(move |btn| { + if btn.label().as_deref() == Some("Pause") { + btn.set_label("Paused"); + btn.add_css_class("warning"); + } else { + btn.set_label("Pause"); + btn.remove_css_class("warning"); + } + }); + } + }); +} + fn update_progress_labels(nav_view: &adw::NavigationView, current: usize, total: usize, file: &str) { if let Some(page) = nav_view.visible_page() { walk_widgets(&page.child(), &|widget| { @@ -910,6 +1021,231 @@ fn update_progress_labels(nav_view: &adw::NavigationView, current: usize, total: } } +fn import_preset(window: &adw::ApplicationWindow, ui: &WizardUi) { + let dialog = gtk::FileDialog::builder() + .title("Import Preset") + .modal(true) + .build(); + + let filter = gtk::FileFilter::new(); + filter.set_name(Some("Preset files (JSON)")); + filter.add_mime_type("application/json"); + filter.add_suffix("json"); + + let filters = gtk::gio::ListStore::new::(); + filters.append(&filter); + dialog.set_filters(Some(&filters)); + dialog.set_default_filter(Some(&filter)); + + let ui = ui.clone(); + dialog.open(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(); + match store.import_from_file(&path) { + Ok(preset) => { + let msg = format!("Imported preset: {}", preset.name); + let toast = adw::Toast::new(&msg); + ui.toast_overlay.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Failed to import: {}", e)); + ui.toast_overlay.add_toast(toast); + } + } + } + }); +} + +fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { + let dialog = adw::AlertDialog::builder() + .heading("Save as Preset") + .body("Enter a name for this workflow preset") + .build(); + + let entry = gtk::Entry::builder() + .placeholder_text("My Workflow") + .margin_start(24) + .margin_end(24) + .build(); + dialog.set_extra_child(Some(&entry)); + + dialog.add_response("cancel", "Cancel"); + dialog.add_response("save", "Save"); + dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested); + dialog.set_default_response(Some("save")); + + let ui = ui.clone(); + dialog.connect_response(None, move |dlg, response| { + if response == "save" { + let extra = dlg.extra_child(); + let name = extra + .as_ref() + .and_then(|w| w.downcast_ref::()) + .map(|e| e.text().to_string()) + .unwrap_or_default(); + + if name.trim().is_empty() { + let toast = adw::Toast::new("Please enter a name for the preset"); + ui.toast_overlay.add_toast(toast); + return; + } + + let cfg = ui.state.job_config.borrow(); + let preset = build_preset_from_config(&cfg, &name); + drop(cfg); + + let store = pixstrip_core::storage::PresetStore::new(); + match store.save(&preset) { + Ok(()) => { + let toast = adw::Toast::new(&format!("Saved preset: {}", name)); + ui.toast_overlay.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Failed to save: {}", e)); + ui.toast_overlay.add_toast(toast); + } + } + } + }); + + dialog.present(Some(window)); +} + +fn build_preset_from_config(cfg: &JobConfig, name: &str) -> pixstrip_core::preset::Preset { + let resize = if cfg.resize_enabled && cfg.resize_width > 0 { + if cfg.resize_height == 0 { + Some(pixstrip_core::operations::ResizeConfig::ByWidth(cfg.resize_width)) + } else { + Some(pixstrip_core::operations::ResizeConfig::FitInBox { + max: pixstrip_core::types::Dimensions { + width: cfg.resize_width, + height: cfg.resize_height, + }, + allow_upscale: cfg.allow_upscale, + }) + } + } else { + None + }; + + let convert = if cfg.convert_enabled { + cfg.convert_format.map(pixstrip_core::operations::ConvertConfig::SingleFormat) + } else { + None + }; + + let compress = if cfg.compress_enabled { + Some(pixstrip_core::operations::CompressConfig::Preset(cfg.quality_preset)) + } else { + None + }; + + let metadata = if cfg.metadata_enabled { + Some(match cfg.metadata_mode { + MetadataMode::StripAll => pixstrip_core::operations::MetadataConfig::StripAll, + MetadataMode::Privacy => pixstrip_core::operations::MetadataConfig::Privacy, + MetadataMode::KeepAll => pixstrip_core::operations::MetadataConfig::KeepAll, + }) + } else { + None + }; + + pixstrip_core::preset::Preset { + name: name.to_string(), + description: build_preset_description(cfg), + icon: "document-save-symbolic".into(), + is_custom: true, + resize, + rotation: None, + flip: None, + convert, + compress, + metadata, + watermark: None, + rename: None, + } +} + +fn build_preset_description(cfg: &JobConfig) -> String { + let mut parts = Vec::new(); + if cfg.resize_enabled && cfg.resize_width > 0 { + if cfg.resize_height == 0 { + parts.push(format!("Resize {}px wide", cfg.resize_width)); + } else { + parts.push(format!("Resize {}x{}", cfg.resize_width, cfg.resize_height)); + } + } + if cfg.convert_enabled && let Some(fmt) = cfg.convert_format { + parts.push(format!("Convert to {:?}", fmt)); + } + if cfg.compress_enabled { + parts.push(format!("Compress {:?}", cfg.quality_preset)); + } + if cfg.metadata_enabled { + parts.push(format!("Metadata: {:?}", cfg.metadata_mode)); + } + if parts.is_empty() { + "Custom workflow".into() + } else { + parts.join(", ") + } +} + +fn update_output_summary(ui: &WizardUi) { + let cfg = ui.state.job_config.borrow(); + if let Some(page) = ui.pages.get(6) { + // Build summary lines + let mut ops = Vec::new(); + if cfg.resize_enabled && cfg.resize_width > 0 { + if cfg.resize_height == 0 { + ops.push(format!("Resize to {}px wide", cfg.resize_width)); + } else { + ops.push(format!("Resize to {}x{}", cfg.resize_width, cfg.resize_height)); + } + } + if cfg.convert_enabled && let Some(fmt) = cfg.convert_format { + ops.push(format!("Convert to {:?}", fmt)); + } + if cfg.compress_enabled { + ops.push(format!("Compress ({:?})", cfg.quality_preset)); + } + if cfg.metadata_enabled { + let mode = match cfg.metadata_mode { + MetadataMode::StripAll => "Strip all metadata", + MetadataMode::Privacy => "Privacy mode", + MetadataMode::KeepAll => "Keep all metadata", + }; + ops.push(mode.to_string()); + } + + let summary_text = if ops.is_empty() { + "No operations configured".to_string() + } else { + ops.join(" -> ") + }; + + walk_widgets(&page.child(), &|widget| { + if let Some(row) = widget.downcast_ref::() { + let subtitle_str = row.subtitle().unwrap_or_default(); + if row.title().as_str() == "No operations configured" + || subtitle_str.contains("->") + || subtitle_str.contains("configured") + { + if ops.is_empty() { + row.set_title("No operations configured"); + row.set_subtitle("Go back and configure your workflow settings"); + } else { + row.set_title(&format!("{} operations", ops.len())); + row.set_subtitle(&summary_text); + } + } + } + }); + } +} + // --- Utility functions --- enum ProcessingMessage { diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index 6a10661..209952e 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -1,6 +1,7 @@ use adw::prelude::*; +use crate::app::AppState; -pub fn build_images_page() -> adw::NavigationPage { +pub fn build_images_page(state: &AppState) -> adw::NavigationPage { let stack = gtk::Stack::builder() .transition_type(gtk::StackTransitionType::Crossfade) .build(); @@ -9,12 +10,40 @@ pub fn build_images_page() -> adw::NavigationPage { let empty_state = build_empty_state(); stack.add_named(&empty_state, Some("empty")); - // Loaded state - thumbnail grid (placeholder for now) - let loaded_state = build_loaded_state(); + // Loaded state - thumbnail grid + let loaded_state = build_loaded_state(state); stack.add_named(&loaded_state, Some("loaded")); stack.set_visible_child_name("empty"); + // Set up drag-and-drop on the entire page + let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY); + drop_target.set_types(&[gtk::gio::File::static_type()]); + + { + let loaded_files = state.loaded_files.clone(); + let stack_ref = stack.clone(); + drop_target.connect_drop(move |_target, value, _x, _y| { + // Try single file + if let Ok(file) = value.get::() + && let Some(path) = file.path() + && is_image_file(&path) + { + let mut files = loaded_files.borrow_mut(); + if !files.contains(&path) { + files.push(path); + } + let count = files.len(); + drop(files); + update_loaded_ui(&stack_ref, count); + return true; + } + false + }); + } + + stack.add_controller(drop_target); + adw::NavigationPage::builder() .title("Add Images") .tag("step-images") @@ -22,6 +51,38 @@ pub fn build_images_page() -> adw::NavigationPage { .build() } +fn is_image_file(path: &std::path::Path) -> bool { + match path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()) { + Some(ext) => matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "webp" | "avif" | "gif" | "tiff" | "tif" | "bmp"), + None => false, + } +} + +fn update_loaded_ui(stack: >k::Stack, count: usize) { + if count > 0 { + stack.set_visible_child_name("loaded"); + } + if let Some(loaded_box) = stack.child_by_name("loaded") { + update_count_label(&loaded_box, count); + } +} + +fn update_count_label(widget: >k::Widget, count: usize) { + if let Some(label) = widget.downcast_ref::() + && label.css_classes().iter().any(|c| c == "heading") + { + label.set_label(&format!("{} images loaded", count)); + return; + } + if let Some(bx) = widget.downcast_ref::() { + let mut child = bx.first_child(); + while let Some(c) = child { + update_count_label(&c, count); + child = c.next_sibling(); + } + } +} + fn build_empty_state() -> gtk::Box { let container = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -87,7 +148,7 @@ fn build_empty_state() -> gtk::Box { container } -fn build_loaded_state() -> gtk::Box { +fn build_loaded_state(state: &AppState) -> gtk::Box { let container = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(0) @@ -104,7 +165,7 @@ fn build_loaded_state() -> gtk::Box { .build(); let count_label = gtk::Label::builder() - .label("0 images (0 B)") + .label("0 images loaded") .hexpand(true) .halign(gtk::Align::Start) .css_classes(["heading"]) @@ -117,28 +178,54 @@ fn build_loaded_state() -> gtk::Box { .build(); add_button.add_css_class("flat"); - let select_all_button = gtk::Button::builder() - .label("Select All") - .tooltip_text("Select all images (Ctrl+A)") + let clear_button = gtk::Button::builder() + .icon_name("edit-clear-all-symbolic") + .tooltip_text("Remove all images") .build(); - select_all_button.add_css_class("flat"); + clear_button.add_css_class("flat"); + + // Wire clear button + { + let files = state.loaded_files.clone(); + let count_label_c = count_label.clone(); + clear_button.connect_clicked(move |btn| { + files.borrow_mut().clear(); + count_label_c.set_label("0 images loaded"); + // Navigate back to empty state by finding parent stack + if let Some(parent) = btn.ancestor(gtk::Stack::static_type()) + && let Some(stack) = parent.downcast_ref::() + { + stack.set_visible_child_name("empty"); + } + }); + } toolbar.append(&count_label); toolbar.append(&add_button); - toolbar.append(&select_all_button); + toolbar.append(&clear_button); let separator = gtk::Separator::new(gtk::Orientation::Horizontal); - // Thumbnail grid placeholder - let grid_placeholder = adw::StatusPage::builder() - .title("Images will appear here") - .icon_name("image-x-generic-symbolic") + // File list showing loaded images + let list_scrolled = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .build(); + let list_box = gtk::ListBox::builder() + .selection_mode(gtk::SelectionMode::None) + .css_classes(["boxed-list"]) + .margin_start(12) + .margin_end(12) + .margin_top(8) + .margin_bottom(8) + .build(); + + list_scrolled.set_child(Some(&list_box)); + container.append(&toolbar); container.append(&separator); - container.append(&grid_placeholder); + container.append(&list_scrolled); container } diff --git a/pixstrip-gtk/src/steps/step_output.rs b/pixstrip-gtk/src/steps/step_output.rs index 64bfad9..1cd4a37 100644 --- a/pixstrip-gtk/src/steps/step_output.rs +++ b/pixstrip-gtk/src/steps/step_output.rs @@ -1,6 +1,7 @@ use adw::prelude::*; +use crate::app::AppState; -pub fn build_output_page() -> adw::NavigationPage { +pub fn build_output_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) @@ -39,6 +40,7 @@ pub fn build_output_page() -> adw::NavigationPage { .title("Output Location") .subtitle("processed/ (subfolder next to originals)") .activatable(true) + .action_name("win.choose-output") .build(); output_row.add_prefix(>k::Image::from_icon_name("folder-symbolic")); @@ -96,6 +98,22 @@ pub fn build_output_page() -> adw::NavigationPage { stats_group.add(&count_row); content.append(&stats_group); + // Wire preserve directory structure + { + let jc = state.job_config.clone(); + structure_row.connect_active_notify(move |row| { + jc.borrow_mut().preserve_dir_structure = row.is_active(); + }); + } + + // Wire overwrite behavior + { + let jc = state.job_config.clone(); + overwrite_row.connect_selected_notify(move |row| { + jc.borrow_mut().overwrite_behavior = row.selected() as u8; + }); + } + scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() diff --git a/pixstrip-gtk/src/steps/step_workflow.rs b/pixstrip-gtk/src/steps/step_workflow.rs index c203e70..46c3775 100644 --- a/pixstrip-gtk/src/steps/step_workflow.rs +++ b/pixstrip-gtk/src/steps/step_workflow.rs @@ -1,7 +1,9 @@ use adw::prelude::*; use pixstrip_core::preset::Preset; +use pixstrip_core::operations::*; +use crate::app::{AppState, JobConfig, MetadataMode}; -pub fn build_workflow_page() -> adw::NavigationPage { +pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) @@ -31,15 +33,23 @@ pub fn build_workflow_page() -> adw::NavigationPage { .homogeneous(true) .build(); - for preset in Preset::all_builtins() { - let card = build_preset_card(&preset); + 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, advance to the next step - builtin_flow.connect_child_activated(|flow, _child| { - flow.activate_action("win.next-step", None).ok(); - }); + // 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); @@ -63,15 +73,42 @@ pub fn build_workflow_page() -> adw::NavigationPage { custom_group.add(&custom_flow); content.append(&custom_group); - // User presets section (initially empty) + // 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); @@ -91,6 +128,101 @@ pub fn build_workflow_page() -> adw::NavigationPage { .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 { .. }) => { + cfg.metadata_enabled = true; + cfg.metadata_mode = MetadataMode::StripAll; + } + None => { + cfg.metadata_enabled = false; + } + } +} + fn build_preset_card(preset: &Preset) -> gtk::Box { let card = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) diff --git a/pixstrip-gtk/src/wizard.rs b/pixstrip-gtk/src/wizard.rs index 2fc6b2f..d2f38d9 100644 --- a/pixstrip-gtk/src/wizard.rs +++ b/pixstrip-gtk/src/wizard.rs @@ -63,12 +63,12 @@ impl WizardState { pub fn build_wizard_pages(state: &AppState) -> Vec { vec![ - steps::step_workflow::build_workflow_page(), - steps::step_images::build_images_page(), + steps::step_workflow::build_workflow_page(state), + steps::step_images::build_images_page(state), steps::step_resize::build_resize_page(state), steps::step_convert::build_convert_page(state), steps::step_compress::build_compress_page(state), steps::step_metadata::build_metadata_page(state), - steps::step_output::build_output_page(), + steps::step_output::build_output_page(state), ] }