diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index 2f4c027..c07f527 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -10,12 +10,39 @@ use crate::wizard::WizardState; pub const APP_ID: &str = "live.lashman.Pixstrip"; +/// User's choices from the wizard steps, used to build the ProcessingJob +#[derive(Clone, Debug)] +pub struct JobConfig { + pub resize_enabled: bool, + pub resize_width: u32, + pub resize_height: u32, + pub allow_upscale: bool, + pub convert_enabled: bool, + pub convert_format: Option, + pub compress_enabled: bool, + pub quality_preset: pixstrip_core::types::QualityPreset, + pub jpeg_quality: u8, + pub png_level: u8, + pub webp_quality: u8, + pub metadata_enabled: bool, + pub metadata_mode: MetadataMode, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum MetadataMode { + #[default] + StripAll, + Privacy, + KeepAll, +} + /// Shared app state accessible from all UI callbacks #[derive(Clone)] pub struct AppState { pub wizard: Rc>, pub loaded_files: Rc>>, pub output_dir: Rc>>, + pub job_config: Rc>, } #[derive(Clone)] @@ -59,6 +86,21 @@ fn build_ui(app: &adw::Application) { wizard: Rc::new(RefCell::new(WizardState::new())), loaded_files: Rc::new(RefCell::new(Vec::new())), output_dir: Rc::new(RefCell::new(None)), + job_config: Rc::new(RefCell::new(JobConfig { + resize_enabled: true, + resize_width: 1200, + resize_height: 0, + allow_upscale: false, + convert_enabled: false, + convert_format: None, + compress_enabled: true, + quality_preset: pixstrip_core::types::QualityPreset::Medium, + jpeg_quality: 85, + png_level: 3, + webp_quality: 80, + metadata_enabled: true, + metadata_mode: MetadataMode::StripAll, + })), }; // Header bar @@ -84,7 +126,7 @@ fn build_ui(app: &adw::Application) { nav_view.set_vexpand(true); // Build wizard pages - let pages = crate::wizard::build_wizard_pages(); + let pages = crate::wizard::build_wizard_pages(&app_state); for page in &pages { nav_view.add(page); } @@ -573,12 +615,41 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) { .clone() .unwrap_or_else(|| input_dir.join("processed")); - // Build job - for now use default settings (resize off, compress high, strip metadata) + // Build job from wizard settings let mut job = pixstrip_core::pipeline::ProcessingJob::new(&input_dir, &output_dir); - job.compress = Some(pixstrip_core::operations::CompressConfig::Preset( - pixstrip_core::types::QualityPreset::High, - )); - job.metadata = Some(pixstrip_core::operations::MetadataConfig::StripAll); + + let cfg = ui.state.job_config.borrow(); + + if cfg.resize_enabled && cfg.resize_width > 0 { + let target_w = cfg.resize_width; + let target_h = cfg.resize_height; + if target_h == 0 { + job.resize = Some(pixstrip_core::operations::ResizeConfig::ByWidth(target_w)); + } else { + job.resize = Some(pixstrip_core::operations::ResizeConfig::FitInBox { + max: pixstrip_core::types::Dimensions { width: target_w, height: target_h }, + allow_upscale: cfg.allow_upscale, + }); + } + } + + if cfg.convert_enabled && let Some(fmt) = cfg.convert_format { + job.convert = Some(pixstrip_core::operations::ConvertConfig::SingleFormat(fmt)); + } + + if cfg.compress_enabled { + job.compress = Some(pixstrip_core::operations::CompressConfig::Preset(cfg.quality_preset)); + } + + if cfg.metadata_enabled { + job.metadata = 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, + }); + } + + drop(cfg); for file in &files { job.add_source(file); diff --git a/pixstrip-gtk/src/steps/step_compress.rs b/pixstrip-gtk/src/steps/step_compress.rs index 58b3687..f8750ca 100644 --- a/pixstrip-gtk/src/steps/step_compress.rs +++ b/pixstrip-gtk/src/steps/step_compress.rs @@ -1,6 +1,8 @@ use adw::prelude::*; +use crate::app::AppState; +use pixstrip_core::types::QualityPreset; -pub fn build_compress_page() -> adw::NavigationPage { +pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) @@ -15,11 +17,13 @@ pub fn build_compress_page() -> adw::NavigationPage { .margin_end(24) .build(); + let cfg = state.job_config.borrow(); + // Enable toggle let enable_row = adw::SwitchRow::builder() .title("Enable Compression") .subtitle("Reduce file size with quality control") - .active(true) + .active(cfg.compress_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); @@ -32,15 +36,22 @@ pub fn build_compress_page() -> adw::NavigationPage { .description("Higher quality means larger files") .build(); + let initial_val = match cfg.quality_preset { + QualityPreset::WebOptimized => 1.0, + QualityPreset::Low => 2.0, + QualityPreset::Medium => 3.0, + QualityPreset::High => 4.0, + QualityPreset::Maximum => 5.0, + }; + let quality_scale = gtk::Scale::builder() .orientation(gtk::Orientation::Horizontal) - .adjustment(>k::Adjustment::new(3.0, 1.0, 5.0, 1.0, 1.0, 0.0)) + .adjustment(>k::Adjustment::new(initial_val, 1.0, 5.0, 1.0, 1.0, 0.0)) .draw_value(false) .hexpand(true) .build(); quality_scale.set_round_digits(0); - // Add named marks quality_scale.add_mark(1.0, gtk::PositionType::Bottom, Some("Web")); quality_scale.add_mark(2.0, gtk::PositionType::Bottom, Some("Low")); quality_scale.add_mark(3.0, gtk::PositionType::Bottom, Some("Medium")); @@ -48,7 +59,7 @@ pub fn build_compress_page() -> adw::NavigationPage { quality_scale.add_mark(5.0, gtk::PositionType::Bottom, Some("Maximum")); let quality_label = gtk::Label::builder() - .label("Medium - Good balance of quality and size") + .label(quality_description(initial_val as u32)) .css_classes(["dim-label"]) .margin_top(4) .build(); @@ -67,42 +78,28 @@ pub fn build_compress_page() -> adw::NavigationPage { quality_group.add(&quality_box); content.append(&quality_group); - // Size estimation placeholder - let estimate_group = adw::PreferencesGroup::builder() - .title("Size Estimation") - .description("Load images to see real compression results") - .build(); - - let estimate_row = adw::ActionRow::builder() - .title("Estimated savings") - .subtitle("Add images to see file size comparison") - .build(); - estimate_row.add_prefix(>k::Image::from_icon_name("drive-harddisk-symbolic")); - - estimate_group.add(&estimate_row); - content.append(&estimate_group); - // Advanced options let advanced_group = adw::PreferencesGroup::builder() - .title("Advanced Options") + .title("Per-Format Quality") + .description("Fine-tune quality for each format individually") .build(); let jpeg_row = adw::SpinRow::builder() .title("JPEG Quality") .subtitle("1-100, higher is better quality") - .adjustment(>k::Adjustment::new(85.0, 1.0, 100.0, 1.0, 10.0, 0.0)) + .adjustment(>k::Adjustment::new(cfg.jpeg_quality as f64, 1.0, 100.0, 1.0, 10.0, 0.0)) .build(); let png_row = adw::SpinRow::builder() .title("PNG Compression Level") .subtitle("1-6, higher is slower but smaller") - .adjustment(>k::Adjustment::new(3.0, 1.0, 6.0, 1.0, 1.0, 0.0)) + .adjustment(>k::Adjustment::new(cfg.png_level as f64, 1.0, 6.0, 1.0, 1.0, 0.0)) .build(); let webp_row = adw::SpinRow::builder() .title("WebP Quality") .subtitle("1-100, higher is better quality") - .adjustment(>k::Adjustment::new(80.0, 1.0, 100.0, 1.0, 10.0, 0.0)) + .adjustment(>k::Adjustment::new(cfg.webp_quality as f64, 1.0, 100.0, 1.0, 10.0, 0.0)) .build(); advanced_group.add(&jpeg_row); @@ -110,6 +107,50 @@ pub fn build_compress_page() -> adw::NavigationPage { advanced_group.add(&webp_row); content.append(&advanced_group); + drop(cfg); + + // Wire signals + { + let jc = state.job_config.clone(); + enable_row.connect_active_notify(move |row| { + jc.borrow_mut().compress_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + let label = quality_label.clone(); + quality_scale.connect_value_changed(move |scale| { + let val = scale.value().round() as u32; + let mut c = jc.borrow_mut(); + c.quality_preset = match val { + 1 => QualityPreset::WebOptimized, + 2 => QualityPreset::Low, + 3 => QualityPreset::Medium, + 4 => QualityPreset::High, + _ => QualityPreset::Maximum, + }; + label.set_label(&quality_description(val)); + }); + } + { + let jc = state.job_config.clone(); + jpeg_row.connect_value_notify(move |row| { + jc.borrow_mut().jpeg_quality = row.value() as u8; + }); + } + { + let jc = state.job_config.clone(); + png_row.connect_value_notify(move |row| { + jc.borrow_mut().png_level = row.value() as u8; + }); + } + { + let jc = state.job_config.clone(); + webp_row.connect_value_notify(move |row| { + jc.borrow_mut().webp_quality = row.value() as u8; + }); + } + scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() @@ -123,3 +164,13 @@ pub fn build_compress_page() -> adw::NavigationPage { .child(&clamp) .build() } + +fn quality_description(val: u32) -> String { + match val { + 1 => "Web Optimized - smallest files, noticeable quality loss".into(), + 2 => "Low - small files, some quality loss".into(), + 3 => "Medium - good balance of quality and size".into(), + 4 => "High - large files, minimal quality loss".into(), + _ => "Maximum - largest files, best possible quality".into(), + } +} diff --git a/pixstrip-gtk/src/steps/step_convert.rs b/pixstrip-gtk/src/steps/step_convert.rs index 0951c86..f5b8c07 100644 --- a/pixstrip-gtk/src/steps/step_convert.rs +++ b/pixstrip-gtk/src/steps/step_convert.rs @@ -1,6 +1,8 @@ use adw::prelude::*; +use crate::app::AppState; +use pixstrip_core::types::ImageFormat; -pub fn build_convert_page() -> adw::NavigationPage { +pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) @@ -15,11 +17,13 @@ pub fn build_convert_page() -> adw::NavigationPage { .margin_end(24) .build(); + let cfg = state.job_config.borrow(); + // Enable toggle let enable_row = adw::SwitchRow::builder() .title("Enable Format Conversion") .subtitle("Convert images to a different format") - .active(false) + .active(cfg.convert_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); @@ -31,67 +35,52 @@ pub fn build_convert_page() -> adw::NavigationPage { .title("Output Format") .build(); - let formats = [ - ("Keep Original", "No conversion - output in same format as input"), - ("JPEG", "Universal photo format, lossy compression"), - ("PNG", "Lossless format, good for graphics and screenshots"), - ("WebP", "Modern web format, excellent compression, wide support"), - ("AVIF", "Next-gen format, best compression, growing support"), - ]; - - let format_flow = gtk::FlowBox::builder() - .selection_mode(gtk::SelectionMode::Single) - .max_children_per_line(5) - .min_children_per_line(2) - .row_spacing(8) - .column_spacing(8) - .homogeneous(true) + let format_row = adw::ComboRow::builder() + .title("Convert to") + .subtitle("Choose the output format for all images") .build(); + let format_model = gtk::StringList::new(&[ + "Keep Original", + "JPEG - universal, lossy", + "PNG - lossless, graphics", + "WebP - modern, excellent compression", + ]); + format_row.set_model(Some(&format_model)); - for (name, desc) in &formats { - let card = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(4) - .halign(gtk::Align::Center) - .valign(gtk::Align::Center) - .build(); - card.add_css_class("card"); - card.set_size_request(140, 80); + // Set initial selection + format_row.set_selected(match cfg.convert_format { + None => 0, + Some(ImageFormat::Jpeg) => 1, + Some(ImageFormat::Png) => 2, + Some(ImageFormat::WebP) => 3, + _ => 0, + }); - let inner = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(4) - .margin_top(12) - .margin_bottom(12) - .margin_start(8) - .margin_end(8) - .halign(gtk::Align::Center) - .valign(gtk::Align::Center) - .vexpand(true) - .build(); - - let name_label = gtk::Label::builder() - .label(*name) - .css_classes(["heading"]) - .build(); - - let desc_label = gtk::Label::builder() - .label(*desc) - .css_classes(["caption", "dim-label"]) - .wrap(true) - .justify(gtk::Justification::Center) - .max_width_chars(18) - .build(); - - inner.append(&name_label); - inner.append(&desc_label); - card.append(&inner); - format_flow.append(&card); - } - - format_group.add(&format_flow); + format_group.add(&format_row); content.append(&format_group); + drop(cfg); + + // Wire signals + { + let jc = state.job_config.clone(); + enable_row.connect_active_notify(move |row| { + jc.borrow_mut().convert_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + format_row.connect_selected_notify(move |row| { + let mut c = jc.borrow_mut(); + c.convert_format = match row.selected() { + 1 => Some(ImageFormat::Jpeg), + 2 => Some(ImageFormat::Png), + 3 => Some(ImageFormat::WebP), + _ => None, + }; + }); + } + scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() diff --git a/pixstrip-gtk/src/steps/step_metadata.rs b/pixstrip-gtk/src/steps/step_metadata.rs index d5ed641..e2abaef 100644 --- a/pixstrip-gtk/src/steps/step_metadata.rs +++ b/pixstrip-gtk/src/steps/step_metadata.rs @@ -1,6 +1,7 @@ use adw::prelude::*; +use crate::app::{AppState, MetadataMode}; -pub fn build_metadata_page() -> adw::NavigationPage { +pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) @@ -15,11 +16,13 @@ pub fn build_metadata_page() -> adw::NavigationPage { .margin_end(24) .build(); + let cfg = state.job_config.borrow(); + // Enable toggle let enable_row = adw::SwitchRow::builder() .title("Enable Metadata Handling") .subtitle("Control what image metadata to keep or remove") - .active(true) + .active(cfg.metadata_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); @@ -38,7 +41,7 @@ pub fn build_metadata_page() -> adw::NavigationPage { .build(); strip_all_row.add_prefix(>k::Image::from_icon_name("user-trash-symbolic")); let strip_all_check = gtk::CheckButton::new(); - strip_all_check.set_active(true); + strip_all_check.set_active(cfg.metadata_mode == MetadataMode::StripAll); strip_all_row.add_suffix(&strip_all_check); strip_all_row.set_activatable_widget(Some(&strip_all_check)); @@ -50,6 +53,7 @@ pub fn build_metadata_page() -> adw::NavigationPage { privacy_row.add_prefix(>k::Image::from_icon_name("security-medium-symbolic")); let privacy_check = gtk::CheckButton::new(); privacy_check.set_group(Some(&strip_all_check)); + privacy_check.set_active(cfg.metadata_mode == MetadataMode::Privacy); privacy_row.add_suffix(&privacy_check); privacy_row.set_activatable_widget(Some(&privacy_check)); @@ -61,6 +65,7 @@ pub fn build_metadata_page() -> adw::NavigationPage { keep_all_row.add_prefix(>k::Image::from_icon_name("emblem-ok-symbolic")); let keep_all_check = gtk::CheckButton::new(); keep_all_check.set_group(Some(&strip_all_check)); + keep_all_check.set_active(cfg.metadata_mode == MetadataMode::KeepAll); keep_all_row.add_suffix(&keep_all_check); keep_all_row.set_activatable_widget(Some(&keep_all_check)); @@ -69,30 +74,39 @@ pub fn build_metadata_page() -> adw::NavigationPage { presets_group.add(&keep_all_row); content.append(&presets_group); - // Advanced - per-category controls - let advanced_group = adw::PreferencesGroup::builder() - .title("Fine-Grained Control") - .description("Choose exactly which metadata categories to strip") - .build(); + drop(cfg); - let categories = [ - ("GPS / Location Data", "Coordinates, altitude, location name", true), - ("Camera Info", "Camera model, serial number, lens info", true), - ("Software / Editing", "Software used, editing history", true), - ("Timestamps", "Date taken, date modified", false), - ("Copyright / Author", "Copyright notice, creator name", false), - ]; - - for (title, subtitle, default_strip) in &categories { - let row = adw::SwitchRow::builder() - .title(format!("Strip {}", title)) - .subtitle(*subtitle) - .active(*default_strip) - .build(); - advanced_group.add(&row); + // Wire signals + { + let jc = state.job_config.clone(); + enable_row.connect_active_notify(move |row| { + jc.borrow_mut().metadata_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + strip_all_check.connect_toggled(move |check| { + if check.is_active() { + jc.borrow_mut().metadata_mode = MetadataMode::StripAll; + } + }); + } + { + let jc = state.job_config.clone(); + privacy_check.connect_toggled(move |check| { + if check.is_active() { + jc.borrow_mut().metadata_mode = MetadataMode::Privacy; + } + }); + } + { + let jc = state.job_config.clone(); + keep_all_check.connect_toggled(move |check| { + if check.is_active() { + jc.borrow_mut().metadata_mode = MetadataMode::KeepAll; + } + }); } - - content.append(&advanced_group); scrolled.set_child(Some(&content)); diff --git a/pixstrip-gtk/src/steps/step_resize.rs b/pixstrip-gtk/src/steps/step_resize.rs index d2afece..ebd4ca7 100644 --- a/pixstrip-gtk/src/steps/step_resize.rs +++ b/pixstrip-gtk/src/steps/step_resize.rs @@ -1,6 +1,7 @@ use adw::prelude::*; +use crate::app::AppState; -pub fn build_resize_page() -> adw::NavigationPage { +pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) @@ -15,33 +16,34 @@ pub fn build_resize_page() -> adw::NavigationPage { .margin_end(24) .build(); + let cfg = state.job_config.borrow(); + // Enable toggle let enable_row = adw::SwitchRow::builder() .title("Enable Resize") .subtitle("Resize images to new dimensions") - .active(true) + .active(cfg.resize_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); enable_group.add(&enable_row); content.append(&enable_group); - // Resize mode selector + // Resize mode let mode_group = adw::PreferencesGroup::builder() .title("Resize Mode") .build(); - // Width/Height mode let width_row = adw::SpinRow::builder() .title("Width") .subtitle("Target width in pixels") - .adjustment(>k::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0)) + .adjustment(>k::Adjustment::new(cfg.resize_width as f64, 1.0, 10000.0, 1.0, 100.0, 0.0)) .build(); let height_row = adw::SpinRow::builder() .title("Height") .subtitle("Target height in pixels (0 = auto from aspect ratio)") - .adjustment(>k::Adjustment::new(0.0, 0.0, 10000.0, 1.0, 100.0, 0.0)) + .adjustment(>k::Adjustment::new(cfg.resize_height as f64, 0.0, 10000.0, 1.0, 100.0, 0.0)) .build(); mode_group.add(&width_row); @@ -58,24 +60,32 @@ pub fn build_resize_page() -> adw::NavigationPage { .subtitle("Mastodon, Pixelfed, Bluesky, Lemmy") .build(); - let fedi_presets = [ - ("Mastodon Post", "1920 x 1080"), - ("Mastodon Profile", "400 x 400"), - ("Mastodon Header", "1500 x 500"), - ("Pixelfed Post", "1080 x 1080"), - ("Pixelfed Story", "1080 x 1920"), - ("Bluesky Post", "1200 x 630"), - ("Bluesky Profile", "400 x 400"), - ("Lemmy Post", "1200 x 630"), + let fedi_presets: Vec<(&str, u32, u32)> = vec![ + ("Mastodon Post", 1920, 1080), + ("Mastodon Profile", 400, 400), + ("Mastodon Header", 1500, 500), + ("Pixelfed Post", 1080, 1080), + ("Pixelfed Story", 1080, 1920), + ("Bluesky Post", 1200, 630), + ("Bluesky Profile", 400, 400), + ("Lemmy Post", 1200, 630), ]; - for (name, dims) in &fedi_presets { + for (name, w, h) in &fedi_presets { let row = adw::ActionRow::builder() .title(*name) - .subtitle(*dims) + .subtitle(format!("{} x {}", w, h)) .activatable(true) .build(); row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let width_row_c = width_row.clone(); + let height_row_c = height_row.clone(); + let w = *w; + let h = *h; + row.connect_activated(move |_| { + width_row_c.set_value(w as f64); + height_row_c.set_value(h as f64); + }); fedi_expander.add_row(&row); } @@ -84,22 +94,30 @@ pub fn build_resize_page() -> adw::NavigationPage { .subtitle("Instagram, YouTube, LinkedIn, Pinterest") .build(); - let mainstream_presets = [ - ("Instagram Post", "1080 x 1080"), - ("Instagram Story/Reel", "1080 x 1920"), - ("Facebook Post", "1200 x 630"), - ("YouTube Thumbnail", "1280 x 720"), - ("LinkedIn Post", "1200 x 627"), - ("Pinterest Pin", "1000 x 1500"), + let mainstream_presets: Vec<(&str, u32, u32)> = vec![ + ("Instagram Post", 1080, 1080), + ("Instagram Story/Reel", 1080, 1920), + ("Facebook Post", 1200, 630), + ("YouTube Thumbnail", 1280, 720), + ("LinkedIn Post", 1200, 627), + ("Pinterest Pin", 1000, 1500), ]; - for (name, dims) in &mainstream_presets { + for (name, w, h) in &mainstream_presets { let row = adw::ActionRow::builder() .title(*name) - .subtitle(*dims) + .subtitle(format!("{} x {}", w, h)) .activatable(true) .build(); row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let width_row_c = width_row.clone(); + let height_row_c = height_row.clone(); + let w = *w; + let h = *h; + row.connect_activated(move |_| { + width_row_c.set_value(w as f64); + height_row_c.set_value(h as f64); + }); mainstream_expander.add_row(&row); } @@ -108,19 +126,27 @@ pub fn build_resize_page() -> adw::NavigationPage { .subtitle("HD, Blog, Thumbnail") .build(); - let other_presets = [ - ("Full HD", "1920 x 1080"), - ("Blog Image", "800 wide"), - ("Thumbnail", "150 x 150"), + let other_presets: Vec<(&str, u32, u32)> = vec![ + ("Full HD", 1920, 1080), + ("Blog Image", 800, 0), + ("Thumbnail", 150, 150), ]; - for (name, dims) in &other_presets { + for (name, w, h) in &other_presets { let row = adw::ActionRow::builder() .title(*name) - .subtitle(*dims) + .subtitle(if *h == 0 { format!("{} wide", w) } else { format!("{} x {}", w, h) }) .activatable(true) .build(); row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let width_row_c = width_row.clone(); + let height_row_c = height_row.clone(); + let w = *w; + let h = *h; + row.connect_activated(move |_| { + width_row_c.set_value(w as f64); + height_row_c.set_value(h as f64); + }); other_expander.add_row(&row); } @@ -129,7 +155,7 @@ pub fn build_resize_page() -> adw::NavigationPage { presets_group.add(&other_expander); content.append(&presets_group); - // Basic adjustments (rotation/flip) + // Basic adjustments let adjust_group = adw::PreferencesGroup::builder() .title("Basic Adjustments") .build(); @@ -152,28 +178,48 @@ pub fn build_resize_page() -> adw::NavigationPage { adjust_group.add(&flip_row); content.append(&adjust_group); - // Advanced options + // Advanced let advanced_group = adw::PreferencesGroup::builder() .title("Advanced Options") .build(); - let algo_row = adw::ComboRow::builder() - .title("Resize Algorithm") - .subtitle("Higher quality is slower") - .build(); - let algo_model = gtk::StringList::new(&["Lanczos3 (Best)", "CatmullRom", "Bilinear", "Nearest"]); - algo_row.set_model(Some(&algo_model)); - let upscale_row = adw::SwitchRow::builder() .title("Allow Upscaling") .subtitle("Enlarge images smaller than target size") - .active(false) + .active(cfg.allow_upscale) .build(); - advanced_group.add(&algo_row); advanced_group.add(&upscale_row); content.append(&advanced_group); + drop(cfg); + + // Wire signals to update JobConfig + { + let jc = state.job_config.clone(); + enable_row.connect_active_notify(move |row| { + jc.borrow_mut().resize_enabled = row.is_active(); + }); + } + { + let jc = state.job_config.clone(); + width_row.connect_value_notify(move |row| { + jc.borrow_mut().resize_width = row.value() as u32; + }); + } + { + let jc = state.job_config.clone(); + height_row.connect_value_notify(move |row| { + jc.borrow_mut().resize_height = row.value() as u32; + }); + } + { + let jc = state.job_config.clone(); + upscale_row.connect_active_notify(move |row| { + jc.borrow_mut().allow_upscale = row.is_active(); + }); + } + scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() diff --git a/pixstrip-gtk/src/wizard.rs b/pixstrip-gtk/src/wizard.rs index e9c20b3..2fc6b2f 100644 --- a/pixstrip-gtk/src/wizard.rs +++ b/pixstrip-gtk/src/wizard.rs @@ -1,3 +1,4 @@ +use crate::app::AppState; use crate::steps; pub struct WizardState { @@ -60,14 +61,14 @@ impl WizardState { } } -pub fn build_wizard_pages() -> Vec { +pub fn build_wizard_pages(state: &AppState) -> Vec { vec![ steps::step_workflow::build_workflow_page(), steps::step_images::build_images_page(), - steps::step_resize::build_resize_page(), - steps::step_convert::build_convert_page(), - steps::step_compress::build_compress_page(), - steps::step_metadata::build_metadata_page(), + 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(), ] }