Fix 12 medium-severity bugs across all crates
- Escape backslashes in Nautilus preset names preventing Python injection - Fix tiled watermarks starting at (spacing,spacing) instead of (0,0) - Fix text watermark width overestimation (1.0x to 0.6x multiplier) - Fix output_dpi forcing re-encoding for metadata-only presets - Fix AVIF/WebP compression detection comparing against wrong preset values - Add shared batch_updating guard for Ctrl+A/Ctrl+Shift+A select actions - Fix overwrite conflict check ignoring preserve_directory_structure - Add changes_filename()/changes_extension() for smarter overwrite checks - Fix watch folder hardcoding "Blog Photos" preset - Fix undo dropping history for partially-trashed batches - Fix skipped files inflating size statistics - Make CLI watch config writes atomic
This commit is contained in:
@@ -115,6 +115,8 @@ pub struct AppState {
|
||||
pub detailed_mode: bool,
|
||||
pub batch_queue: Rc<RefCell<BatchQueue>>,
|
||||
pub expanded_sections: Rc<RefCell<std::collections::HashMap<String, bool>>>,
|
||||
/// Guard flag to suppress per-checkbox callbacks during bulk select/deselect
|
||||
pub batch_updating: Rc<std::cell::Cell<bool>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -327,6 +329,7 @@ fn build_ui(app: &adw::Application) {
|
||||
detailed_mode: app_cfg.skill_level.is_advanced(),
|
||||
batch_queue: Rc::new(RefCell::new(BatchQueue::default())),
|
||||
expanded_sections: Rc::new(RefCell::new(sess_state.expanded_sections.clone())),
|
||||
batch_updating: Rc::new(std::cell::Cell::new(false)),
|
||||
job_config: Rc::new(RefCell::new(JobConfig {
|
||||
preset_mode: false,
|
||||
resize_enabled: if remember { sess_state.resize_enabled.unwrap_or(true) } else { true },
|
||||
@@ -942,7 +945,9 @@ fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
&& let Some(stack) = page.child().and_downcast::<gtk::Stack>()
|
||||
&& let Some(loaded) = stack.child_by_name("loaded")
|
||||
{
|
||||
ui.state.batch_updating.set(true);
|
||||
crate::steps::step_images::set_all_checkboxes_in(&loaded, true);
|
||||
ui.state.batch_updating.set(false);
|
||||
let files = ui.state.loaded_files.borrow();
|
||||
let count = files.len();
|
||||
drop(files);
|
||||
@@ -969,7 +974,9 @@ fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
&& let Some(stack) = page.child().and_downcast::<gtk::Stack>()
|
||||
&& let Some(loaded) = stack.child_by_name("loaded")
|
||||
{
|
||||
ui.state.batch_updating.set(true);
|
||||
crate::steps::step_images::set_all_checkboxes_in(&loaded, false);
|
||||
ui.state.batch_updating.set(false);
|
||||
let files = ui.state.loaded_files.borrow();
|
||||
let count = files.len();
|
||||
drop(files);
|
||||
@@ -1799,11 +1806,14 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
// Check if user has customized per-format quality values beyond the preset defaults
|
||||
let preset_jpeg = cfg.quality_preset.jpeg_quality();
|
||||
let preset_webp = cfg.quality_preset.webp_quality();
|
||||
let preset_avif = cfg.quality_preset.avif_quality();
|
||||
let preset_avif_speed = cfg.quality_preset.avif_speed();
|
||||
let preset_webp_effort = cfg.quality_preset.webp_effort();
|
||||
let has_custom = cfg.jpeg_quality != preset_jpeg
|
||||
|| cfg.webp_quality != preset_webp as u8
|
||||
|| cfg.avif_quality != preset_webp as u8
|
||||
|| cfg.avif_speed != 6
|
||||
|| cfg.webp_effort != 4;
|
||||
|| cfg.avif_quality != preset_avif
|
||||
|| cfg.avif_speed != preset_avif_speed
|
||||
|| cfg.webp_effort != preset_webp_effort;
|
||||
|
||||
if has_custom {
|
||||
job.compress = Some(pixstrip_core::operations::CompressConfig::Custom {
|
||||
@@ -1967,8 +1977,9 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
}
|
||||
|
||||
// Check for existing output files when "Ask" overwrite behavior is set.
|
||||
// Skip check if rename or format conversion is active (output names will differ).
|
||||
let has_rename_or_convert = job.rename.is_some() || job.convert.is_some();
|
||||
// Skip check if rename or format conversion will actually change output names.
|
||||
let has_rename_or_convert = job.rename.as_ref().is_some_and(|r| r.changes_filename())
|
||||
|| job.convert.as_ref().is_some_and(|c| c.changes_extension());
|
||||
if ask_overwrite && !has_rename_or_convert {
|
||||
let output_dir = ui.state.output_dir.borrow().clone()
|
||||
.unwrap_or_else(|| {
|
||||
@@ -1980,7 +1991,19 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
let conflicts: Vec<String> = files.iter()
|
||||
.filter_map(|f| {
|
||||
let name = f.file_name()?;
|
||||
let out_path = output_dir.join(name);
|
||||
let out_path = if job.preserve_directory_structure {
|
||||
if let Ok(rel) = f.strip_prefix(&job.input_dir) {
|
||||
if let Some(parent) = rel.parent() {
|
||||
output_dir.join(parent).join(name)
|
||||
} else {
|
||||
output_dir.join(name)
|
||||
}
|
||||
} else {
|
||||
output_dir.join(name)
|
||||
}
|
||||
} else {
|
||||
output_dir.join(name)
|
||||
};
|
||||
if out_path.exists() { Some(name.to_string_lossy().into()) } else { None }
|
||||
})
|
||||
.take(10)
|
||||
@@ -3464,9 +3487,14 @@ fn build_watch_folder_panel() -> gtk::Box {
|
||||
if let Ok(file) = result
|
||||
&& let Some(path) = file.path()
|
||||
{
|
||||
// Use first available preset name
|
||||
let default_preset = pixstrip_core::preset::Preset::all_builtins()
|
||||
.first()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| "Blog Photos".to_string());
|
||||
let new_folder = pixstrip_core::watcher::WatchFolder {
|
||||
path: path.clone(),
|
||||
preset_name: "Blog Photos".to_string(),
|
||||
preset_name: default_preset,
|
||||
recursive: false,
|
||||
active: true,
|
||||
};
|
||||
|
||||
@@ -550,8 +550,8 @@ fn build_loaded_state(state: &AppState) -> gtk::Box {
|
||||
.spacing(0)
|
||||
.build();
|
||||
|
||||
// Guard flag to prevent O(n^2) update cascade during batch checkbox operations
|
||||
let batch_updating: Rc<std::cell::Cell<bool>> = Rc::new(std::cell::Cell::new(false));
|
||||
// Use the shared batch_updating flag from AppState
|
||||
let batch_updating = state.batch_updating.clone();
|
||||
|
||||
// Toolbar
|
||||
let toolbar = gtk::Box::builder()
|
||||
|
||||
Reference in New Issue
Block a user