Add all wizard step UIs: workflow, images, resize, convert, compress, metadata, output

Full Adwaita widget-based layouts for all 7 wizard steps with
PreferencesGroups, SwitchRows, SpinRows, ComboRows, FlowBoxes,
ExpanderRows for social media presets, quality slider with named
marks, metadata radio group, and output configuration.
This commit is contained in:
2026-03-06 11:08:38 +02:00
parent 20f4c24538
commit a66db2b3bb
10 changed files with 980 additions and 26 deletions

View File

@@ -1,5 +1,6 @@
mod app; mod app;
mod step_indicator; mod step_indicator;
mod steps;
mod wizard; mod wizard;
use gtk::prelude::*; use gtk::prelude::*;

View File

@@ -0,0 +1,7 @@
pub mod step_compress;
pub mod step_convert;
pub mod step_images;
pub mod step_metadata;
pub mod step_output;
pub mod step_resize;
pub mod step_workflow;

View File

@@ -0,0 +1,125 @@
use adw::prelude::*;
pub fn build_compress_page() -> 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();
// Enable toggle
let enable_row = adw::SwitchRow::builder()
.title("Enable Compression")
.subtitle("Reduce file size with quality control")
.active(true)
.build();
let enable_group = adw::PreferencesGroup::new();
enable_group.add(&enable_row);
content.append(&enable_group);
// Quality slider
let quality_group = adw::PreferencesGroup::builder()
.title("Quality Level")
.description("Higher quality means larger files")
.build();
let quality_scale = gtk::Scale::builder()
.orientation(gtk::Orientation::Horizontal)
.adjustment(&gtk::Adjustment::new(3.0, 1.0, 5.0, 1.0, 1.0, 0.0))
.draw_value(false)
.hexpand(true)
.build();
quality_scale.set_round_digits(0);
// Add named marks
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(3.0, gtk::PositionType::Bottom, Some("Medium"));
quality_scale.add_mark(4.0, gtk::PositionType::Bottom, Some("High"));
quality_scale.add_mark(5.0, gtk::PositionType::Bottom, Some("Maximum"));
let quality_label = gtk::Label::builder()
.label("Medium - Good balance of quality and size")
.css_classes(["dim-label"])
.margin_top(4)
.build();
let quality_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.margin_top(8)
.margin_bottom(8)
.margin_start(12)
.margin_end(12)
.build();
quality_box.append(&quality_scale);
quality_box.append(&quality_label);
quality_group.add(&quality_box);
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(&gtk::Image::from_icon_name("drive-harddisk-symbolic"));
estimate_group.add(&estimate_row);
content.append(&estimate_group);
// Advanced options
let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced Options")
.build();
let jpeg_row = adw::SpinRow::builder()
.title("JPEG Quality")
.subtitle("1-100, higher is better quality")
.adjustment(&gtk::Adjustment::new(85.0, 1.0, 100.0, 1.0, 10.0, 0.0))
.build();
let png_row = adw::SpinRow::builder()
.title("PNG Compression Level")
.subtitle("1-6, higher is slower but smaller")
.adjustment(&gtk::Adjustment::new(3.0, 1.0, 6.0, 1.0, 1.0, 0.0))
.build();
let webp_row = adw::SpinRow::builder()
.title("WebP Quality")
.subtitle("1-100, higher is better quality")
.adjustment(&gtk::Adjustment::new(80.0, 1.0, 100.0, 1.0, 10.0, 0.0))
.build();
advanced_group.add(&jpeg_row);
advanced_group.add(&png_row);
advanced_group.add(&webp_row);
content.append(&advanced_group);
scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder()
.maximum_size(600)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Compress")
.tag("step-compress")
.child(&clamp)
.build()
}

View File

@@ -0,0 +1,107 @@
use adw::prelude::*;
pub fn build_convert_page() -> 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();
// Enable toggle
let enable_row = adw::SwitchRow::builder()
.title("Enable Format Conversion")
.subtitle("Convert images to a different format")
.active(false)
.build();
let enable_group = adw::PreferencesGroup::new();
enable_group.add(&enable_row);
content.append(&enable_group);
// Format selection
let format_group = adw::PreferencesGroup::builder()
.title("Output Format")
.build();
let formats = [
("Keep Original", "No conversion - output in same format as input"),
("JPEG", "Universal photo format, lossy compression"),
("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();
for (name, desc) in &formats {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.build();
card.add_css_class("card");
card.set_size_request(140, 80);
let inner = gtk::Box::builder()
.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);
scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder()
.maximum_size(600)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Convert")
.tag("step-convert")
.child(&clamp)
.build()
}

View File

@@ -0,0 +1,142 @@
use adw::prelude::*;
pub fn build_images_page() -> adw::NavigationPage {
let stack = gtk::Stack::builder()
.transition_type(gtk::StackTransitionType::Crossfade)
.build();
// Empty state - drop zone
let empty_state = build_empty_state();
stack.add_named(&empty_state, Some("empty"));
// Loaded state - thumbnail grid (placeholder for now)
let loaded_state = build_loaded_state();
stack.add_named(&loaded_state, Some("loaded"));
stack.set_visible_child_name("empty");
adw::NavigationPage::builder()
.title("Add Images")
.tag("step-images")
.child(&stack)
.build()
}
fn build_empty_state() -> gtk::Box {
let container = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.vexpand(true)
.hexpand(true)
.build();
let drop_zone = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.vexpand(true)
.margin_top(48)
.margin_bottom(48)
.margin_start(48)
.margin_end(48)
.build();
drop_zone.add_css_class("card");
let inner = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.margin_top(48)
.margin_bottom(48)
.margin_start(64)
.margin_end(64)
.halign(gtk::Align::Center)
.build();
let icon = gtk::Image::builder()
.icon_name("folder-pictures-symbolic")
.pixel_size(64)
.css_classes(["dim-label"])
.build();
let title = gtk::Label::builder()
.label("Drop images here")
.css_classes(["title-2"])
.build();
let subtitle = gtk::Label::builder()
.label("or click Browse to select files")
.css_classes(["dim-label"])
.build();
let browse_button = gtk::Button::builder()
.label("Browse Files")
.tooltip_text("Add image files (Ctrl+O)")
.halign(gtk::Align::Center)
.build();
browse_button.add_css_class("suggested-action");
browse_button.add_css_class("pill");
inner.append(&icon);
inner.append(&title);
inner.append(&subtitle);
inner.append(&browse_button);
drop_zone.append(&inner);
container.append(&drop_zone);
container
}
fn build_loaded_state() -> gtk::Box {
let container = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(0)
.build();
// Toolbar
let toolbar = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.margin_start(12)
.margin_end(12)
.margin_top(8)
.margin_bottom(8)
.build();
let count_label = gtk::Label::builder()
.label("0 images (0 B)")
.hexpand(true)
.halign(gtk::Align::Start)
.css_classes(["heading"])
.build();
let add_button = gtk::Button::builder()
.icon_name("list-add-symbolic")
.tooltip_text("Add more images")
.build();
add_button.add_css_class("flat");
let select_all_button = gtk::Button::builder()
.label("Select All")
.tooltip_text("Select all images (Ctrl+A)")
.build();
select_all_button.add_css_class("flat");
toolbar.append(&count_label);
toolbar.append(&add_button);
toolbar.append(&select_all_button);
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
// Thumbnail grid placeholder
let grid_placeholder = adw::StatusPage::builder()
.title("Images will appear here")
.icon_name("image-x-generic-symbolic")
.vexpand(true)
.build();
container.append(&toolbar);
container.append(&separator);
container.append(&grid_placeholder);
container
}

View File

@@ -0,0 +1,109 @@
use adw::prelude::*;
pub fn build_metadata_page() -> 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();
// Enable toggle
let enable_row = adw::SwitchRow::builder()
.title("Enable Metadata Handling")
.subtitle("Control what image metadata to keep or remove")
.active(true)
.build();
let enable_group = adw::PreferencesGroup::new();
enable_group.add(&enable_row);
content.append(&enable_group);
// Quick presets
let presets_group = adw::PreferencesGroup::builder()
.title("Metadata Mode")
.build();
let strip_all_row = adw::ActionRow::builder()
.title("Strip All")
.subtitle("Remove all metadata - smallest files, maximum privacy")
.activatable(true)
.build();
strip_all_row.add_prefix(&gtk::Image::from_icon_name("user-trash-symbolic"));
let strip_all_check = gtk::CheckButton::new();
strip_all_check.set_active(true);
strip_all_row.add_suffix(&strip_all_check);
strip_all_row.set_activatable_widget(Some(&strip_all_check));
let privacy_row = adw::ActionRow::builder()
.title("Privacy Mode")
.subtitle("Strip GPS and camera serial, keep copyright")
.activatable(true)
.build();
privacy_row.add_prefix(&gtk::Image::from_icon_name("security-medium-symbolic"));
let privacy_check = gtk::CheckButton::new();
privacy_check.set_group(Some(&strip_all_check));
privacy_row.add_suffix(&privacy_check);
privacy_row.set_activatable_widget(Some(&privacy_check));
let keep_all_row = adw::ActionRow::builder()
.title("Keep All")
.subtitle("Preserve all original metadata")
.activatable(true)
.build();
keep_all_row.add_prefix(&gtk::Image::from_icon_name("emblem-ok-symbolic"));
let keep_all_check = gtk::CheckButton::new();
keep_all_check.set_group(Some(&strip_all_check));
keep_all_row.add_suffix(&keep_all_check);
keep_all_row.set_activatable_widget(Some(&keep_all_check));
presets_group.add(&strip_all_row);
presets_group.add(&privacy_row);
presets_group.add(&keep_all_row);
content.append(&presets_group);
// Advanced - per-category controls
let advanced_group = adw::PreferencesGroup::builder()
.title("Fine-Grained Control")
.description("Choose exactly which metadata categories to strip")
.build();
let categories = [
("GPS / Location Data", "Coordinates, altitude, location name", true),
("Camera Info", "Camera model, serial number, lens info", true),
("Software / Editing", "Software used, editing history", true),
("Timestamps", "Date taken, date modified", false),
("Copyright / Author", "Copyright notice, creator name", false),
];
for (title, subtitle, default_strip) in &categories {
let row = adw::SwitchRow::builder()
.title(format!("Strip {}", title))
.subtitle(*subtitle)
.active(*default_strip)
.build();
advanced_group.add(&row);
}
content.append(&advanced_group);
scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder()
.maximum_size(600)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Metadata")
.tag("step-metadata")
.child(&clamp)
.build()
}

View File

@@ -0,0 +1,110 @@
use adw::prelude::*;
pub fn build_output_page() -> 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
let summary_group = adw::PreferencesGroup::builder()
.title("Operation Summary")
.description("Review your processing settings before starting")
.build();
let summary_placeholder = adw::ActionRow::builder()
.title("No operations configured")
.subtitle("Go back and configure your workflow settings")
.build();
summary_placeholder.add_prefix(&gtk::Image::from_icon_name("dialog-information-symbolic"));
summary_group.add(&summary_placeholder);
content.append(&summary_group);
// Output directory
let output_group = adw::PreferencesGroup::builder()
.title("Output Directory")
.build();
let output_row = adw::ActionRow::builder()
.title("Output Location")
.subtitle("processed/ (subfolder next to originals)")
.activatable(true)
.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)
.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 structure_row = adw::SwitchRow::builder()
.title("Preserve Directory Structure")
.subtitle("Keep subfolder hierarchy in output")
.active(false)
.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_group.add(&overwrite_row);
content.append(&overwrite_group);
// Image count
let stats_group = adw::PreferencesGroup::builder()
.title("Batch Info")
.build();
let count_row = adw::ActionRow::builder()
.title("Images to process")
.subtitle("0 images")
.build();
count_row.add_prefix(&gtk::Image::from_icon_name("image-x-generic-symbolic"));
stats_group.add(&count_row);
content.append(&stats_group);
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()
}

View File

@@ -0,0 +1,189 @@
use adw::prelude::*;
pub fn build_resize_page() -> 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();
// Enable toggle
let enable_row = adw::SwitchRow::builder()
.title("Enable Resize")
.subtitle("Resize images to new dimensions")
.active(true)
.build();
let enable_group = adw::PreferencesGroup::new();
enable_group.add(&enable_row);
content.append(&enable_group);
// Resize mode selector
let mode_group = adw::PreferencesGroup::builder()
.title("Resize Mode")
.build();
// Width/Height mode
let width_row = adw::SpinRow::builder()
.title("Width")
.subtitle("Target width in pixels")
.adjustment(&gtk::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0))
.build();
let height_row = adw::SpinRow::builder()
.title("Height")
.subtitle("Target height in pixels (0 = auto from aspect ratio)")
.adjustment(&gtk::Adjustment::new(0.0, 0.0, 10000.0, 1.0, 100.0, 0.0))
.build();
mode_group.add(&width_row);
mode_group.add(&height_row);
content.append(&mode_group);
// Social media presets
let presets_group = adw::PreferencesGroup::builder()
.title("Quick Dimension Presets")
.build();
let fedi_expander = adw::ExpanderRow::builder()
.title("Fediverse / Open Platforms")
.subtitle("Mastodon, Pixelfed, Bluesky, Lemmy")
.build();
let fedi_presets = [
("Mastodon Post", "1920 x 1080"),
("Mastodon Profile", "400 x 400"),
("Mastodon Header", "1500 x 500"),
("Pixelfed Post", "1080 x 1080"),
("Pixelfed Story", "1080 x 1920"),
("Bluesky Post", "1200 x 630"),
("Bluesky Profile", "400 x 400"),
("Lemmy Post", "1200 x 630"),
];
for (name, dims) in &fedi_presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(*dims)
.activatable(true)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
fedi_expander.add_row(&row);
}
let mainstream_expander = adw::ExpanderRow::builder()
.title("Mainstream Platforms")
.subtitle("Instagram, YouTube, LinkedIn, Pinterest")
.build();
let mainstream_presets = [
("Instagram Post", "1080 x 1080"),
("Instagram Story/Reel", "1080 x 1920"),
("Facebook Post", "1200 x 630"),
("YouTube Thumbnail", "1280 x 720"),
("LinkedIn Post", "1200 x 627"),
("Pinterest Pin", "1000 x 1500"),
];
for (name, dims) in &mainstream_presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(*dims)
.activatable(true)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
mainstream_expander.add_row(&row);
}
let other_expander = adw::ExpanderRow::builder()
.title("Common Sizes")
.subtitle("HD, Blog, Thumbnail")
.build();
let other_presets = [
("Full HD", "1920 x 1080"),
("Blog Image", "800 wide"),
("Thumbnail", "150 x 150"),
];
for (name, dims) in &other_presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(*dims)
.activatable(true)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
other_expander.add_row(&row);
}
presets_group.add(&fedi_expander);
presets_group.add(&mainstream_expander);
presets_group.add(&other_expander);
content.append(&presets_group);
// Basic adjustments (rotation/flip)
let adjust_group = adw::PreferencesGroup::builder()
.title("Basic Adjustments")
.build();
let rotate_row = adw::ComboRow::builder()
.title("Rotate")
.subtitle("Rotation applied after resize")
.build();
let rotate_model = gtk::StringList::new(&["None", "90 clockwise", "180", "270 clockwise", "Auto-orient (EXIF)"]);
rotate_row.set_model(Some(&rotate_model));
let flip_row = adw::ComboRow::builder()
.title("Flip")
.subtitle("Mirror the image")
.build();
let flip_model = gtk::StringList::new(&["None", "Horizontal", "Vertical"]);
flip_row.set_model(Some(&flip_model));
adjust_group.add(&rotate_row);
adjust_group.add(&flip_row);
content.append(&adjust_group);
// Advanced options
let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced Options")
.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()
.title("Allow Upscaling")
.subtitle("Enlarge images smaller than target size")
.active(false)
.build();
advanced_group.add(&algo_row);
advanced_group.add(&upscale_row);
content.append(&advanced_group);
scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder()
.maximum_size(600)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Resize")
.tag("step-resize")
.child(&clamp)
.build()
}

View File

@@ -0,0 +1,179 @@
use adw::prelude::*;
use pixstrip_core::preset::Preset;
pub fn build_workflow_page() -> 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();
// Built-in presets section
let builtin_group = adw::PreferencesGroup::builder()
.title("Built-in Workflows")
.description("Select a preset to get started quickly")
.build();
let builtin_flow = gtk::FlowBox::builder()
.selection_mode(gtk::SelectionMode::Single)
.max_children_per_line(4)
.min_children_per_line(2)
.row_spacing(8)
.column_spacing(8)
.homogeneous(true)
.build();
for preset in Preset::all_builtins() {
let card = build_preset_card(&preset);
builtin_flow.append(&card);
}
builtin_group.add(&builtin_flow);
content.append(&builtin_group);
// Custom workflow section
let custom_group = adw::PreferencesGroup::builder()
.title("Custom Workflow")
.description("Choose which operations to include")
.build();
let custom_card = build_custom_card();
let custom_flow = gtk::FlowBox::builder()
.selection_mode(gtk::SelectionMode::None)
.max_children_per_line(4)
.min_children_per_line(2)
.build();
custom_flow.append(&custom_card);
custom_group.add(&custom_flow);
content.append(&custom_group);
// User presets section (initially empty)
let user_group = adw::PreferencesGroup::builder()
.title("Your Presets")
.description("Import or save your own workflows")
.build();
let import_button = gtk::Button::builder()
.label("Import Preset")
.icon_name("document-open-symbolic")
.build();
import_button.add_css_class("flat");
user_group.add(&import_button);
content.append(&user_group);
scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder()
.maximum_size(800)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Choose a Workflow")
.tag("step-workflow")
.child(&clamp)
.build()
}
fn build_preset_card(preset: &Preset) -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.halign(gtk::Align::Center)
.valign(gtk::Align::Start)
.build();
card.add_css_class("card");
card.set_size_request(180, 120);
let inner = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.margin_top(16)
.margin_bottom(16)
.margin_start(12)
.margin_end(12)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.vexpand(true)
.build();
let icon = gtk::Image::builder()
.icon_name(&preset.icon)
.pixel_size(32)
.build();
let name_label = gtk::Label::builder()
.label(&preset.name)
.css_classes(["heading"])
.build();
let desc_label = gtk::Label::builder()
.label(&preset.description)
.css_classes(["caption", "dim-label"])
.wrap(true)
.justify(gtk::Justification::Center)
.max_width_chars(20)
.build();
inner.append(&icon);
inner.append(&name_label);
inner.append(&desc_label);
card.append(&inner);
card
}
fn build_custom_card() -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.halign(gtk::Align::Center)
.valign(gtk::Align::Start)
.build();
card.add_css_class("card");
card.set_size_request(180, 120);
let inner = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.margin_top(16)
.margin_bottom(16)
.margin_start(12)
.margin_end(12)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.vexpand(true)
.build();
let icon = gtk::Image::builder()
.icon_name("applications-system-symbolic")
.pixel_size(32)
.build();
let name_label = gtk::Label::builder()
.label("Custom...")
.css_classes(["heading"])
.build();
let desc_label = gtk::Label::builder()
.label("Pick your own operations")
.css_classes(["caption", "dim-label"])
.wrap(true)
.justify(gtk::Justification::Center)
.build();
inner.append(&icon);
inner.append(&name_label);
inner.append(&desc_label);
card.append(&inner);
card
}

View File

@@ -1,3 +1,5 @@
use crate::steps;
pub struct WizardState { pub struct WizardState {
pub current_step: usize, pub current_step: usize,
pub total_steps: usize, pub total_steps: usize,
@@ -59,30 +61,13 @@ impl WizardState {
} }
pub fn build_wizard_pages() -> Vec<adw::NavigationPage> { pub fn build_wizard_pages() -> Vec<adw::NavigationPage> {
let steps = [ vec![
("step-workflow", "Choose a Workflow", "image-x-generic-symbolic", "Select a preset or build a custom workflow"), steps::step_workflow::build_workflow_page(),
("step-images", "Add Images", "folder-pictures-symbolic", "Drop images here or click Browse"), steps::step_images::build_images_page(),
("step-resize", "Resize", "view-fullscreen-symbolic", "Set output dimensions"), steps::step_resize::build_resize_page(),
("step-convert", "Convert", "document-save-symbolic", "Choose output format"), steps::step_convert::build_convert_page(),
("step-compress", "Compress", "system-file-manager-symbolic", "Set compression quality"), steps::step_compress::build_compress_page(),
("step-metadata", "Metadata", "security-high-symbolic", "Control metadata privacy"), steps::step_metadata::build_metadata_page(),
("step-output", "Output & Process", "emblem-ok-symbolic", "Review and process"), steps::step_output::build_output_page(),
]; ]
steps
.iter()
.map(|(tag, title, icon, description)| {
let status_page = adw::StatusPage::builder()
.title(*title)
.description(*description)
.icon_name(*icon)
.build();
adw::NavigationPage::builder()
.title(*title)
.tag(*tag)
.child(&status_page)
.build()
})
.collect()
} }