Files
pixstrip/pixstrip-gtk/src/steps/step_output.rs

163 lines
5.0 KiB
Rust

use adw::prelude::*;
use crate::app::AppState;
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(&gtk::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(&gtk::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")
.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_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(&gtk::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 clamp = adw::Clamp::builder()
.maximum_size(600)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Output & Process")
.tag("step-output")
.child(&clamp)
.build()
}
fn format_size(bytes: u64) -> String {
if bytes < 1024 {
format!("{} B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}