diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index b80079b..708107a 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -21,6 +21,15 @@ pub struct JobConfig { // Adjustments pub rotation: u32, pub flip: u32, + pub brightness: i32, + pub contrast: i32, + pub saturation: i32, + pub sharpen: bool, + pub grayscale: bool, + pub sepia: bool, + pub crop_aspect_ratio: u32, + pub trim_whitespace: bool, + pub canvas_padding: u32, // Convert pub convert_enabled: bool, pub convert_format: Option, @@ -142,6 +151,15 @@ fn build_ui(app: &adw::Application) { allow_upscale: false, rotation: 0, flip: 0, + brightness: 0, + contrast: 0, + saturation: 0, + sharpen: false, + grayscale: false, + sepia: false, + crop_aspect_ratio: 0, + trim_whitespace: false, + canvas_padding: 0, convert_enabled: if remember { sess_state.convert_enabled.unwrap_or(false) } else { false }, convert_format: None, compress_enabled: if remember { sess_state.compress_enabled.unwrap_or(true) } else { true }, @@ -1705,6 +1723,43 @@ fn update_output_summary(ui: &WizardUi) { }; ops.push(fl.to_string()); } + if cfg.brightness != 0 { + ops.push(format!("Brightness {:+}", cfg.brightness)); + } + if cfg.contrast != 0 { + ops.push(format!("Contrast {:+}", cfg.contrast)); + } + if cfg.saturation != 0 { + ops.push(format!("Saturation {:+}", cfg.saturation)); + } + if cfg.sharpen { + ops.push("Sharpen".to_string()); + } + if cfg.grayscale { + ops.push("Grayscale".to_string()); + } + if cfg.sepia { + ops.push("Sepia".to_string()); + } + if cfg.crop_aspect_ratio > 0 { + let ratio = match cfg.crop_aspect_ratio { + 1 => "1:1", + 2 => "4:3", + 3 => "3:2", + 4 => "16:9", + 5 => "9:16", + 6 => "3:4", + 7 => "2:3", + _ => "Custom", + }; + ops.push(format!("Crop {}", ratio)); + } + if cfg.trim_whitespace { + ops.push("Trim whitespace".to_string()); + } + if cfg.canvas_padding > 0 { + ops.push(format!("Padding {}px", cfg.canvas_padding)); + } let summary_text = if ops.is_empty() { "No operations configured".to_string() diff --git a/pixstrip-gtk/src/steps/step_adjustments.rs b/pixstrip-gtk/src/steps/step_adjustments.rs index 3ef591b..aa687ef 100644 --- a/pixstrip-gtk/src/steps/step_adjustments.rs +++ b/pixstrip-gtk/src/steps/step_adjustments.rs @@ -50,12 +50,51 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { rotate_group.add(&flip_row); content.append(&rotate_group); - // Advanced adjustments in an expander - let advanced_group = adw::PreferencesGroup::builder() + // Crop and canvas group + let crop_group = adw::PreferencesGroup::builder() + .title("Crop and Canvas") + .build(); + + let crop_row = adw::ComboRow::builder() + .title("Crop to Aspect Ratio") + .subtitle("Crop images to a specific aspect ratio from center") + .build(); + let crop_model = gtk::StringList::new(&[ + "None", + "1:1 (Square)", + "4:3", + "3:2", + "16:9", + "9:16 (Portrait)", + "3:4 (Portrait)", + "2:3 (Portrait)", + ]); + crop_row.set_model(Some(&crop_model)); + crop_row.set_selected(cfg.crop_aspect_ratio); + + let trim_row = adw::SwitchRow::builder() + .title("Trim Whitespace") + .subtitle("Remove uniform borders around the image") + .active(cfg.trim_whitespace) + .build(); + + let padding_row = adw::SpinRow::builder() + .title("Canvas Padding") + .subtitle("Add uniform padding around the image (pixels)") + .adjustment(>k::Adjustment::new(cfg.canvas_padding as f64, 0.0, 500.0, 1.0, 10.0, 0.0)) + .build(); + + crop_group.add(&crop_row); + crop_group.add(&trim_row); + crop_group.add(&padding_row); + content.append(&crop_group); + + // Image adjustments + let adjust_group = adw::PreferencesGroup::builder() .title("Image Adjustments") .build(); - let advanced_expander = adw::ExpanderRow::builder() + let adjust_expander = adw::ExpanderRow::builder() .title("Advanced Adjustments") .subtitle("Brightness, contrast, saturation, effects") .show_enable_switch(false) @@ -64,83 +103,71 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { // Brightness slider (-100 to +100) let brightness_row = adw::ActionRow::builder() .title("Brightness") - .subtitle("0") + .subtitle(format!("{}", cfg.brightness)) .build(); let brightness_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0); - brightness_scale.set_value(0.0); + brightness_scale.set_value(cfg.brightness as f64); brightness_scale.set_hexpand(true); brightness_scale.set_valign(gtk::Align::Center); brightness_scale.set_size_request(200, -1); brightness_scale.set_draw_value(false); - let br_label = brightness_row.clone(); - brightness_scale.connect_value_changed(move |scale| { - br_label.set_subtitle(&format!("{:.0}", scale.value())); - }); brightness_row.add_suffix(&brightness_scale); - advanced_expander.add_row(&brightness_row); + adjust_expander.add_row(&brightness_row); // Contrast slider (-100 to +100) let contrast_row = adw::ActionRow::builder() .title("Contrast") - .subtitle("0") + .subtitle(format!("{}", cfg.contrast)) .build(); let contrast_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0); - contrast_scale.set_value(0.0); + contrast_scale.set_value(cfg.contrast as f64); contrast_scale.set_hexpand(true); contrast_scale.set_valign(gtk::Align::Center); contrast_scale.set_size_request(200, -1); contrast_scale.set_draw_value(false); - let ct_label = contrast_row.clone(); - contrast_scale.connect_value_changed(move |scale| { - ct_label.set_subtitle(&format!("{:.0}", scale.value())); - }); contrast_row.add_suffix(&contrast_scale); - advanced_expander.add_row(&contrast_row); + adjust_expander.add_row(&contrast_row); // Saturation slider (-100 to +100) let saturation_row = adw::ActionRow::builder() .title("Saturation") - .subtitle("0") + .subtitle(format!("{}", cfg.saturation)) .build(); let saturation_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, -100.0, 100.0, 1.0); - saturation_scale.set_value(0.0); + saturation_scale.set_value(cfg.saturation as f64); saturation_scale.set_hexpand(true); saturation_scale.set_valign(gtk::Align::Center); saturation_scale.set_size_request(200, -1); saturation_scale.set_draw_value(false); - let sat_label = saturation_row.clone(); - saturation_scale.connect_value_changed(move |scale| { - sat_label.set_subtitle(&format!("{:.0}", scale.value())); - }); saturation_row.add_suffix(&saturation_scale); - advanced_expander.add_row(&saturation_row); + adjust_expander.add_row(&saturation_row); // Sharpen after resize let sharpen_row = adw::SwitchRow::builder() .title("Sharpen after resize") .subtitle("Apply subtle sharpening to resized images") - .active(false) + .active(cfg.sharpen) .build(); - advanced_expander.add_row(&sharpen_row); + adjust_expander.add_row(&sharpen_row); // Grayscale let grayscale_row = adw::SwitchRow::builder() .title("Grayscale") .subtitle("Convert images to black and white") - .active(false) + .active(cfg.grayscale) .build(); - advanced_expander.add_row(&grayscale_row); + adjust_expander.add_row(&grayscale_row); // Sepia let sepia_row = adw::SwitchRow::builder() .title("Sepia") .subtitle("Apply a warm vintage tone") - .active(false) + .active(cfg.sepia) .build(); - advanced_expander.add_row(&sepia_row); + adjust_expander.add_row(&sepia_row); - advanced_group.add(&advanced_expander); - content.append(&advanced_group); + adjust_group.add(&adjust_expander); + content.append(&adjust_group); drop(cfg); @@ -157,6 +184,69 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { jc.borrow_mut().flip = row.selected(); }); } + { + let jc = state.job_config.clone(); + crop_row.connect_selected_notify(move |row| { + jc.borrow_mut().crop_aspect_ratio = row.selected(); + }); + } + { + let jc = state.job_config.clone(); + trim_row.connect_active_notify(move |row| { + jc.borrow_mut().trim_whitespace = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + padding_row.connect_value_notify(move |row| { + jc.borrow_mut().canvas_padding = row.value() as u32; + }); + } + { + let jc = state.job_config.clone(); + let label = brightness_row; + brightness_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as i32; + jc.borrow_mut().brightness = val; + label.set_subtitle(&format!("{}", val)); + }); + } + { + let jc = state.job_config.clone(); + let label = contrast_row; + contrast_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as i32; + jc.borrow_mut().contrast = val; + label.set_subtitle(&format!("{}", val)); + }); + } + { + let jc = state.job_config.clone(); + let label = saturation_row; + saturation_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as i32; + jc.borrow_mut().saturation = val; + label.set_subtitle(&format!("{}", val)); + }); + } + { + let jc = state.job_config.clone(); + sharpen_row.connect_active_notify(move |row| { + jc.borrow_mut().sharpen = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + grayscale_row.connect_active_notify(move |row| { + jc.borrow_mut().grayscale = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + sepia_row.connect_active_notify(move |row| { + jc.borrow_mut().sepia = row.is_active(); + }); + } scrolled.set_child(Some(&content)); diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index 3ce54f0..36b06df 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -304,6 +304,18 @@ fn build_loaded_state(state: &AppState) -> gtk::Box { .build(); add_button.add_css_class("flat"); + let select_all_button = gtk::Button::builder() + .icon_name("edit-select-all-symbolic") + .tooltip_text("Select all images (Ctrl+A)") + .build(); + select_all_button.add_css_class("flat"); + + let deselect_all_button = gtk::Button::builder() + .icon_name("edit-clear-symbolic") + .tooltip_text("Deselect all images (Ctrl+Shift+A)") + .build(); + deselect_all_button.add_css_class("flat"); + let clear_button = gtk::Button::builder() .icon_name("edit-clear-all-symbolic") .tooltip_text("Remove all images") @@ -326,6 +338,8 @@ fn build_loaded_state(state: &AppState) -> gtk::Box { } toolbar.append(&count_label); + toolbar.append(&select_all_button); + toolbar.append(&deselect_all_button); toolbar.append(&add_button); toolbar.append(&clear_button);