Wire all wizard step controls to shared JobConfig state

All four configurable steps (resize, convert, compress, metadata) now
have signal handlers that update the shared JobConfig via AppState.
The run_processing function builds ProcessingJob from actual user
choices instead of hardcoded values. Fixed clippy warnings (collapsed
if-let chain, removed needless borrow).
This commit is contained in:
2026-03-06 11:51:01 +02:00
parent eeb418ccdd
commit b855955786
6 changed files with 333 additions and 161 deletions

View File

@@ -10,12 +10,39 @@ use crate::wizard::WizardState;
pub const APP_ID: &str = "live.lashman.Pixstrip"; 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<pixstrip_core::types::ImageFormat>,
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 /// Shared app state accessible from all UI callbacks
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub wizard: Rc<RefCell<WizardState>>, pub wizard: Rc<RefCell<WizardState>>,
pub loaded_files: Rc<RefCell<Vec<std::path::PathBuf>>>, pub loaded_files: Rc<RefCell<Vec<std::path::PathBuf>>>,
pub output_dir: Rc<RefCell<Option<std::path::PathBuf>>>, pub output_dir: Rc<RefCell<Option<std::path::PathBuf>>>,
pub job_config: Rc<RefCell<JobConfig>>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -59,6 +86,21 @@ fn build_ui(app: &adw::Application) {
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 {
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 // Header bar
@@ -84,7 +126,7 @@ fn build_ui(app: &adw::Application) {
nav_view.set_vexpand(true); nav_view.set_vexpand(true);
// Build wizard pages // Build wizard pages
let pages = crate::wizard::build_wizard_pages(); let pages = crate::wizard::build_wizard_pages(&app_state);
for page in &pages { for page in &pages {
nav_view.add(page); nav_view.add(page);
} }
@@ -573,12 +615,41 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
.clone() .clone()
.unwrap_or_else(|| input_dir.join("processed")); .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); 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, let cfg = ui.state.job_config.borrow();
));
job.metadata = Some(pixstrip_core::operations::MetadataConfig::StripAll); 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 { for file in &files {
job.add_source(file); job.add_source(file);

View File

@@ -1,6 +1,8 @@
use adw::prelude::*; 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() let scrolled = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never) .hscrollbar_policy(gtk::PolicyType::Never)
.vexpand(true) .vexpand(true)
@@ -15,11 +17,13 @@ pub fn build_compress_page() -> adw::NavigationPage {
.margin_end(24) .margin_end(24)
.build(); .build();
let cfg = state.job_config.borrow();
// Enable toggle // Enable toggle
let enable_row = adw::SwitchRow::builder() let enable_row = adw::SwitchRow::builder()
.title("Enable Compression") .title("Enable Compression")
.subtitle("Reduce file size with quality control") .subtitle("Reduce file size with quality control")
.active(true) .active(cfg.compress_enabled)
.build(); .build();
let enable_group = adw::PreferencesGroup::new(); let enable_group = adw::PreferencesGroup::new();
@@ -32,15 +36,22 @@ pub fn build_compress_page() -> adw::NavigationPage {
.description("Higher quality means larger files") .description("Higher quality means larger files")
.build(); .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() let quality_scale = gtk::Scale::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.adjustment(&gtk::Adjustment::new(3.0, 1.0, 5.0, 1.0, 1.0, 0.0)) .adjustment(&gtk::Adjustment::new(initial_val, 1.0, 5.0, 1.0, 1.0, 0.0))
.draw_value(false) .draw_value(false)
.hexpand(true) .hexpand(true)
.build(); .build();
quality_scale.set_round_digits(0); quality_scale.set_round_digits(0);
// Add named marks
quality_scale.add_mark(1.0, gtk::PositionType::Bottom, Some("Web")); 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(2.0, gtk::PositionType::Bottom, Some("Low"));
quality_scale.add_mark(3.0, gtk::PositionType::Bottom, Some("Medium")); 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")); quality_scale.add_mark(5.0, gtk::PositionType::Bottom, Some("Maximum"));
let quality_label = gtk::Label::builder() 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"]) .css_classes(["dim-label"])
.margin_top(4) .margin_top(4)
.build(); .build();
@@ -67,42 +78,28 @@ pub fn build_compress_page() -> adw::NavigationPage {
quality_group.add(&quality_box); quality_group.add(&quality_box);
content.append(&quality_group); 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(&gtk::Image::from_icon_name("drive-harddisk-symbolic"));
estimate_group.add(&estimate_row);
content.append(&estimate_group);
// Advanced options // Advanced options
let advanced_group = adw::PreferencesGroup::builder() let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced Options") .title("Per-Format Quality")
.description("Fine-tune quality for each format individually")
.build(); .build();
let jpeg_row = adw::SpinRow::builder() let jpeg_row = adw::SpinRow::builder()
.title("JPEG Quality") .title("JPEG Quality")
.subtitle("1-100, higher is better quality") .subtitle("1-100, higher is better quality")
.adjustment(&gtk::Adjustment::new(85.0, 1.0, 100.0, 1.0, 10.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.jpeg_quality as f64, 1.0, 100.0, 1.0, 10.0, 0.0))
.build(); .build();
let png_row = adw::SpinRow::builder() let png_row = adw::SpinRow::builder()
.title("PNG Compression Level") .title("PNG Compression Level")
.subtitle("1-6, higher is slower but smaller") .subtitle("1-6, higher is slower but smaller")
.adjustment(&gtk::Adjustment::new(3.0, 1.0, 6.0, 1.0, 1.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.png_level as f64, 1.0, 6.0, 1.0, 1.0, 0.0))
.build(); .build();
let webp_row = adw::SpinRow::builder() let webp_row = adw::SpinRow::builder()
.title("WebP Quality") .title("WebP Quality")
.subtitle("1-100, higher is better quality") .subtitle("1-100, higher is better quality")
.adjustment(&gtk::Adjustment::new(80.0, 1.0, 100.0, 1.0, 10.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.webp_quality as f64, 1.0, 100.0, 1.0, 10.0, 0.0))
.build(); .build();
advanced_group.add(&jpeg_row); advanced_group.add(&jpeg_row);
@@ -110,6 +107,50 @@ pub fn build_compress_page() -> adw::NavigationPage {
advanced_group.add(&webp_row); advanced_group.add(&webp_row);
content.append(&advanced_group); 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)); scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder() let clamp = adw::Clamp::builder()
@@ -123,3 +164,13 @@ pub fn build_compress_page() -> adw::NavigationPage {
.child(&clamp) .child(&clamp)
.build() .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(),
}
}

View File

@@ -1,6 +1,8 @@
use adw::prelude::*; 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() let scrolled = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never) .hscrollbar_policy(gtk::PolicyType::Never)
.vexpand(true) .vexpand(true)
@@ -15,11 +17,13 @@ pub fn build_convert_page() -> adw::NavigationPage {
.margin_end(24) .margin_end(24)
.build(); .build();
let cfg = state.job_config.borrow();
// Enable toggle // Enable toggle
let enable_row = adw::SwitchRow::builder() let enable_row = adw::SwitchRow::builder()
.title("Enable Format Conversion") .title("Enable Format Conversion")
.subtitle("Convert images to a different format") .subtitle("Convert images to a different format")
.active(false) .active(cfg.convert_enabled)
.build(); .build();
let enable_group = adw::PreferencesGroup::new(); let enable_group = adw::PreferencesGroup::new();
@@ -31,67 +35,52 @@ pub fn build_convert_page() -> adw::NavigationPage {
.title("Output Format") .title("Output Format")
.build(); .build();
let formats = [ let format_row = adw::ComboRow::builder()
("Keep Original", "No conversion - output in same format as input"), .title("Convert to")
("JPEG", "Universal photo format, lossy compression"), .subtitle("Choose the output format for all images")
("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)
.build(); .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 { // Set initial selection
let card = gtk::Box::builder() format_row.set_selected(match cfg.convert_format {
.orientation(gtk::Orientation::Vertical) None => 0,
.spacing(4) Some(ImageFormat::Jpeg) => 1,
.halign(gtk::Align::Center) Some(ImageFormat::Png) => 2,
.valign(gtk::Align::Center) Some(ImageFormat::WebP) => 3,
.build(); _ => 0,
card.add_css_class("card"); });
card.set_size_request(140, 80);
let inner = gtk::Box::builder() format_group.add(&format_row);
.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);
content.append(&format_group); 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)); scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder() let clamp = adw::Clamp::builder()

View File

@@ -1,6 +1,7 @@
use adw::prelude::*; 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() let scrolled = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never) .hscrollbar_policy(gtk::PolicyType::Never)
.vexpand(true) .vexpand(true)
@@ -15,11 +16,13 @@ pub fn build_metadata_page() -> adw::NavigationPage {
.margin_end(24) .margin_end(24)
.build(); .build();
let cfg = state.job_config.borrow();
// Enable toggle // Enable toggle
let enable_row = adw::SwitchRow::builder() let enable_row = adw::SwitchRow::builder()
.title("Enable Metadata Handling") .title("Enable Metadata Handling")
.subtitle("Control what image metadata to keep or remove") .subtitle("Control what image metadata to keep or remove")
.active(true) .active(cfg.metadata_enabled)
.build(); .build();
let enable_group = adw::PreferencesGroup::new(); let enable_group = adw::PreferencesGroup::new();
@@ -38,7 +41,7 @@ pub fn build_metadata_page() -> adw::NavigationPage {
.build(); .build();
strip_all_row.add_prefix(&gtk::Image::from_icon_name("user-trash-symbolic")); strip_all_row.add_prefix(&gtk::Image::from_icon_name("user-trash-symbolic"));
let strip_all_check = gtk::CheckButton::new(); 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.add_suffix(&strip_all_check);
strip_all_row.set_activatable_widget(Some(&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(&gtk::Image::from_icon_name("security-medium-symbolic")); privacy_row.add_prefix(&gtk::Image::from_icon_name("security-medium-symbolic"));
let privacy_check = gtk::CheckButton::new(); let privacy_check = gtk::CheckButton::new();
privacy_check.set_group(Some(&strip_all_check)); 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.add_suffix(&privacy_check);
privacy_row.set_activatable_widget(Some(&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(&gtk::Image::from_icon_name("emblem-ok-symbolic")); keep_all_row.add_prefix(&gtk::Image::from_icon_name("emblem-ok-symbolic"));
let keep_all_check = gtk::CheckButton::new(); let keep_all_check = gtk::CheckButton::new();
keep_all_check.set_group(Some(&strip_all_check)); 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.add_suffix(&keep_all_check);
keep_all_row.set_activatable_widget(Some(&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); presets_group.add(&keep_all_row);
content.append(&presets_group); content.append(&presets_group);
// Advanced - per-category controls drop(cfg);
let advanced_group = adw::PreferencesGroup::builder()
.title("Fine-Grained Control")
.description("Choose exactly which metadata categories to strip")
.build();
let categories = [ // Wire signals
("GPS / Location Data", "Coordinates, altitude, location name", true), {
("Camera Info", "Camera model, serial number, lens info", true), let jc = state.job_config.clone();
("Software / Editing", "Software used, editing history", true), enable_row.connect_active_notify(move |row| {
("Timestamps", "Date taken, date modified", false), jc.borrow_mut().metadata_enabled = row.is_active();
("Copyright / Author", "Copyright notice, creator name", false), });
]; }
{
for (title, subtitle, default_strip) in &categories { let jc = state.job_config.clone();
let row = adw::SwitchRow::builder() strip_all_check.connect_toggled(move |check| {
.title(format!("Strip {}", title)) if check.is_active() {
.subtitle(*subtitle) jc.borrow_mut().metadata_mode = MetadataMode::StripAll;
.active(*default_strip) }
.build(); });
advanced_group.add(&row); }
{
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)); scrolled.set_child(Some(&content));

View File

@@ -1,6 +1,7 @@
use adw::prelude::*; 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() let scrolled = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never) .hscrollbar_policy(gtk::PolicyType::Never)
.vexpand(true) .vexpand(true)
@@ -15,33 +16,34 @@ pub fn build_resize_page() -> adw::NavigationPage {
.margin_end(24) .margin_end(24)
.build(); .build();
let cfg = state.job_config.borrow();
// Enable toggle // Enable toggle
let enable_row = adw::SwitchRow::builder() let enable_row = adw::SwitchRow::builder()
.title("Enable Resize") .title("Enable Resize")
.subtitle("Resize images to new dimensions") .subtitle("Resize images to new dimensions")
.active(true) .active(cfg.resize_enabled)
.build(); .build();
let enable_group = adw::PreferencesGroup::new(); let enable_group = adw::PreferencesGroup::new();
enable_group.add(&enable_row); enable_group.add(&enable_row);
content.append(&enable_group); content.append(&enable_group);
// Resize mode selector // Resize mode
let mode_group = adw::PreferencesGroup::builder() let mode_group = adw::PreferencesGroup::builder()
.title("Resize Mode") .title("Resize Mode")
.build(); .build();
// Width/Height mode
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(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.resize_width as f64, 1.0, 10000.0, 1.0, 100.0, 0.0))
.build(); .build();
let height_row = adw::SpinRow::builder() let height_row = adw::SpinRow::builder()
.title("Height") .title("Height")
.subtitle("Target height in pixels (0 = auto from aspect ratio)") .subtitle("Target height in pixels (0 = auto from aspect ratio)")
.adjustment(&gtk::Adjustment::new(0.0, 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); mode_group.add(&width_row);
@@ -58,24 +60,32 @@ pub fn build_resize_page() -> adw::NavigationPage {
.subtitle("Mastodon, Pixelfed, Bluesky, Lemmy") .subtitle("Mastodon, Pixelfed, Bluesky, Lemmy")
.build(); .build();
let fedi_presets = [ let fedi_presets: Vec<(&str, u32, u32)> = vec![
("Mastodon Post", "1920 x 1080"), ("Mastodon Post", 1920, 1080),
("Mastodon Profile", "400 x 400"), ("Mastodon Profile", 400, 400),
("Mastodon Header", "1500 x 500"), ("Mastodon Header", 1500, 500),
("Pixelfed Post", "1080 x 1080"), ("Pixelfed Post", 1080, 1080),
("Pixelfed Story", "1080 x 1920"), ("Pixelfed Story", 1080, 1920),
("Bluesky Post", "1200 x 630"), ("Bluesky Post", 1200, 630),
("Bluesky Profile", "400 x 400"), ("Bluesky Profile", 400, 400),
("Lemmy Post", "1200 x 630"), ("Lemmy Post", 1200, 630),
]; ];
for (name, dims) in &fedi_presets { for (name, w, h) in &fedi_presets {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title(*name) .title(*name)
.subtitle(*dims) .subtitle(format!("{} x {}", w, h))
.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_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); fedi_expander.add_row(&row);
} }
@@ -84,22 +94,30 @@ pub fn build_resize_page() -> adw::NavigationPage {
.subtitle("Instagram, YouTube, LinkedIn, Pinterest") .subtitle("Instagram, YouTube, LinkedIn, Pinterest")
.build(); .build();
let mainstream_presets = [ let mainstream_presets: Vec<(&str, u32, u32)> = vec![
("Instagram Post", "1080 x 1080"), ("Instagram Post", 1080, 1080),
("Instagram Story/Reel", "1080 x 1920"), ("Instagram Story/Reel", 1080, 1920),
("Facebook Post", "1200 x 630"), ("Facebook Post", 1200, 630),
("YouTube Thumbnail", "1280 x 720"), ("YouTube Thumbnail", 1280, 720),
("LinkedIn Post", "1200 x 627"), ("LinkedIn Post", 1200, 627),
("Pinterest Pin", "1000 x 1500"), ("Pinterest Pin", 1000, 1500),
]; ];
for (name, dims) in &mainstream_presets { for (name, w, h) in &mainstream_presets {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title(*name) .title(*name)
.subtitle(*dims) .subtitle(format!("{} x {}", w, h))
.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_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); mainstream_expander.add_row(&row);
} }
@@ -108,19 +126,27 @@ pub fn build_resize_page() -> adw::NavigationPage {
.subtitle("HD, Blog, Thumbnail") .subtitle("HD, Blog, Thumbnail")
.build(); .build();
let other_presets = [ let other_presets: Vec<(&str, u32, u32)> = vec![
("Full HD", "1920 x 1080"), ("Full HD", 1920, 1080),
("Blog Image", "800 wide"), ("Blog Image", 800, 0),
("Thumbnail", "150 x 150"), ("Thumbnail", 150, 150),
]; ];
for (name, dims) in &other_presets { for (name, w, h) in &other_presets {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title(*name) .title(*name)
.subtitle(*dims) .subtitle(if *h == 0 { format!("{} wide", w) } else { format!("{} x {}", w, h) })
.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_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); other_expander.add_row(&row);
} }
@@ -129,7 +155,7 @@ pub fn build_resize_page() -> adw::NavigationPage {
presets_group.add(&other_expander); presets_group.add(&other_expander);
content.append(&presets_group); content.append(&presets_group);
// Basic adjustments (rotation/flip) // Basic adjustments
let adjust_group = adw::PreferencesGroup::builder() let adjust_group = adw::PreferencesGroup::builder()
.title("Basic Adjustments") .title("Basic Adjustments")
.build(); .build();
@@ -152,28 +178,48 @@ pub fn build_resize_page() -> adw::NavigationPage {
adjust_group.add(&flip_row); adjust_group.add(&flip_row);
content.append(&adjust_group); content.append(&adjust_group);
// Advanced options // Advanced
let advanced_group = adw::PreferencesGroup::builder() let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced Options") .title("Advanced Options")
.build(); .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() let upscale_row = adw::SwitchRow::builder()
.title("Allow Upscaling") .title("Allow Upscaling")
.subtitle("Enlarge images smaller than target size") .subtitle("Enlarge images smaller than target size")
.active(false) .active(cfg.allow_upscale)
.build(); .build();
advanced_group.add(&algo_row);
advanced_group.add(&upscale_row); advanced_group.add(&upscale_row);
content.append(&advanced_group); 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)); scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder() let clamp = adw::Clamp::builder()

View File

@@ -1,3 +1,4 @@
use crate::app::AppState;
use crate::steps; use crate::steps;
pub struct WizardState { pub struct WizardState {
@@ -60,14 +61,14 @@ impl WizardState {
} }
} }
pub fn build_wizard_pages() -> Vec<adw::NavigationPage> { pub fn build_wizard_pages(state: &AppState) -> Vec<adw::NavigationPage> {
vec![ vec![
steps::step_workflow::build_workflow_page(), steps::step_workflow::build_workflow_page(),
steps::step_images::build_images_page(), steps::step_images::build_images_page(),
steps::step_resize::build_resize_page(), steps::step_resize::build_resize_page(state),
steps::step_convert::build_convert_page(), steps::step_convert::build_convert_page(state),
steps::step_compress::build_compress_page(), steps::step_compress::build_compress_page(state),
steps::step_metadata::build_metadata_page(), steps::step_metadata::build_metadata_page(state),
steps::step_output::build_output_page(), steps::step_output::build_output_page(),
] ]
} }