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:
@@ -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);
|
||||||
|
|||||||
@@ -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(>k::Adjustment::new(3.0, 1.0, 5.0, 1.0, 1.0, 0.0))
|
.adjustment(>k::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(>k::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(>k::Adjustment::new(85.0, 1.0, 100.0, 1.0, 10.0, 0.0))
|
.adjustment(>k::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(>k::Adjustment::new(3.0, 1.0, 6.0, 1.0, 1.0, 0.0))
|
.adjustment(>k::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(>k::Adjustment::new(80.0, 1.0, 100.0, 1.0, 10.0, 0.0))
|
.adjustment(>k::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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(>k::Image::from_icon_name("user-trash-symbolic"));
|
strip_all_row.add_prefix(>k::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(>k::Image::from_icon_name("security-medium-symbolic"));
|
privacy_row.add_prefix(>k::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(>k::Image::from_icon_name("emblem-ok-symbolic"));
|
keep_all_row.add_prefix(>k::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));
|
||||||
|
|
||||||
|
|||||||
@@ -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(>k::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0))
|
.adjustment(>k::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(>k::Adjustment::new(0.0, 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);
|
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(>k::Image::from_icon_name("go-next-symbolic"));
|
row.add_suffix(>k::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(>k::Image::from_icon_name("go-next-symbolic"));
|
row.add_suffix(>k::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(>k::Image::from_icon_name("go-next-symbolic"));
|
row.add_suffix(>k::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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user