Wire remaining UI elements: presets, drag-drop, import/save, output summary

- Workflow preset cards now apply their config to JobConfig on selection
- User presets section shows saved custom presets from PresetStore
- Import Preset button opens file dialog and imports JSON presets
- Save as Preset button in results page saves current workflow
- Images step supports drag-and-drop for image files
- Images loaded state shows file list and clear button
- Output step dynamically shows operation summary when navigated to
- Output step wires preserve directory structure and overwrite behavior
- Results page displays individual error details in expandable section
- Pause button toggles visual state on processing page
This commit is contained in:
2026-03-06 12:01:50 +02:00
parent b855955786
commit a7f1df2ba5
5 changed files with 602 additions and 29 deletions

View File

@@ -1,7 +1,9 @@
use adw::prelude::*;
use pixstrip_core::preset::Preset;
use pixstrip_core::operations::*;
use crate::app::{AppState, JobConfig, MetadataMode};
pub fn build_workflow_page() -> adw::NavigationPage {
pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage {
let scrolled = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.vexpand(true)
@@ -31,15 +33,23 @@ pub fn build_workflow_page() -> adw::NavigationPage {
.homogeneous(true)
.build();
for preset in Preset::all_builtins() {
let card = build_preset_card(&preset);
let builtins = Preset::all_builtins();
for preset in &builtins {
let card = build_preset_card(preset);
builtin_flow.append(&card);
}
// When a preset card is activated, advance to the next step
builtin_flow.connect_child_activated(|flow, _child| {
flow.activate_action("win.next-step", None).ok();
});
// When a preset card is activated, apply it to JobConfig and advance
{
let jc = state.job_config.clone();
builtin_flow.connect_child_activated(move |flow, child| {
let idx = child.index() as usize;
if let Some(preset) = builtins.get(idx) {
apply_preset_to_config(&mut jc.borrow_mut(), preset);
}
flow.activate_action("win.next-step", None).ok();
});
}
builtin_group.add(&builtin_flow);
content.append(&builtin_group);
@@ -63,15 +73,42 @@ pub fn build_workflow_page() -> adw::NavigationPage {
custom_group.add(&custom_flow);
content.append(&custom_group);
// User presets section (initially empty)
// User presets section
let user_group = adw::PreferencesGroup::builder()
.title("Your Presets")
.description("Import or save your own workflows")
.build();
// Show saved user presets
let store = pixstrip_core::storage::PresetStore::new();
if let Ok(presets) = store.list() {
for preset in &presets {
if !preset.is_custom {
continue;
}
let row = adw::ActionRow::builder()
.title(&preset.name)
.subtitle(&preset.description)
.activatable(true)
.build();
row.add_prefix(&gtk::Image::from_icon_name(&preset.icon));
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let jc = state.job_config.clone();
let p = preset.clone();
row.connect_activated(move |r| {
apply_preset_to_config(&mut jc.borrow_mut(), &p);
r.activate_action("win.next-step", None).ok();
});
user_group.add(&row);
}
}
let import_button = gtk::Button::builder()
.label("Import Preset")
.icon_name("document-open-symbolic")
.action_name("win.import-preset")
.build();
import_button.add_css_class("flat");
user_group.add(&import_button);
@@ -91,6 +128,101 @@ pub fn build_workflow_page() -> adw::NavigationPage {
.build()
}
fn apply_preset_to_config(cfg: &mut JobConfig, preset: &Preset) {
// Resize
match &preset.resize {
Some(ResizeConfig::ByWidth(w)) => {
cfg.resize_enabled = true;
cfg.resize_width = *w;
cfg.resize_height = 0;
cfg.allow_upscale = false;
}
Some(ResizeConfig::ByHeight(h)) => {
cfg.resize_enabled = true;
cfg.resize_width = 0;
cfg.resize_height = *h;
cfg.allow_upscale = false;
}
Some(ResizeConfig::FitInBox { max, allow_upscale }) => {
cfg.resize_enabled = true;
cfg.resize_width = max.width;
cfg.resize_height = max.height;
cfg.allow_upscale = *allow_upscale;
}
Some(ResizeConfig::Exact(dims)) => {
cfg.resize_enabled = true;
cfg.resize_width = dims.width;
cfg.resize_height = dims.height;
cfg.allow_upscale = true;
}
None => {
cfg.resize_enabled = false;
}
}
// Convert
match &preset.convert {
Some(ConvertConfig::SingleFormat(fmt)) => {
cfg.convert_enabled = true;
cfg.convert_format = Some(*fmt);
}
Some(_) => {
cfg.convert_enabled = true;
cfg.convert_format = None;
}
None => {
cfg.convert_enabled = false;
cfg.convert_format = None;
}
}
// Compress
match &preset.compress {
Some(CompressConfig::Preset(q)) => {
cfg.compress_enabled = true;
cfg.quality_preset = *q;
}
Some(CompressConfig::Custom { jpeg_quality, png_level, webp_quality, .. }) => {
cfg.compress_enabled = true;
if let Some(jq) = jpeg_quality {
cfg.jpeg_quality = *jq;
}
if let Some(pl) = png_level {
cfg.png_level = *pl;
}
if let Some(wq) = webp_quality {
cfg.webp_quality = *wq as u8;
}
}
None => {
cfg.compress_enabled = false;
}
}
// Metadata
match &preset.metadata {
Some(MetadataConfig::StripAll) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::StripAll;
}
Some(MetadataConfig::Privacy) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::Privacy;
}
Some(MetadataConfig::KeepAll) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::KeepAll;
}
Some(MetadataConfig::Custom { .. }) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::StripAll;
}
None => {
cfg.metadata_enabled = false;
}
}
}
fn build_preset_card(preset: &Preset) -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)