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:
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod step_indicator;
|
||||
mod steps;
|
||||
mod wizard;
|
||||
|
||||
use gtk::prelude::*;
|
||||
|
||||
7
pixstrip-gtk/src/steps/mod.rs
Normal file
7
pixstrip-gtk/src/steps/mod.rs
Normal 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;
|
||||
125
pixstrip-gtk/src/steps/step_compress.rs
Normal file
125
pixstrip-gtk/src/steps/step_compress.rs
Normal 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(>k::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(>k::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(>k::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(>k::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(>k::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()
|
||||
}
|
||||
107
pixstrip-gtk/src/steps/step_convert.rs
Normal file
107
pixstrip-gtk/src/steps/step_convert.rs
Normal 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()
|
||||
}
|
||||
142
pixstrip-gtk/src/steps/step_images.rs
Normal file
142
pixstrip-gtk/src/steps/step_images.rs
Normal 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
|
||||
}
|
||||
109
pixstrip-gtk/src/steps/step_metadata.rs
Normal file
109
pixstrip-gtk/src/steps/step_metadata.rs
Normal 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(>k::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(>k::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(>k::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()
|
||||
}
|
||||
110
pixstrip-gtk/src/steps/step_output.rs
Normal file
110
pixstrip-gtk/src/steps/step_output.rs
Normal 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(>k::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(>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)
|
||||
.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 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(>k::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()
|
||||
}
|
||||
189
pixstrip-gtk/src/steps/step_resize.rs
Normal file
189
pixstrip-gtk/src/steps/step_resize.rs
Normal 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(>k::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(>k::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(>k::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(>k::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(>k::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()
|
||||
}
|
||||
179
pixstrip-gtk/src/steps/step_workflow.rs
Normal file
179
pixstrip-gtk/src/steps/step_workflow.rs
Normal 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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::steps;
|
||||
|
||||
pub struct WizardState {
|
||||
pub current_step: usize,
|
||||
pub total_steps: usize,
|
||||
@@ -59,30 +61,13 @@ impl WizardState {
|
||||
}
|
||||
|
||||
pub fn build_wizard_pages() -> Vec<adw::NavigationPage> {
|
||||
let steps = [
|
||||
("step-workflow", "Choose a Workflow", "image-x-generic-symbolic", "Select a preset or build a custom workflow"),
|
||||
("step-images", "Add Images", "folder-pictures-symbolic", "Drop images here or click Browse"),
|
||||
("step-resize", "Resize", "view-fullscreen-symbolic", "Set output dimensions"),
|
||||
("step-convert", "Convert", "document-save-symbolic", "Choose output format"),
|
||||
("step-compress", "Compress", "system-file-manager-symbolic", "Set compression quality"),
|
||||
("step-metadata", "Metadata", "security-high-symbolic", "Control metadata privacy"),
|
||||
("step-output", "Output & Process", "emblem-ok-symbolic", "Review and process"),
|
||||
];
|
||||
|
||||
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()
|
||||
vec![
|
||||
steps::step_workflow::build_workflow_page(),
|
||||
steps::step_images::build_images_page(),
|
||||
steps::step_resize::build_resize_page(),
|
||||
steps::step_convert::build_convert_page(),
|
||||
steps::step_compress::build_compress_page(),
|
||||
steps::step_metadata::build_metadata_page(),
|
||||
steps::step_output::build_output_page(),
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user