Add session memory, resize mode tabs, improved output summary

This commit is contained in:
2026-03-06 12:44:16 +02:00
parent 9efcbd082e
commit 3284e066a0
4 changed files with 195 additions and 48 deletions

View File

@@ -167,6 +167,7 @@ impl Default for ConfigStore {
// --- Session Store --- // --- Session Store ---
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SessionState { pub struct SessionState {
pub last_input_dir: Option<String>, pub last_input_dir: Option<String>,
pub last_output_dir: Option<String>, pub last_output_dir: Option<String>,
@@ -175,6 +176,18 @@ pub struct SessionState {
pub window_width: Option<i32>, pub window_width: Option<i32>,
pub window_height: Option<i32>, pub window_height: Option<i32>,
pub window_maximized: bool, pub window_maximized: bool,
// Last-used wizard settings
pub resize_enabled: Option<bool>,
pub resize_width: Option<u32>,
pub resize_height: Option<u32>,
pub convert_enabled: Option<bool>,
pub convert_format: Option<String>,
pub compress_enabled: Option<bool>,
pub quality_preset: Option<String>,
pub metadata_enabled: Option<bool>,
pub metadata_mode: Option<String>,
pub watermark_enabled: Option<bool>,
pub rename_enabled: Option<bool>,
} }
pub struct SessionStore { pub struct SessionStore {

View File

@@ -163,6 +163,7 @@ fn save_and_load_session() {
window_width: Some(1024), window_width: Some(1024),
window_height: Some(768), window_height: Some(768),
window_maximized: false, window_maximized: false,
..Default::default()
}; };
session_store.save(&session).unwrap(); session_store.save(&session).unwrap();

View File

@@ -124,39 +124,46 @@ fn setup_shortcuts(app: &adw::Application) {
} }
fn build_ui(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 { let app_state = AppState {
wizard: Rc::new(RefCell::new(WizardState::new())), wizard: Rc::new(RefCell::new(WizardState::new())),
loaded_files: Rc::new(RefCell::new(Vec::new())), loaded_files: Rc::new(RefCell::new(Vec::new())),
output_dir: Rc::new(RefCell::new(None)), output_dir: Rc::new(RefCell::new(None)),
job_config: Rc::new(RefCell::new(JobConfig { job_config: Rc::new(RefCell::new(JobConfig {
resize_enabled: true, resize_enabled: if remember { sess_state.resize_enabled.unwrap_or(true) } else { true },
resize_width: 1200, resize_width: if remember { sess_state.resize_width.unwrap_or(1200) } else { 1200 },
resize_height: 0, resize_height: if remember { sess_state.resize_height.unwrap_or(0) } else { 0 },
allow_upscale: false, allow_upscale: false,
rotation: 0, rotation: 0,
flip: 0, flip: 0,
convert_enabled: false, convert_enabled: if remember { sess_state.convert_enabled.unwrap_or(false) } else { false },
convert_format: None, 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, quality_preset: pixstrip_core::types::QualityPreset::Medium,
jpeg_quality: 85, jpeg_quality: 85,
png_level: 3, png_level: 3,
webp_quality: 80, webp_quality: 80,
metadata_enabled: true, metadata_enabled: if remember { sess_state.metadata_enabled.unwrap_or(true) } else { true },
metadata_mode: MetadataMode::StripAll, metadata_mode: MetadataMode::StripAll,
strip_gps: true, strip_gps: true,
strip_camera: true, strip_camera: true,
strip_software: true, strip_software: true,
strip_timestamps: true, strip_timestamps: true,
strip_copyright: 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_text: String::new(),
watermark_image_path: None, watermark_image_path: None,
watermark_position: 8, // BottomRight watermark_position: 8, // BottomRight
watermark_opacity: 0.5, watermark_opacity: 0.5,
watermark_font_size: 24.0, watermark_font_size: 24.0,
watermark_use_image: false, 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_prefix: String::new(),
rename_suffix: String::new(), rename_suffix: String::new(),
rename_counter_start: 1, rename_counter_start: 1,
@@ -254,21 +261,43 @@ fn build_ui(app: &adw::Application) {
window.maximize(); window.maximize();
} }
// Save window size on close // Save window size and wizard settings on close
window.connect_close_request(|win| { {
let session = pixstrip_core::storage::SessionStore::new(); let app_state_for_close = app_state.clone();
let mut state = session.load().unwrap_or_default(); window.connect_close_request(move |win| {
state.window_maximized = win.is_maximized(); let session = pixstrip_core::storage::SessionStore::new();
if !win.is_maximized() { let mut state = session.load().unwrap_or_default();
let (w, h) = (win.default_size().0, win.default_size().1); state.window_maximized = win.is_maximized();
if w > 0 && h > 0 { if !win.is_maximized() {
state.window_width = Some(w); let (w, h) = (win.default_size().0, win.default_size().1);
state.window_height = Some(h); if w > 0 && h > 0 {
state.window_width = Some(w);
state.window_height = Some(h);
}
} }
}
let _ = session.save(&state); // Save last-used wizard settings if remember_settings is enabled
glib::Propagation::Proceed 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 { let ui = WizardUi {
nav_view, nav_view,

View File

@@ -29,16 +29,35 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
enable_group.add(&enable_row); enable_group.add(&enable_row);
content.append(&enable_group); content.append(&enable_group);
// Resize dimensions // Mode selector using GtkStack with a stack switcher
let mode_group = adw::PreferencesGroup::builder() let mode_stack = gtk::Stack::builder()
.title("Dimensions") .transition_type(gtk::StackTransitionType::Crossfade)
.description("Set target width and height. Set height to 0 to preserve aspect ratio.") .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(); .build();
let width_row = adw::SpinRow::builder() let width_row = adw::SpinRow::builder()
.title("Width") .title("Width")
.subtitle("Target width in pixels") .subtitle("Target width in pixels")
.adjustment(&gtk::Adjustment::new(cfg.resize_width as f64, 1.0, 10000.0, 1.0, 100.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0))
.build(); .build();
let height_row = adw::SpinRow::builder() let height_row = adw::SpinRow::builder()
@@ -47,17 +66,27 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.adjustment(&gtk::Adjustment::new(cfg.resize_height as f64, 0.0, 10000.0, 1.0, 100.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.resize_height as f64, 0.0, 10000.0, 1.0, 100.0, 0.0))
.build(); .build();
mode_group.add(&width_row); wh_group.add(&width_row);
mode_group.add(&height_row); wh_group.add(&height_row);
content.append(&mode_group); wh_box.append(&wh_group);
// Social media presets mode_stack.add_titled(&wh_box, Some("width-height"), "Width / Height");
let presets_group = adw::PreferencesGroup::builder()
.title("Quick Dimension Presets") // --- Mode 2: Preset Dimensions ---
.description("Click a preset to fill in the dimensions above") let preset_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.build(); .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 build_preset_section = |title: &str, subtitle: &str, presets: &[(&str, u32, u32)]| -> adw::ExpanderRow {
let expander = adw::ExpanderRow::builder() let expander = adw::ExpanderRow::builder()
.title(title) .title(title)
@@ -71,13 +100,16 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.activatable(true) .activatable(true)
.build(); .build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic")); row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let width_c = width_row.clone(); let width_c = width_for_preset.clone();
let height_c = height_row.clone(); let height_c = height_for_preset.clone();
let w = *w; let w = *w;
let h = *h; let h = *h;
let stack_c = mode_stack.clone();
row.connect_activated(move |_| { row.connect_activated(move |_| {
width_c.set_value(w as f64); width_c.set_value(w as f64);
height_c.set_value(h 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); expander.add_row(&row);
} }
@@ -96,10 +128,11 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
("Pixelfed Story", 1080, 1920), ("Pixelfed Story", 1080, 1920),
("Bluesky Post", 1200, 630), ("Bluesky Post", 1200, 630),
("Bluesky Profile", 400, 400), ("Bluesky Profile", 400, 400),
("Bluesky Banner", 1500, 500),
("Lemmy Post", 1200, 630), ("Lemmy Post", 1200, 630),
("PeerTube Thumbnail", 1280, 720), ("PeerTube Thumbnail", 1280, 720),
("Friendica Post", 1200, 630), ("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", "Mainstream Platforms",
"Instagram, YouTube, LinkedIn, Facebook, TikTok", "Instagram, YouTube, LinkedIn, Facebook, TikTok",
&[ &[
("Instagram Post", 1080, 1080), ("Instagram Post Square", 1080, 1080),
("Instagram Portrait", 1080, 1350), ("Instagram Post Portrait", 1080, 1350),
("Instagram Story/Reel", 1080, 1920), ("Instagram Story/Reel", 1080, 1920),
("Facebook Post", 1200, 630), ("Facebook Post", 1200, 630),
("Facebook Cover", 820, 312), ("Facebook Cover", 820, 312),
@@ -119,10 +152,8 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
("LinkedIn Cover", 1584, 396), ("LinkedIn Cover", 1584, 396),
("LinkedIn Profile", 400, 400), ("LinkedIn Profile", 400, 400),
("Pinterest Pin", 1000, 1500), ("Pinterest Pin", 1000, 1500),
("TikTok Profile", 200, 200), ("TikTok Video Cover", 1080, 1920),
("Threads Post", 1080, 1080), ("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), ("4K UHD", 3840, 2160),
("Full HD", 1920, 1080), ("Full HD", 1920, 1080),
("HD Ready", 1280, 720), ("HD Ready", 1280, 720),
("Blog Wide", 800, 0), ("Blog Standard", 800, 0),
("Blog Standard", 800, 600),
("Email Header", 600, 200), ("Email Header", 600, 200),
("Large Thumbnail", 300, 300), ("Large Thumbnail", 300, 300),
("Small Thumbnail", 150, 150), ("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(&fedi_expander);
presets_group.add(&mainstream_expander); presets_group.add(&mainstream_expander);
presets_group.add(&common_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(&gtk::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(&gtk::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() let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced")
.build();
let advanced_expander = adw::ExpanderRow::builder()
.title("Advanced Options") .title("Advanced Options")
.subtitle("Resize algorithm, DPI, upscale behavior")
.show_enable_switch(false)
.build(); .build();
let upscale_row = adw::SwitchRow::builder() let upscale_row = adw::SwitchRow::builder()
@@ -158,7 +241,28 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.active(cfg.allow_upscale) .active(cfg.allow_upscale)
.build(); .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(&gtk::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); content.append(&advanced_group);
drop(cfg); drop(cfg);