From f3668c45c315920a4a9136206e3d2cb41ddd3324 Mon Sep 17 00:00:00 2001 From: lashman Date: Sun, 8 Mar 2026 14:18:15 +0200 Subject: [PATCH] Improve UX, add popover tour, metadata, and hicolor icons - Redesign tutorial tour from modal dialogs to popovers pointing at actual UI elements - Add beginner-friendly improvements: help buttons, tooltips, welcome wizard enhancements - Add AppStream metainfo with screenshots, branding, categories, keywords, provides - Update desktop file with GTK category and SingleMainWindow - Add hicolor icon theme with all sizes (16-512px) - Fix debounce SourceId panic in rename step - Various step UI improvements and bug fixes --- .../128x128/apps/live.lashman.Pixstrip.png | Bin 0 -> 13281 bytes .../16x16/apps/live.lashman.Pixstrip.png | Bin 0 -> 894 bytes .../256x256/apps/live.lashman.Pixstrip.png | Bin 0 -> 31627 bytes .../32x32/apps/live.lashman.Pixstrip.png | Bin 0 -> 2347 bytes .../48x48/apps/live.lashman.Pixstrip.png | Bin 0 -> 4052 bytes .../512x512/apps/live.lashman.Pixstrip.png | Bin 0 -> 77707 bytes .../64x64/apps/live.lashman.Pixstrip.png | Bin 0 -> 5864 bytes data/live.lashman.Pixstrip.desktop | 3 +- data/live.lashman.Pixstrip.metainfo.xml | 123 +- pixstrip-cli/src/main.rs | 1082 ++++++++++++++--- pixstrip-core/src/preset.rs | 11 + pixstrip-core/src/storage.rs | 1 + pixstrip-gtk/src/app.rs | 455 +++++-- pixstrip-gtk/src/processing.rs | 52 +- pixstrip-gtk/src/settings.rs | 42 +- pixstrip-gtk/src/step_indicator.rs | 20 + pixstrip-gtk/src/steps/step_adjustments.rs | 40 +- pixstrip-gtk/src/steps/step_compress.rs | 105 +- pixstrip-gtk/src/steps/step_convert.rs | 15 +- pixstrip-gtk/src/steps/step_images.rs | 19 + pixstrip-gtk/src/steps/step_metadata.rs | 42 +- pixstrip-gtk/src/steps/step_rename.rs | 37 +- pixstrip-gtk/src/steps/step_resize.rs | 75 +- pixstrip-gtk/src/steps/step_watermark.rs | 73 +- pixstrip-gtk/src/steps/step_workflow.rs | 298 ++++- pixstrip-gtk/src/tutorial.rs | 272 +++-- 26 files changed, 2292 insertions(+), 473 deletions(-) create mode 100644 data/icons/hicolor/128x128/apps/live.lashman.Pixstrip.png create mode 100644 data/icons/hicolor/16x16/apps/live.lashman.Pixstrip.png create mode 100644 data/icons/hicolor/256x256/apps/live.lashman.Pixstrip.png create mode 100644 data/icons/hicolor/32x32/apps/live.lashman.Pixstrip.png create mode 100644 data/icons/hicolor/48x48/apps/live.lashman.Pixstrip.png create mode 100644 data/icons/hicolor/512x512/apps/live.lashman.Pixstrip.png create mode 100644 data/icons/hicolor/64x64/apps/live.lashman.Pixstrip.png diff --git a/data/icons/hicolor/128x128/apps/live.lashman.Pixstrip.png b/data/icons/hicolor/128x128/apps/live.lashman.Pixstrip.png new file mode 100644 index 0000000000000000000000000000000000000000..a724dbcc7438ce3917f6d23bc5c158ed76ebfcb9 GIT binary patch literal 13281 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 WaZ|41lGlHB017fHuxcrj;Qt3292a2# literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/16x16/apps/live.lashman.Pixstrip.png b/data/icons/hicolor/16x16/apps/live.lashman.Pixstrip.png new file mode 100644 index 0000000000000000000000000000000000000000..48b9483665e5bd5aa4c6f1df7aa415c34f0e6714 GIT binary patch literal 894 zcmV-^1A+XBP)Azq-tiV;nTy_rbeZ6T0ASxGO{t-{jS2m);lN{ERjcuDvN_!l~- zdCcr8kmv{Be*?_i*2c2(c{L7%t$bY zy;c4IAr(BuNBs+_ZLYVw{b_XdbYOqqKJ*WMh|kA{Fn&6Vi9#OdzWx&NM`MaQ6fDJ# z5<;qcK&8xxT&-1y(cGZ7^y?3py;{WV(mZAsFXHUgA_~_E_;z^;b61MkvGZ{ijqBh^ zo+TQ2&<9k>j2sJ#T72?ppEq^wBEFc*<9KljC+4SdtT>JQ!VG4 zXoHSzEsuHMT$;i0ua4oum02v`SiqmRZ{m;Nf5LLU4R?MygInicRn5%?Y9-AzHI)IC zJjrv^rLY_8)%C@B{Pyz_zF#O{`s@je9zBBn2j50#`xZ>}XYgKkOi`DEJ3LEtvbK~& z(=mIkv<0KV@XUm%>MTu#SXc#VNCnJIDqsXuSVWX2qpD8oD7e<$o6;24hSKR<5X3P>WBPhKo3-FLJRzt{28HSy=~j;h*OU7U#|0J UR$!n%rvLx|07*qoM6N<$f;CmJ7XSbN literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/256x256/apps/live.lashman.Pixstrip.png b/data/icons/hicolor/256x256/apps/live.lashman.Pixstrip.png new file mode 100644 index 0000000000000000000000000000000000000000..527862c6bc95662aa46f613aea959f85dbf8c880 GIT binary patch literal 31627 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 literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/32x32/apps/live.lashman.Pixstrip.png b/data/icons/hicolor/32x32/apps/live.lashman.Pixstrip.png new file mode 100644 index 0000000000000000000000000000000000000000..ea94598e041ad8f6ed8bb53c0295dfd41ac4f809 GIT binary patch literal 2347 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 literal 0 HcmV?d00001 diff --git a/data/icons/hicolor/64x64/apps/live.lashman.Pixstrip.png b/data/icons/hicolor/64x64/apps/live.lashman.Pixstrip.png new file mode 100644 index 0000000000000000000000000000000000000000..5f134a060916a17485dfba707b3cab4a01fd9de1 GIT binary patch literal 5864 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.lashman.Pixstrip CC0-1.0 CC0-1.0 + Pixstrip Batch image processor - resize, convert, compress, and more

- Pixstrip is a batch image processor for Linux that combines resize, convert, - compress, metadata strip, watermark, rename, and basic image adjustments into - a single wizard-driven workflow. + Pixstrip is a native GTK4/libadwaita batch image processor for Linux + that combines resize, convert, compress, metadata strip, watermark, + rename, and image adjustments into a single wizard-driven workflow. + It processes everything locally with no cloud dependency.

-

Features include:

+

Key features:

  • Resize images by width, height, fit-in-box, or social media presets
  • Convert between JPEG, PNG, WebP, AVIF, GIF, and TIFF
  • @@ -21,42 +23,137 @@
  • Add text or image watermarks with positioning and rotation
  • Rename files with templates, counters, regex, and EXIF variables
  • Adjust brightness, contrast, saturation, and apply effects
  • -
  • Built-in presets for common workflows
  • +
  • Built-in presets for common workflows with one-click processing
  • +
  • Custom workflow builder with step-by-step wizard
  • Watch folders for automatic processing
  • -
  • Full CLI with feature parity
  • +
  • Processing history with undo via system trash
  • +
  • Full CLI with feature parity for scripting and automation
  • +
  • File manager integration for Nautilus, Nemo, Thunar, and Dolphin
- live.lashman.Pixstrip.desktop + live.lashman.Pixstrip - https://git.lashman.live/lashman/pixstrip - https://git.lashman.live/lashman/pixstrip/issues + live.lashman.Pixstrip.desktop lashman + https://git.lashman.live/lashman/pixstrip + https://git.lashman.live/lashman/pixstrip/issues + https://git.lashman.live/lashman/pixstrip + https://ko-fi.com/lashman + https://git.lashman.live/lashman/pixstrip/issues + https://git.lashman.live/lashman/pixstrip + + lashman@robotbrush.com + + + + Workflow selection with built-in presets for common image tasks + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/01.png + + + Image selection with drag-and-drop and batch file management + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/02.png + + + Resize step with width, height, fit-in-box, and social media presets + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/03.png + + + Format conversion between JPEG, PNG, WebP, AVIF, GIF, and TIFF + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/04.png + + + Compression with live before/after preview and file size estimates + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/05.png + + + Metadata stripping with selective EXIF field management + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/06.png + + + Watermark placement with text and image options + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/07.png + + + Batch rename with templates, counters, and EXIF variables + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/08.png + + + Image adjustments for brightness, contrast, and saturation + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/09.png + + + Settings with output preferences and file manager integration + https://git.lashman.live/lashman/pixstrip/raw/branch/main/data/screenshots/10.png + + + - #99c1f1 - #1a5fb4 + #57a773 + #263226 + + Graphics + ImageProcessing + GTK + + + + Image + Photo + Resize + Convert + Compress + Batch + Metadata + Watermark + Rename + EXIF + WebP + AVIF + + 360 + + keyboard + pointing + + pointing keyboard touch + + pixstrip-gtk + pixstrip + + - + -

Initial release with full wizard workflow, 8 built-in presets, CLI parity, watch folders, and file manager integration.

+

Initial release of Pixstrip with core features:

+
    +
  • Wizard-driven batch processing with 8 built-in presets
  • +
  • Resize, convert, compress, metadata strip, watermark, rename, and adjust
  • +
  • Optimized encoders: mozjpeg, oxipng, libwebp, and ravif
  • +
  • Live compression preview with before/after comparison
  • +
  • Watch folders for automatic processing
  • +
  • Processing history with undo via system trash
  • +
  • Full CLI with feature parity
  • +
  • File manager integration for Nautilus, Nemo, Thunar, and Dolphin
  • +
diff --git a/pixstrip-cli/src/main.rs b/pixstrip-cli/src/main.rs index afdfa35..a07d17f 100644 --- a/pixstrip-cli/src/main.rs +++ b/pixstrip-cli/src/main.rs @@ -27,6 +27,33 @@ fn load_watches(watches_path: &std::path::Path) -> Vec + pixstrip process --help")] struct Cli { #[command(subcommand)] command: Commands, @@ -36,98 +63,356 @@ struct Cli { #[allow(clippy::large_enum_variant)] enum Commands { /// Process images with a preset or custom operations + #[command(after_long_help = "\ +EXAMPLES: + Resize all JPEGs in a folder to 1200px wide: + pixstrip process photos/ --resize 1200 -o output/ + + Fit images within a 1920x1080 box (no upscaling): + pixstrip process photos/ --resize fit:1920x1080 + + Convert all PNGs to WebP with high quality: + pixstrip process *.png --format webp --quality high + + Per-format conversion (PNGs to WebP, TIFFs to JPEG): + pixstrip process photos/ --format-map \"png=webp,tiff=jpeg\" + + Fine-tune compression per format: + pixstrip process photos/ --jpeg-quality 90 --webp-quality 85 --avif-quality 70 + + Strip location and camera data, keep copyright: + pixstrip process photos/ --strip-gps --strip-camera + + Adjust brightness/contrast and convert to grayscale: + pixstrip process photos/ --brightness 10 --contrast 20 --grayscale + + Add a tiled diagonal watermark in red: + pixstrip process photos/ --watermark \"(c) 2026\" --watermark-tiled \\ + --watermark-rotation 45 --watermark-color ff0000 --watermark-opacity 0.3 + + Add an image watermark (logo) scaled to 15%: + pixstrip process photos/ --watermark-image logo.png --watermark-scale 0.15 + + Rename to lowercase with hyphens, add prefix: + pixstrip process photos/ --rename-prefix \"blog-\" --rename-case lower \\ + --rename-spaces hyphen + + Rename with regex (remove \"IMG_\" prefix): + pixstrip process photos/ --rename-regex-find \"^IMG_\" --rename-regex-replace \"\" + + Sequential numbering (replace filename with counter): + pixstrip process photos/ --rename-counter-position replace-name \\ + --rename-counter-start 1 --rename-counter-padding 4 + + Use a saved preset: + pixstrip process photos/ --preset \"Blog Photos\" + + Override preset options (preset + custom flags): + pixstrip process photos/ --preset \"Blog Photos\" --resize 800 + + Combine multiple operations: + pixstrip process photos/ -r --resize fit:1920x1080 --format webp \\ + --quality high --strip-metadata --rename-case lower \\ + --watermark \"(c) 2026\" --overwrite skip + +TEMPLATE VARIABLES (for --rename-template): + {name} Original filename (without extension) + {ext} File extension + {counter} Sequential number (padding from --rename-counter-padding) + {counter:N} Sequential number with N-digit padding + {width} Image width in pixels + {height} Image height in pixels + {date} File modification date (YYYY-MM-DD) + + Example: --rename-template \"{name}_{width}x{height}.{ext}\"")] Process { /// Input files or directories #[arg(required = true)] input: Vec, - /// Preset name to use + /// Preset name to use (see 'pixstrip preset list' for available presets) #[arg(long)] preset: Option, - /// Output directory + /// Output directory [default: /processed] #[arg(short, long)] output: Option, - /// Resize to width (e.g., "1200" or "1200x900") - #[arg(long)] + // --- Resize --- + + /// Resize images. Formats: "1200" (width), "1200x900" (exact), "fit:1200x900" (fit in box) + #[arg(long, help_heading = "Resize")] resize: Option, - /// Output format (jpeg, png, webp, avif, gif, tiff) - #[arg(long)] - format: Option, - - /// Quality preset (maximum, high, medium, low, web) - #[arg(long)] - quality: Option, - - /// Strip all metadata - #[arg(long)] - strip_metadata: bool, - - /// Rotate images (90, 180, 270, auto) - #[arg(long)] - rotate: Option, - - /// Flip images (horizontal, vertical) - #[arg(long)] - flip: Option, - - /// Add text watermark (e.g., "(c) 2026 My Name") - #[arg(long)] - watermark: Option, - - /// Watermark position (top-left, top-center, top-right, center, bottom-left, bottom-center, bottom-right) - #[arg(long, default_value = "bottom-right")] - watermark_position: String, - - /// Watermark opacity (0.0-1.0) - #[arg(long, default_value = "0.5", value_parser = parse_opacity)] - watermark_opacity: f32, - - /// Rename with prefix - #[arg(long)] - rename_prefix: Option, - - /// Rename with suffix - #[arg(long)] - rename_suffix: Option, - - /// Rename template (e.g., "{name}_{counter:3}.{ext}") - #[arg(long)] - rename_template: Option, - - /// Resize algorithm (lanczos3, catmullrom, bilinear, nearest) - #[arg(long, default_value = "lanczos3")] + /// Resize algorithm + #[arg(long, default_value = "lanczos3", help_heading = "Resize", + value_parser = ["lanczos3", "catmullrom", "bilinear", "nearest"])] algorithm: String, - /// Overwrite behavior (auto-rename, overwrite, skip) - #[arg(long, default_value = "auto-rename")] + /// Allow upscaling when using fit-in-box resize mode + #[arg(long, help_heading = "Resize")] + allow_upscale: bool, + + // --- Convert --- + + /// Convert all images to one format + #[arg(long, help_heading = "Convert", + value_parser = ["jpeg", "jpg", "png", "webp", "avif", "gif", "tiff", "bmp"])] + format: Option, + + /// Per-format conversion mapping (e.g., "png=webp,tiff=jpeg"). Cannot combine with --format + #[arg(long, help_heading = "Convert")] + format_map: Option, + + // --- Compress --- + + /// Quality preset + #[arg(long, help_heading = "Compress", + value_parser = ["maximum", "max", "high", "medium", "med", "low", "web"])] + quality: Option, + + /// JPEG quality (1-100). Overrides --quality for JPEG output + #[arg(long, value_parser = clap::value_parser!(u8).range(1..=100), help_heading = "Compress")] + jpeg_quality: Option, + + /// PNG compression level (1-12, lower = larger but faster). Overrides --quality for PNG + #[arg(long, value_parser = clap::value_parser!(u8).range(1..=12), help_heading = "Compress")] + png_level: Option, + + /// WebP quality (1-100). Overrides --quality for WebP output + #[arg(long, value_parser = clap::value_parser!(u8).range(1..=100), help_heading = "Compress")] + webp_quality: Option, + + /// AVIF quality (1-100). Overrides --quality for AVIF output + #[arg(long, value_parser = clap::value_parser!(u8).range(1..=100), help_heading = "Compress")] + avif_quality: Option, + + /// AVIF encoding speed (0=slowest/best, 10=fastest/worst) + #[arg(long, value_parser = clap::value_parser!(u8).range(0..=10), help_heading = "Compress")] + avif_speed: Option, + + /// Enable progressive JPEG encoding (better for web, slightly larger files) + #[arg(long, help_heading = "Compress")] + progressive_jpeg: bool, + + // --- Metadata --- + + /// Metadata handling mode. "privacy" strips GPS, camera, and software info + #[arg(long, help_heading = "Metadata", + value_parser = ["strip-all", "keep-all", "privacy"])] + metadata: Option, + + /// Strip GPS/location data + #[arg(long, help_heading = "Metadata")] + strip_gps: bool, + + /// Strip camera make/model/settings info + #[arg(long, help_heading = "Metadata")] + strip_camera: bool, + + /// Strip software/editor tags + #[arg(long, help_heading = "Metadata")] + strip_software: bool, + + /// Strip date/time timestamps + #[arg(long, help_heading = "Metadata")] + strip_timestamps: bool, + + /// Strip copyright and author info + #[arg(long, help_heading = "Metadata")] + strip_copyright: bool, + + /// Strip all metadata (shorthand for --metadata strip-all) + #[arg(long, help_heading = "Metadata")] + strip_metadata: bool, + + // --- Rotation / Flip --- + + /// Rotate images. "auto" reads EXIF orientation and corrects it + #[arg(long, help_heading = "Transform", + value_parser = ["90", "180", "270", "auto", "none"])] + rotate: Option, + + /// Flip/mirror images + #[arg(long, help_heading = "Transform", + value_parser = ["horizontal", "vertical", "h", "v", "none"])] + flip: Option, + + // --- Adjustments --- + + /// Brightness adjustment (-100 to 100, 0 = no change) + #[arg(long, allow_hyphen_values = true, help_heading = "Adjustments")] + brightness: Option, + + /// Contrast adjustment (-100 to 100, 0 = no change) + #[arg(long, allow_hyphen_values = true, help_heading = "Adjustments")] + contrast: Option, + + /// Saturation adjustment (-100 to 100, 0 = no change) + #[arg(long, allow_hyphen_values = true, help_heading = "Adjustments")] + saturation: Option, + + /// Apply sharpening filter + #[arg(long, help_heading = "Adjustments")] + sharpen: bool, + + /// Convert to grayscale (black and white) + #[arg(long, help_heading = "Adjustments")] + grayscale: bool, + + /// Apply sepia tone (vintage look) + #[arg(long, help_heading = "Adjustments")] + sepia: bool, + + /// Crop to aspect ratio (e.g., "16:9", "4:3", "1:1", "3:2") + #[arg(long, help_heading = "Adjustments")] + crop_aspect_ratio: Option, + + /// Auto-trim whitespace/solid-color borders + #[arg(long, help_heading = "Adjustments")] + trim_whitespace: bool, + + /// Add uniform canvas padding in pixels (applied after all other adjustments) + #[arg(long, help_heading = "Adjustments")] + canvas_padding: Option, + + // --- Watermark --- + + /// Add text watermark. Cannot combine with --watermark-image + #[arg(long, help_heading = "Watermark")] + watermark: Option, + + /// Add image watermark from file. Cannot combine with --watermark + #[arg(long, help_heading = "Watermark")] + watermark_image: Option, + + /// Watermark position on the image + #[arg(long, default_value = "bottom-right", help_heading = "Watermark", + value_parser = ["top-left", "top-center", "top-right", + "center", "bottom-left", "bottom-center", "bottom-right", + "tl", "tc", "tr", "c", "bl", "bc", "br"])] + watermark_position: String, + + /// Watermark opacity (0.0 = invisible, 1.0 = fully opaque) + #[arg(long, default_value = "0.5", value_parser = parse_opacity, help_heading = "Watermark")] + watermark_opacity: f32, + + /// Font size for text watermarks in points + #[arg(long, default_value = "24", help_heading = "Watermark")] + watermark_font_size: f32, + + /// Font family name for text watermarks (searches system fonts) + #[arg(long, help_heading = "Watermark")] + watermark_font: Option, + + /// Text color as hex RGB or RGBA (e.g., "ffffff", "ff0000", "00000080") + #[arg(long, default_value = "ffffff", help_heading = "Watermark")] + watermark_color: String, + + /// Rotate watermark in degrees (45, -45, 90, or any value) + #[arg(long, help_heading = "Watermark")] + watermark_rotation: Option, + + /// Tile (repeat) the watermark across the entire image + #[arg(long, help_heading = "Watermark")] + watermark_tiled: bool, + + /// Watermark distance from image edges in pixels + #[arg(long, default_value = "10", help_heading = "Watermark")] + watermark_margin: u32, + + /// Scale for image watermarks (0.0-1.0, fraction of image size) + #[arg(long, default_value = "0.25", value_parser = parse_opacity, help_heading = "Watermark")] + watermark_scale: f32, + + // --- Rename --- + + /// Add prefix before filename (e.g., "blog-" turns "photo.jpg" into "blog-photo.jpg") + #[arg(long, help_heading = "Rename")] + rename_prefix: Option, + + /// Add suffix after filename (e.g., "-web" turns "photo.jpg" into "photo-web.jpg") + #[arg(long, help_heading = "Rename")] + rename_suffix: Option, + + /// Rename using a template. Overrides --rename-prefix/--rename-suffix. See TEMPLATE VARIABLES below + #[arg(long, help_heading = "Rename")] + rename_template: Option, + + /// Convert filename case + #[arg(long, help_heading = "Rename", + value_parser = ["lower", "upper", "title", "none"])] + rename_case: Option, + + /// Replace spaces in filenames + #[arg(long, help_heading = "Rename", + value_parser = ["underscore", "hyphen", "dot", "camelcase", "remove"])] + rename_spaces: Option, + + /// Filter special characters in filenames + #[arg(long, help_heading = "Rename", + value_parser = ["filesystem-safe", "web-safe", "hyphens-underscores", "hyphens", "alphanumeric", "none"])] + rename_special_chars: Option, + + /// Regex pattern to find in filenames (paired with --rename-regex-replace) + #[arg(long, help_heading = "Rename")] + rename_regex_find: Option, + + /// Replacement for regex matches (supports $1, $2 capture groups) + #[arg(long, help_heading = "Rename")] + rename_regex_replace: Option, + + /// Where to insert the counter in the filename + #[arg(long, default_value = "after-suffix", help_heading = "Rename", + value_parser = ["before-prefix", "before-name", "after-name", "after-suffix", "replace-name"])] + rename_counter_position: String, + + /// Counter starting number + #[arg(long, default_value = "1", help_heading = "Rename")] + rename_counter_start: u32, + + /// Counter zero-padding width (3 = "001", 4 = "0001") + #[arg(long, default_value = "3", help_heading = "Rename")] + rename_counter_padding: u32, + + // --- Output --- + + /// What to do when output file already exists + #[arg(long, default_value = "auto-rename", help_heading = "Output", + value_parser = ["auto-rename", "overwrite", "skip"])] overwrite: String, - /// Include subdirectories - #[arg(short, long)] + /// Preserve subdirectory structure from input in the output folder + #[arg(long, help_heading = "Output")] + preserve_dirs: bool, + + /// Set output DPI/resolution metadata (e.g., 72, 150, 300) + #[arg(long, help_heading = "Output")] + output_dpi: Option, + + /// Scan input directories recursively for images + #[arg(short, long, help_heading = "Output")] recursive: bool, }, - /// Manage presets + /// Manage presets (list, create, delete, import, export) Preset { #[command(subcommand)] action: PresetAction, }, - /// Manage watch folders + /// Manage watch folders for automatic processing of new images Watch { #[command(subcommand)] action: WatchAction, }, - /// View processing history + /// View processing history (recent batches, file counts, sizes, and times) History, - /// Undo last batch operation (moves output files to trash) + /// Undo recent batch operations by moving output files to system trash Undo { - /// Undo the last N batches (default 1) + /// Number of recent batches to undo #[arg(long, default_value = "1")] last: usize, }, @@ -135,40 +420,58 @@ enum Commands { #[derive(Subcommand)] enum WatchAction { - /// Add a watch folder with a linked preset + /// Add a folder to watch for new images, with a preset to apply automatically Add { - /// Folder path to watch + /// Folder path to watch for new images path: String, - /// Preset name to apply + /// Preset to apply to new images (see 'pixstrip preset list') #[arg(long)] preset: String, - /// Watch subdirectories recursively + /// Also watch subdirectories #[arg(short, long)] recursive: bool, }, - /// List configured watch folders + /// List all configured watch folders and their linked presets List, - /// Remove a watch folder + /// Remove a folder from the watch list Remove { - /// Folder path to remove + /// Folder path to stop watching path: String, }, - /// Start watching configured folders (blocks until Ctrl+C) + /// Start watching all active folders. Blocks until Ctrl+C. Processed files go to /processed/ Start, } #[derive(Subcommand)] enum PresetAction { - /// List all presets + /// List all available presets (built-in and user-created) List, - /// Export a preset to a file - Export { + /// Create a new empty user preset. Edit it by exporting, modifying the JSON, and re-importing + Create { + /// Name for the new preset name: String, + /// Optional description + #[arg(long, default_value = "")] + description: String, + }, + /// Delete a user preset. Built-in presets cannot be deleted + Delete { + /// Name of the preset to delete + name: String, + }, + /// Export a preset to a JSON file for backup or sharing + Export { + /// Name of the preset to export + name: String, + /// Output file path (e.g., "my-preset.json") #[arg(short, long)] output: String, }, - /// Import a preset from a file - Import { path: String }, + /// Import a preset from a JSON file + Import { + /// Path to the preset JSON file + path: String, + }, } fn main() { @@ -180,19 +483,60 @@ fn main() { preset, output, resize, + algorithm, + allow_upscale, format, + format_map, quality, + jpeg_quality, + png_level, + webp_quality, + avif_quality, + avif_speed, + progressive_jpeg, + metadata, + strip_gps, + strip_camera, + strip_software, + strip_timestamps, + strip_copyright, strip_metadata, rotate, flip, + brightness, + contrast, + saturation, + sharpen, + grayscale, + sepia, + crop_aspect_ratio, + trim_whitespace, + canvas_padding, watermark, + watermark_image, watermark_position, watermark_opacity, + watermark_font_size, + watermark_font, + watermark_color, + watermark_rotation, + watermark_tiled, + watermark_margin, + watermark_scale, rename_prefix, rename_suffix, rename_template, - algorithm, + rename_case, + rename_spaces, + rename_special_chars, + rename_regex_find, + rename_regex_replace, + rename_counter_position, + rename_counter_start, + rename_counter_padding, overwrite, + preserve_dirs, + output_dpi, recursive, } => { cmd_process(CmdProcessArgs { @@ -200,24 +544,67 @@ fn main() { preset, output, resize, + algorithm, + allow_upscale, format, + format_map, quality, + jpeg_quality, + png_level, + webp_quality, + avif_quality, + avif_speed, + progressive_jpeg, + metadata, + strip_gps, + strip_camera, + strip_software, + strip_timestamps, + strip_copyright, strip_metadata, rotate, flip, + brightness, + contrast, + saturation, + sharpen, + grayscale, + sepia, + crop_aspect_ratio, + trim_whitespace, + canvas_padding, watermark, + watermark_image, watermark_position, watermark_opacity, + watermark_font_size, + watermark_font, + watermark_color, + watermark_rotation, + watermark_tiled, + watermark_margin, + watermark_scale, rename_prefix, rename_suffix, rename_template, - algorithm, + rename_case, + rename_spaces, + rename_special_chars, + rename_regex_find, + rename_regex_replace, + rename_counter_position, + rename_counter_start, + rename_counter_padding, overwrite, + preserve_dirs, + output_dpi, recursive, }); } Commands::Preset { action } => match action { PresetAction::List => cmd_preset_list(), + PresetAction::Create { name, description } => cmd_preset_create(&name, &description), + PresetAction::Delete { name } => cmd_preset_delete(&name), PresetAction::Export { name, output } => cmd_preset_export(&name, &output), PresetAction::Import { path } => cmd_preset_import(&path), }, @@ -240,20 +627,70 @@ struct CmdProcessArgs { input: Vec, preset: Option, output: Option, + // Resize resize: Option, + algorithm: String, + allow_upscale: bool, + // Convert format: Option, + format_map: Option, + // Compress quality: Option, + jpeg_quality: Option, + png_level: Option, + webp_quality: Option, + avif_quality: Option, + avif_speed: Option, + progressive_jpeg: bool, + // Metadata + metadata: Option, + strip_gps: bool, + strip_camera: bool, + strip_software: bool, + strip_timestamps: bool, + strip_copyright: bool, strip_metadata: bool, + // Rotation / Flip rotate: Option, flip: Option, + // Adjustments + brightness: Option, + contrast: Option, + saturation: Option, + sharpen: bool, + grayscale: bool, + sepia: bool, + crop_aspect_ratio: Option, + trim_whitespace: bool, + canvas_padding: Option, + // Watermark watermark: Option, + watermark_image: Option, watermark_position: String, watermark_opacity: f32, + watermark_font_size: f32, + watermark_font: Option, + watermark_color: String, + watermark_rotation: Option, + watermark_tiled: bool, + watermark_margin: u32, + watermark_scale: f32, + // Rename rename_prefix: Option, rename_suffix: Option, rename_template: Option, - algorithm: String, + rename_case: Option, + rename_spaces: Option, + rename_special_chars: Option, + rename_regex_find: Option, + rename_regex_replace: Option, + rename_counter_position: String, + rename_counter_start: u32, + rename_counter_padding: u32, + // Output overwrite: String, + preserve_dirs: bool, + output_dpi: Option, recursive: bool, } @@ -309,66 +746,10 @@ fn cmd_process(args: CmdProcessArgs) { }; // Override with CLI flags + + // --- Resize --- if let Some(ref resize_str) = args.resize { - job.resize = Some(parse_resize(resize_str)); - } - if let Some(ref fmt_str) = args.format { - let fmt = parse_format(fmt_str).unwrap_or_else(|| std::process::exit(1)); - job.convert = Some(ConvertConfig::SingleFormat(fmt)); - } - if let Some(ref q_str) = args.quality { - let preset = parse_quality(q_str).unwrap_or_else(|| std::process::exit(1)); - job.compress = Some(CompressConfig::Preset(preset)); - } - if args.strip_metadata { - job.metadata = Some(MetadataConfig::StripAll); - } - if let Some(ref rot) = args.rotate { - job.rotation = Some(parse_rotation(rot)); - } - if let Some(ref fl) = args.flip { - job.flip = Some(parse_flip(fl)); - } - if let Some(ref text) = args.watermark { - let position = parse_watermark_position(&args.watermark_position); - job.watermark = Some(WatermarkConfig::Text { - text: text.clone(), - position, - font_size: 24.0, - opacity: args.watermark_opacity, - color: [255, 255, 255, 255], - font_family: None, - rotation: None, - tiled: false, - margin: 10, - }); - } - if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() { - if let Some(ref tmpl) = args.rename_template { - if args.rename_prefix.is_some() || args.rename_suffix.is_some() { - eprintln!("Warning: --rename-template overrides --rename-prefix/--rename-suffix"); - } - if !tmpl.contains('{') { - eprintln!("Warning: rename template '{}' has no placeholders. Use {{name}}, {{counter}}, {{ext}}, etc.", tmpl); - } - if !tmpl.contains("{ext}") && !tmpl.contains('.') { - eprintln!("Warning: rename template has no {{ext}} or file extension - output files may lack extensions"); - } - } - job.rename = Some(RenameConfig { - prefix: args.rename_prefix.unwrap_or_default(), - suffix: args.rename_suffix.unwrap_or_default(), - counter_start: 1, - counter_padding: 3, - counter_enabled: args.rename_template.is_some(), - counter_position: 3, - template: args.rename_template, - case_mode: 0, - replace_spaces: 0, - special_chars: 0, - regex_find: String::new(), - regex_replace: String::new(), - }); + job.resize = Some(parse_resize(resize_str, args.allow_upscale)); } job.resize_algorithm = match args.algorithm.to_lowercase().as_str() { @@ -382,6 +763,169 @@ fn cmd_process(args: CmdProcessArgs) { } }; + // --- Convert --- + if args.format.is_some() && args.format_map.is_some() { + eprintln!("Error: cannot use both --format and --format-map at the same time"); + std::process::exit(1); + } + if let Some(ref map_str) = args.format_map { + let mappings = parse_format_map(map_str); + if !mappings.is_empty() { + job.convert = Some(ConvertConfig::FormatMapping(mappings)); + } + } else if let Some(ref fmt_str) = args.format { + let fmt = parse_format(fmt_str).unwrap_or_else(|| std::process::exit(1)); + job.convert = Some(ConvertConfig::SingleFormat(fmt)); + } + + // --- Compress --- + if args.jpeg_quality.is_some() || args.png_level.is_some() || args.webp_quality.is_some() || args.avif_quality.is_some() { + // Per-format custom quality overrides the preset + job.compress = Some(CompressConfig::Custom { + jpeg_quality: args.jpeg_quality, + png_level: args.png_level, + webp_quality: args.webp_quality.map(|q| q as f32), + avif_quality: args.avif_quality.map(|q| q as f32), + }); + } else if let Some(ref q_str) = args.quality { + let preset = parse_quality(q_str).unwrap_or_else(|| std::process::exit(1)); + job.compress = Some(CompressConfig::Preset(preset)); + } + + if let Some(speed) = args.avif_speed { + job.avif_speed = speed; + } + if args.progressive_jpeg { + job.progressive_jpeg = true; + } + + // --- Metadata --- + if args.strip_metadata { + job.metadata = Some(MetadataConfig::StripAll); + } else if args.strip_gps || args.strip_camera || args.strip_software || args.strip_timestamps || args.strip_copyright { + job.metadata = Some(MetadataConfig::Custom { + strip_gps: args.strip_gps, + strip_camera: args.strip_camera, + strip_software: args.strip_software, + strip_timestamps: args.strip_timestamps, + strip_copyright: args.strip_copyright, + }); + } else if let Some(ref mode) = args.metadata { + job.metadata = Some(parse_metadata_mode(mode)); + } + + // --- Rotation / Flip --- + if let Some(ref rot) = args.rotate { + job.rotation = Some(parse_rotation(rot)); + } + if let Some(ref fl) = args.flip { + job.flip = Some(parse_flip(fl)); + } + + // --- Adjustments --- + let has_adjustments = args.brightness.is_some() + || args.contrast.is_some() + || args.saturation.is_some() + || args.sharpen + || args.grayscale + || args.sepia + || args.crop_aspect_ratio.is_some() + || args.trim_whitespace + || args.canvas_padding.is_some(); + if has_adjustments { + job.adjustments = Some(AdjustmentsConfig { + brightness: args.brightness.unwrap_or(0).clamp(-100, 100), + contrast: args.contrast.unwrap_or(0).clamp(-100, 100), + saturation: args.saturation.unwrap_or(0).clamp(-100, 100), + sharpen: args.sharpen, + grayscale: args.grayscale, + sepia: args.sepia, + crop_aspect_ratio: args.crop_aspect_ratio.as_deref().and_then(parse_aspect_ratio), + trim_whitespace: args.trim_whitespace, + canvas_padding: args.canvas_padding.unwrap_or(0), + }); + } + + // --- Watermark --- + if args.watermark.is_some() && args.watermark_image.is_some() { + eprintln!("Error: cannot use both --watermark (text) and --watermark-image at the same time"); + std::process::exit(1); + } + let wm_position = parse_watermark_position(&args.watermark_position); + let wm_rotation = args.watermark_rotation.as_deref().map(parse_watermark_rotation); + if let Some(ref text) = args.watermark { + let color = parse_hex_color(&args.watermark_color); + job.watermark = Some(WatermarkConfig::Text { + text: text.clone(), + position: wm_position, + font_size: args.watermark_font_size, + opacity: args.watermark_opacity, + color, + font_family: args.watermark_font.clone(), + rotation: wm_rotation, + tiled: args.watermark_tiled, + margin: args.watermark_margin, + }); + } else if let Some(ref img_path) = args.watermark_image { + let path = PathBuf::from(img_path); + if !path.exists() { + eprintln!("Watermark image not found: {}", img_path); + std::process::exit(1); + } + job.watermark = Some(WatermarkConfig::Image { + path, + position: wm_position, + opacity: args.watermark_opacity, + scale: args.watermark_scale, + rotation: wm_rotation, + tiled: args.watermark_tiled, + margin: args.watermark_margin, + }); + } + + // --- Rename --- + let has_rename = args.rename_prefix.is_some() + || args.rename_suffix.is_some() + || args.rename_template.is_some() + || args.rename_case.is_some() + || args.rename_spaces.is_some() + || args.rename_special_chars.is_some() + || args.rename_regex_find.is_some(); + if has_rename { + if let Some(ref tmpl) = args.rename_template { + if args.rename_prefix.is_some() || args.rename_suffix.is_some() { + eprintln!("Warning: --rename-template overrides --rename-prefix/--rename-suffix"); + } + if !tmpl.contains('{') { + eprintln!("Warning: rename template '{}' has no placeholders. Use {{name}}, {{counter}}, {{ext}}, etc.", tmpl); + } + if !tmpl.contains("{ext}") && !tmpl.contains('.') { + eprintln!("Warning: rename template has no {{ext}} or file extension - output files may lack extensions"); + } + } + let case_mode = args.rename_case.as_deref().map(parse_case_mode).unwrap_or(0); + let replace_spaces = args.rename_spaces.as_deref().map(parse_space_mode).unwrap_or(0); + let special_chars = args.rename_special_chars.as_deref().map(parse_special_chars_mode).unwrap_or(0); + let counter_position = parse_counter_position(&args.rename_counter_position); + let counter_enabled = args.rename_template.is_some() || counter_position != 3; + + job.rename = Some(RenameConfig { + prefix: args.rename_prefix.unwrap_or_default(), + suffix: args.rename_suffix.unwrap_or_default(), + counter_start: args.rename_counter_start, + counter_padding: args.rename_counter_padding, + counter_enabled, + counter_position, + template: args.rename_template, + case_mode, + replace_spaces, + special_chars, + regex_find: args.rename_regex_find.unwrap_or_default(), + regex_replace: args.rename_regex_replace.unwrap_or_default(), + }); + } + + // --- Output --- job.overwrite_behavior = match args.overwrite.to_lowercase().as_str() { "overwrite" | "always" => OverwriteAction::Overwrite, "skip" => OverwriteAction::Skip, @@ -392,6 +936,13 @@ fn cmd_process(args: CmdProcessArgs) { } }; + if args.preserve_dirs { + job.preserve_directory_structure = true; + } + if let Some(dpi) = args.output_dpi { + job.output_dpi = dpi; + } + for file in &source_files { job.add_source(file); } @@ -488,6 +1039,46 @@ fn cmd_preset_list() { } } +fn cmd_preset_create(name: &str, description: &str) { + let store = PresetStore::new(); + // Check if name conflicts with a builtin + let lower = name.to_lowercase(); + if Preset::all_builtins().iter().any(|p| p.name.to_lowercase() == lower) { + eprintln!("Cannot create preset '{}': conflicts with a built-in preset name", name); + std::process::exit(1); + } + let preset = Preset { + name: name.to_string(), + description: description.to_string(), + is_custom: true, + ..Preset::default() + }; + match store.save(&preset) { + Ok(()) => println!("Created preset '{}'", name), + Err(e) => { + eprintln!("Failed to create preset: {}", e); + std::process::exit(1); + } + } +} + +fn cmd_preset_delete(name: &str) { + let store = PresetStore::new(); + // Don't allow deleting builtins + let lower = name.to_lowercase(); + if Preset::all_builtins().iter().any(|p| p.name.to_lowercase() == lower) { + eprintln!("Cannot delete built-in preset '{}'", name); + std::process::exit(1); + } + match store.delete(name) { + Ok(()) => println!("Deleted preset '{}'", name), + Err(e) => { + eprintln!("Failed to delete preset '{}': {}", name, e); + std::process::exit(1); + } + } +} + fn cmd_preset_export(name: &str, output: &str) { let preset = find_preset(name).unwrap_or_else(|| { eprintln!("Preset '{}' not found. Use 'pixstrip preset list' to see available presets.", name); @@ -912,7 +1503,31 @@ fn find_preset(name: &str) -> Option { None } -fn parse_resize(s: &str) -> ResizeConfig { +fn parse_resize(s: &str, allow_upscale: bool) -> ResizeConfig { + // Support "fit:WxH" for fit-in-box mode + if let Some(dims) = s.strip_prefix("fit:") { + let (w, h) = dims.split_once('x').unwrap_or_else(|| { + eprintln!("Invalid fit-in-box syntax: '{}'. Use 'fit:1200x900'", s); + std::process::exit(1); + }); + let width: u32 = w.parse().unwrap_or_else(|_| { + eprintln!("Invalid fit width: '{}'", w); + std::process::exit(1); + }); + let height: u32 = h.parse().unwrap_or_else(|_| { + eprintln!("Invalid fit height: '{}'", h); + std::process::exit(1); + }); + if width == 0 || height == 0 { + eprintln!("Fit dimensions must be greater than zero"); + std::process::exit(1); + } + return ResizeConfig::FitInBox { + max: Dimensions { width, height }, + allow_upscale, + }; + } + if let Some((w, h)) = s.split_once('x') { let width: u32 = w.parse().unwrap_or_else(|_| { eprintln!("Invalid resize width: '{}'", w); @@ -929,7 +1544,7 @@ fn parse_resize(s: &str) -> ResizeConfig { ResizeConfig::Exact(Dimensions { width, height }) } else { let width: u32 = s.parse().unwrap_or_else(|_| { - eprintln!("Invalid resize value: '{}'. Use a width like '1200' or dimensions like '1200x900'", s); + eprintln!("Invalid resize value: '{}'. Use a width like '1200', dimensions like '1200x900', or 'fit:1200x900'", s); std::process::exit(1); }); if width == 0 { @@ -1022,6 +1637,152 @@ fn parse_opacity(s: &str) -> std::result::Result { Ok(v) } +fn parse_format_map(s: &str) -> Vec<(ImageFormat, ImageFormat)> { + let mut mappings = Vec::new(); + for pair in s.split(',') { + let pair = pair.trim(); + if pair.is_empty() { + continue; + } + let (from, to) = pair.split_once('=').unwrap_or_else(|| { + eprintln!("Invalid format mapping: '{}'. Use 'from=to' (e.g., 'png=webp')", pair); + std::process::exit(1); + }); + let from_fmt = parse_format(from.trim()).unwrap_or_else(|| std::process::exit(1)); + let to_fmt = parse_format(to.trim()).unwrap_or_else(|| std::process::exit(1)); + if from_fmt == to_fmt { + eprintln!("Warning: format mapping '{}={}' maps to itself, skipping", from.trim(), to.trim()); + continue; + } + mappings.push((from_fmt, to_fmt)); + } + mappings +} + +fn parse_metadata_mode(s: &str) -> MetadataConfig { + match s.to_lowercase().replace(' ', "-").as_str() { + "strip-all" | "strip" | "none" => MetadataConfig::StripAll, + "keep-all" | "keep" | "all" => MetadataConfig::KeepAll, + "privacy" => MetadataConfig::Privacy, + other => { + eprintln!("Unknown metadata mode: '{}'. Supported: strip-all, keep-all, privacy", other); + eprintln!("For selective stripping, use --strip-gps, --strip-camera, etc."); + std::process::exit(1); + } + } +} + +fn parse_hex_color(s: &str) -> [u8; 4] { + let hex = s.strip_prefix('#').unwrap_or(s); + if hex.len() == 6 || hex.len() == 8 { + if let Ok(val) = u32::from_str_radix(&hex[..6], 16) { + let r = ((val >> 16) & 0xFF) as u8; + let g = ((val >> 8) & 0xFF) as u8; + let b = (val & 0xFF) as u8; + let a = if hex.len() == 8 { + u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) + } else { + 255 + }; + return [r, g, b, a]; + } + } + eprintln!("Warning: invalid hex color '{}', using white. Format: rrggbb or rrggbbaa", s); + [255, 255, 255, 255] +} + +fn parse_watermark_rotation(s: &str) -> WatermarkRotation { + match s { + "45" => WatermarkRotation::Degrees45, + "-45" => WatermarkRotation::DegreesNeg45, + "90" => WatermarkRotation::Degrees90, + other => { + let deg: f32 = other.parse().unwrap_or_else(|_| { + eprintln!("Invalid watermark rotation: '{}'. Use 45, -45, 90, or a custom degree value", other); + std::process::exit(1); + }); + WatermarkRotation::Custom(deg) + } + } +} + +fn parse_aspect_ratio(s: &str) -> Option<(f64, f64)> { + if let Some((w, h)) = s.split_once(':') { + let w: f64 = w.parse().unwrap_or_else(|_| { + eprintln!("Invalid aspect ratio width: '{}'", w); + std::process::exit(1); + }); + let h: f64 = h.parse().unwrap_or_else(|_| { + eprintln!("Invalid aspect ratio height: '{}'", h); + std::process::exit(1); + }); + if w <= 0.0 || h <= 0.0 { + eprintln!("Aspect ratio values must be positive"); + std::process::exit(1); + } + Some((w, h)) + } else { + eprintln!("Invalid aspect ratio: '{}'. Use format like '16:9' or '4:3'", s); + std::process::exit(1); + } +} + +fn parse_case_mode(s: &str) -> u32 { + match s.to_lowercase().as_str() { + "lower" | "lowercase" => 1, + "upper" | "uppercase" => 2, + "title" | "titlecase" => 3, + "none" => 0, + other => { + eprintln!("Unknown case mode: '{}'. Supported: lower, upper, title, none", other); + std::process::exit(1); + } + } +} + +fn parse_space_mode(s: &str) -> u32 { + match s.to_lowercase().as_str() { + "underscore" | "underscores" => 1, + "hyphen" | "hyphens" | "dash" => 2, + "dot" | "dots" | "period" => 3, + "camelcase" | "camel" => 4, + "remove" | "none" => 5, + other => { + eprintln!("Unknown space mode: '{}'. Supported: underscore, hyphen, dot, camelcase, remove", other); + std::process::exit(1); + } + } +} + +fn parse_special_chars_mode(s: &str) -> u32 { + match s.to_lowercase().replace(' ', "-").as_str() { + "filesystem-safe" | "filesystem" | "safe" => 1, + "web-safe" | "web" => 2, + "hyphens-underscores" | "hyphens-and-underscores" => 3, + "hyphens" | "hyphens-only" => 4, + "alphanumeric" | "alnum" => 5, + "keep" | "none" => 0, + other => { + eprintln!("Unknown special chars mode: '{}'. Supported: filesystem-safe, web-safe, hyphens-underscores, hyphens, alphanumeric, none", other); + std::process::exit(1); + } + } +} + +fn parse_counter_position(s: &str) -> u32 { + match s.to_lowercase().replace(' ', "-").as_str() { + "before-prefix" => 0, + "before-name" => 1, + "after-name" => 2, + "after-suffix" => 3, + "replace-name" | "replace" => 4, + other => { + eprintln!("Unknown counter position: '{}'. Supported: before-prefix, before-name, after-name, after-suffix, replace-name", other); + std::process::exit(1); + } + } +} + fn format_bytes(bytes: u64) -> String { if bytes < 1024 { format!("{} B", bytes) @@ -1165,13 +1926,58 @@ mod tests { #[test] fn parse_resize_width_only() { - let config = parse_resize("1200"); + let config = parse_resize("1200", false); assert!(matches!(config, ResizeConfig::ByWidth(1200))); } #[test] fn parse_resize_exact() { - let config = parse_resize("1200x900"); + let config = parse_resize("1200x900", false); assert!(matches!(config, ResizeConfig::Exact(Dimensions { width: 1200, height: 900 }))); } + + #[test] + fn parse_resize_fit_in_box() { + let config = parse_resize("fit:1200x900", false); + assert!(matches!(config, ResizeConfig::FitInBox { max: Dimensions { width: 1200, height: 900 }, allow_upscale: false })); + let config = parse_resize("fit:800x600", true); + assert!(matches!(config, ResizeConfig::FitInBox { max: Dimensions { width: 800, height: 600 }, allow_upscale: true })); + } + + #[test] + fn parse_hex_color_valid() { + assert_eq!(parse_hex_color("ff0000"), [255, 0, 0, 255]); + assert_eq!(parse_hex_color("#00ff00"), [0, 255, 0, 255]); + assert_eq!(parse_hex_color("0000ff80"), [0, 0, 255, 128]); + } + + #[test] + fn parse_case_mode_valid() { + assert_eq!(parse_case_mode("lower"), 1); + assert_eq!(parse_case_mode("upper"), 2); + assert_eq!(parse_case_mode("title"), 3); + assert_eq!(parse_case_mode("none"), 0); + } + + #[test] + fn parse_space_mode_valid() { + assert_eq!(parse_space_mode("underscore"), 1); + assert_eq!(parse_space_mode("hyphen"), 2); + assert_eq!(parse_space_mode("dot"), 3); + assert_eq!(parse_space_mode("camelcase"), 4); + assert_eq!(parse_space_mode("remove"), 5); + } + + #[test] + fn parse_metadata_mode_valid() { + assert!(matches!(parse_metadata_mode("strip-all"), MetadataConfig::StripAll)); + assert!(matches!(parse_metadata_mode("keep-all"), MetadataConfig::KeepAll)); + assert!(matches!(parse_metadata_mode("privacy"), MetadataConfig::Privacy)); + } + + #[test] + fn parse_aspect_ratio_valid() { + assert_eq!(parse_aspect_ratio("16:9"), Some((16.0, 9.0))); + assert_eq!(parse_aspect_ratio("1:1"), Some((1.0, 1.0))); + } } diff --git a/pixstrip-core/src/preset.rs b/pixstrip-core/src/preset.rs index 0ff0447..4cae0f9 100644 --- a/pixstrip-core/src/preset.rs +++ b/pixstrip-core/src/preset.rs @@ -10,6 +10,7 @@ pub struct Preset { pub name: String, pub description: String, pub icon: String, + pub icon_color: String, pub is_custom: bool, pub resize: Option, pub rotation: Option, @@ -27,6 +28,7 @@ impl Default for Preset { name: String::new(), description: String::new(), icon: "image-x-generic-symbolic".into(), + icon_color: String::new(), is_custom: true, resize: None, rotation: None, @@ -90,6 +92,7 @@ impl Preset { name: "Blog Photos".into(), description: "Resize 1200px wide, JPEG quality High, strip all metadata".into(), icon: "image-x-generic-symbolic".into(), + icon_color: "accent".into(), is_custom: false, resize: Some(ResizeConfig::ByWidth(1200)), rotation: None, @@ -107,6 +110,7 @@ impl Preset { name: "Social Media".into(), description: "Resize to fit 1080x1080, compress Medium, strip metadata".into(), icon: "system-users-symbolic".into(), + icon_color: "success".into(), is_custom: false, resize: Some(ResizeConfig::FitInBox { max: Dimensions { @@ -130,6 +134,7 @@ impl Preset { name: "Web Optimization".into(), description: "Convert to WebP, compress High, sequential rename".into(), icon: "web-browser-symbolic".into(), + icon_color: "accent".into(), is_custom: false, resize: None, rotation: None, @@ -160,6 +165,7 @@ impl Preset { name: "Email Friendly".into(), description: "Resize 800px wide, JPEG quality Medium".into(), icon: "mail-unread-symbolic".into(), + icon_color: "warning".into(), is_custom: false, resize: Some(ResizeConfig::ByWidth(800)), rotation: None, @@ -177,6 +183,7 @@ impl Preset { name: "Privacy Clean".into(), description: "Strip all metadata, no other changes".into(), icon: "security-high-symbolic".into(), + icon_color: "error".into(), is_custom: false, resize: None, rotation: None, @@ -194,6 +201,7 @@ impl Preset { name: "Photographer Export".into(), description: "Resize 2048px, compress High, privacy metadata, rename by date".into(), icon: "camera-photo-symbolic".into(), + icon_color: "success".into(), is_custom: false, resize: Some(ResizeConfig::ByWidth(2048)), rotation: None, @@ -224,6 +232,7 @@ impl Preset { name: "Archive Compress".into(), description: "Lossless compression, preserve metadata".into(), icon: "folder-symbolic".into(), + icon_color: "warning".into(), is_custom: false, resize: None, rotation: None, @@ -241,6 +250,7 @@ impl Preset { name: "Print Ready".into(), description: "Maximum quality, convert to PNG, keep all metadata".into(), icon: "printer-symbolic".into(), + icon_color: "success".into(), is_custom: false, resize: None, rotation: None, @@ -258,6 +268,7 @@ impl Preset { name: "Fediverse Ready".into(), description: "Resize 1920x1080, convert to WebP, compress High, strip metadata".into(), icon: "network-server-symbolic".into(), + icon_color: "accent".into(), is_custom: false, resize: Some(ResizeConfig::FitInBox { max: Dimensions { diff --git a/pixstrip-core/src/storage.rs b/pixstrip-core/src/storage.rs index 49cf3ee..c6290c1 100644 --- a/pixstrip-core/src/storage.rs +++ b/pixstrip-core/src/storage.rs @@ -189,6 +189,7 @@ pub struct SessionState { pub resize_enabled: Option, pub resize_width: Option, pub resize_height: Option, + pub adjustments_enabled: Option, pub convert_enabled: Option, pub convert_format: Option, pub compress_enabled: Option, diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index c1ba3cc..1d85c4b 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -339,7 +339,7 @@ fn build_ui(app: &adw::Application) { allow_upscale: false, resize_algorithm: 0, output_dpi: 72, - adjustments_enabled: false, + adjustments_enabled: if remember { sess_state.adjustments_enabled.unwrap_or(false) } else { false }, rotation: 0, flip: 0, brightness: 0, @@ -445,6 +445,10 @@ fn build_ui(app: &adw::Application) { .tooltip_text("Help for this step") .build(); help_button.add_css_class("flat"); + help_button.update_property(&[ + gtk::accessible::Property::Label("Help for this step"), + ]); + help_button.set_widget_name("tour-help-button"); header.pack_end(&help_button); // Hamburger menu @@ -455,6 +459,7 @@ fn build_ui(app: &adw::Application) { .primary(true) .tooltip_text("Main Menu") .build(); + menu_button.set_widget_name("tour-menu-button"); header.pack_end(&menu_button); // Step indicator @@ -462,6 +467,7 @@ fn build_ui(app: &adw::Application) { // Navigation view for wizard content let nav_view = adw::NavigationView::new(); + nav_view.set_widget_name("tour-content"); nav_view.set_vexpand(true); nav_view.update_property(&[ gtk::accessible::Property::Label("Wizard steps. Use Alt+Left/Right to navigate."), @@ -485,6 +491,7 @@ fn build_ui(app: &adw::Application) { .tooltip_text("Go to next step (Alt+Right)") .build(); next_button.add_css_class("suggested-action"); + next_button.set_widget_name("tour-next-button"); let bottom_box = gtk::CenterBox::new(); bottom_box.set_start_widget(Some(&back_button)); @@ -514,6 +521,9 @@ fn build_ui(app: &adw::Application) { .tooltip_text("Watch Folders") .build(); watch_button.add_css_class("flat"); + watch_button.update_property(&[ + gtk::accessible::Property::Label("Toggle watch folders panel"), + ]); header.pack_start(&watch_button); { @@ -531,6 +541,7 @@ fn build_ui(app: &adw::Application) { .child(step_indicator.widget()) .build(); indicator_scroll.set_size_request(-1, 52); + indicator_scroll.set_widget_name("tour-step-indicator"); content_box.append(&indicator_scroll); content_box.append(&nav_view); content_box.append(&watch_revealer); @@ -608,6 +619,7 @@ fn build_ui(app: &adw::Application) { state.resize_enabled = Some(cfg.resize_enabled); state.resize_width = Some(cfg.resize_width); state.resize_height = Some(cfg.resize_height); + state.adjustments_enabled = Some(cfg.adjustments_enabled); state.convert_enabled = Some(cfg.convert_enabled); state.convert_format = cfg.convert_format.map(|f| format!("{:?}", f)); state.compress_enabled = Some(cfg.compress_enabled); @@ -1487,14 +1499,18 @@ fn show_history_dialog(window: &adw::ApplicationWindow) { .subtitle(&format!("{} - {}", time_label, subtitle)) .show_enable_switch(false) .build(); - row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic")); + let history_icon = gtk::Image::from_icon_name("image-x-generic-symbolic"); + history_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + row.add_prefix(&history_icon); // Detail rows inside expander let input_row = adw::ActionRow::builder() .title("Input") .subtitle(&entry.input_dir) .build(); - input_row.add_prefix(>k::Image::from_icon_name("folder-symbolic")); + let input_icon = gtk::Image::from_icon_name("folder-symbolic"); + input_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + input_row.add_prefix(&input_icon); row.add_row(&input_row); let output_row = adw::ActionRow::builder() @@ -1502,7 +1518,9 @@ fn show_history_dialog(window: &adw::ApplicationWindow) { .subtitle(&entry.output_dir) .activatable(true) .build(); - output_row.add_prefix(>k::Image::from_icon_name("folder-open-symbolic")); + let output_icon = gtk::Image::from_icon_name("folder-open-symbolic"); + output_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + output_row.add_prefix(&output_icon); let out_dir = entry.output_dir.clone(); output_row.connect_activated(move |_| { let uri = gtk::gio::File::for_path(&out_dir).uri(); @@ -1522,7 +1540,9 @@ fn show_history_dialog(window: &adw::ApplicationWindow) { savings )) .build(); - size_row.add_prefix(>k::Image::from_icon_name("drive-harddisk-symbolic")); + let size_icon = gtk::Image::from_icon_name("drive-harddisk-symbolic"); + size_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + size_row.add_prefix(&size_icon); row.add_row(&size_row); if entry.failed > 0 { @@ -1530,7 +1550,9 @@ fn show_history_dialog(window: &adw::ApplicationWindow) { .title("Errors") .subtitle(&format!("{} files failed", entry.failed)) .build(); - err_row.add_prefix(>k::Image::from_icon_name("dialog-warning-symbolic")); + let err_icon = gtk::Image::from_icon_name("dialog-warning-symbolic"); + err_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + err_row.add_prefix(&err_icon); row.add_row(&err_row); } @@ -2138,7 +2160,7 @@ fn continue_processing( } ProcessingMessage::Error(err) => { mark_current_queue_batch(&ui_for_rx, false, Some(&err)); - let toast = adw::Toast::new(&format!("Processing failed: {}", err)); + let toast = adw::Toast::new(&format!("Processing failed: {}. Try with fewer images or check that the output folder exists.", err)); ui_for_rx.toast_overlay.add_toast(toast); ui_for_rx.back_button.set_visible(true); ui_for_rx.next_button.set_visible(true); @@ -2415,18 +2437,18 @@ fn undo_last_batch(ui: &WizardUi) { let entries = match history.list() { Ok(e) => e, Err(_) => { - ui.toast_overlay.add_toast(adw::Toast::new("No processing history available")); + ui.toast_overlay.add_toast(adw::Toast::new("No processing history available. Process a batch first before undoing.")); return; } }; let Some(last) = entries.last() else { - ui.toast_overlay.add_toast(adw::Toast::new("No batches to undo")); + ui.toast_overlay.add_toast(adw::Toast::new("No batches to undo. Process some images first.")); return; }; if last.output_files.is_empty() { - ui.toast_overlay.add_toast(adw::Toast::new("No output files recorded for last batch")); + ui.toast_overlay.add_toast(adw::Toast::new("No output files recorded for last batch. The batch may have been already undone.")); return; } @@ -2457,7 +2479,7 @@ fn paste_images_from_clipboard(window: &adw::ApplicationWindow, ui: &WizardUi) { // Save the texture to a temp file let temp_dir = std::env::temp_dir().join("pixstrip-clipboard"); if std::fs::create_dir_all(&temp_dir).is_err() { - ui.toast_overlay.add_toast(adw::Toast::new("Failed to create temporary directory")); + ui.toast_overlay.add_toast(adw::Toast::new("Failed to create temporary directory. Check disk space and permissions on /tmp.")); return; } let timestamp = std::time::SystemTime::now() @@ -2480,10 +2502,10 @@ fn paste_images_from_clipboard(window: &adw::ApplicationWindow, ui: &WizardUi) { toast.set_timeout(2); ui.toast_overlay.add_toast(toast); } else { - ui.toast_overlay.add_toast(adw::Toast::new("Failed to save clipboard image")); + ui.toast_overlay.add_toast(adw::Toast::new("Failed to save clipboard image. The image format may be unsupported.")); } } else { - ui.toast_overlay.add_toast(adw::Toast::new("No image found in clipboard")); + ui.toast_overlay.add_toast(adw::Toast::new("No image found in clipboard. Copy an image first, then try pasting again.")); } }); } @@ -2672,7 +2694,7 @@ fn import_preset(window: &adw::ApplicationWindow, ui: &WizardUi) { ui.toast_overlay.add_toast(toast); } Err(e) => { - let toast = adw::Toast::new(&format!("Failed to import: {}", e)); + let toast = adw::Toast::new(&format!("Failed to import preset: {}. Make sure the file is a valid .pixstrip-preset file.", e)); ui.toast_overlay.add_toast(toast); } } @@ -2732,6 +2754,90 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { .build(); name_group.add(&desc_entry); + // Icon picker + let icon_names = [ + ("user-bookmarks-symbolic", "Bookmark"), + ("image-x-generic-symbolic", "Image"), + ("camera-photo-symbolic", "Camera"), + ("emblem-photos-symbolic", "Photos"), + ("applications-graphics-symbolic", "Graphics"), + ("starred-symbolic", "Star"), + ("emblem-favorite-symbolic", "Heart"), + ("folder-symbolic", "Folder"), + ("preferences-color-symbolic", "Color"), + ("emblem-system-symbolic", "Gear"), + ]; + let icon_string_list = gtk::StringList::new(&icon_names.map(|(_, label)| label)); + let icon_combo = adw::ComboRow::builder() + .title("Icon") + .model(&icon_string_list) + .build(); + // Show icon preview as prefix + let icon_preview = gtk::Image::builder() + .icon_name("user-bookmarks-symbolic") + .pixel_size(24) + .build(); + icon_preview.set_accessible_role(gtk::AccessibleRole::Presentation); + icon_combo.add_prefix(&icon_preview); + name_group.add(&icon_combo); + + // Color picker + let color_labels = ["Default", "Blue", "Green", "Yellow", "Red"]; + let color_values = ["", "accent", "success", "warning", "error"]; + let color_string_list = gtk::StringList::new(&color_labels); + let color_combo = adw::ComboRow::builder() + .title("Icon Color") + .model(&color_string_list) + .build(); + let color_preview = gtk::Image::builder() + .icon_name("user-bookmarks-symbolic") + .pixel_size(24) + .build(); + color_preview.set_accessible_role(gtk::AccessibleRole::Presentation); + color_combo.add_prefix(&color_preview); + name_group.add(&color_combo); + + // Update icon preview when icon selection changes + { + let ip = icon_preview.clone(); + let cp = color_preview.clone(); + let cc = color_combo.clone(); + icon_combo.connect_selected_notify(move |combo| { + let idx = combo.selected() as usize; + if idx < icon_names.len() { + let icon_name = icon_names[idx].0; + ip.set_icon_name(Some(icon_name)); + cp.set_icon_name(Some(icon_name)); + // Re-apply color class + for cv in &color_values { + if !cv.is_empty() { + cp.remove_css_class(cv); + } + } + let cidx = cc.selected() as usize; + if cidx < color_values.len() && !color_values[cidx].is_empty() { + cp.add_css_class(color_values[cidx]); + } + } + }); + } + + // Update color preview when color selection changes + { + let cp = color_preview; + color_combo.connect_selected_notify(move |combo| { + let idx = combo.selected() as usize; + for cv in &color_values { + if !cv.is_empty() { + cp.remove_css_class(cv); + } + } + if idx < color_values.len() && !color_values[idx].is_empty() { + cp.add_css_class(color_values[idx]); + } + }); + } + let save_new_button = gtk::Button::builder() .label("Save New Preset") .halign(gtk::Align::Center) @@ -2770,9 +2876,15 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { let ui_c = ui.clone(); let dlg_c = dialog.clone(); let pname = preset_name.clone(); + let store_ref = pixstrip_core::storage::PresetStore::new(); + let existing_preset = store_ref.load(preset_name).ok(); + let existing_icon = existing_preset.as_ref().map(|p| p.icon.clone()).unwrap_or_default(); + let existing_color = existing_preset.as_ref().map(|p| p.icon_color.clone()).unwrap_or_default(); row.connect_activated(move |_| { let cfg = ui_c.state.job_config.borrow(); - let preset = build_preset_from_config(&cfg, &pname, None); + let ei = existing_icon.as_str(); + let ec = existing_color.as_str(); + let preset = build_preset_from_config(&cfg, &pname, None, Some(ei), Some(ec)); drop(cfg); let store = pixstrip_core::storage::PresetStore::new(); @@ -2782,7 +2894,7 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { ui_c.toast_overlay.add_toast(toast); } Err(e) => { - let toast = adw::Toast::new(&format!("Failed to update: {}", e)); + let toast = adw::Toast::new(&format!("Failed to update preset: {}. The preset file may be read-only.", e)); ui_c.toast_overlay.add_toast(toast); } } @@ -2801,6 +2913,8 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { let dlg_c = dialog.clone(); let entry_c = name_entry.clone(); let desc_c = desc_entry.clone(); + let icon_combo_c = icon_combo; + let color_combo_c = color_combo; save_new_button.connect_clicked(move |_| { let name = entry_c.text().to_string(); if name.trim().is_empty() { @@ -2810,8 +2924,12 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { } let desc_text = desc_c.text().to_string(); + let icon_idx = icon_combo_c.selected() as usize; + let selected_icon = icon_names.get(icon_idx).map(|(name, _)| *name); + let color_idx = color_combo_c.selected() as usize; + let selected_color = color_values.get(color_idx).copied(); let cfg = ui_c.state.job_config.borrow(); - let preset = build_preset_from_config(&cfg, &name, Some(&desc_text)); + let preset = build_preset_from_config(&cfg, &name, Some(&desc_text), selected_icon, selected_color); drop(cfg); let store = pixstrip_core::storage::PresetStore::new(); @@ -2821,7 +2939,7 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { ui_c.toast_overlay.add_toast(toast); } Err(e) => { - let toast = adw::Toast::new(&format!("Failed to save: {}", e)); + let toast = adw::Toast::new(&format!("Failed to save preset: {}. Check that the presets folder is writable.", e)); ui_c.toast_overlay.add_toast(toast); } } @@ -2836,7 +2954,7 @@ fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) { dialog.present(Some(window)); } -fn build_preset_from_config(cfg: &JobConfig, name: &str, description: Option<&str>) -> pixstrip_core::preset::Preset { +fn build_preset_from_config(cfg: &JobConfig, name: &str, description: Option<&str>, icon: Option<&str>, icon_color: Option<&str>) -> pixstrip_core::preset::Preset { let resize = if cfg.resize_enabled && cfg.resize_width > 0 { if cfg.resize_height == 0 { Some(pixstrip_core::operations::ResizeConfig::ByWidth(cfg.resize_width)) @@ -2980,7 +3098,8 @@ fn build_preset_from_config(cfg: &JobConfig, name: &str, description: Option<&st .filter(|d| !d.trim().is_empty()) .map(|d| d.to_string()) .unwrap_or_else(|| build_preset_description(cfg)), - icon: "user-bookmarks-symbolic".into(), + icon: icon.unwrap_or("user-bookmarks-symbolic").to_string(), + icon_color: icon_color.unwrap_or("").to_string(), is_custom: true, resize, rotation, @@ -3188,11 +3307,12 @@ pub fn walk_widgets(widget: &Option, f: &dyn Fn(>k::Widget)) { } +#[allow(deprecated)] // ShortcutLabel deprecated in 4.18 with no replacement yet fn show_shortcuts_window(window: &adw::ApplicationWindow) { let dialog = adw::Dialog::builder() .title("Keyboard Shortcuts") - .content_width(420) - .content_height(480) + .content_width(460) + .content_height(520) .build(); let toolbar_view = adw::ToolbarView::new(); @@ -3201,56 +3321,75 @@ fn show_shortcuts_window(window: &adw::ApplicationWindow) { let scroll = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) + .vexpand(true) .build(); let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) - .margin_start(16) - .margin_end(16) - .margin_top(8) - .margin_bottom(16) - .spacing(16) + .margin_start(24) + .margin_end(24) + .margin_top(12) + .margin_bottom(24) + .spacing(18) .build(); let sections: &[(&str, &[(&str, &str)])] = &[ ("Wizard Navigation", &[ - ("Alt + Right", "Next step"), - ("Alt + Left", "Previous step"), - ("Alt + 1-9", "Jump to step"), - ("Ctrl + Return", "Process images"), + ("Right", "Next step"), + ("Left", "Previous step"), + ("1", "Jump to step (1-9)"), + ("Return", "Process images"), ("Escape", "Cancel or go back"), ]), ("File Management", &[ - ("Ctrl + O", "Add files"), - ("Ctrl + V", "Paste image from clipboard"), - ("Ctrl + A", "Select all images"), - ("Ctrl + Shift + A", "Deselect all images"), + ("o", "Add files"), + ("v", "Paste image from clipboard"), + ("a", "Select all images"), + ("a", "Deselect all images"), ("Delete", "Remove selected images"), ]), ("Application", &[ - ("Ctrl + ,", "Settings"), + ("comma", "Settings"), ("F1", "Keyboard shortcuts"), - ("Ctrl + Z", "Undo last batch"), - ("Ctrl + Q", "Quit"), + ("z", "Undo last batch"), + ("q", "Quit"), ]), ]; for (section_title, shortcuts) in sections { - let group = adw::PreferencesGroup::builder() - .title(*section_title) + let group = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(6) .build(); + let title_label = gtk::Label::builder() + .label(*section_title) + .css_classes(["title-4"]) + .halign(gtk::Align::Start) + .margin_bottom(2) + .build(); + group.append(&title_label); + for (accel, description) in *shortcuts { - let row = adw::ActionRow::builder() - .title(*description) + let row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(12) .build(); - let label = gtk::Label::builder() - .label(*accel) - .css_classes(["dim-label", "monospace"]) - .valign(gtk::Align::Center) + + let desc_label = gtk::Label::builder() + .label(*description) + .halign(gtk::Align::Start) + .hexpand(true) .build(); - row.add_suffix(&label); - group.add(&row); + + let shortcut_label = gtk::ShortcutLabel::builder() + .accelerator(*accel) + .halign(gtk::Align::End) + .build(); + + row.append(&desc_label); + row.append(&shortcut_label); + group.append(&row); } content.append(&group); @@ -3288,82 +3427,143 @@ fn apply_accessibility_settings() { } fn show_step_help(window: &adw::ApplicationWindow, step: usize) { - let (title, body) = match step { - 0 => ("Workflow", concat!( - "Choose a preset to start quickly, or configure each step manually.\n\n", - "Presets apply recommended settings for common tasks like web optimization, ", - "social media, or print preparation. You can customize any preset after applying it.\n\n", - "Use Import/Export to share presets with others." + let (title, icon_name, body) = match step { + 0 => ("Workflow", "view-grid-symbolic", concat!( + "Pick a built-in preset to start quickly, or select the Custom card to choose ", + "which operations to include.\n\n", + "Built-in presets auto-advance to the Images step with recommended settings. ", + "Custom mode shows toggle switches for each operation (Resize, Adjustments, Convert, ", + "Compress, Metadata, Watermark, Rename).\n\n", + "Your saved presets appear below the built-in ones. Use Import to load a .pixstrip-preset file, ", + "or drag one onto this page." )), - 1 => ("Images", concat!( + 1 => ("Images", "image-x-generic-symbolic", concat!( "Add the images you want to process.\n\n", "- Drag and drop files or folders onto this area\n", - "- Use Browse to pick files from a file dialog\n", + "- Drag image URLs from a web browser\n", + "- Click Browse Files or press Ctrl+O\n", "- Press Ctrl+V to paste from clipboard\n\n", - "Use checkboxes to include or exclude individual images. ", + "When dropping a folder with subfolders, you'll be asked whether to include them. ", + "Use checkboxes on each thumbnail to include or exclude images. ", "Ctrl+A selects all, Ctrl+Shift+A deselects all." )), - 2 => ("Resize", concat!( - "Scale images to specific dimensions.\n\n", - "Choose a preset size or enter custom dimensions. Width-only or height-only ", - "resizing preserves the original aspect ratio.\n\n", - "Enable 'Allow upscale' if you need images smaller than the target to be enlarged." + 2 => ("Resize", "view-fullscreen-symbolic", concat!( + "Scale images to specific dimensions with a live preview.\n\n", + "Pick a category and preset size, or enter custom width and height. ", + "Toggle between pixel and percentage units. Lock the aspect ratio to keep proportions.\n\n", + "Choose Exact Size or Fit Within Box mode. Enable Allow Upscaling to enlarge smaller images. ", + "Expand Advanced Settings for resize algorithm (Lanczos3, CatmullRom, etc.) and output DPI." )), - 3 => ("Adjustments", concat!( - "Fine-tune image appearance.\n\n", - "Adjust brightness, contrast, and saturation with sliders. ", - "Apply rotation, flipping, grayscale, or sepia effects.\n\n", - "Crop to a specific aspect ratio or trim whitespace borders automatically." + 3 => ("Adjustments", "preferences-color-symbolic", concat!( + "Fine-tune image appearance with a live preview.\n\n", + "Orientation: rotate (including auto-orient from EXIF) and flip.\n", + "Color: adjust brightness, contrast, and saturation with sliders.\n", + "Effects: toggle grayscale, sepia, or sharpen.\n", + "Crop and Canvas: crop to an aspect ratio, trim whitespace borders, or add padding." )), - 4 => ("Convert", concat!( + 4 => ("Convert", "document-save-as-symbolic", concat!( "Change image file format.\n\n", - "Convert between JPEG, PNG, WebP, AVIF, GIF, TIFF, and BMP. ", - "Each format has trade-offs between quality, file size, and compatibility.\n\n", - "WebP and AVIF offer the best compression for web use." + "Select a target format from the card grid (JPEG, PNG, WebP, AVIF) or use the ", + "Other Formats dropdown for GIF, TIFF, and BMP. Keep Original preserves each file's format.\n\n", + "Enable Progressive JPEG for gradual loading in browsers. Use Format Mapping to override ", + "the output format for specific input types (e.g. convert PNG to WebP but keep JPEG as-is)." )), - 5 => ("Compress", concat!( + 5 => ("Compress", "drive-harddisk-symbolic", concat!( "Reduce file size while preserving quality.\n\n", - "Choose a quality preset (Lossless, High, Balanced, Small, Tiny) or set custom ", - "quality values per format.\n\n", - "Expand Advanced Options for fine control over WebP encoding effort and AVIF speed." + "Use the quality slider to set the overall level from Low to Maximum. ", + "The split preview shows a side-by-side before/after comparison - drag the divider ", + "or use Left/Right arrow keys to compare.\n\n", + "Expand Per-Format Quality for fine control over JPEG quality, PNG compression level, ", + "WebP quality and effort, and AVIF quality and speed." )), - 6 => ("Metadata", concat!( - "Control what metadata is kept or removed.\n\n", - "Strip All removes everything. Privacy mode keeps copyright and camera info but ", - "removes GPS and timestamps. Custom mode lets you pick exactly what to strip.\n\n", - "Removing metadata reduces file size and protects privacy." + 6 => ("Metadata", "dialog-password-symbolic", concat!( + "Control what image metadata is kept or removed.\n\n", + "- Strip All: remove everything for smallest files and maximum privacy\n", + "- Privacy: strip GPS and camera serial, keep copyright\n", + "- Photographer: keep copyright and camera model, strip GPS and software\n", + "- Keep All: preserve all original metadata\n", + "- Custom: choose exactly which categories to strip (GPS, camera, software, timestamps, copyright)" )), - 7 => ("Watermark", concat!( - "Add a text or image watermark.\n\n", - "Choose text or logo mode. Position the watermark using the visual grid. ", - "Expand Advanced Options for opacity, rotation, tiling, margin, and scale controls.\n\n", - "Logo watermarks support PNG images with transparency." + 7 => ("Watermark", "emblem-photos-symbolic", concat!( + "Add a text or image watermark with a live preview.\n\n", + "Text mode: enter your text, choose a font and size.\n", + "Image mode: select a logo file (PNG with transparency works best).\n\n", + "Position the watermark using the 3x3 grid. Expand Advanced Options for text color, ", + "opacity, rotation, tiling, margin, and scale controls." )), - 8 => ("Rename", concat!( - "Rename output files using patterns.\n\n", - "Add a prefix, suffix, or use a full template with placeholders:\n", - "- {name} - original filename\n", - "- {n} - counter number\n", - "- {date} - current date\n", - "- {ext} - original extension\n\n", - "Expand Advanced Options for case conversion and find-and-replace." + 8 => ("Rename", "document-edit-symbolic", concat!( + "Rename output files with a live preview showing before and after names.\n\n", + "Simple options: add a prefix or suffix, replace spaces, filter special characters, ", + "convert case, and add a sequential counter.\n\n", + "Expand Advanced for a template engine with variables like {name}, {counter}, {date}, ", + "{exif_date}, {camera}, {width}, {height}, and more. Also includes find-and-replace with regex." )), - 9 => ("Output", concat!( - "Review settings and choose where to save.\n\n", - "The summary shows all operations that will be applied. ", + 9 => ("Output", "folder-download-symbolic", concat!( + "Review and start processing.\n\n", + "The operation summary lists all enabled steps and their settings. ", "Choose an output folder or use the default 'processed' subfolder.\n\n", - "Set overwrite behavior for when output files already exist. ", - "Press Process or Ctrl+Enter to start." + "Toggle Preserve Directory Structure to keep subfolder hierarchy in output. ", + "Set overwrite behavior for existing files. Press Process or Ctrl+Enter to start." )), - _ => ("Help", "No help available for this step."), + _ => ("Help", "help-about-symbolic", "No help available for this step."), }; - let dialog = adw::AlertDialog::builder() - .heading(format!("Help: {}", title)) - .body(body) + let dialog = adw::Dialog::builder() + .title(format!("Help: {}", title)) + .content_width(420) + .content_height(360) .build(); - dialog.add_response("ok", "Got it"); - dialog.set_default_response(Some("ok")); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(12) + .margin_top(24) + .margin_bottom(24) + .margin_start(24) + .margin_end(24) + .build(); + + let icon = gtk::Image::builder() + .icon_name(icon_name) + .pixel_size(64) + .halign(gtk::Align::Center) + .build(); + icon.add_css_class("accent"); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); + content.append(&icon); + + let heading = gtk::Label::builder() + .label(title) + .css_classes(["title-2"]) + .halign(gtk::Align::Center) + .build(); + content.append(&heading); + + let body_label = gtk::Label::builder() + .label(body) + .wrap(true) + .halign(gtk::Align::Center) + .justify(gtk::Justification::Center) + .xalign(0.5) + .vexpand(true) + .build(); + content.append(&body_label); + + let close_button = gtk::Button::builder() + .label("Got it") + .halign(gtk::Align::Center) + .build(); + close_button.add_css_class("suggested-action"); + close_button.add_css_class("pill"); + + let dlg = dialog.clone(); + close_button.connect_clicked(move |_| { + dlg.close(); + }); + + content.append(&close_button); + + dialog.set_child(Some(&content)); dialog.present(Some(window)); } @@ -3415,6 +3615,9 @@ fn build_watch_folder_panel() -> gtk::Box { .tooltip_text("Add watch folder") .build(); add_btn.add_css_class("flat"); + add_btn.update_property(&[ + gtk::accessible::Property::Label("Add watch folder"), + ]); header_box.append(&add_btn); inner.append(&header_box); @@ -3451,15 +3654,29 @@ fn build_watch_folder_panel() -> gtk::Box { .title(display_name) .subtitle(&folder.preset_name) .build(); - row.add_prefix(>k::Image::from_icon_name("folder-visiting-symbolic")); + let folder_icon = gtk::Image::from_icon_name("folder-visiting-symbolic"); + folder_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + row.add_prefix(&folder_icon); // Status indicator + let status_icon = gtk::Image::builder() + .icon_name("emblem-ok-symbolic") + .pixel_size(12) + .build(); + status_icon.set_accessible_role(gtk::AccessibleRole::Presentation); let status = gtk::Label::builder() .label("Watching") .css_classes(["caption", "accent"]) .valign(gtk::Align::Center) .build(); - row.add_suffix(&status); + let status_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(4) + .valign(gtk::Align::Center) + .build(); + status_box.append(&status_icon); + status_box.append(&status); + row.add_suffix(&status_box); list_box.append(&row); } @@ -3518,14 +3735,28 @@ fn build_watch_folder_panel() -> gtk::Box { .title(&display_name) .subtitle(&new_folder.preset_name) .build(); - row.add_prefix(>k::Image::from_icon_name("folder-visiting-symbolic")); + let folder_icon = gtk::Image::from_icon_name("folder-visiting-symbolic"); + folder_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + row.add_prefix(&folder_icon); + let dyn_status_icon = gtk::Image::builder() + .icon_name("emblem-ok-symbolic") + .pixel_size(12) + .build(); + dyn_status_icon.set_accessible_role(gtk::AccessibleRole::Presentation); let status = gtk::Label::builder() .label("Watching") .css_classes(["caption", "accent"]) .valign(gtk::Align::Center) .build(); - row.add_suffix(&status); + let dyn_status_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(4) + .valign(gtk::Align::Center) + .build(); + dyn_status_box.append(&dyn_status_icon); + dyn_status_box.append(&status); + row.add_suffix(&dyn_status_box); list_box_c.append(&row); list_box_c.set_visible(true); @@ -3644,7 +3875,11 @@ fn refresh_queue_list(ui: &WizardUi) { .title(&batch.name) .subtitle(&format!("{} images - {}", batch.files.len(), status_text)) .build(); - row.add_prefix(>k::Image::from_icon_name(status_icon)); + let batch_icon = gtk::Image::from_icon_name(status_icon); + batch_icon.update_property(&[ + gtk::accessible::Property::Label(&status_text), + ]); + row.add_prefix(&batch_icon); // Add remove button for pending batches if batch.status == BatchStatus::Pending { @@ -3686,7 +3921,7 @@ fn add_current_batch_to_queue(ui: &WizardUi) { }; if files.is_empty() { - ui.toast_overlay.add_toast(adw::Toast::new("No images to queue")); + ui.toast_overlay.add_toast(adw::Toast::new("No images to queue. Go to Step 2 to add images first.")); return; } diff --git a/pixstrip-gtk/src/processing.rs b/pixstrip-gtk/src/processing.rs index ade748d..6929481 100644 --- a/pixstrip-gtk/src/processing.rs +++ b/pixstrip-gtk/src/processing.rs @@ -142,31 +142,41 @@ pub fn build_results_page() -> adw::NavigationPage { .title("Images processed") .subtitle("0 images") .build(); - images_row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic")); + let images_icon = gtk::Image::from_icon_name("image-x-generic-symbolic"); + images_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + images_row.add_prefix(&images_icon); let size_before_row = adw::ActionRow::builder() .title("Original size") .subtitle("0 B") .build(); - size_before_row.add_prefix(>k::Image::from_icon_name("drive-harddisk-symbolic")); + let size_before_icon = gtk::Image::from_icon_name("drive-harddisk-symbolic"); + size_before_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + size_before_row.add_prefix(&size_before_icon); let size_after_row = adw::ActionRow::builder() .title("Output size") .subtitle("0 B") .build(); - size_after_row.add_prefix(>k::Image::from_icon_name("drive-harddisk-symbolic")); + let size_after_icon = gtk::Image::from_icon_name("drive-harddisk-symbolic"); + size_after_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + size_after_row.add_prefix(&size_after_icon); let savings_row = adw::ActionRow::builder() .title("Space saved") .subtitle("0%") .build(); - savings_row.add_prefix(>k::Image::from_icon_name("emblem-ok-symbolic")); + let savings_icon = gtk::Image::from_icon_name("emblem-ok-symbolic"); + savings_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + savings_row.add_prefix(&savings_icon); let time_row = adw::ActionRow::builder() .title("Processing time") .subtitle("0s") .build(); - time_row.add_prefix(>k::Image::from_icon_name("preferences-system-time-symbolic")); + let time_icon = gtk::Image::from_icon_name("preferences-system-time-symbolic"); + time_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + time_row.add_prefix(&time_icon); stats_group.add(&images_row); stats_group.add(&size_before_row); @@ -195,32 +205,48 @@ pub fn build_results_page() -> adw::NavigationPage { .subtitle("View processed images in file manager") .activatable(true) .build(); - open_row.add_prefix(>k::Image::from_icon_name("folder-open-symbolic")); - open_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let open_icon = gtk::Image::from_icon_name("folder-open-symbolic"); + open_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + open_row.add_prefix(&open_icon); + let open_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + open_arrow.set_accessible_role(gtk::AccessibleRole::Presentation); + open_row.add_suffix(&open_arrow); let process_more_row = adw::ActionRow::builder() .title("Process Another Batch") .subtitle("Start over with new images") .activatable(true) .build(); - process_more_row.add_prefix(>k::Image::from_icon_name("view-refresh-symbolic")); - process_more_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let more_icon = gtk::Image::from_icon_name("view-refresh-symbolic"); + more_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + process_more_row.add_prefix(&more_icon); + let more_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + more_arrow.set_accessible_role(gtk::AccessibleRole::Presentation); + process_more_row.add_suffix(&more_arrow); let save_preset_row = adw::ActionRow::builder() .title("Save as Preset") .subtitle("Save this workflow for future use") .activatable(true) .build(); - save_preset_row.add_prefix(>k::Image::from_icon_name("document-save-symbolic")); - save_preset_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let save_icon = gtk::Image::from_icon_name("document-save-symbolic"); + save_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + save_preset_row.add_prefix(&save_icon); + let save_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + save_arrow.set_accessible_role(gtk::AccessibleRole::Presentation); + save_preset_row.add_suffix(&save_arrow); let add_queue_row = adw::ActionRow::builder() .title("Add to Queue") .subtitle("Queue another batch with different images") .activatable(true) .build(); - add_queue_row.add_prefix(>k::Image::from_icon_name("view-list-symbolic")); - add_queue_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let queue_icon = gtk::Image::from_icon_name("view-list-symbolic"); + queue_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + add_queue_row.add_prefix(&queue_icon); + let queue_arrow = gtk::Image::from_icon_name("go-next-symbolic"); + queue_arrow.set_accessible_role(gtk::AccessibleRole::Presentation); + add_queue_row.add_suffix(&queue_arrow); action_group.add(&open_row); action_group.add(&process_more_row); diff --git a/pixstrip-gtk/src/settings.rs b/pixstrip-gtk/src/settings.rs index 89edd9e..beb57fa 100644 --- a/pixstrip-gtk/src/settings.rs +++ b/pixstrip-gtk/src/settings.rs @@ -57,7 +57,9 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { .activatable(true) .visible(config.output_fixed_path.is_some()) .build(); - fixed_path_row.add_prefix(>k::Image::from_icon_name("folder-open-symbolic")); + let fp_icon = gtk::Image::from_icon_name("folder-open-symbolic"); + fp_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + fixed_path_row.add_prefix(&fp_icon); let choose_fixed_btn = gtk::Button::builder() .icon_name("document-open-symbolic") @@ -65,6 +67,9 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { .valign(gtk::Align::Center) .build(); choose_fixed_btn.add_css_class("flat"); + choose_fixed_btn.update_property(&[ + gtk::accessible::Property::Label("Choose output folder"), + ]); fixed_path_row.add_suffix(&choose_fixed_btn); // Shared state for fixed path @@ -164,7 +169,34 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { .build(); reset_button.add_css_class("destructive-action"); + // Reset welcome wizard / tutorial + let reset_welcome_state: std::rc::Rc> = std::rc::Rc::new(Cell::new(false)); + + let reset_welcome_row = adw::ActionRow::builder() + .title("Reset welcome wizard") + .subtitle("Show the setup wizard and tutorial again on next launch") + .build(); + + let reset_welcome_btn = gtk::Button::builder() + .label("Reset") + .valign(gtk::Align::Center) + .build(); + reset_welcome_btn.add_css_class("destructive-action"); + + { + let rws = reset_welcome_state.clone(); + let row = reset_welcome_row.clone(); + reset_welcome_btn.connect_clicked(move |btn| { + rws.set(true); + btn.set_sensitive(false); + row.set_subtitle("Will show on next launch"); + }); + } + + reset_welcome_row.add_suffix(&reset_welcome_btn); + ui_group.add(&skill_row); + ui_group.add(&reset_welcome_row); general_page.add(&ui_group); // File Manager Integration @@ -432,6 +464,9 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { .css_classes(["boxed-list"]) .build(); watch_list.set_widget_name("watch-folder-list"); + watch_list.update_property(&[ + gtk::accessible::Property::Label("Configured watch folders for automatic processing"), + ]); // Shared state for watch folders let watch_folders_state: std::rc::Rc>> = @@ -564,9 +599,10 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { // Save settings when the dialog closes dialog.connect_closed(move |_| { + let welcome_reset = reset_welcome_state.get(); let new_config = AppConfig { - first_run_complete: true, - tutorial_complete: true, // preserve if settings are being saved + first_run_complete: !welcome_reset, + tutorial_complete: !welcome_reset, output_subfolder: subfolder_row.text().to_string(), output_fixed_path: if output_mode_row.selected() == 1 { fixed_path_state.borrow().clone() diff --git a/pixstrip-gtk/src/step_indicator.rs b/pixstrip-gtk/src/step_indicator.rs index 0413563..e596e01 100644 --- a/pixstrip-gtk/src/step_indicator.rs +++ b/pixstrip-gtk/src/step_indicator.rs @@ -49,10 +49,24 @@ impl StepIndicator { container.append(&grid); // First step starts as current + let total = dots.len(); if let Some(first) = dots.first() { first.icon.set_icon_name(Some("radio-checked-symbolic")); first.button.set_sensitive(true); first.label.add_css_class("accent"); + first.button.update_property(&[ + gtk::accessible::Property::Label( + &format!("Step 1 of {}: {} (current)", total, first.label.label()) + ), + ]); + } + // Label all non-current dots + for (i, dot) in dots.iter().enumerate().skip(1) { + dot.button.update_property(&[ + gtk::accessible::Property::Label( + &format!("Step {} of {}: {}", i + 1, total, dot.label.label()) + ), + ]); } Self { @@ -164,11 +178,17 @@ impl StepIndicator { pub fn set_completed(&self, actual_index: usize) { let dots = self.dots.borrow(); let map = self.step_map.borrow(); + let total = dots.len(); if let Some(visual_i) = map.iter().position(|&i| i == actual_index) { if let Some(dot) = dots.get(visual_i) { dot.icon.set_icon_name(Some("emblem-ok-symbolic")); dot.button.set_sensitive(true); dot.label.remove_css_class("accent"); + dot.button.update_property(&[ + gtk::accessible::Property::Label( + &format!("Step {} of {}: {} (completed)", visual_i + 1, total, dot.label.label()) + ), + ]); } } } diff --git a/pixstrip-gtk/src/steps/step_adjustments.rs b/pixstrip-gtk/src/steps/step_adjustments.rs index 11eb853..6d0b6a4 100644 --- a/pixstrip-gtk/src/steps/step_adjustments.rs +++ b/pixstrip-gtk/src/steps/step_adjustments.rs @@ -25,6 +25,7 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .title("Enable Adjustments") .subtitle("Rotate, flip, brightness, contrast, effects") .active(cfg.adjustments_enabled) + .tooltip_text("Toggle image adjustments on or off") .build(); enable_group.add(&enable_row); outer.append(&enable_group); @@ -37,6 +38,10 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .vexpand(true) .build(); preview_picture.set_can_target(true); + preview_picture.set_focusable(true); + preview_picture.update_property(&[ + gtk::accessible::Property::Label("Adjustments preview - press Space to cycle images"), + ]); let info_label = gtk::Label::builder() .label("No images loaded") @@ -78,6 +83,7 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .title("Rotate") .subtitle("Rotation applied to all images") .use_subtitle(true) + .tooltip_text("Rotate all images by a fixed angle or auto-orient from EXIF") .build(); rotate_row.set_model(Some(>k::StringList::new(&[ "None", @@ -93,6 +99,7 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .title("Flip") .subtitle("Mirror the image") .use_subtitle(true) + .tooltip_text("Mirror images horizontally or vertically") .build(); flip_row.set_model(Some(>k::StringList::new(&["None", "Horizontal", "Vertical"]))); flip_row.set_list_factory(Some(&super::full_text_list_factory())); @@ -130,6 +137,9 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .tooltip_text("Reset to 0") .has_frame(false) .build(); + reset_btn.update_property(&[ + gtk::accessible::Property::Label(&format!("Reset {} to 0", title)), + ]); reset_btn.set_sensitive(value != 0); row.add_suffix(&scale); @@ -204,6 +214,7 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .title("Crop to Aspect Ratio") .subtitle("Crop from center to a specific ratio") .use_subtitle(true) + .tooltip_text("Crop from center to a specific aspect ratio") .build(); crop_row.set_model(Some(>k::StringList::new(&[ "None", @@ -222,12 +233,14 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .title("Trim Whitespace") .subtitle("Remove uniform borders around the image") .active(cfg.trim_whitespace) + .tooltip_text("Detect and remove uniform borders around the image") .build(); let padding_row = adw::SpinRow::builder() .title("Canvas Padding") .subtitle("Add uniform padding (pixels)") .adjustment(>k::Adjustment::new(cfg.canvas_padding as f64, 0.0, 500.0, 1.0, 10.0, 0.0)) + .tooltip_text("Add a white border around each image in pixels") .build(); crop_group.add(&crop_row); @@ -506,6 +519,27 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { preview_picture.add_controller(click); } + // Keyboard support for preview cycling (Space/Enter) + { + let key = gtk::EventControllerKey::new(); + let pidx = preview_index.clone(); + let files = state.loaded_files.clone(); + let up = update_preview.clone(); + key.connect_key_pressed(move |_, keyval, _, _| { + if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return { + let loaded = files.borrow(); + if loaded.len() > 1 { + let next = (pidx.get() + 1) % loaded.len(); + pidx.set(next); + up(); + } + return glib::Propagation::Stop; + } + glib::Propagation::Proceed + }); + preview_picture.add_controller(key); + } + // === Wire signals === { @@ -694,12 +728,16 @@ pub fn build_adjustments_page(state: &AppState) -> adw::NavigationPage { .child(&outer) .build(); - // Refresh preview and sensitivity when navigating to this page + // Sync enable toggle, refresh preview and sensitivity when navigating to this page { let up = update_preview.clone(); let lf = state.loaded_files.clone(); let ctrl = controls.clone(); + let jc = state.job_config.clone(); + let er = enable_row.clone(); page.connect_map(move |_| { + let enabled = jc.borrow().adjustments_enabled; + er.set_active(enabled); ctrl.set_sensitive(!lf.borrow().is_empty()); up(); }); diff --git a/pixstrip-gtk/src/steps/step_compress.rs b/pixstrip-gtk/src/steps/step_compress.rs index 2457073..48fd945 100644 --- a/pixstrip-gtk/src/steps/step_compress.rs +++ b/pixstrip-gtk/src/steps/step_compress.rs @@ -38,6 +38,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { .title("Enable Compression") .subtitle("Reduce file size with quality control") .active(cfg.compress_enabled) + .tooltip_text("Toggle compression on or off") .build(); let enable_group = adw::PreferencesGroup::new(); @@ -234,6 +235,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { let compressed_pixbuf: Rc>> = Rc::new(RefCell::new(None)); let divider_dragging = Rc::new(Cell::new(false)); let image_dragging = Rc::new(Cell::new(false)); + let divider_hint_visible = Rc::new(Cell::new(true)); // Pan state for cover-fill preview let pan_x: Rc> = Rc::new(Cell::new(0.0)); @@ -256,6 +258,17 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { gtk::accessible::Property::Label("Compression quality comparison. Drag the vertical divider to compare original and compressed image. Drag elsewhere to pan."), ]); + // Hint label shown over the preview until the user first interacts with the divider + let divider_hint_label = gtk::Label::builder() + .label("Drag the divider to compare before and after") + .css_classes(["dim-label", "caption"]) + .halign(gtk::Align::Center) + .valign(gtk::Align::Center) + .build(); + divider_hint_label.update_property(&[ + gtk::accessible::Property::Label("Hint: drag the divider to compare before and after compression"), + ]); + // Draw function - cover fill with pan support { let dp = divider_pos.clone(); @@ -376,12 +389,19 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { let dspy = drag_start_pan_y.clone(); let px = pan_x.clone(); let py = pan_y.clone(); + let hint_vis = divider_hint_visible.clone(); + let hint_lbl = divider_hint_label.clone(); drag_gesture.connect_drag_begin(move |_, x, _| { let w = drawing.width() as f64; let current = *dp.borrow() * w; if (x - current).abs() < 30.0 { dd.set(true); id.set(false); + // Hide the hint on first divider interaction + if hint_vis.get() { + hint_vis.set(false); + hint_lbl.set_visible(false); + } } else { dd.set(false); id.set(true); @@ -437,10 +457,62 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { } preview_drawing.add_controller(drag_gesture); + // Keyboard support for divider: Left/Right to move divider, Space to reset to center + { + let dp = divider_pos.clone(); + let drawing = preview_drawing.clone(); + let hint_vis = divider_hint_visible.clone(); + let hint_lbl = divider_hint_label.clone(); + let key = gtk::EventControllerKey::new(); + key.connect_key_pressed(move |_, keyval, _, _| { + let step = 0.02; + match keyval { + gtk::gdk::Key::Left => { + let new_pos = (*dp.borrow() - step).clamp(0.05, 0.95); + *dp.borrow_mut() = new_pos; + drawing.queue_draw(); + if hint_vis.get() { + hint_vis.set(false); + hint_lbl.set_visible(false); + } + return gtk::glib::Propagation::Stop; + } + gtk::gdk::Key::Right => { + let new_pos = (*dp.borrow() + step).clamp(0.05, 0.95); + *dp.borrow_mut() = new_pos; + drawing.queue_draw(); + if hint_vis.get() { + hint_vis.set(false); + hint_lbl.set_visible(false); + } + return gtk::glib::Propagation::Stop; + } + gtk::gdk::Key::space => { + *dp.borrow_mut() = 0.5; + drawing.queue_draw(); + if hint_vis.get() { + hint_vis.set(false); + hint_lbl.set_visible(false); + } + return gtk::glib::Propagation::Stop; + } + _ => {} + } + gtk::glib::Propagation::Proceed + }); + preview_drawing.set_focusable(true); + preview_drawing.add_controller(key); + } + + let preview_overlay = gtk::Overlay::builder() + .child(&preview_drawing) + .build(); + preview_overlay.add_overlay(÷r_hint_label); + let preview_frame = gtk::Frame::builder() .halign(gtk::Align::Fill) .build(); - preview_frame.set_child(Some(&preview_drawing)); + preview_frame.set_child(Some(&preview_overlay)); preview_group.add(&size_box); preview_group.add(&preview_frame); @@ -480,11 +552,22 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { frame.add_css_class("accent"); } + let file_name = files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image"); let btn = gtk::Button::builder() .child(&frame) .has_frame(false) - .tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image")) + .tooltip_text(file_name) .build(); + let selected_label = if i == 0 { "currently selected" } else { "" }; + btn.update_property(&[ + gtk::accessible::Property::Label( + &if selected_label.is_empty() { + format!("Preview thumbnail: {}", file_name) + } else { + format!("Preview thumbnail: {} ({})", file_name, selected_label) + } + ), + ]); thumb_box.append(&btn); } @@ -816,7 +899,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { .child(&scrolled) .build(); - // On page map: refresh thumbnail strip, preview, and show/hide per-format rows + // On page map: sync enable toggle, refresh thumbnail strip, preview, and show/hide per-format rows { let up = update_preview.clone(); let jc = state.job_config.clone(); @@ -832,7 +915,10 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { let ts = thumb_scrolled.clone(); let pidx = preview_index.clone(); let up2 = update_preview.clone(); + let er = enable_row.clone(); page.connect_map(move |_| { + let enabled = jc.borrow().compress_enabled; + er.set_active(enabled); // Rebuild thumbnail strip from current file list while let Some(child) = tb.first_child() { tb.remove(&child); @@ -856,11 +942,22 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage { let up_c = up2.clone(); let tb_c = tb.clone(); let current_idx = i; + let file_name = files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image"); let btn = gtk::Button::builder() .child(&frame) .has_frame(false) - .tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image")) + .tooltip_text(file_name) .build(); + let is_selected = i == *pidx.borrow(); + btn.update_property(&[ + gtk::accessible::Property::Label( + &if is_selected { + format!("Preview thumbnail: {} (currently selected)", file_name) + } else { + format!("Preview thumbnail: {}", file_name) + } + ), + ]); btn.connect_clicked(move |_| { *pidx_c.borrow_mut() = current_idx; up_c(true); diff --git a/pixstrip-gtk/src/steps/step_convert.rs b/pixstrip-gtk/src/steps/step_convert.rs index 7aba909..3e9871f 100644 --- a/pixstrip-gtk/src/steps/step_convert.rs +++ b/pixstrip-gtk/src/steps/step_convert.rs @@ -78,6 +78,7 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { .title("Enable Format Conversion") .subtitle("Convert images to a different format") .active(cfg.convert_enabled) + .tooltip_text("Toggle format conversion on or off") .build(); let enable_group = adw::PreferencesGroup::new(); @@ -101,6 +102,10 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { .margin_bottom(4) .build(); + flow.update_property(&[ + gtk::accessible::Property::Label("Output format selection grid"), + ]); + let initial_format = cfg.convert_format; for (name, desc, icon_name, _fmt) in CARD_FORMATS { @@ -112,6 +117,9 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { .build(); card.add_css_class("card"); card.set_size_request(130, 110); + card.update_property(&[ + gtk::accessible::Property::Label(&format!("{}: {}", name, desc.replace('\n', ", "))), + ]); let inner = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -129,6 +137,7 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { .icon_name(*icon_name) .pixel_size(28) .build(); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); let name_label = gtk::Label::builder() .label(*name) @@ -212,6 +221,7 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { .title("Progressive JPEG") .subtitle("Loads gradually in browsers, slightly larger file size") .active(cfg.progressive_jpeg) + .tooltip_text("Creates JPEG files that load gradually in web browsers") .build(); jpeg_group.add(&progressive_row); @@ -315,12 +325,15 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage { .child(&scrolled) .build(); - // Rebuild format mapping rows when navigating to this page + // Sync enable toggle and rebuild format mapping rows when navigating to this page { let files = state.loaded_files.clone(); let list = mapping_list; let jc = state.job_config.clone(); + let er = enable_row.clone(); page.connect_map(move |_| { + let enabled = jc.borrow().convert_enabled; + er.set_active(enabled); rebuild_format_mapping(&list, &files.borrow(), &jc); }); } diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index f46dabf..8faf466 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -500,6 +500,7 @@ fn build_empty_state() -> gtk::Box { .pixel_size(64) .css_classes(["dim-label"]) .build(); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); let title = gtk::Label::builder() .label("Drop images here") @@ -529,6 +530,16 @@ fn build_empty_state() -> gtk::Box { browse_button.add_css_class("suggested-action"); browse_button.add_css_class("pill"); + let hint = gtk::Label::builder() + .label("Start by adding your images below, then use the Next button to configure each processing step.") + .css_classes(["dim-label", "caption"]) + .halign(gtk::Align::Center) + .justify(gtk::Justification::Center) + .wrap(true) + .margin_bottom(8) + .build(); + + inner.append(&hint); inner.append(&icon); inner.append(&title); inner.append(&subtitle); @@ -778,11 +789,19 @@ fn build_loaded_state(state: &AppState) -> gtk::Box { } }); + // Set accessible label on thumbnail picture + picture.update_property(&[ + gtk::accessible::Property::Label(&format!("Thumbnail of {}", file_name)), + ]); + // Set checkbox state let check = find_check_button(overlay.upcast_ref::()); if let Some(ref check) = check { let is_excluded = excluded.borrow().contains(&path); check.set_active(!is_excluded); + check.update_property(&[ + gtk::accessible::Property::Label(&format!("Include {} in processing", file_name)), + ]); // Wire checkbox toggle let excl = excluded.clone(); diff --git a/pixstrip-gtk/src/steps/step_metadata.rs b/pixstrip-gtk/src/steps/step_metadata.rs index 489983d..86ccaf3 100644 --- a/pixstrip-gtk/src/steps/step_metadata.rs +++ b/pixstrip-gtk/src/steps/step_metadata.rs @@ -23,6 +23,7 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .title("Enable Metadata Handling") .subtitle("Control what image metadata to keep or remove") .active(cfg.metadata_enabled) + .tooltip_text("Toggle metadata handling on or off") .build(); let enable_group = adw::PreferencesGroup::new(); @@ -39,7 +40,9 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .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_icon = gtk::Image::from_icon_name("user-trash-symbolic"); + strip_all_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + strip_all_row.add_prefix(&strip_all_icon); let strip_all_check = gtk::CheckButton::new(); strip_all_check.set_active(cfg.metadata_mode == MetadataMode::StripAll); strip_all_row.add_suffix(&strip_all_check); @@ -50,7 +53,9 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .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_icon = gtk::Image::from_icon_name("security-medium-symbolic"); + privacy_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + privacy_row.add_prefix(&privacy_icon); let privacy_check = gtk::CheckButton::new(); privacy_check.set_group(Some(&strip_all_check)); privacy_check.set_active(cfg.metadata_mode == MetadataMode::Privacy); @@ -62,7 +67,9 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .subtitle("Preserve all original metadata") .activatable(true) .build(); - keep_all_row.add_prefix(>k::Image::from_icon_name("emblem-ok-symbolic")); + let keep_all_icon = gtk::Image::from_icon_name("emblem-ok-symbolic"); + keep_all_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + keep_all_row.add_prefix(&keep_all_icon); let keep_all_check = gtk::CheckButton::new(); keep_all_check.set_group(Some(&strip_all_check)); keep_all_check.set_active(cfg.metadata_mode == MetadataMode::KeepAll); @@ -74,7 +81,9 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .subtitle("Keep copyright and camera model, strip GPS and software") .activatable(true) .build(); - photographer_row.add_prefix(>k::Image::from_icon_name("camera-photo-symbolic")); + let photographer_icon = gtk::Image::from_icon_name("camera-photo-symbolic"); + photographer_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + photographer_row.add_prefix(&photographer_icon); let photographer_check = gtk::CheckButton::new(); photographer_check.set_group(Some(&strip_all_check)); photographer_row.add_suffix(&photographer_check); @@ -85,7 +94,9 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .subtitle("Choose exactly which metadata categories to strip") .activatable(true) .build(); - custom_row.add_prefix(>k::Image::from_icon_name("emblem-system-symbolic")); + let custom_icon = gtk::Image::from_icon_name("emblem-system-symbolic"); + custom_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + custom_row.add_prefix(&custom_icon); let custom_check = gtk::CheckButton::new(); custom_check.set_group(Some(&strip_all_check)); custom_check.set_active(cfg.metadata_mode == MetadataMode::Custom); @@ -109,30 +120,35 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { .title("GPS / Location") .subtitle("GPS coordinates, location name, altitude") .active(cfg.strip_gps) + .tooltip_text("Strip GPS coordinates, location name, and altitude") .build(); let camera_row = adw::SwitchRow::builder() .title("Camera Info") .subtitle("Camera model, serial number, lens data") .active(cfg.strip_camera) + .tooltip_text("Strip camera model, serial number, and lens data") .build(); let software_row = adw::SwitchRow::builder() .title("Software") .subtitle("Editing software, processing history") .active(cfg.strip_software) + .tooltip_text("Strip editing software and processing history") .build(); let timestamps_row = adw::SwitchRow::builder() .title("Timestamps") .subtitle("Date taken, date modified, date digitized") .active(cfg.strip_timestamps) + .tooltip_text("Strip date taken, date modified, date digitized") .build(); let copyright_row = adw::SwitchRow::builder() .title("Copyright / Author") .subtitle("Copyright notice, artist name, credits") .active(cfg.strip_copyright) + .tooltip_text("Strip copyright notice, artist name, and credits") .build(); custom_group.add(&gps_row); @@ -260,9 +276,21 @@ pub fn build_metadata_page(state: &AppState) -> adw::NavigationPage { scrolled.set_child(Some(&content)); - adw::NavigationPage::builder() + let page = adw::NavigationPage::builder() .title("Metadata") .tag("step-metadata") .child(&scrolled) - .build() + .build(); + + // Sync enable toggle when navigating to this page + { + let jc = state.job_config.clone(); + let er = enable_row.clone(); + page.connect_map(move |_| { + let enabled = jc.borrow().metadata_enabled; + er.set_active(enabled); + }); + } + + page } diff --git a/pixstrip-gtk/src/steps/step_rename.rs b/pixstrip-gtk/src/steps/step_rename.rs index d6a04a2..be6fce2 100644 --- a/pixstrip-gtk/src/steps/step_rename.rs +++ b/pixstrip-gtk/src/steps/step_rename.rs @@ -25,6 +25,7 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { .title("Enable Rename") .subtitle("Rename output files with prefix, suffix, or template") .active(cfg.rename_enabled) + .tooltip_text("Toggle file renaming on or off") .build(); enable_group.add(&enable_row); outer.append(&enable_group); @@ -74,6 +75,7 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { .visible(false) .build(); conflict_banner.add_css_class("card"); + conflict_banner.set_accessible_role(gtk::AccessibleRole::Alert); let conflict_icon = gtk::Image::builder() .icon_name("dialog-warning-symbolic") @@ -128,6 +130,7 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { .label("Reset to defaults") .halign(gtk::Align::Start) .margin_top(4) + .tooltip_text("Reset all rename options to their defaults") .build(); reset_button.add_css_class("pill"); @@ -140,11 +143,13 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { let prefix_row = adw::EntryRow::builder() .title("Prefix") .text(&cfg.rename_prefix) + .tooltip_text("Text added before the original filename") .build(); let suffix_row = adw::EntryRow::builder() .title("Suffix") .text(&cfg.rename_suffix) + .tooltip_text("Text added after the original filename") .build(); let replace_spaces_row = adw::ComboRow::builder() @@ -258,6 +263,7 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { let template_row = adw::EntryRow::builder() .title("Template") .text(&cfg.rename_template) + .tooltip_text("Use variables like {name}, {date}, {counter:3} to build filenames") .build(); // Template preset chips @@ -375,6 +381,11 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { .child(&chip_box) .has_frame(false) .build(); + btn.update_property(&[ + gtk::accessible::Property::Label( + &format!("Insert {} - {}", var_name, description) + ), + ]); let tr = template_row.clone(); let var_text = var_name.to_string(); @@ -411,11 +422,13 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { let find_row = adw::EntryRow::builder() .title("Find (regex)") .text(&cfg.rename_find) + .tooltip_text("Regular expression pattern to match in filenames") .build(); let replace_row = adw::EntryRow::builder() .title("Replace with") .text(&cfg.rename_replace) + .tooltip_text("Replacement text for matched pattern") .build(); advanced_expander.add_row(&template_row); @@ -603,9 +616,17 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { .max_width_chars(50) .build(); - // Highlight conflicts - if name_counts.get(new_full.as_str()).copied().unwrap_or(0) > 1 { + // Highlight conflicts with both color AND icon indicator + let is_conflict = name_counts.get(new_full.as_str()).copied().unwrap_or(0) > 1; + if is_conflict { new_name_label.add_css_class("error"); + let conflict_icon = gtk::Image::builder() + .icon_name("dialog-warning-symbolic") + .pixel_size(12) + .tooltip_text("Duplicate filename") + .build(); + conflict_icon.add_css_class("warning"); + new_line.append(&conflict_icon); } new_line.append(&arrow_label); @@ -643,9 +664,13 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { id.remove(); } let up2 = up.clone(); + let ds2 = ds.clone(); let id = gtk::glib::timeout_add_local_once( std::time::Duration::from_millis(150), - move || { up2(); }, + move || { + ds2.set(None); + up2(); + }, ); ds.set(Some(id)); }) @@ -860,10 +885,14 @@ pub fn build_rename_page(state: &AppState) -> adw::NavigationPage { .child(&outer) .build(); - // Refresh preview when navigating to this page + // Sync enable toggle and refresh preview when navigating to this page { let up = update_preview.clone(); + let jc = state.job_config.clone(); + let er = enable_row.clone(); page.connect_map(move |_| { + let enabled = jc.borrow().rename_enabled; + er.set_active(enabled); up(); }); } diff --git a/pixstrip-gtk/src/steps/step_resize.rs b/pixstrip-gtk/src/steps/step_resize.rs index 26c5a3f..f4e3568 100644 --- a/pixstrip-gtk/src/steps/step_resize.rs +++ b/pixstrip-gtk/src/steps/step_resize.rs @@ -109,6 +109,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .title("Enable Resize") .subtitle("Scale images to new dimensions") .active(cfg.resize_enabled) + .tooltip_text("Toggle resizing of images on or off") .build(); enable_group.add(&enable_row); outer.append(&enable_group); @@ -179,6 +180,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { let category_row = adw::ComboRow::builder() .title("Category") .use_subtitle(true) + .tooltip_text("Choose a category of size presets") .build(); category_row.set_model(Some(>k::StringList::new(CATEGORIES))); category_row.set_list_factory(Some(&super::full_text_list_factory())); @@ -187,6 +189,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .title("Size") .subtitle("Select a preset to fill dimensions") .use_subtitle(true) + .tooltip_text("Pick a preset size to fill dimensions") .build(); rebuild_size_model(&size_row, 0); @@ -214,6 +217,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .label("W") .css_classes(["dim-label"]) .build(); + w_label.set_accessible_role(gtk::AccessibleRole::Presentation); let width_spin = gtk::SpinButton::builder() .adjustment(>k::Adjustment::new( cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0, @@ -250,12 +254,28 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .label("H") .css_classes(["dim-label"]) .build(); + h_label.set_accessible_role(gtk::AccessibleRole::Presentation); // Unit segmented toggle (px / %) let unit_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); unit_box.add_css_class("linked"); - let px_btn = gtk::Button::builder().label("px").build(); - let pct_btn = gtk::Button::builder().label("%").build(); + unit_box.update_property(&[ + gtk::accessible::Property::Label("Dimension unit toggle"), + ]); + let px_btn = gtk::Button::builder() + .label("px") + .tooltip_text("Use pixel dimensions (currently active)") + .build(); + px_btn.update_property(&[ + gtk::accessible::Property::Label("Pixels - currently active"), + ]); + let pct_btn = gtk::Button::builder() + .label("%") + .tooltip_text("Use percentage dimensions") + .build(); + pct_btn.update_property(&[ + gtk::accessible::Property::Label("Percentage"), + ]); px_btn.add_css_class("suggested-action"); unit_box.append(&px_btn); unit_box.append(&pct_btn); @@ -273,6 +293,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .title("Mode") .subtitle("How dimensions are applied to images") .use_subtitle(true) + .tooltip_text("Exact stretches to dimensions; Fit keeps aspect ratio") .build(); mode_row.set_model(Some(>k::StringList::new(&[ "Exact Size", @@ -285,6 +306,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .title("Allow Upscaling") .subtitle("Enlarge images smaller than target size") .active(cfg.allow_upscale) + .tooltip_text("When off, images smaller than target are left as-is") .build(); dims_group.add(&mode_row); @@ -568,9 +590,15 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { if btn.is_active() { lb.set_icon_name("changes-prevent-symbolic"); lb.set_tooltip_text(Some("Aspect ratio locked")); + lb.update_property(&[ + gtk::accessible::Property::Label("Aspect ratio locked - click to unlock"), + ]); } else { lb.set_icon_name("changes-allow-symbolic"); lb.set_tooltip_text(Some("Aspect ratio unlocked")); + lb.update_property(&[ + gtk::accessible::Property::Label("Aspect ratio unlocked - click to lock"), + ]); } }); } @@ -711,6 +739,14 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { ip.set(false); px.add_css_class("suggested-action"); pct.remove_css_class("suggested-action"); + px.update_property(&[ + gtk::accessible::Property::Label("Pixels - currently active"), + ]); + pct.update_property(&[ + gtk::accessible::Property::Label("Percentage"), + ]); + px.set_tooltip_text(Some("Use pixel dimensions (currently active)")); + pct.set_tooltip_text(Some("Use percentage dimensions")); let dims = get_first_image_dims(&files.borrow()); let pct_w = ws.value(); @@ -755,6 +791,14 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { ip.set(true); pct.add_css_class("suggested-action"); px.remove_css_class("suggested-action"); + pct.update_property(&[ + gtk::accessible::Property::Label("Percentage - currently active"), + ]); + px.update_property(&[ + gtk::accessible::Property::Label("Pixels"), + ]); + pct.set_tooltip_text(Some("Use percentage dimensions (currently active)")); + px.set_tooltip_text(Some("Use pixel dimensions")); let dims = get_first_image_dims(&files.borrow()); let cur_w = ws.value(); @@ -852,10 +896,31 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { gesture.set_state(gtk::EventSequenceState::Claimed); }); thumb_picture.set_can_target(true); + thumb_picture.set_focusable(true); thumb_picture.add_controller(click); thumb_picture.set_cursor_from_name(Some("pointer")); } + // Keyboard support for preview cycling (Space/Enter) + { + let pi = preview_index.clone(); + let rt = render_thumb.clone(); + let lf = loaded_files.clone(); + let key = gtk::EventControllerKey::new(); + key.connect_key_pressed(move |_, keyval, _, _| { + if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return { + let count = lf.borrow().len(); + if count > 1 { + pi.set((pi.get() + 1) % count); + rt(); + } + return glib::Propagation::Stop; + } + glib::Propagation::Proceed + }); + thumb_picture.add_controller(key); + } + // Initial render { let rt = render_thumb.clone(); @@ -868,10 +933,14 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .child(&outer) .build(); - // Re-render on page map + // Sync enable toggle and re-render on page map { let rt = render_thumb.clone(); + let jc = state.job_config.clone(); + let er = enable_row.clone(); page.connect_map(move |_| { + let enabled = jc.borrow().resize_enabled; + er.set_active(enabled); rt(); }); } diff --git a/pixstrip-gtk/src/steps/step_watermark.rs b/pixstrip-gtk/src/steps/step_watermark.rs index a4be52a..6f0570e 100644 --- a/pixstrip-gtk/src/steps/step_watermark.rs +++ b/pixstrip-gtk/src/steps/step_watermark.rs @@ -25,6 +25,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .title("Enable Watermark") .subtitle("Add text or image watermark to processed images") .active(cfg.watermark_enabled) + .tooltip_text("Toggle watermark on or off") .build(); enable_group.add(&enable_row); outer.append(&enable_group); @@ -37,6 +38,10 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .vexpand(true) .build(); preview_picture.set_can_target(true); + preview_picture.set_focusable(true); + preview_picture.update_property(&[ + gtk::accessible::Property::Label("Watermark preview - press Space to cycle images"), + ]); let info_label = gtk::Label::builder() .label("No images loaded") @@ -78,6 +83,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .title("Type") .subtitle("Choose text or image watermark") .use_subtitle(true) + .tooltip_text("Choose between text or image/logo overlay") .build(); type_row.set_model(Some(>k::StringList::new(&["Text Watermark", "Image Watermark"]))); type_row.set_list_factory(Some(&super::full_text_list_factory())); @@ -95,6 +101,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { let text_row = adw::EntryRow::builder() .title("Watermark Text") .text(&cfg.watermark_text) + .tooltip_text("The text that appears as a watermark on each image") .build(); let font_row = adw::ActionRow::builder() @@ -115,12 +122,16 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { let desc = gtk::pango::FontDescription::from_string(&cfg.watermark_font_family); font_button.set_font_desc(&desc); } + font_button.update_property(&[ + gtk::accessible::Property::Label("Choose watermark font"), + ]); font_row.add_suffix(&font_button); let font_size_row = adw::SpinRow::builder() .title("Font Size") .subtitle("Size in pixels") .adjustment(>k::Adjustment::new(cfg.watermark_font_size as f64, 8.0, 200.0, 1.0, 10.0, 0.0)) + .tooltip_text("Size of watermark text in pixels") .build(); text_group.add(&text_row); @@ -144,7 +155,9 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { ) .activatable(true) .build(); - image_path_row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic")); + let image_prefix_icon = gtk::Image::from_icon_name("image-x-generic-symbolic"); + image_prefix_icon.set_accessible_role(gtk::AccessibleRole::Presentation); + image_path_row.add_prefix(&image_prefix_icon); let choose_image_button = gtk::Button::builder() .icon_name("document-open-symbolic") @@ -152,6 +165,9 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .valign(gtk::Align::Center) .has_frame(false) .build(); + choose_image_button.update_property(&[ + gtk::accessible::Property::Label("Choose logo image"), + ]); image_path_row.add_suffix(&choose_image_button); image_group.add(&image_path_row); @@ -287,6 +303,9 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .rgba(&initial_color) .valign(gtk::Align::Center) .build(); + color_button.update_property(&[ + gtk::accessible::Property::Label("Choose watermark text color"), + ]); color_row.add_suffix(&color_button); // Opacity slider + reset @@ -300,12 +319,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { opacity_scale.set_hexpand(false); opacity_scale.set_valign(gtk::Align::Center); opacity_scale.set_width_request(180); + opacity_scale.update_property(&[ + gtk::accessible::Property::Label("Watermark opacity, 0 to 100 percent"), + ]); let opacity_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 50%") .has_frame(false) .build(); + opacity_reset.update_property(&[ + gtk::accessible::Property::Label("Reset opacity to 50%"), + ]); opacity_reset.set_sensitive((cfg.watermark_opacity - 0.5).abs() > 0.01); opacity_row.add_suffix(&opacity_scale); opacity_row.add_suffix(&opacity_reset); @@ -321,12 +346,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { rotation_scale.set_hexpand(false); rotation_scale.set_valign(gtk::Align::Center); rotation_scale.set_width_request(180); + rotation_scale.update_property(&[ + gtk::accessible::Property::Label("Watermark rotation, -180 to +180 degrees"), + ]); let rotation_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 0 degrees") .has_frame(false) .build(); + rotation_reset.update_property(&[ + gtk::accessible::Property::Label("Reset rotation to 0 degrees"), + ]); rotation_reset.set_sensitive(cfg.watermark_rotation != 0); rotation_row.add_suffix(&rotation_scale); rotation_row.add_suffix(&rotation_reset); @@ -336,6 +367,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .title("Tiled / Repeated") .subtitle("Repeat watermark across the entire image") .active(cfg.watermark_tiled) + .tooltip_text("Repeat the watermark in a grid pattern across the entire image") .build(); // Margin slider + reset @@ -349,12 +381,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { margin_scale.set_hexpand(false); margin_scale.set_valign(gtk::Align::Center); margin_scale.set_width_request(180); + margin_scale.update_property(&[ + gtk::accessible::Property::Label("Watermark margin from edges, 0 to 200 pixels"), + ]); let margin_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 10 px") .has_frame(false) .build(); + margin_reset.update_property(&[ + gtk::accessible::Property::Label("Reset margin to 10 pixels"), + ]); margin_reset.set_sensitive(cfg.watermark_margin != 10); margin_row.add_suffix(&margin_scale); margin_row.add_suffix(&margin_reset); @@ -371,12 +409,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { scale_scale.set_hexpand(false); scale_scale.set_valign(gtk::Align::Center); scale_scale.set_width_request(180); + scale_scale.update_property(&[ + gtk::accessible::Property::Label("Watermark scale, 1 to 100 percent of image"), + ]); let scale_reset = gtk::Button::builder() .icon_name("edit-undo-symbolic") .valign(gtk::Align::Center) .tooltip_text("Reset to 20%") .has_frame(false) .build(); + scale_reset.update_property(&[ + gtk::accessible::Property::Label("Reset scale to 20%"), + ]); scale_reset.set_sensitive((cfg.watermark_scale - 20.0).abs() > 0.5); scale_row.add_suffix(&scale_scale); scale_row.add_suffix(&scale_reset); @@ -573,6 +617,27 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { preview_picture.add_controller(click); } + // Keyboard support for preview cycling (Space/Enter) + { + let key = gtk::EventControllerKey::new(); + let pidx = preview_index.clone(); + let files = state.loaded_files.clone(); + let up = update_preview.clone(); + key.connect_key_pressed(move |_, keyval, _, _| { + if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return { + let loaded = files.borrow(); + if loaded.len() > 1 { + let next = (pidx.get() + 1) % loaded.len(); + pidx.set(next); + up(); + } + return gtk::glib::Propagation::Stop; + } + gtk::glib::Propagation::Proceed + }); + preview_picture.add_controller(key); + } + // === Wire signals === // Enable toggle @@ -857,12 +922,16 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .child(&outer) .build(); - // Refresh preview and sensitivity when navigating to this page + // Sync enable toggle, refresh preview and sensitivity when navigating to this page { let up = update_preview.clone(); let lf = state.loaded_files.clone(); let ctrl = controls.clone(); + let jc = state.job_config.clone(); + let er = enable_row.clone(); page.connect_map(move |_| { + let enabled = jc.borrow().watermark_enabled; + er.set_active(enabled); ctrl.set_sensitive(!lf.borrow().is_empty()); up(); }); diff --git a/pixstrip-gtk/src/steps/step_workflow.rs b/pixstrip-gtk/src/steps/step_workflow.rs index 8e342ed..1496166 100644 --- a/pixstrip-gtk/src/steps/step_workflow.rs +++ b/pixstrip-gtk/src/steps/step_workflow.rs @@ -33,6 +33,10 @@ pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage { .homogeneous(true) .build(); + builtin_flow.update_property(&[ + gtk::accessible::Property::Label("Workflow preset selection grid"), + ]); + // Custom card is always first (index 0) let custom_card = build_custom_card(); builtin_flow.append(&custom_card); @@ -181,20 +185,37 @@ pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage { .description("Import or save your own workflows") .build(); - // Container for dynamically-rebuilt user preset rows - let user_rows_box = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .spacing(0) + // FlowBox for user preset cards (same look as built-in presets) + let user_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(); - user_group.add(&user_rows_box); + user_flow.update_property(&[ + gtk::accessible::Property::Label("Your saved preset selection grid"), + ]); - let import_button = gtk::Button::builder() - .label("Import Preset") - .icon_name("document-open-symbolic") - .action_name("win.import-preset") + let user_clamp = adw::Clamp::builder() + .maximum_size(1200) + .child(&user_flow) .build(); - import_button.add_css_class("flat"); - user_group.add(&import_button); + + let user_empty_label = gtk::Label::builder() + .label("No saved presets yet. Process images and save your workflow as a preset, or import one.") + .css_classes(["dim-label"]) + .halign(gtk::Align::Center) + .wrap(true) + .justify(gtk::Justification::Center) + .margin_top(8) + .margin_bottom(8) + .build(); + + user_group.add(&user_clamp); + user_group.add(&user_empty_label); + content.append(&user_group); content.append(&custom_group); @@ -228,39 +249,94 @@ pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage { // Refresh user presets every time this page is shown { - let jc = state.job_config.clone(); - let rows_box = user_rows_box.clone(); + let uf = user_flow.clone(); + let uel = user_empty_label.clone(); page.connect_map(move |_| { - // Clear existing rows - while let Some(child) = rows_box.first_child() { - rows_box.remove(&child); - } + // Clear existing cards + uf.remove_all(); let store = pixstrip_core::storage::PresetStore::new(); + let mut has_custom = false; if let Ok(presets) = store.list() { for preset in &presets { if !preset.is_custom { continue; } - let list_box = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(["boxed-list"]) + has_custom = true; + + let overlay = gtk::Overlay::new(); + + // Card body (same style as built-in presets) + let card = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .hexpand(true) + .vexpand(false) + .build(); + card.add_css_class("card"); + card.set_size_request(180, 140); + card.update_property(&[ + gtk::accessible::Property::Label(&format!("{}: {}", preset.name, preset.description)), + ]); + + let inner = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(4) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .halign(gtk::Align::Center) + .valign(gtk::Align::Center) + .vexpand(true) .build(); - let row = adw::ActionRow::builder() - .title(&preset.name) - .subtitle(&preset.description) - .activatable(true) + let icon = gtk::Image::builder() + .icon_name(&preset.icon) + .pixel_size(32) + .build(); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); + if !preset.icon_color.is_empty() { + icon.add_css_class(&preset.icon_color); + } + + let name_label = gtk::Label::builder() + .label(&preset.name) + .css_classes(["heading"]) + .ellipsize(gtk::pango::EllipsizeMode::End) + .max_width_chars(16) + .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); + overlay.set_child(Some(&card)); + + // Action buttons overlay (top-right corner) + let actions_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(0) + .halign(gtk::Align::End) + .valign(gtk::Align::Start) + .margin_top(2) + .margin_end(2) .build(); - row.add_prefix(>k::Image::from_icon_name(&preset.icon)); - // Export button let export_btn = gtk::Button::builder() .icon_name("document-save-as-symbolic") .tooltip_text("Export preset") - .valign(gtk::Align::Center) .build(); export_btn.add_css_class("flat"); + export_btn.add_css_class("circular"); let preset_for_export = preset.clone(); export_btn.connect_clicked(move |btn| { let p = preset_for_export.clone(); @@ -280,40 +356,80 @@ pub fn build_workflow_page(state: &AppState) -> adw::NavigationPage { }); } }); - row.add_suffix(&export_btn); - // Delete button let delete_btn = gtk::Button::builder() .icon_name("user-trash-symbolic") .tooltip_text("Delete preset") - .valign(gtk::Align::Center) .build(); delete_btn.add_css_class("flat"); + delete_btn.add_css_class("circular"); delete_btn.add_css_class("error"); let pname = preset.name.clone(); - let list_box_ref = list_box.clone(); - let rows_box_ref = rows_box.clone(); - delete_btn.connect_clicked(move |_| { + let uf_ref = uf.clone(); + let uel_ref = uel.clone(); + delete_btn.connect_clicked(move |btn| { let store = pixstrip_core::storage::PresetStore::new(); let _ = store.delete(&pname); - rows_box_ref.remove(&list_box_ref); - }); - row.add_suffix(&delete_btn); - - row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); - - let jc2 = jc.clone(); - let p = preset.clone(); - row.connect_activated(move |r| { - let mut cfg = jc2.borrow_mut(); - apply_preset_to_config(&mut cfg, &p); - cfg.preset_mode = true; - drop(cfg); - r.activate_action("win.next-step", None).ok(); + // Remove the FlowBoxChild containing this card + if let Some(child) = btn.ancestor(gtk::FlowBoxChild::static_type()) { + if let Some(fbc) = child.downcast_ref::() { + uf_ref.remove(fbc); + // Show empty label if only the import card is left + let mut c = uf_ref.first_child(); + let mut count = 0; + while let Some(w) = c { + count += 1; + c = w.next_sibling(); + } + uel_ref.set_visible(count <= 1); + } + } }); - list_box.append(&row); - rows_box.append(&list_box); + actions_box.append(&export_btn); + actions_box.append(&delete_btn); + overlay.add_overlay(&actions_box); + + uf.append(&overlay); + } + } + uel.set_visible(!has_custom); + + // Always append an "Import Preset" card at the end + let import_card = build_import_card(); + uf.append(&import_card); + }); + } + + // Wire user preset card activation + { + let jc = state.job_config.clone(); + user_flow.connect_child_activated(move |flow, child| { + // Count total children to know which is the import card (always last) + let mut total = 0usize; + let mut c = flow.first_child(); + while let Some(w) = c { + total += 1; + c = w.next_sibling(); + } + + let activated_idx = child.index() as usize; + + // Last card is always the import card + if activated_idx == total - 1 { + flow.activate_action("win.import-preset", None).ok(); + return; + } + + let store = pixstrip_core::storage::PresetStore::new(); + if let Ok(presets) = store.list() { + let custom_presets: Vec<_> = presets.iter().filter(|p| p.is_custom).collect(); + if let Some(preset) = custom_presets.get(activated_idx) { + let mut cfg = jc.borrow_mut(); + apply_preset_to_config(&mut cfg, preset); + cfg.preset_mode = true; + drop(cfg); + flow.activate_action("win.next-step", None).ok(); } } }); @@ -430,15 +546,18 @@ fn build_custom_card() -> gtk::Box { .vexpand(false) .build(); card.add_css_class("card"); - card.set_size_request(180, 120); + card.set_size_request(180, 140); + card.update_property(&[ + gtk::accessible::Property::Label("Custom: Pick and choose operations"), + ]); let inner = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) - .margin_top(6) - .margin_bottom(6) - .margin_start(8) - .margin_end(8) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .vexpand(true) @@ -448,6 +567,7 @@ fn build_custom_card() -> gtk::Box { .icon_name("emblem-system-symbolic") .pixel_size(32) .build(); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); let name_label = gtk::Label::builder() .label("Custom") @@ -470,6 +590,59 @@ fn build_custom_card() -> gtk::Box { card } +fn build_import_card() -> gtk::Box { + let card = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .hexpand(true) + .vexpand(false) + .build(); + card.add_css_class("card"); + card.set_size_request(180, 140); + card.set_tooltip_text(Some("Import a .pixstrip-preset file from disk")); + card.update_property(&[ + gtk::accessible::Property::Label("Import preset from file"), + ]); + + let inner = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(4) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .halign(gtk::Align::Center) + .valign(gtk::Align::Center) + .vexpand(true) + .build(); + + let icon = gtk::Image::builder() + .icon_name("folder-open-symbolic") + .pixel_size(32) + .build(); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); + + let name_label = gtk::Label::builder() + .label("Import Preset") + .css_classes(["heading"]) + .build(); + + let desc_label = gtk::Label::builder() + .label("Load a preset from file") + .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_preset_card(preset: &Preset) -> gtk::Box { let card = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -478,15 +651,18 @@ fn build_preset_card(preset: &Preset) -> gtk::Box { .vexpand(false) .build(); card.add_css_class("card"); - card.set_size_request(180, 120); + card.set_size_request(180, 140); + card.update_property(&[ + gtk::accessible::Property::Label(&format!("{}: {}", preset.name, preset.description)), + ]); let inner = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) - .margin_top(6) - .margin_bottom(6) - .margin_start(8) - .margin_end(8) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .vexpand(true) @@ -496,6 +672,10 @@ fn build_preset_card(preset: &Preset) -> gtk::Box { .icon_name(&preset.icon) .pixel_size(32) .build(); + icon.set_accessible_role(gtk::AccessibleRole::Presentation); + if !preset.icon_color.is_empty() { + icon.add_css_class(&preset.icon_color); + } let name_label = gtk::Label::builder() .label(&preset.name) diff --git a/pixstrip-gtk/src/tutorial.rs b/pixstrip-gtk/src/tutorial.rs index bcc5c64..f910c6e 100644 --- a/pixstrip-gtk/src/tutorial.rs +++ b/pixstrip-gtk/src/tutorial.rs @@ -1,46 +1,7 @@ use adw::prelude::*; use gtk::glib; -struct TourStop { - title: &'static str, - description: &'static str, - icon: &'static str, -} - -const TOUR_STOPS: &[TourStop] = &[ - TourStop { - title: "Step Indicator", - description: "This bar shows your progress through the wizard. Click any completed step to jump back to it.", - icon: "view-list-symbolic", - }, - TourStop { - title: "Choose a Workflow", - description: "Start by picking a preset that matches what you need, or build a custom workflow from scratch.", - icon: "applications-graphics-symbolic", - }, - TourStop { - title: "Add Your Images", - description: "Drag and drop files here, or use the Add button. You can paste from the clipboard too.", - icon: "image-x-generic-symbolic", - }, - TourStop { - title: "Navigation", - description: "Use the Back and Next buttons to move between steps, or press Alt+Left/Right. Disabled steps are automatically skipped.", - icon: "go-next-symbolic", - }, - TourStop { - title: "Main Menu", - description: "Access settings, keyboard shortcuts, processing history, and preset management from here.", - icon: "open-menu-symbolic", - }, - TourStop { - title: "You're Ready!", - description: "That's everything you need to know. Each step also has a help button (?) in the header bar for detailed guidance.", - icon: "emblem-ok-symbolic", - }, -]; - -/// Show the tutorial overlay if the user hasn't completed it yet. +/// Show the tutorial tour if the user hasn't completed it yet. /// Called after the welcome wizard closes on first launch. pub fn show_tutorial_if_needed(window: &adw::ApplicationWindow) { let config_store = pixstrip_core::storage::ConfigStore::new(); @@ -53,28 +14,74 @@ pub fn show_tutorial_if_needed(window: &adw::ApplicationWindow) { // Small delay to let the welcome dialog fully dismiss let win = window.clone(); glib::timeout_add_local_once(std::time::Duration::from_millis(400), move || { - show_tour_dialog(&win, 0); + show_tour_stop(&win, 0); }); } -fn show_tour_dialog(window: &adw::ApplicationWindow, stop_index: usize) { - let stop = &TOUR_STOPS[stop_index]; - let total = TOUR_STOPS.len(); +/// Tour stops: (title, description, widget_name, popover_position) +fn tour_stops() -> Vec<(&'static str, &'static str, &'static str, gtk::PositionType)> { + vec![ + ( + "Choose a Workflow", + "Pick a preset that matches your needs, or scroll down to build a custom workflow from scratch.", + "tour-content", + gtk::PositionType::Bottom, + ), + ( + "Track Your Progress", + "This bar shows where you are in the wizard. Click any completed step to jump back to it.", + "tour-step-indicator", + gtk::PositionType::Bottom, + ), + ( + "Navigation", + "Use Back and Next to move between steps, or press Alt+Left / Alt+Right. Disabled steps are automatically skipped.", + "tour-next-button", + gtk::PositionType::Top, + ), + ( + "Main Menu", + "Settings, keyboard shortcuts, processing history, and preset management live here.", + "tour-menu-button", + gtk::PositionType::Bottom, + ), + ( + "Get Help", + "Every step has a help button with detailed guidance specific to that step.", + "tour-help-button", + gtk::PositionType::Bottom, + ), + ] +} - let dialog = adw::Dialog::builder() - .title("Quick Tour") - .content_width(420) - .content_height(300) - .build(); +fn show_tour_stop(window: &adw::ApplicationWindow, index: usize) { + let stops = tour_stops(); + let total = stops.len(); + if index >= total { + mark_tutorial_complete(); + return; + } + let (title, description, widget_name, position) = stops[index]; + + // Find the target widget by name in the widget tree + let Some(root) = window.content() else { return }; + let Some(target) = find_widget_by_name(&root, widget_name) else { + // Widget not found - skip to next stop + show_tour_stop(window, index + 1); + return; + }; + + // Build popover content let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) - .spacing(16) - .margin_top(24) - .margin_bottom(24) - .margin_start(24) - .margin_end(24) + .spacing(8) + .margin_top(12) + .margin_bottom(12) + .margin_start(16) + .margin_end(16) .build(); + content.set_size_request(280, -1); // Progress dots let dots_box = gtk::Box::builder() @@ -82,12 +89,11 @@ fn show_tour_dialog(window: &adw::ApplicationWindow, stop_index: usize) { .spacing(6) .halign(gtk::Align::Center) .build(); - for i in 0..total { let dot = gtk::Label::builder() - .label(if i == stop_index { "\u{25CF}" } else { "\u{25CB}" }) + .label(if i == index { "\u{25CF}" } else { "\u{25CB}" }) .build(); - if i == stop_index { + if i == index { dot.add_css_class("accent"); } else { dot.add_css_class("dim-label"); @@ -96,94 +102,132 @@ fn show_tour_dialog(window: &adw::ApplicationWindow, stop_index: usize) { } content.append(&dots_box); - // Icon - let icon = gtk::Image::builder() - .icon_name(stop.icon) - .pixel_size(64) - .halign(gtk::Align::Center) - .build(); - icon.add_css_class("accent"); - content.append(&icon); - // Title - let title = gtk::Label::builder() - .label(stop.title) - .css_classes(["title-2"]) - .halign(gtk::Align::Center) + let title_label = gtk::Label::builder() + .label(title) + .css_classes(["title-3"]) + .halign(gtk::Align::Start) .build(); - content.append(&title); - - // Step counter - let counter = gtk::Label::builder() - .label(&format!("{} of {}", stop_index + 1, total)) - .css_classes(["dim-label", "caption"]) - .halign(gtk::Align::Center) - .build(); - content.append(&counter); + content.append(&title_label); // Description - let desc = gtk::Label::builder() - .label(stop.description) + let desc_label = gtk::Label::builder() + .label(description) .wrap(true) - .halign(gtk::Align::Center) - .justify(gtk::Justification::Center) + .max_width_chars(36) + .halign(gtk::Align::Start) + .xalign(0.0) .build(); - content.append(&desc); + content.append(&desc_label); + + // Step counter + let counter_label = gtk::Label::builder() + .label(&format!("{} of {}", index + 1, total)) + .css_classes(["dim-label", "caption"]) + .halign(gtk::Align::Start) + .build(); + content.append(&counter_label); // Buttons let button_box = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(12) - .halign(gtk::Align::Center) - .margin_top(8) + .halign(gtk::Align::End) + .margin_top(4) .build(); - let skip_button = gtk::Button::builder() + let skip_btn = gtk::Button::builder() .label("Skip Tour") + .tooltip_text("Close the tour and start using Pixstrip") .build(); - skip_button.add_css_class("flat"); + skip_btn.add_css_class("flat"); - let is_last = stop_index + 1 >= total; - let next_label = if is_last { "Get Started" } else { "Next" }; - let next_button = gtk::Button::builder() - .label(next_label) + let is_last = index + 1 >= total; + let next_btn = gtk::Button::builder() + .label(if is_last { "Done" } else { "Next" }) + .tooltip_text(if is_last { "Finish the tour" } else { "Go to the next tour stop" }) .build(); - next_button.add_css_class("suggested-action"); - next_button.add_css_class("pill"); + next_btn.add_css_class("suggested-action"); + next_btn.add_css_class("pill"); - button_box.append(&skip_button); - button_box.append(&next_button); + button_box.append(&skip_btn); + button_box.append(&next_btn); content.append(&button_box); - dialog.set_child(Some(&content)); + // Create popover attached to the target widget + let popover = gtk::Popover::builder() + .child(&content) + .position(position) + .autohide(false) + .has_arrow(true) + .build(); + popover.set_parent(&target); - // Wire skip - mark tutorial complete and close + // For the content area (large widget), point to the upper portion + // where the preset cards are visible + if widget_name == "tour-content" { + let w = target.width(); + if w > 0 { + let rect = gtk::gdk::Rectangle::new(w / 2 - 10, 40, 20, 20); + popover.set_pointing_to(Some(&rect)); + } + } + + // Accessible label for screen readers + popover.update_property(&[ + gtk::accessible::Property::Label( + &format!("Tour step {} of {}: {}", index + 1, total, title) + ), + ]); + + // Wire skip button { - let dlg = dialog.clone(); - skip_button.connect_clicked(move |_| { + let pop = popover.clone(); + skip_btn.connect_clicked(move |_| { mark_tutorial_complete(); - dlg.close(); + pop.popdown(); + let p = pop.clone(); + glib::idle_add_local_once(move || { + p.unparent(); + }); }); } - // Wire next + // Wire next button { - let dlg = dialog.clone(); + let pop = popover.clone(); let win = window.clone(); - next_button.connect_clicked(move |_| { - dlg.close(); - if is_last { - mark_tutorial_complete(); - } else { - let w = win.clone(); - glib::idle_add_local_once(move || { - show_tour_dialog(&w, stop_index + 1); - }); - } + next_btn.connect_clicked(move |_| { + pop.popdown(); + let p = pop.clone(); + let w = win.clone(); + glib::idle_add_local_once(move || { + p.unparent(); + if is_last { + mark_tutorial_complete(); + } else { + show_tour_stop(&w, index + 1); + } + }); }); } - dialog.present(Some(window)); + popover.popup(); +} + +/// Recursively search the widget tree for a widget with the given name. +fn find_widget_by_name(root: >k::Widget, name: &str) -> Option { + if root.widget_name().as_str() == name { + return Some(root.clone()); + } + let mut child = root.first_child(); + while let Some(c) = child { + if let Some(found) = find_widget_by_name(&c, name) { + return Some(found); + } + child = c.next_sibling(); + } + None } fn mark_tutorial_complete() {