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";
/// 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
#[derive(Clone)]
pub struct AppState {
pub wizard: Rc<RefCell<WizardState>>,
pub loaded_files: Rc<RefCell<Vec<std::path::PathBuf>>>,
pub output_dir: Rc<RefCell<Option<std::path::PathBuf>>>,
pub job_config: Rc<RefCell<JobConfig>>,
}
#[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);

View File

@@ -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(&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)
.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(&gtk::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(&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();
let png_row = adw::SpinRow::builder()
.title("PNG Compression Level")
.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();
let webp_row = adw::SpinRow::builder()
.title("WebP 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();
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(),
}
}

View File

@@ -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()

View File

@@ -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(&gtk::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(&gtk::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(&gtk::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));

View File

@@ -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(&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();
let height_row = adw::SpinRow::builder()
.title("Height")
.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();
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(&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);
}
@@ -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(&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);
}
@@ -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(&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);
}
@@ -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()

View File

@@ -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<adw::NavigationPage> {
pub fn build_wizard_pages(state: &AppState) -> Vec<adw::NavigationPage> {
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(),
]
}