diff --git a/pixstrip-core/src/storage.rs b/pixstrip-core/src/storage.rs index 8fa50a5..224ba73 100644 --- a/pixstrip-core/src/storage.rs +++ b/pixstrip-core/src/storage.rs @@ -167,6 +167,7 @@ impl Default for ConfigStore { // --- Session Store --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] pub struct SessionState { pub last_input_dir: Option, pub last_output_dir: Option, @@ -175,6 +176,18 @@ pub struct SessionState { pub window_width: Option, pub window_height: Option, pub window_maximized: bool, + // Last-used wizard settings + pub resize_enabled: Option, + pub resize_width: Option, + pub resize_height: Option, + pub convert_enabled: Option, + pub convert_format: Option, + pub compress_enabled: Option, + pub quality_preset: Option, + pub metadata_enabled: Option, + pub metadata_mode: Option, + pub watermark_enabled: Option, + pub rename_enabled: Option, } pub struct SessionStore { diff --git a/pixstrip-core/tests/storage_tests.rs b/pixstrip-core/tests/storage_tests.rs index 6a331d7..6a6d3b7 100644 --- a/pixstrip-core/tests/storage_tests.rs +++ b/pixstrip-core/tests/storage_tests.rs @@ -163,6 +163,7 @@ fn save_and_load_session() { window_width: Some(1024), window_height: Some(768), window_maximized: false, + ..Default::default() }; session_store.save(&session).unwrap(); diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index fcdbb4c..e5ba75e 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -124,39 +124,46 @@ fn setup_shortcuts(app: &adw::Application) { } fn build_ui(app: &adw::Application) { + // Restore last-used wizard settings from session + let sess = pixstrip_core::storage::SessionStore::new(); + let sess_state = sess.load().unwrap_or_default(); + let cfg_store = pixstrip_core::storage::ConfigStore::new(); + let app_cfg = cfg_store.load().unwrap_or_default(); + let remember = app_cfg.remember_settings; + let app_state = AppState { 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, + resize_enabled: if remember { sess_state.resize_enabled.unwrap_or(true) } else { true }, + resize_width: if remember { sess_state.resize_width.unwrap_or(1200) } else { 1200 }, + resize_height: if remember { sess_state.resize_height.unwrap_or(0) } else { 0 }, allow_upscale: false, rotation: 0, flip: 0, - convert_enabled: false, + convert_enabled: if remember { sess_state.convert_enabled.unwrap_or(false) } else { false }, convert_format: None, - compress_enabled: true, + compress_enabled: if remember { sess_state.compress_enabled.unwrap_or(true) } else { true }, quality_preset: pixstrip_core::types::QualityPreset::Medium, jpeg_quality: 85, png_level: 3, webp_quality: 80, - metadata_enabled: true, + metadata_enabled: if remember { sess_state.metadata_enabled.unwrap_or(true) } else { true }, metadata_mode: MetadataMode::StripAll, strip_gps: true, strip_camera: true, strip_software: true, strip_timestamps: true, strip_copyright: true, - watermark_enabled: false, + watermark_enabled: if remember { sess_state.watermark_enabled.unwrap_or(false) } else { false }, watermark_text: String::new(), watermark_image_path: None, watermark_position: 8, // BottomRight watermark_opacity: 0.5, watermark_font_size: 24.0, watermark_use_image: false, - rename_enabled: false, + rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false }, rename_prefix: String::new(), rename_suffix: String::new(), rename_counter_start: 1, @@ -254,21 +261,43 @@ fn build_ui(app: &adw::Application) { window.maximize(); } - // Save window size on close - window.connect_close_request(|win| { - let session = pixstrip_core::storage::SessionStore::new(); - let mut state = session.load().unwrap_or_default(); - state.window_maximized = win.is_maximized(); - if !win.is_maximized() { - let (w, h) = (win.default_size().0, win.default_size().1); - if w > 0 && h > 0 { - state.window_width = Some(w); - state.window_height = Some(h); + // Save window size and wizard settings on close + { + let app_state_for_close = app_state.clone(); + window.connect_close_request(move |win| { + let session = pixstrip_core::storage::SessionStore::new(); + let mut state = session.load().unwrap_or_default(); + state.window_maximized = win.is_maximized(); + if !win.is_maximized() { + let (w, h) = (win.default_size().0, win.default_size().1); + if w > 0 && h > 0 { + state.window_width = Some(w); + state.window_height = Some(h); + } } - } - let _ = session.save(&state); - glib::Propagation::Proceed - }); + + // Save last-used wizard settings if remember_settings is enabled + let config_store = pixstrip_core::storage::ConfigStore::new(); + let config = config_store.load().unwrap_or_default(); + if config.remember_settings { + let cfg = app_state_for_close.job_config.borrow(); + state.resize_enabled = Some(cfg.resize_enabled); + state.resize_width = Some(cfg.resize_width); + state.resize_height = Some(cfg.resize_height); + state.convert_enabled = Some(cfg.convert_enabled); + state.convert_format = cfg.convert_format.map(|f| format!("{:?}", f)); + state.compress_enabled = Some(cfg.compress_enabled); + state.quality_preset = Some(format!("{:?}", cfg.quality_preset)); + state.metadata_enabled = Some(cfg.metadata_enabled); + state.metadata_mode = Some(format!("{:?}", cfg.metadata_mode)); + state.watermark_enabled = Some(cfg.watermark_enabled); + state.rename_enabled = Some(cfg.rename_enabled); + } + + let _ = session.save(&state); + glib::Propagation::Proceed + }); + } let ui = WizardUi { nav_view, diff --git a/pixstrip-gtk/src/steps/step_resize.rs b/pixstrip-gtk/src/steps/step_resize.rs index 5d0fdca..8d1d62d 100644 --- a/pixstrip-gtk/src/steps/step_resize.rs +++ b/pixstrip-gtk/src/steps/step_resize.rs @@ -29,16 +29,35 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { enable_group.add(&enable_row); content.append(&enable_group); - // Resize dimensions - let mode_group = adw::PreferencesGroup::builder() - .title("Dimensions") - .description("Set target width and height. Set height to 0 to preserve aspect ratio.") + // Mode selector using GtkStack with a stack switcher + let mode_stack = gtk::Stack::builder() + .transition_type(gtk::StackTransitionType::Crossfade) + .build(); + + let switcher = gtk::StackSwitcher::builder() + .stack(&mode_stack) + .halign(gtk::Align::Center) + .margin_top(6) + .margin_bottom(6) + .build(); + + content.append(&switcher); + + // --- Mode 1: Width/Height --- + let wh_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(12) + .build(); + + let wh_group = adw::PreferencesGroup::builder() + .title("Target Dimensions") + .description("Set width and/or height. Set either to 0 to maintain aspect ratio.") .build(); let width_row = adw::SpinRow::builder() .title("Width") .subtitle("Target width in pixels") - .adjustment(>k::Adjustment::new(cfg.resize_width as f64, 1.0, 10000.0, 1.0, 100.0, 0.0)) + .adjustment(>k::Adjustment::new(cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0)) .build(); let height_row = adw::SpinRow::builder() @@ -47,17 +66,27 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .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); - mode_group.add(&height_row); - content.append(&mode_group); + wh_group.add(&width_row); + wh_group.add(&height_row); + wh_box.append(&wh_group); - // Social media presets - let presets_group = adw::PreferencesGroup::builder() - .title("Quick Dimension Presets") - .description("Click a preset to fill in the dimensions above") + mode_stack.add_titled(&wh_box, Some("width-height"), "Width / Height"); + + // --- Mode 2: Preset Dimensions --- + let preset_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(12) .build(); - // Helper to build preset expander sections + let presets_group = adw::PreferencesGroup::builder() + .title("Quick Dimension Presets") + .description("Select a preset to set the dimensions") + .build(); + + // Clone for closures + let width_for_preset = width_row.clone(); + let height_for_preset = height_row.clone(); + let build_preset_section = |title: &str, subtitle: &str, presets: &[(&str, u32, u32)]| -> adw::ExpanderRow { let expander = adw::ExpanderRow::builder() .title(title) @@ -71,13 +100,16 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .activatable(true) .build(); row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); - let width_c = width_row.clone(); - let height_c = height_row.clone(); + let width_c = width_for_preset.clone(); + let height_c = height_for_preset.clone(); let w = *w; let h = *h; + let stack_c = mode_stack.clone(); row.connect_activated(move |_| { width_c.set_value(w as f64); height_c.set_value(h as f64); + // Switch to width/height tab to show the values + stack_c.set_visible_child_name("width-height"); }); expander.add_row(&row); } @@ -96,10 +128,11 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { ("Pixelfed Story", 1080, 1920), ("Bluesky Post", 1200, 630), ("Bluesky Profile", 400, 400), + ("Bluesky Banner", 1500, 500), ("Lemmy Post", 1200, 630), ("PeerTube Thumbnail", 1280, 720), ("Friendica Post", 1200, 630), - ("Funkwhale Cover", 500, 500), + ("Funkwhale Cover", 1400, 1400), ], ); @@ -107,8 +140,8 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { "Mainstream Platforms", "Instagram, YouTube, LinkedIn, Facebook, TikTok", &[ - ("Instagram Post", 1080, 1080), - ("Instagram Portrait", 1080, 1350), + ("Instagram Post Square", 1080, 1080), + ("Instagram Post Portrait", 1080, 1350), ("Instagram Story/Reel", 1080, 1920), ("Facebook Post", 1200, 630), ("Facebook Cover", 820, 312), @@ -119,10 +152,8 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { ("LinkedIn Cover", 1584, 396), ("LinkedIn Profile", 400, 400), ("Pinterest Pin", 1000, 1500), - ("TikTok Profile", 200, 200), + ("TikTok Video Cover", 1080, 1920), ("Threads Post", 1080, 1080), - ("Twitter/X Post", 1200, 675), - ("Twitter/X Header", 1500, 500), ], ); @@ -133,8 +164,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { ("4K UHD", 3840, 2160), ("Full HD", 1920, 1080), ("HD Ready", 1280, 720), - ("Blog Wide", 800, 0), - ("Blog Standard", 800, 600), + ("Blog Standard", 800, 0), ("Email Header", 600, 200), ("Large Thumbnail", 300, 300), ("Small Thumbnail", 150, 150), @@ -145,11 +175,64 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { presets_group.add(&fedi_expander); presets_group.add(&mainstream_expander); presets_group.add(&common_expander); - content.append(&presets_group); + preset_box.append(&presets_group); - // Advanced options + mode_stack.add_titled(&preset_box, Some("presets"), "Presets"); + + // --- Mode 3: Fit in Box --- + let fit_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(12) + .build(); + + let fit_group = adw::PreferencesGroup::builder() + .title("Fit in Bounding Box") + .description("Images are scaled down to fit within these maximum dimensions while maintaining their aspect ratio. Images smaller than the box are not enlarged.") + .build(); + + let max_width_row = adw::SpinRow::builder() + .title("Maximum Width") + .subtitle("Images wider than this are scaled down") + .adjustment(>k::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0)) + .build(); + + let max_height_row = adw::SpinRow::builder() + .title("Maximum Height") + .subtitle("Images taller than this are scaled down") + .adjustment(>k::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0)) + .build(); + + fit_group.add(&max_width_row); + fit_group.add(&max_height_row); + fit_box.append(&fit_group); + + // Wire fit-in-box to update width/height + { + let width_c = width_row.clone(); + let height_c = height_row.clone(); + max_width_row.connect_value_notify(move |row| { + width_c.set_value(row.value()); + }); + let height_c2 = height_row.clone(); + max_height_row.connect_value_notify(move |row| { + height_c2.set_value(row.value()); + }); + let _ = height_c; // suppress unused + } + + mode_stack.add_titled(&fit_box, Some("fit-box"), "Fit in Box"); + + content.append(&mode_stack); + + // Advanced options (AdwExpanderRow per design doc) let advanced_group = adw::PreferencesGroup::builder() + .title("Advanced") + .build(); + + let advanced_expander = adw::ExpanderRow::builder() .title("Advanced Options") + .subtitle("Resize algorithm, DPI, upscale behavior") + .show_enable_switch(false) .build(); let upscale_row = adw::SwitchRow::builder() @@ -158,7 +241,28 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .active(cfg.allow_upscale) .build(); - advanced_group.add(&upscale_row); + let algorithm_row = adw::ComboRow::builder() + .title("Resize Algorithm") + .subtitle("Method used for pixel interpolation") + .build(); + let algo_model = gtk::StringList::new(&[ + "Lanczos3 (Best quality)", + "CatmullRom (Good quality, faster)", + "Bilinear (Fast, lower quality)", + "Nearest (Fastest, pixelated)", + ]); + algorithm_row.set_model(Some(&algo_model)); + + let dpi_row = adw::SpinRow::builder() + .title("DPI") + .subtitle("Output resolution in dots per inch") + .adjustment(>k::Adjustment::new(72.0, 72.0, 600.0, 1.0, 10.0, 0.0)) + .build(); + + advanced_expander.add_row(&upscale_row); + advanced_expander.add_row(&algorithm_row); + advanced_expander.add_row(&dpi_row); + advanced_group.add(&advanced_expander); content.append(&advanced_group); drop(cfg);