diff --git a/pixstrip-gtk/src/main.rs b/pixstrip-gtk/src/main.rs index e8ce251..99f055f 100644 --- a/pixstrip-gtk/src/main.rs +++ b/pixstrip-gtk/src/main.rs @@ -1,5 +1,6 @@ mod app; mod step_indicator; +mod steps; mod wizard; use gtk::prelude::*; diff --git a/pixstrip-gtk/src/steps/mod.rs b/pixstrip-gtk/src/steps/mod.rs new file mode 100644 index 0000000..8e39277 --- /dev/null +++ b/pixstrip-gtk/src/steps/mod.rs @@ -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; diff --git a/pixstrip-gtk/src/steps/step_compress.rs b/pixstrip-gtk/src/steps/step_compress.rs new file mode 100644 index 0000000..58b3687 --- /dev/null +++ b/pixstrip-gtk/src/steps/step_compress.rs @@ -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() +} diff --git a/pixstrip-gtk/src/steps/step_convert.rs b/pixstrip-gtk/src/steps/step_convert.rs new file mode 100644 index 0000000..0951c86 --- /dev/null +++ b/pixstrip-gtk/src/steps/step_convert.rs @@ -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() +} diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs new file mode 100644 index 0000000..043f0a8 --- /dev/null +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -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 +} diff --git a/pixstrip-gtk/src/steps/step_metadata.rs b/pixstrip-gtk/src/steps/step_metadata.rs new file mode 100644 index 0000000..d5ed641 --- /dev/null +++ b/pixstrip-gtk/src/steps/step_metadata.rs @@ -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() +} diff --git a/pixstrip-gtk/src/steps/step_output.rs b/pixstrip-gtk/src/steps/step_output.rs new file mode 100644 index 0000000..5191750 --- /dev/null +++ b/pixstrip-gtk/src/steps/step_output.rs @@ -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() +} diff --git a/pixstrip-gtk/src/steps/step_resize.rs b/pixstrip-gtk/src/steps/step_resize.rs new file mode 100644 index 0000000..d2afece --- /dev/null +++ b/pixstrip-gtk/src/steps/step_resize.rs @@ -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() +} diff --git a/pixstrip-gtk/src/steps/step_workflow.rs b/pixstrip-gtk/src/steps/step_workflow.rs new file mode 100644 index 0000000..a598f53 --- /dev/null +++ b/pixstrip-gtk/src/steps/step_workflow.rs @@ -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 +} diff --git a/pixstrip-gtk/src/wizard.rs b/pixstrip-gtk/src/wizard.rs index d94553a..e9c20b3 100644 --- a/pixstrip-gtk/src/wizard.rs +++ b/pixstrip-gtk/src/wizard.rs @@ -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 { - 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(), + ] }