257 lines
9.3 KiB
Rust
257 lines
9.3 KiB
Rust
use adw::prelude::*;
|
|
use crate::app::AppState;
|
|
use crate::utils::format_size;
|
|
|
|
pub fn build_output_page(state: &AppState) -> adw::NavigationPage {
|
|
let scrolled = gtk::ScrolledWindow::builder()
|
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
.vexpand(true)
|
|
.build();
|
|
|
|
let content = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.spacing(12)
|
|
.margin_top(12)
|
|
.margin_bottom(12)
|
|
.margin_start(24)
|
|
.margin_end(24)
|
|
.build();
|
|
|
|
// Operation summary - dynamically rebuilt when this step is shown
|
|
let summary_group = adw::PreferencesGroup::builder()
|
|
.title("Operation Summary")
|
|
.description("Review your processing settings before starting")
|
|
.build();
|
|
|
|
let summary_box = gtk::ListBox::builder()
|
|
.selection_mode(gtk::SelectionMode::None)
|
|
.css_classes(["boxed-list"])
|
|
.build();
|
|
summary_box.set_widget_name("ops-summary-list");
|
|
|
|
summary_group.add(&summary_box);
|
|
content.append(&summary_group);
|
|
|
|
// Output directory
|
|
let output_group = adw::PreferencesGroup::builder()
|
|
.title("Output Directory")
|
|
.build();
|
|
|
|
let default_output = state.output_dir.borrow()
|
|
.as_ref()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|| "processed/ (subfolder next to originals)".to_string());
|
|
|
|
let output_row = adw::ActionRow::builder()
|
|
.title("Output Location")
|
|
.subtitle(&default_output)
|
|
.activatable(true)
|
|
.action_name("win.choose-output")
|
|
.build();
|
|
output_row.add_prefix(>k::Image::from_icon_name("folder-symbolic"));
|
|
|
|
let choose_button = gtk::Button::builder()
|
|
.icon_name("folder-open-symbolic")
|
|
.tooltip_text("Choose output folder")
|
|
.valign(gtk::Align::Center)
|
|
.action_name("win.choose-output")
|
|
.build();
|
|
choose_button.add_css_class("flat");
|
|
output_row.add_suffix(&choose_button);
|
|
output_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic"));
|
|
|
|
let cfg = state.job_config.borrow();
|
|
|
|
let structure_row = adw::SwitchRow::builder()
|
|
.title("Preserve Directory Structure")
|
|
.subtitle("Keep subfolder hierarchy in output")
|
|
.active(cfg.preserve_dir_structure)
|
|
.build();
|
|
|
|
output_group.add(&output_row);
|
|
output_group.add(&structure_row);
|
|
content.append(&output_group);
|
|
|
|
// Overwrite behavior
|
|
let overwrite_group = adw::PreferencesGroup::builder()
|
|
.title("If Files Already Exist")
|
|
.build();
|
|
|
|
let overwrite_row = adw::ComboRow::builder()
|
|
.title("Overwrite Behavior")
|
|
.subtitle("What to do when output file already exists")
|
|
.use_subtitle(true)
|
|
.build();
|
|
let overwrite_model = gtk::StringList::new(&[
|
|
"Ask before overwriting",
|
|
"Auto-rename with suffix",
|
|
"Always overwrite",
|
|
"Skip existing files",
|
|
]);
|
|
overwrite_row.set_model(Some(&overwrite_model));
|
|
overwrite_row.set_list_factory(Some(&super::full_text_list_factory()));
|
|
overwrite_row.set_selected(cfg.overwrite_behavior as u32);
|
|
|
|
overwrite_group.add(&overwrite_row);
|
|
content.append(&overwrite_group);
|
|
|
|
// Image count - dynamically updated
|
|
let stats_group = adw::PreferencesGroup::builder()
|
|
.title("Batch Info")
|
|
.build();
|
|
|
|
let excluded = state.excluded_files.borrow();
|
|
let files = state.loaded_files.borrow();
|
|
let included_count = files.iter().filter(|p| !excluded.contains(*p)).count();
|
|
let total_size: u64 = files.iter()
|
|
.filter(|p| !excluded.contains(*p))
|
|
.filter_map(|p| std::fs::metadata(p).ok())
|
|
.map(|m| m.len())
|
|
.sum();
|
|
drop(files);
|
|
drop(excluded);
|
|
|
|
let count_row = adw::ActionRow::builder()
|
|
.title("Images to process")
|
|
.subtitle(format!("{} images ({})", included_count, format_size(total_size)))
|
|
.build();
|
|
count_row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic"));
|
|
|
|
stats_group.add(&count_row);
|
|
content.append(&stats_group);
|
|
|
|
drop(cfg);
|
|
|
|
// Wire preserve directory structure
|
|
{
|
|
let jc = state.job_config.clone();
|
|
structure_row.connect_active_notify(move |row| {
|
|
jc.borrow_mut().preserve_dir_structure = row.is_active();
|
|
});
|
|
}
|
|
|
|
// Wire overwrite behavior
|
|
{
|
|
let jc = state.job_config.clone();
|
|
overwrite_row.connect_selected_notify(move |row| {
|
|
jc.borrow_mut().overwrite_behavior = row.selected() as u8;
|
|
});
|
|
}
|
|
|
|
scrolled.set_child(Some(&content));
|
|
|
|
let page = adw::NavigationPage::builder()
|
|
.title("Output & Process")
|
|
.tag("step-output")
|
|
.child(&scrolled)
|
|
.build();
|
|
|
|
// Refresh stats and summary when navigating to this page
|
|
{
|
|
let lf = state.loaded_files.clone();
|
|
let ef = state.excluded_files.clone();
|
|
let jc = state.job_config.clone();
|
|
let od = state.output_dir.clone();
|
|
let cr = count_row.clone();
|
|
let or = output_row.clone();
|
|
let sb = summary_box.clone();
|
|
page.connect_map(move |_| {
|
|
// Update image count and size
|
|
let files = lf.borrow();
|
|
let excluded = ef.borrow();
|
|
let included_count = files.iter().filter(|p| !excluded.contains(*p)).count();
|
|
let total_size: u64 = files.iter()
|
|
.filter(|p| !excluded.contains(*p))
|
|
.filter_map(|p| std::fs::metadata(p).ok())
|
|
.map(|m| m.len())
|
|
.sum();
|
|
cr.set_subtitle(&format!("{} images ({})", included_count, format_size(total_size)));
|
|
drop(files);
|
|
drop(excluded);
|
|
|
|
// Update output directory display
|
|
let dir_text = od.borrow()
|
|
.as_ref()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|| "processed/ (subfolder next to originals)".to_string());
|
|
or.set_subtitle(&dir_text);
|
|
|
|
// Build operation summary
|
|
while let Some(child) = sb.first_child() {
|
|
sb.remove(&child);
|
|
}
|
|
|
|
let cfg = jc.borrow();
|
|
let mut ops: Vec<(&str, String)> = Vec::new();
|
|
|
|
if cfg.resize_enabled {
|
|
let mode = match cfg.resize_mode {
|
|
0 => format!("{}x{} (exact)", cfg.resize_width, cfg.resize_height),
|
|
_ => format!("fit {}x{}", cfg.resize_width, cfg.resize_height),
|
|
};
|
|
ops.push(("Resize", mode));
|
|
}
|
|
if cfg.adjustments_enabled {
|
|
let mut parts = Vec::new();
|
|
if cfg.rotation > 0 { parts.push("rotate"); }
|
|
if cfg.flip > 0 { parts.push("flip"); }
|
|
if cfg.brightness != 0 { parts.push("brightness"); }
|
|
if cfg.contrast != 0 { parts.push("contrast"); }
|
|
if cfg.saturation != 0 { parts.push("saturation"); }
|
|
if cfg.grayscale { parts.push("grayscale"); }
|
|
if cfg.sepia { parts.push("sepia"); }
|
|
if cfg.sharpen { parts.push("sharpen"); }
|
|
if cfg.crop_aspect_ratio > 0 { parts.push("crop"); }
|
|
if cfg.trim_whitespace { parts.push("trim"); }
|
|
if cfg.canvas_padding > 0 { parts.push("padding"); }
|
|
let desc = if parts.is_empty() { "enabled".into() } else { parts.join(", ") };
|
|
ops.push(("Adjustments", desc));
|
|
}
|
|
if cfg.convert_enabled {
|
|
let fmt = cfg.convert_format.map(|f| f.extension().to_uppercase())
|
|
.unwrap_or_else(|| "per-format mapping".into());
|
|
ops.push(("Convert", fmt));
|
|
}
|
|
if cfg.compress_enabled {
|
|
ops.push(("Compress", cfg.quality_preset.label().into()));
|
|
}
|
|
if cfg.metadata_enabled {
|
|
let mode = match &cfg.metadata_mode {
|
|
crate::app::MetadataMode::StripAll => "strip all",
|
|
crate::app::MetadataMode::KeepAll => "keep all",
|
|
crate::app::MetadataMode::Privacy => "privacy mode",
|
|
crate::app::MetadataMode::Custom => "custom",
|
|
};
|
|
ops.push(("Metadata", mode.into()));
|
|
}
|
|
if cfg.watermark_enabled {
|
|
let wm_type = if cfg.watermark_use_image { "image" } else { "text" };
|
|
ops.push(("Watermark", wm_type.into()));
|
|
}
|
|
if cfg.rename_enabled {
|
|
ops.push(("Rename", "enabled".into()));
|
|
}
|
|
|
|
if ops.is_empty() {
|
|
let row = adw::ActionRow::builder()
|
|
.title("No operations enabled")
|
|
.subtitle("Go back and enable at least one operation")
|
|
.build();
|
|
row.add_prefix(>k::Image::from_icon_name("dialog-warning-symbolic"));
|
|
sb.append(&row);
|
|
} else {
|
|
for (name, desc) in &ops {
|
|
let row = adw::ActionRow::builder()
|
|
.title(*name)
|
|
.subtitle(desc.as_str())
|
|
.build();
|
|
row.add_prefix(>k::Image::from_icon_name("emblem-ok-symbolic"));
|
|
sb.append(&row);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
page
|
|
}
|