Add session memory, resize mode tabs, improved output summary
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(>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();
|
.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(>k::Adjustment::new(cfg.resize_height as f64, 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();
|
.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(>k::Image::from_icon_name("go-next-symbolic"));
|
row.add_suffix(>k::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(>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()
|
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(>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);
|
content.append(&advanced_group);
|
||||||
|
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|||||||
Reference in New Issue
Block a user