Compare commits

..

126 Commits

Author SHA1 Message Date
522883183d Add screenshots, update metainfo, and fix AppImage build script
- Add 18 screenshots sorted by workflow order to data/screenshots/
- Update metainfo with actual screenshot entries and correct dimensions
- Fix build script to copy all hicolor icon sizes and use linuxdeploy --output
- Add exported icon PNGs to icons/
2026-03-08 14:50:59 +02:00
f3668c45c3 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
2026-03-08 14:18:15 +02:00
8d754017fa Clean up low-severity code quality issues
- Share IMAGE_EXTENSIONS between discovery.rs and watcher.rs (DRY)
- Extract compute_renamed_path() to deduplicate ~100 lines in executor
- Extract estimate_text_dimensions() to deduplicate watermark calc (3 copies)
- Fix encoder fallback defaults: WebP 85, AVIF 63 (match QualityPreset::High)
- Extract watch_config_dir() and load_watches() helpers in CLI (4 copies)
- Remove redundant else branches after unwrap_or_default()
- Rename misleading chrono_timestamp() to unix_timestamp()
2026-03-08 00:22:24 +02:00
7e5d19ab03 Fix 12 medium-severity bugs across all crates
- Escape backslashes in Nautilus preset names preventing Python injection
- Fix tiled watermarks starting at (spacing,spacing) instead of (0,0)
- Fix text watermark width overestimation (1.0x to 0.6x multiplier)
- Fix output_dpi forcing re-encoding for metadata-only presets
- Fix AVIF/WebP compression detection comparing against wrong preset values
- Add shared batch_updating guard for Ctrl+A/Ctrl+Shift+A select actions
- Fix overwrite conflict check ignoring preserve_directory_structure
- Add changes_filename()/changes_extension() for smarter overwrite checks
- Fix watch folder hardcoding "Blog Photos" preset
- Fix undo dropping history for partially-trashed batches
- Fix skipped files inflating size statistics
- Make CLI watch config writes atomic
2026-03-07 23:35:32 +02:00
1a174d40a7 Fix 5 deferred performance/UX issues from audit
M8: Pre-compile regex once before rename preview loop instead of
recompiling per file. Adds apply_simple_compiled() to RenameConfig.

M9: Cache font data in watermark module using OnceLock (default font)
and Mutex<HashMap> (named fonts) to avoid repeated filesystem walks
during preview updates.

M12: Add 150ms debounce to watermark opacity, rotation, margin, and
scale sliders to avoid spawning preview threads on every pixel of
slider movement.

M13: Add 150ms debounce to compress per-format quality sliders (JPEG,
PNG, WebP, AVIF) for the same reason.

M14: Move thumbnail loading to background threads instead of blocking
the GTK main loop. Each thumbnail is decoded via image crate in a
spawned thread and delivered to the main thread via channel polling.
2026-03-07 23:11:00 +02:00
9fcbe237bd Fix 16 medium-severity bugs from audit
CLI: add UTC suffix to timestamps, validate image extensions on
single-file input, canonicalize watch paths for reliable matching,
derive counter_enabled from template presence, warn when undo count
exceeds available batches.

Core: apply space/special-char transforms in template rename path,
warn on metadata preservation for unsupported formats, derive AVIF
speed from compress preset quality level.

GTK: use buffer size for apples-to-apples compress preview comparison,
shorten approximate format labels, cache file sizes to avoid repeated
syscalls on checkbox toggle, add batch-update guard to prevent O(n^2)
in select/deselect all, use widget names for reliable progress/log
lookup, add unique suffix for duplicate download filenames.
2026-03-07 23:02:57 +02:00
9ef33fa90f Fix CLI/GTK history timestamp format mismatch
CLI now stores Unix seconds (matching GTK) so age-based history
pruning works correctly. Human-readable formatting applied only
at display time in cmd_history and cmd_undo.
2026-03-07 22:35:25 +02:00
e65ed866eb Fix 5 high-risk bugs from 13th audit pass
- cleanup_placeholder now removes 1-byte marker files on error (regression)
- {width}/{height} template vars replaced with "0" when dimensions unavailable
- Text watermark width multiplier 0.6->1.0 to prevent CJK/wide char clipping
- cmd_undo preserves history entries when trash operations fail
- cmd_watch_remove exits on write failure instead of silent discard
2026-03-07 22:27:37 +02:00
d1cab8a691 Fix 40+ bugs from audit passes 9-12
- PNG chunk parsing overflow protection with checked arithmetic
- Font directory traversal bounded with global result limit
- find_unique_path TOCTOU race fixed with create_new + marker byte
- Watch mode "processed" dir exclusion narrowed to prevent false skips
- Metadata copy now checks format support before little_exif calls
- Clipboard temp files cleaned up on app exit
- Atomic writes for file manager integration scripts
- BMP format support added to encoder and convert step
- Regex DoS protection with DFA size limit
- Watermark NaN/negative scale guard
- Selective EXIF stripping for privacy/custom metadata modes
- CLI watch mode: file stability checks, per-file history saves
- High contrast toggle preserves and restores original theme
- Image list deduplication uses O(1) HashSet lookups
- Saturation/trim/padding overflow guards in adjustments
2026-03-07 22:14:48 +02:00
adef810691 Fix 30 critical and high severity bugs from audit passes 6-8
Critical fixes:
- Prevent path traversal via rename templates (sanitize_filename)
- Prevent input == output data loss (paths_are_same check)
- Undo now uses actual executor output paths instead of scanning directory
- Filter empty paths from output_files (prevents trashing CWD on undo)
- Sanitize URL download filenames to prevent path traversal writes

High severity fixes:
- Fix EXIF orientation 5/7 transforms per spec
- Atomic file creation in find_unique_path (TOCTOU race)
- Clean up 0-byte placeholder files on encoding failure
- Cap canvas padding to 10000px, total dimensions to 65535
- Clamp crop dimensions to minimum 1px
- Clamp DPI to 65535 before u16 cast in JPEG encoder
- Force pixel path for non-JPEG/TIFF metadata stripping
- Fast path now applies regex find/replace on rename stem
- Add output_dpi to needs_pixel_processing check
- Cap watermark image scale dimensions to 16384
- Cap template counter padding to 10
- Cap URL download size to 100MB
- Fix progress bar NaN when total is zero
- Fix calculate_eta underflow when current > total
- Fix loaded.len()-1 underflow in preview callbacks
- Replace ListItem downcast unwrap with if-let
- Fix resize preview division by zero on degenerate images
- Clamp rename cursor position to prevent overflow panic
- Watch mode: skip output dirs to prevent infinite loop
- Watch mode: drop tx sender so channel closes on exit
- Watch mode: add delay for partially-written files
- Watch mode: warn and skip unmatched files instead of wrong preset
- Clean temp download directory on app close
- Replace action downcast unwrap with checked if-let
- Add BatchResult.output_files for accurate undo tracking
2026-03-07 20:49:10 +02:00
b432cc7431 Fix 26 bugs, edge cases, and consistency issues from fifth audit pass
Critical: undo toast now trashes only batch output files (not entire dir),
JPEG scanline write errors propagated, selective metadata write result returned.

High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio
rejection, FM integration toggle infinite recursion guard, saturating counter
arithmetic in executor.

Medium: PNG compression level passed to oxipng, pct mode updates job_config,
external file loading updates step indicator, CLI undo removes history entries,
watch config write failures reported, fast-copy path reads image dimensions for
rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl),
CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot
fix, generation guards on all preview threads to cancel stale results, default
DPI aligned to 0, watermark text width uses char count not byte length.

Low: binary path escaped in Nautilus extension, file dialog filter aligned with
discovery, reset_wizard clears preset_mode and output_dir.
2026-03-07 19:47:23 +02:00
270a7db60d Fix step indicator layout warnings on narrow windows 2026-03-06 18:44:41 +02:00
4f23d25511 Add AppImage packaging, app icon, and AppStream metainfo 2026-03-06 18:31:18 +02:00
704f556867 Add EXIF auto-orient and aspect ratio lock toggle
- Implement auto_orient_from_exif() that reads EXIF Orientation tag
  and applies the correct rotation/flip for all 8 EXIF orientations
- Add aspect ratio lock toggle to resize step Width/Height mode
- When lock is active, changing width auto-calculates height from
  the first loaded image's aspect ratio, and vice versa
- Uses recursive update guard (Cell<bool>) to prevent infinite loops
2026-03-06 18:18:04 +02:00
0a7a6ee95c Implement watermark rotation for text and image watermarks
- Add rotate_watermark_image() using imageproc rotate_about_center
  for 45/-45 degree rotations and image::rotate90 for 90 degrees
- Add render_text_to_image() helper that renders text to a
  transparent buffer for rotation before compositing
- Apply rotation to single text, tiled text, single image, and
  tiled image watermark modes
- Fix tiled image watermark to use actual overlay dimensions
  (which change after rotation) instead of pre-rotation values
2026-03-06 18:14:53 +02:00
a666fbad05 Fix pipeline order, add selective metadata stripping, rename case/regex
- Move watermark step after compress in processing pipeline to match
  design doc order (resize, adjustments, convert, compress, metadata,
  watermark, rename)
- Implement selective EXIF metadata stripping for Privacy and Custom
  modes using little_exif tag filtering (GPS, camera, software,
  timestamps, copyright categories)
- Add case conversion support to rename (none/lower/upper/title)
- Add regex find-and-replace on original filenames
- Wire case and regex controls in rename step UI to JobConfig
- Add regex crate dependency to pixstrip-core
2026-03-06 18:12:18 +02:00
5104d66aaf Add EXIF-based rename template variables
Support {date}, {exif_date}, {camera}, and {original_ext} in rename
templates. Reads DateTimeOriginal and Model from EXIF metadata via
little_exif when these variables are used in templates.
2026-03-06 18:03:12 +02:00
5352b67887 Add thumbnail re-render preview to resize step
Shows an actual resized thumbnail alongside the dimension rectangle
visualization. Updates live when width/height values change.
2026-03-06 17:56:44 +02:00
e2aee57bd9 Integrate watch folder monitoring into GTK app
Active watch folders from settings are now monitored using the notify
crate. When new images appear, they are automatically processed using
the folder's linked preset. Toast notifications inform the user.
2026-03-06 17:53:41 +02:00
afabdf3548 Persist advanced options expand/collapse state per section
Each ExpanderRow now remembers its expanded state between sessions
using a per-section key stored in SessionState. Replaces the global
detailed_mode toggle with granular per-section persistence.
2026-03-06 17:49:07 +02:00
d8bb1a726a Add watermark tiling, rotation types, margin/scale controls
Wire tiled, margin, and scale UI controls to JobConfig and pass
through to WatermarkConfig. Add tiled text and image watermark
implementations that repeat across the full image. Add font family
filesystem search for named fonts. Add WatermarkRotation enum.
2026-03-06 17:36:07 +02:00
45247cdac5 Add collapsible watch folders panel in main window
Toggle button in the header bar reveals a bottom panel showing active
watch folders with their linked presets and watching status. The add
button opens Settings for full watch folder configuration.
2026-03-06 17:29:02 +02:00
0460763d42 Add fixed output folder option in settings
Users can now choose between subfolder-next-to-originals or a fixed
output folder in Settings > General. The fixed path is selectable via
a folder picker dialog and persisted across sessions.
2026-03-06 17:23:57 +02:00
fbb9cddbb8 Add PNG DPI support via pHYs chunk and fix font picker
PNG files now embed pHYs chunk for DPI when output_dpi is set, matching
the existing JPEG DPI support. Also fixed FontDialogButton signal handler
to properly unwrap the Option<FontDescription>.
2026-03-06 17:16:43 +02:00
d9ce1f8731 Add font family selector for watermark text
Font picker using GTK FontDialog/FontDialogButton lets users choose
any installed system font for text watermarks. The selected font family
is passed through the processing pipeline and used to find the matching
font file on disk.
2026-03-06 17:12:23 +02:00
3109f97786 Add overwrite confirmation dialog and preset file drop import
- When overwrite behavior is "Ask", show dialog before processing
  listing conflicting files with Overwrite/Skip/Auto-rename options
- Workflow page accepts .pixstrip-preset file drag-and-drop for import
- Split run_processing into two phases to support async dialog
2026-03-06 17:07:53 +02:00
f8fd073735 Add drag-and-drop import for .pixstrip-preset files
- Workflow page accepts .pixstrip-preset file drops
- Dropped preset files are imported and applied to current config
- Saved to user presets automatically on successful import
2026-03-06 17:06:02 +02:00
0ceec7eec4 Add preset export/delete buttons and single-instance support
- User preset rows now have export and delete action buttons
- Export opens file save dialog for .pixstrip-preset files
- Delete removes preset from storage and UI
- Single-instance via GIO HANDLES_OPEN for file manager integration
- Screen reader announcements on step navigation
2026-03-06 17:04:18 +02:00
683422f8f5 Improve screen reader support for wizard navigation
- Announce step number and name when navigating between steps
- Add accessible label to batch queue toggle button
- Add accessible label to navigation view with shortcut hint
- Step transitions update accessible property for screen readers
2026-03-06 17:02:04 +02:00
b9775d5632 Add single-instance file handling via GIO HANDLES_OPEN
- App flag HANDLES_OPEN enables receiving files from file manager
- connect_open handler filters for image files and activates window
- load-external-files action receives paths and adds to loaded files
- Auto-navigates to step 2 (images) when files arrive externally
- Second instance sends files to existing window instead of launching
2026-03-06 16:58:23 +02:00
a7dda7ad6f Enhance history dialog with expandable details and DPI support
- History entries now use ExpanderRow with detailed sub-rows
- Show formatted timestamp, input/output directories, size breakdown
- Clickable output directory row opens file manager
- Per-entry undo and open folder action buttons
- Error count shown for batches with failures
2026-03-06 16:56:23 +02:00
1ab21307d6 Apply output DPI to JPEG files via mozjpeg pixel density
- Add output_dpi field to EncoderOptions
- Set JFIF pixel density header in JPEG output when DPI > 0
- Wire output_dpi from ProcessingJob through to encoder in both
  parallel and sequential execution paths
2026-03-06 16:53:32 +02:00
5a56e25c2c Add sequential batch queue processing
- After a batch completes, automatically start next pending batch
- Mark completed/failed batches in queue with correct status
- Failed batches don't block the queue - next pending batch starts
- Queue processes batches in order, one at a time
2026-03-06 16:17:25 +02:00
a0bb00eddf Add Watch Folders settings page with full CRUD
- New settings page: Watch Folders with add/remove/edit controls
- Each watch folder has: path, linked preset dropdown, recursive toggle,
  active/inactive switch, and remove button
- Watch folder config persisted in AppConfig
- Empty state message when no folders configured
2026-03-06 16:13:14 +02:00
e976ca2c0a Add rename template presets and watermark color picker
- Rename step: quick-fill buttons for common patterns (Date+Name,
  EXIF Date+Name, Sequential, Dimensions, Camera+Date, Web-safe)
- Watermark step: color picker in advanced options using ColorDialogButton
- Add watermark_color field to JobConfig, wire through to core
2026-03-06 16:10:05 +02:00
45aaa02f19 Add URL drag-and-drop support for images from web browsers
Users can now drag image URLs from web browsers into the image
step. URLs ending in common image extensions are downloaded to a
temp directory and added to the batch. Uses GIO for the download
in a background thread.
2026-03-06 16:02:39 +02:00
fe12316bc4 Add thumbnail selection for compression and watermark previews
Users can now click different batch images in a thumbnail strip to
switch which image is used for the quality comparison preview and
watermark position preview. Shows up to 10 thumbnails with accent
highlight on the selected one.
2026-03-06 15:59:44 +02:00
fcb4b0e727 Improve output step with individual operation summary rows
Replace single-line arrow-delimited summary with a proper ListBox
showing each enabled operation as its own row with check icons.
Dynamically rebuilt when navigating to the output step. Also make
walk_widgets public for cross-module use.
2026-03-06 15:57:06 +02:00
a09462fd53 Add batch queue with slide-out side panel
Queue panel with OverlaySplitView sidebar. Users can add batches
from the results page via "Add to Queue" action. Queue shows
pending/active/completed batches with status icons. Toggle via
header bar button. Batches can be removed while pending.
2026-03-06 15:52:18 +02:00
ced65f10ec Add tutorial overlay tour after welcome wizard
Semi-transparent dialog-based tour with 6 stops covering step
indicator, workflow selection, image adding, navigation, menu,
and a final ready message. Skippable at any time. State persisted
via tutorial_complete flag in AppConfig.
2026-03-06 15:48:06 +02:00
33659a323b Enhance save preset dialog with settings summary and update option
Replace simple alert dialog with full dialog showing workflow summary,
new preset name entry, and option to update existing user presets.
Never overwrites built-in presets.
2026-03-06 15:43:45 +02:00
1d33be1e3d Wire DPI setting from resize step through to processing job
Add output_dpi field to JobConfig, ProcessingJob, and Preset.
Connect the DPI SpinRow in the resize step's advanced options to
update the config value.
2026-03-06 15:41:25 +02:00
eb8da4b3e9 Add live watermark preview with position and opacity updates
Shows a thumbnail of the first batch image with the watermark text
overlaid. Preview updates in real-time as user changes text, position
(9-point grid), and opacity.
2026-03-06 15:39:10 +02:00
b50147404a Add file manager integration install/uninstall logic
Implements actual extension file creation for Nautilus (Python
extension), Nemo (.nemo_action files), Thunar (custom actions XML),
and Dolphin (KDE service menu .desktop). Each extension creates a
"Process with Pixstrip" submenu with all presets listed. Toggle
switches in welcome wizard and settings now call install/uninstall.
2026-03-06 15:37:25 +02:00
5bdeb8a2e3 Wire skill level and accessibility settings to UI
- Add detailed_mode to AppState, derived from skill_level setting
- Expand advanced option sections by default in Detailed mode
  (resize, convert, compress, watermark steps)
- Fix high contrast to use HighContrast GTK theme
- Add completion sound via canberra-gtk-play
2026-03-06 15:28:02 +02:00
d6f7fc9c88 Initialize overwrite behavior from app settings
The overwrite behavior in the output step now defaults to the value
configured in Settings rather than always defaulting to "Ask".
2026-03-06 15:24:51 +02:00
4dd868078a Add completion sound, preserve directory structure support
- Play system notification sound via canberra-gtk-play when enabled
- Implement preserve_directory_structure in output_path_for to maintain
  relative paths from input directory in output
2026-03-06 15:24:04 +02:00
1e3ffaadd3 Add history pruning with configurable max entries and max days
- Add history_max_entries (default 50) and history_max_days (default 30)
  to AppConfig
- Add prune() method to HistoryStore that removes old entries by age
  and count limits
- Call prune after each history entry is added in the GUI
- Preserve history settings through settings dialog save/load cycle
2026-03-06 15:22:17 +02:00
5e83cb09ef Add --algorithm and --overwrite flags to CLI
Support resize algorithm selection (lanczos3/catmullrom/bilinear/nearest)
and overwrite behavior (auto-rename/overwrite/skip) in the CLI process
command, matching the GUI functionality.
2026-03-06 15:19:34 +02:00
a29256921e Wire resize algorithm selection, overwrite behavior, and fix rotation/flip scope
- Add ResizeAlgorithm enum (Lanczos3/CatmullRom/Bilinear/Nearest) to core
- Thread algorithm selection from UI ComboRow through ProcessingJob to resize_image
- Add OverwriteBehavior enum (AutoRename/Overwrite/Skip) to core
- Implement overwrite handling in executor with auto-rename suffix logic
- Wire overwrite behavior from output step through to processing job
- Fix rotation/flip to apply when resize step is enabled, not just adjustments
2026-03-06 15:17:59 +02:00
064194df3d Fix imported presets not persisting, remove duplicate help handler 2026-03-06 15:12:06 +02:00
5881fb6eae Restore session settings for convert format, quality preset, and metadata mode
- Parse stored format/quality/metadata strings back to enum values on launch
- Remove duplicate help button handler
- Session now properly round-trips all wizard state between launches
2026-03-06 15:10:28 +02:00
fdaedd8d1a Wire progressive JPEG, AVIF speed, and custom per-format quality to encoder
- Add EncoderOptions struct with progressive_jpeg and avif_speed fields
- Pass encoder options through ProcessingJob to PipelineExecutor
- mozjpeg set_progressive_mode() called when progressive JPEG enabled
- AVIF encoder speed now configurable (was hardcoded to 6)
- run_processing uses CompressConfig::Custom when user overrides preset defaults
- Executor properly handles AVIF quality and PNG level in Custom mode
2026-03-06 15:07:54 +02:00
52b7a7fed2 Wire help button to show contextual step help dialog
Connect the header bar help button to the existing show_step_help
function, showing context-aware help for the current wizard step.
Remove duplicate function definition.
2026-03-06 14:59:29 +02:00
edc5b1acee Guard output summary adjustments behind adjustments_enabled flag
Rotation, flip, brightness, contrast, and other adjustment operations
only appear in the output step summary when the adjustments step is
enabled in the workflow configuration.
2026-03-06 14:57:38 +02:00
abd8393079 Auto-show What's New dialog on first launch after update
Track last_seen_version in session state. Show the What's New dialog
automatically when the version changes, but not on very first run
(where the welcome wizard is shown instead).
2026-03-06 14:54:43 +02:00
d1b811aa6a Add wizard step-skipping for disabled operations
Steps for disabled operations (resize, adjustments, convert, compress,
metadata, watermark, rename) are automatically skipped when navigating
forward or backward. Jump-to-step also respects skip logic.
2026-03-06 14:53:04 +02:00
23c1f33d32 Support per-format conversion mapping in processing pipeline
Build FormatMapping when per-format overrides are set in the convert
step advanced options, falling back to SingleFormat when no overrides
are configured.
2026-03-06 14:51:17 +02:00
2c28c092d4 Wire missing UI controls to job config
- Add adjustments_enabled field and guard rotation/flip/adjustments behind it
- Wire adjustments toggle in workflow step
- Wire progressive JPEG toggle in convert and compress steps
- Wire format mapping ComboRows (JPEG/PNG/WebP/TIFF) in convert step
- Wire AVIF quality, WebP effort, AVIF speed controls in compress step
- Initialize all new controls from current config values
2026-03-06 14:50:12 +02:00
3aeb05c9a0 Replace custom shortcuts dialog with GtkShortcutsWindow, fix Process More button
- Use proper GtkShortcutsWindow with ShortcutsSection/Group/Shortcut widgets
  instead of custom AdwDialog with ActionRows
- Hide step indicator during processing and results screens
- Fix "Process More" button re-triggering processing instead of resetting wizard
- Add accessible label to resize step size preview DrawingArea
2026-03-06 14:46:54 +02:00
6dd81e5900 Add squoosh-style compression preview with draggable divider
Split-view comparison showing original vs compressed image side by
side. Draggable vertical divider with handle circle. Shows file sizes
and savings percentage. Compresses a sample image in a background
thread and renders via cairo with pixbuf clipping.
2026-03-06 14:29:01 +02:00
f71b55da72 Add image adjustments pipeline (brightness, contrast, crop, effects)
New AdjustmentsConfig with brightness, contrast, saturation, sharpen,
grayscale, sepia, crop to aspect ratio, trim whitespace, and canvas
padding. All wired from UI through to executor.

- Saturation uses luminance-based color blending
- Sepia uses standard matrix transformation
- Crop calculates center crop from aspect ratio
- Trim whitespace detects uniform border by corner pixel comparison
- Canvas padding adds white border around image
2026-03-06 14:24:14 +02:00
8212969e9d Wire all missing operations into pipeline executor
Executor now applies rotation, flip, resize, watermark, format
conversion, compression, renaming, and metadata handling. Previously
only resize, convert, and compress were active.

- Rotation: CW90/180/270 via image crate methods
- Flip: horizontal/vertical via image crate methods
- Watermark: text (imageproc + ab_glyph) and image overlay with
  alpha blending, positioned via WatermarkPosition enum
- Rename: apply_simple or template-based renaming with counter
- Metadata: re-encoding strips EXIF; KeepAll copies back via little_exif
2026-03-06 14:20:47 +02:00
5822959907 Add accessible labels to thumbnail grid and toolbar 2026-03-06 14:08:52 +02:00
8e50fa5e87 Add visual size preview to resize step
Show proportional rectangles comparing original image dimensions
(gray) vs target output dimensions (blue). Preview updates live
as the user changes width/height values. Uses first loaded image
for actual dimensions when available.
2026-03-06 14:05:52 +02:00
3070980241 Add CSS provider for thumbnail grid styling
Load application CSS at startup with rounded corners for thumbnail
frames and grid padding.
2026-03-06 14:02:49 +02:00
fc38c261c3 Replace image list with GtkGridView thumbnail grid
Upgrade the Add Images step from a plain ListBox to a GtkGridView
with thumbnail rendering. Each item shows a scaled thumbnail with
a checkbox overlay for include/exclude, and filename label below.
Thumbnails load asynchronously using Pixbuf at reduced size.
Uses GObject subclass (ImageItem) for the list model.
2026-03-06 14:01:52 +02:00
fa7936abbd Implement parallel image processing with rayon thread pool
The executor now uses rayon's thread pool for parallel processing when
thread_count > 1. Progress updates are sent via mpsc channel from worker
threads. Falls back to sequential processing for thread_count = 1.
2026-03-06 13:57:14 +02:00
b79ce58338 Add accessible value updates to processing progress bar
Screen readers now announce progress bar value with descriptive
text like "Processing 5 of 47: sunset.jpg" during batch processing,
instead of just reporting the fraction.
2026-03-06 13:53:32 +02:00
abd794b3b9 Wire thread count and error behavior settings into executor
The PipelineExecutor now stores thread_count and pause_on_error
fields. When pause_on_error is enabled, the executor sets the
pause flag on failures so the user can review errors. The GUI
reads these settings from AppConfig before starting processing.
2026-03-06 13:52:08 +02:00
be081307c4 Add accessible labels to sliders and watermark position grid
Screen readers now announce the purpose and range of brightness,
contrast, saturation, and compression quality sliders. The
watermark position grid frame also has a descriptive label.
2026-03-06 13:51:01 +02:00
bcf57927e9 Add per-step contextual help button in header bar
A "?" button in the header bar shows an AlertDialog with help
text specific to the current wizard step, explaining what each
step does and key shortcuts available.
2026-03-06 13:48:13 +02:00
257fd53bbc Add Ctrl+V clipboard paste for images
Reads image textures from the system clipboard, saves as temporary
PNG files, and adds them to the batch. Shows toast notification
on success or when no image is found.
2026-03-06 13:46:20 +02:00
0ca15536ae Add Escape key shortcut and subfolder prompt for folder drops
Escape now goes to previous wizard step (cancel/go back). When
a folder with subfolders is dropped on the images step, an alert
dialog asks whether to include subfolder images. The choice is
remembered for the rest of the session.
2026-03-06 13:45:06 +02:00
fa7a8f54bb Add accessible labels to processing progress bar, results icon, drop zone
- Progress bar gets "Processing progress" accessible label
- Results success icon gets "Success" accessible label
- Image drop zone gets descriptive accessible label for screen readers
2026-03-06 13:41:30 +02:00
07d47b6b3f Replace watermark position dropdown with visual 3x3 grid
- 9-point position grid using ToggleButtons in a Grid layout
- Visual feedback with radio-checked/radio-symbolic icons
- Position label below grid shows current selection name
- Much better UX than ComboRow for spatial position selection
2026-03-06 13:38:12 +02:00
e02d677e5c Add Ctrl+Z undo last batch and register select/deselect shortcuts
- Ctrl+Z moves output files from last batch to system trash via GIO
- Select all / deselect all actions now registered as window actions
- Shortcuts registered: Ctrl+A, Ctrl+Shift+A, Ctrl+Z
2026-03-06 13:35:45 +02:00
137bb77faa Add Ctrl+A and Ctrl+Shift+A keyboard shortcuts for select/deselect all
- Register select-all-images and deselect-all-images actions
- Wire Ctrl+A to clear exclusion set, Ctrl+Shift+A to exclude all
- Both shortcuts update checkbox state and count label in images step
- Make set_all_checkboxes_in public for cross-module access
2026-03-06 13:33:06 +02:00
819ac963de Respect excluded files in output step counts and processing validation
- Output step now shows only included (non-excluded) image count and size
- Navigate-to-step refresh accounts for exclusions
- Process action checks for included images, not just loaded files
2026-03-06 13:31:35 +02:00
a74010a121 Add file manager integration toggles and reset button to Settings
- Settings now has a File Manager Integration group with toggle rows
  for each detected file manager (Nautilus, Nemo, Thunar, Dolphin)
- Added Reset to Defaults button that restores all settings to defaults
2026-03-06 13:29:25 +02:00
2911c608c2 Add per-image checkboxes and wire Select All / Deselect All buttons
- Each image row now has a CheckButton for include/exclude from processing
- Select All clears exclusion set, Deselect All adds all files to it
- Count label shows "X/Y images selected" when some are excluded
- Processing respects excluded files - only processes checked images
- Clear All also resets exclusion set
- AppState gains excluded_files HashSet for tracking
2026-03-06 13:24:18 +02:00
32ea206c8c Add WebP effort, AVIF speed controls, case conversion and regex rename
- Compress step: WebP Encoding Effort (0-6) and AVIF Encoding Speed (1-10)
- Rename step: Case Conversion combo (lowercase/UPPERCASE/Title Case)
- Rename step: Find and Replace group with regex pattern and replacement
2026-03-06 13:19:11 +02:00
7840765e5b Add File Manager Integration page to welcome wizard
Add fourth welcome wizard page that detects installed file managers
(Nautilus, Nemo, Thunar, Dolphin) and offers integration toggles for
each. Follows the design doc specification for the first-run experience.
2026-03-06 13:16:11 +02:00
b78f1cd7c4 Add What's New dialog, accessibility labels, focus management
Add What's New dialog accessible from hamburger menu showing version
changelog. Add accessible property labels to step indicator for screen
reader support with current step/total announcements. Add focus
management on step transitions - focus moves to first interactive
element when navigating to a new step.
2026-03-06 13:13:39 +02:00
ea8444f039 Add format mapping to convert step, orientation controls to resize step
Add per-format mapping rows in convert step advanced options so each
input format can target a different output format. Move basic
orientation controls (rotate/flip) into the resize step per design doc
specification that these should be folded into the resize step.
2026-03-06 13:11:00 +02:00
3ae84297d5 Add crop/trim/canvas padding to adjustments, wire all sliders to config
Add crop to aspect ratio (8 ratios), trim whitespace, and canvas padding
controls to the adjustments step per design doc. Wire brightness,
contrast, saturation, sharpen, grayscale, and sepia to JobConfig. Add
Select All / Deselect All toolbar buttons to images step. Include new
adjustment operations in output step summary.
2026-03-06 13:09:45 +02:00
7c260c3534 Add Photographer metadata preset and improve quality descriptions
Add Photographer mode to metadata step that keeps copyright and camera
model while stripping GPS and software data. Improve compress quality
descriptions with estimated file size reduction percentages.
2026-03-06 13:05:07 +02:00
0234f872bc Add custom workflow operation toggles, improve output step summary
Workflow step: replace the auto-advancing Custom card with a proper
operation checklist using SwitchRow toggles for each operation (Resize,
Adjustments, Convert, Compress, Metadata, Watermark, Rename). Wired
to job config so selections persist through the wizard.

Output step: show actual file size alongside image count. Refresh
both count and size dynamically when navigating to the output step.
2026-03-06 13:00:52 +02:00
8f6e4382c4 Add visual format cards, per-image remove, shortcuts dialog, wire threads
Convert step: replace ComboRow with visual format card grid showing
icon, name, and description for each format. Much more beginner-friendly.

Images step: add per-image remove button on each file row so users
can exclude individual images from the batch.

Shortcuts: use adw::Dialog with structured layout since GtkShortcutsWindow
is deprecated in GTK 4.18+. Add file management and undo shortcuts.

Settings: wire thread count selection to actually save/restore the
ThreadCount config value instead of always defaulting to Auto.
2026-03-06 12:58:43 +02:00
29770be8b5 Enhance rename preview to show 5 files, add watermark advanced expander
Rename step now shows preview for first 5 loaded files (or fallback
examples) with incrementing counters instead of a single line.

Watermark step gains an advanced expander with rotation, tiling,
margin, and scale options alongside the existing opacity control.
2026-03-06 12:54:35 +02:00
81f92e5b35 Enhance adjustments with sliders/effects, add undo toast, compress AVIF/progressive 2026-03-06 12:47:12 +02:00
3284e066a0 Add session memory, resize mode tabs, improved output summary 2026-03-06 12:44:16 +02:00
9efcbd082e Add real pause, desktop notifications, undo, accessibility, CLI watch 2026-03-06 12:38:16 +02:00
b21b9edb36 Add rotate, flip, watermark, and rename CLI flags with helper functions 2026-03-06 12:33:21 +02:00
e969c4165e Add window persistence, clickable step indicator, file list improvements
- Window size/position remembered between sessions via SessionStore
- Step indicator dots now clickable to navigate directly to that step,
  with keyboard shortcut hints in tooltips
- File list in add-files dialog shows format and size per image,
  header shows total count and total size
- Welcome dialog now saves skill level choice to config
- SessionState extended with window_width, window_height, window_maximized
2026-03-06 12:27:19 +02:00
4fc4ea7017 Improve Images, Compress, Output, Workflow steps
- Images step: folder drag-and-drop with recursive image scanning, per-file
  list with format and size info, total file size in header, supported
  formats label in empty state
- Compress step: per-format quality controls moved into AdwExpanderRow,
  improved quality level descriptions
- Output step: dynamic image count with total size from loaded_files,
  initial overwrite behavior from config
- Workflow step: properly handle MetadataConfig::Custom in preset import,
  mapping all custom metadata fields to JobConfig
2026-03-06 12:22:15 +02:00
e8cdddd08d Add ETA calculation, activity log, keyboard shortcuts, expand format options
- Processing: ETA calculated from elapsed time and progress, per-image log
  entries added to activity log with auto-scroll
- Keyboard shortcuts: Ctrl+Q quit, Ctrl+, settings, Ctrl+?/F1 shortcuts dialog
- Shortcuts dialog: AdwAlertDialog with all keyboard shortcuts listed
- Hamburger menu: added Keyboard Shortcuts entry
- Convert step: added AVIF, GIF, TIFF format options with descriptions
- Resize step: removed duplicate rotate/flip (now in Adjustments step),
  added missing social media presets (PeerTube, Friendica, Funkwhale,
  Instagram Portrait, Facebook Cover/Profile, LinkedIn Cover/Profile,
  TikTok, YouTube Channel Art, Threads, Twitter/X, 4K, etc.)
2026-03-06 12:20:04 +02:00
8154324929 Add Adjustments, Watermark, Rename wizard steps; expand to 10-step wizard
- New step_adjustments: rotation (5 options) and flip (3 options)
- New step_watermark: text/image watermark with position, opacity, font size
- New step_rename: prefix/suffix/counter with live preview and template engine
- Updated step_metadata: added Custom mode with per-category checkboxes
  (GPS, camera, software, timestamps, copyright) with show/hide toggle
- Expanded JobConfig with all operation fields (watermark, rename, metadata custom)
- Updated wizard from 7 to 10 steps in correct pipeline order
- Fixed page index references from 6 to 9 for output step
- Added MetadataMode::Custom handling in preset builder and output summary
2026-03-06 12:15:02 +02:00
a7f1df2ba5 Wire remaining UI elements: presets, drag-drop, import/save, output summary
- Workflow preset cards now apply their config to JobConfig on selection
- User presets section shows saved custom presets from PresetStore
- Import Preset button opens file dialog and imports JSON presets
- Save as Preset button in results page saves current workflow
- Images step supports drag-and-drop for image files
- Images loaded state shows file list and clear button
- Output step dynamically shows operation summary when navigated to
- Output step wires preserve directory structure and overwrite behavior
- Results page displays individual error details in expandable section
- Pause button toggles visual state on processing page
2026-03-06 12:01:50 +02:00
b855955786 Wire all wizard step controls to shared JobConfig state
All four configurable steps (resize, convert, compress, metadata) now
have signal handlers that update the shared JobConfig via AppState.
The run_processing function builds ProcessingJob from actual user
choices instead of hardcoded values. Fixed clippy warnings (collapsed
if-let chain, removed needless borrow).
2026-03-06 11:51:01 +02:00
eeb418ccdd Wire settings dialog to persist config via ConfigStore
- Load current settings from ConfigStore on dialog open
- All switches, combos, and entries reflect saved values
- Save all settings back to ConfigStore on dialog close
- Covers: output, overwrite, skill level, threads, errors,
  accessibility, and notification preferences
2026-03-06 11:44:16 +02:00
f353bbe5e6 Wire welcome dialog with navigation and first-run detection
- Welcome dialog buttons navigate between pages
- Done button closes dialog and marks first_run_complete
- Show welcome dialog on first launch only
- Add first_run_complete field to AppConfig with serde(default)
2026-03-06 11:43:25 +02:00
c20e0db2ff Wire up step buttons: Browse, preset cards, output directory picker
- Browse Files button triggers win.add-files action
- Add More button in loaded state triggers win.add-files action
- Preset card activation (click) advances to next wizard step
- Custom workflow card activation advances to next step
- Choose output folder button opens folder dialog
- Output step shows current image count when navigated to
- Clean up dead code in update_count_in_box
2026-03-06 11:41:46 +02:00
b6aae711ec Wire up all GTK UI actions to real functionality
- Settings menu opens PreferencesDialog
- History menu shows HistoryStore entries in a dialog
- Add Files (Ctrl+O) opens FileDialog with image MIME filters
- Process button runs PipelineExecutor in background thread
- Progress bar updates via mpsc channel polled with glib timeout
- Cancel button sets AtomicBool flag to stop processing
- Results page shows real stats (images, sizes, savings, time)
- Open Output Folder launches default file manager
- Process Another Batch resets wizard to step 1
- Toast notifications via ToastOverlay for feedback
- History entries saved after each processing run
- Remove dead_code allows from processing.rs and settings.rs
2026-03-06 11:37:32 +02:00
eb16149824 Add welcome wizard, desktop entry, and Nautilus extension
First-run welcome dialog with skill level and output location setup.
Desktop entry file for GNOME app launcher integration.
Nautilus Python extension with dynamic 'Process with Pixstrip' submenu
that reads both built-in and user presets.
2026-03-06 11:18:28 +02:00
e1c2e11165 Add file watcher for watch folder functionality
inotify-based folder watcher using the notify crate that detects new
image files, ignores non-image files, and supports start/stop lifecycle.
WatchFolder config struct for preset-linked watched directories.
2026-03-06 11:17:02 +02:00
06860163f4 Implement fully functional CLI with process, preset, history, and undo commands
Process command discovers images, builds pipeline from preset or CLI
flags (--resize, --format, --quality, --strip-metadata, --recursive),
executes with progress output, prints results with size savings, and
saves to history. Preset list/export/import use the storage module.
2026-03-06 11:13:33 +02:00
1587764b1e Add processing, results, and settings UI screens
Processing screen with progress bar, activity log, and pause/cancel.
Results screen with stats summary, error section, and action buttons.
Settings dialog with General, Processing, Accessibility, Notifications.
2026-03-06 11:10:38 +02:00
a66db2b3bb Add all wizard step UIs: workflow, images, resize, convert, compress, metadata, output
Full Adwaita widget-based layouts for all 7 wizard steps with
PreferencesGroups, SwitchRows, SpinRows, ComboRows, FlowBoxes,
ExpanderRows for social media presets, quality slider with named
marks, metadata radio group, and output configuration.
2026-03-06 11:08:38 +02:00
20f4c24538 Add GTK app shell with wizard navigation, step indicator, and actions
7-step wizard flow (Workflow, Images, Resize, Convert, Compress,
Metadata, Output) with AdwNavigationView, step indicator dots,
Back/Next buttons, keyboard shortcuts (Alt+arrows, Alt+1-9),
and hamburger menu with Settings and History placeholders.
2026-03-06 11:03:11 +02:00
be7d345aa9 Add storage module for presets, config, session, and history persistence
Preset save/load/list/delete/import/export, config JSON persistence,
session state save/restore, and processing history log with append/clear.
All stored as JSON under ~/.config/pixstrip/.
2026-03-06 02:14:57 +02:00
8ced89a00f Add pipeline executor with progress reporting and cancellation
Sequential execution with per-image progress callbacks, cancellation
via atomic flag, batch result tracking (success/fail/sizes/timing).
Phase 5 complete - 85 tests passing, zero clippy warnings.
2026-03-06 02:08:11 +02:00
d4aef0b774 Add metadata stripping, watermark positioning, and rename template modules
Metadata: JPEG EXIF stripping (APP1 segment removal).
Watermark: 9-position grid calculation with margin support.
Rename: template parser with {name}, {ext}, {counter:N}, {width}, {height}
  and collision resolution with auto-suffix.
Phase 4 complete - 79 tests passing, zero clippy warnings.
2026-03-06 02:06:01 +02:00
ea4ea9c9c4 Add metadata, watermark, and rename dependencies: little_exif, imageproc, ab_glyph 2026-03-06 02:03:04 +02:00
52931daf53 Add output encoders (mozjpeg, oxipng, webp, avif) and integration tests
OutputEncoder dispatches to specialized encoders per format.
JPEG: mozjpeg with quality control. PNG: oxipng lossless optimization.
WebP: libwebp encoding. AVIF: ravif via image crate.
GIF/TIFF: fallback via image crate.
Phase 3 complete - 59 tests passing, zero clippy warnings.
2026-03-06 02:02:27 +02:00
cacd52e85b Add output encoder dependencies: mozjpeg, oxipng, webp 2026-03-06 01:59:52 +02:00
a658036326 Fix clippy: collapse nested if in discovery module
Phase 2 complete - 49 tests passing, zero clippy warnings.
2026-03-06 01:59:00 +02:00
fea51ed220 Add CLI skeleton with clap: process, preset, history, and undo subcommands 2026-03-06 01:57:43 +02:00
175324e1aa Add minimal GTK4/libadwaita window with header bar and status page 2026-03-06 01:57:03 +02:00
545bf27fe4 Add SIMD-accelerated resize operation using fast_image_resize
Lanczos3 filter, supports ByWidth, ByHeight, FitInBox, Exact modes.
All 5 resize tests passing.
2026-03-06 01:54:57 +02:00
c445f71163 Add ImageLoader and file discovery modules
ImageLoader: load image info (dimensions, format, file size) and pixels.
Discovery: find image files by extension, flat or recursive, single file or directory.
All 9 tests passing.
2026-03-06 01:50:46 +02:00
5c93dbf829 Add image processing dependencies: magick_rust, fast_image_resize, image, rayon, walkdir 2026-03-06 01:49:12 +02:00
d630d2fb3d Add AppConfig with overwrite behavior, skill level, thread count settings
All 3 config tests passing.
2026-03-06 01:43:22 +02:00
e7142604d4 Add Preset type with 8 built-in presets and JSON serialization
All 5 preset tests passing.
2026-03-06 01:42:35 +02:00
715d8ab626 Add ProcessingJob type with source management and output path resolution
All 6 pipeline tests passing.
2026-03-06 01:41:23 +02:00
0203044a43 Add operation configuration types: resize, convert, compress, metadata, watermark, rename
All 11 operation tests passing.
2026-03-06 01:40:24 +02:00
3 changed files with 109 additions and 469 deletions

345
README.md
View File

@@ -1,345 +0,0 @@
<div align="center">
# Pixstrip
**Your images. Your machine. Your rules.**
A native Linux batch image processor that keeps everything local, respects your privacy, and gets out of your way.
Resize. Convert. Compress. Strip metadata. Watermark. Rename. Adjust.
All in one wizard - or one command.
![License](https://img.shields.io/badge/license-CC0_Public_Domain-brightgreen?style=flat-square)
![Platform](https://img.shields.io/badge/platform-Linux_x86__64-blue?style=flat-square)
![Rust](https://img.shields.io/badge/built_with-Rust-orange?style=flat-square)
![GTK4](https://img.shields.io/badge/toolkit-GTK4_+_libadwaita-4a86cf?style=flat-square)
![Version](https://img.shields.io/badge/version-0.1.0-purple?style=flat-square)
---
[Download AppImage](https://git.lashman.live/lashman/pixstrip/releases) · [Report a Bug](https://git.lashman.live/lashman/pixstrip/issues) · [Source Code](https://git.lashman.live/lashman/pixstrip)
</div>
---
## 🧭 What is Pixstrip
Pixstrip is a batch image processor for Linux built with GTK4 and libadwaita. It exists because preparing images for the web, for clients, for print, or for sharing shouldn't require proprietary software, cloud uploads, or a subscription.
Everything runs locally on your machine. No accounts. No telemetry. No cloud. No surprises. You own the tool and you own the output.
The GUI walks you through a step-by-step wizard where you enable only the operations you need. The CLI gives you the same power in a single command for scripting and automation. Both share the same processing engine, the same presets, and the same results.
## 📦 Installation
Grab the AppImage from the [releases page](https://git.lashman.live/lashman/pixstrip/releases) and run it. That's it.
```bash
chmod +x Pixstrip-x86_64.AppImage
./Pixstrip-x86_64.AppImage
```
Everything is bundled inside - GTK4, libadwaita, all shared libraries. No package manager, no dependency chain, no root access needed. Download, run, own it.
## 🔧 Operations
Pixstrip combines seven image operations into a single workflow. Enable what you need, skip what you don't. Each operation is a step in the wizard with its own controls, previews, and help text.
### ✂️ Resize
Scale images to exact dimensions, or let Pixstrip figure out the math. Five resize modes cover everything from rigid pixel targets to flexible fit-in-box scaling that preserves aspect ratio.
- **Width** - set a target width, height scales proportionally
- **Height** - set a target height, width scales proportionally
- **Exact** - force both width and height (may distort aspect ratio)
- **Fit in box** - scale to fit within a bounding box without cropping or stretching
- **Social media presets** - Mastodon Post, Pixelfed Square, Pixelfed Portrait, Lemmy Thumbnail, and more built in
Resize uses Lanczos3 interpolation by default. Catmull-Rom, bilinear, and nearest-neighbor are available for specific needs. Upscaling is disabled by default to prevent quality loss - enable it explicitly when you need it.
### 🎨 Adjustments
Tune the look of your images before they leave your hands. All adjustments apply non-destructively to the output - your originals are never touched.
- **Brightness** - lighten or darken (-100 to +100)
- **Contrast** - increase or reduce tonal range (-100 to +100)
- **Saturation** - boost or mute color intensity (-100 to +100)
- **Rotation** - 90, 180, 270 degrees, or flip horizontal/vertical
- **Crop to aspect ratio** - freeform or preset ratios (1:1, 4:3, 16:9, 3:2)
- **Trim whitespace** - auto-detect and remove white or near-white borders
- **Canvas padding** - add uniform padding around images
Four one-click effects are available: **Grayscale**, **Sepia**, **Sharpen**, and a combined **Grayscale + Sharpen** for crisp black-and-white output.
### 🔄 Convert
Change image formats across your entire batch. Pixstrip supports eight formats and knows which conversions make sense.
| Format | Type | Best for |
|---|---|---|
| JPEG | Lossy | Photos, web, email - universal support |
| PNG | Lossless | Graphics, screenshots, transparency |
| WebP | Lossy/Lossless | Modern web - smaller than JPEG at same quality |
| AVIF | Lossy/Lossless | Next-gen web - smallest files, growing support |
| GIF | Lossless (256 colors) | Simple graphics, legacy compatibility |
| TIFF | Lossless | Archival, print, professional workflows |
| BMP | Uncompressed | Legacy systems, raw pixel data |
**Per-format mapping** handles mixed batches intelligently. Instead of converting everything to one format, you can set rules: PNGs become WebP, TIFFs become JPEG, everything else stays as-is. One pass, multiple conversions.
### 📐 Compress
Squeeze file sizes without visible quality loss. Pixstrip uses the best open-source encoders available - the same ones used by the tools that big platforms keep behind paywalls.
- **mozjpeg** for JPEG - typically 10-15% smaller than standard JPEG at identical visual quality
- **oxipng** for PNG - lossless recompression that shaves bytes without touching pixels
- **libwebp** for WebP - Google's encoder with fine-grained quality control
- **ravif** for AVIF - the rav1e encoder with configurable speed/quality tradeoffs
Five quality presets (Maximum, High, Medium, Low, Web) give you quick access to sensible defaults. Per-format quality overrides let you set exact values: JPEG quality 92, WebP quality 85, AVIF quality 70 - all in the same batch.
The compression step includes a live **before/after preview** with a draggable split view. See exactly what you're trading before you commit. The preview calculates real file sizes from a sample image so you know the actual savings.
### 🛡️ Metadata
Your photos carry more information than you think. GPS coordinates, camera serial numbers, editing software history, timestamps - all embedded in EXIF data that travels with every file you share.
Pixstrip gives you five metadata modes:
- **Strip All** - remove everything for the smallest files and maximum privacy
- **Privacy Mode** - strip GPS, camera serial, and device info while keeping copyright and basic camera settings
- **Photographer Mode** - keep copyright, camera model, and lens data while stripping GPS and software info
- **Keep All** - preserve every byte of metadata exactly as it is
- **Custom** - toggle individual categories: GPS/Location, Camera Info, Software, Timestamps, Copyright/Author
Custom mode puts you in full control of what leaves your machine and what doesn't. Your data, your choice about who sees it.
### 💧 Watermark
Protect your work or brand your output with text or image watermarks. The watermark step provides a live preview that updates as you change settings.
**Text watermarks** support:
- Custom text with any system font, adjustable size, and bold/italic styling
- Nine-position placement grid (corners, edges, center)
- Rotation from -180 to +180 degrees
- Opacity from fully transparent to fully opaque
- Custom color with hex input
- **Tiling mode** - repeat the watermark across the entire image in a diagonal pattern
**Image watermarks** overlay a PNG or other image file (great for logos):
- Same nine-position grid and opacity controls
- Scale relative to the target image (5% to 100%)
- Transparency in the watermark image is preserved
### ✏️ Rename
Batch rename files with a flexible system that handles everything from simple prefix/suffix additions to complex template-driven naming with EXIF variables.
**Simple rename** operations:
- Add prefix or suffix to existing filenames
- Replace spaces with hyphens, underscores, or remove them
- Strip special characters
- Convert case: lowercase, UPPERCASE, Title Case
- Add sequential counters with configurable start number and zero-padding
**Template engine** for advanced renaming:
| Variable | Expands to |
|---|---|
| `{name}` | Original filename without extension |
| `{ext}` | File extension |
| `{counter}` | Sequential number |
| `{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) |
**Find and replace** with full regex support for surgical renaming. Remove prefixes, rewrite patterns, extract parts of filenames - anything regex can express.
The rename step shows a **live preview** of the first five files so you see exactly what the output filenames will be before processing. A stats bar at the bottom shows total files, naming conflicts, file extensions, and the longest resulting filename.
## ⚡ Presets
Presets save a complete workflow configuration and apply it in one click. Pixstrip ships with eight built-in presets that cover the most common batch processing tasks.
| Preset | What it does |
|---|---|
| **Blog Photos** | Resize 1200px wide, JPEG high quality, strip all metadata |
| **Social Media** | Fit 1080x1080, compress medium, strip metadata, sequential rename |
| **Web Optimization** | Convert to WebP, compress high, strip metadata, sequential rename |
| **Email Friendly** | Resize 800px wide, JPEG medium quality |
| **Privacy Clean** | Strip all metadata, no other changes |
| **Photographer Export** | Resize 2048px, compress high, privacy metadata mode, rename by date |
| **Archive Compress** | Lossless compression, preserve all metadata |
| **Print Ready** | Maximum quality, convert to PNG, keep all metadata |
**Create your own presets** from any workflow you build. After processing, hit Save as Preset to name it, pick an icon and color, and have it appear on the workflow page for next time. Export presets to share them with others - import presets others have shared with you.
Presets belong to you. They live as plain JSON files in `~/.config/pixstrip/presets/` - readable, editable, shareable, version-controllable. No proprietary format, no lock-in.
## 🖥️ Using the GUI
**Start with a preset** by clicking any card on the workflow page. Pixstrip loads the preset's settings and jumps straight to the image selection step. Add your images, review the output summary, and hit Process. Done.
**Build a custom workflow** by selecting the Custom card. Toggle the operations you want in the checklist at the bottom of the page, then click Next to step through each one. Only the operations you enabled appear in the wizard - everything else is skipped automatically.
The **step indicator** across the top tracks your progress. Each step shows as a dot with a label. Click any completed step to jump back and change settings. The Back and Next buttons (or Alt+Left / Alt+Right) move between steps.
Every step has a **help button** in the header bar that opens a detailed explanation of what the step does, what the options mean, and which keyboard shortcuts are available. A guided **tour** runs on first launch to walk you through the interface.
When processing finishes, the results page shows how many images were processed, original and output sizes, percentage saved, and processing time. From there you can open the output folder, process another batch, queue more images, or save the workflow as a preset.
## ⌨️ Using the CLI
The CLI provides the same operations with the same engine. Every flag maps to a GUI option.
```bash
# Resize all images in a folder to 1200px wide
pixstrip process photos/ --resize 1200
# Convert PNGs to WebP at high quality
pixstrip process *.png --format webp --quality high
# Fit within 1920x1080, strip metadata, rename to lowercase
pixstrip process photos/ --resize fit:1920x1080 --strip-metadata --rename-case lower
# Use a saved preset
pixstrip process photos/ --preset "Blog Photos"
# Override a preset option
pixstrip process photos/ --preset "Blog Photos" --resize 800
# Per-format compression tuning
pixstrip process photos/ --jpeg-quality 90 --webp-quality 85 --avif-quality 70
# Tiled watermark with custom color and angle
pixstrip process photos/ --watermark "(c) 2026" --watermark-tiled \
--watermark-rotation 45 --watermark-color ff0000 --watermark-opacity 0.3
# Image watermark (logo)
pixstrip process photos/ --watermark-image logo.png --watermark-scale 0.15
# Regex rename - remove "IMG_" prefix
pixstrip process photos/ --rename-regex-find "^IMG_" --rename-regex-replace ""
# Sequential numbering with 4-digit padding
pixstrip process photos/ --rename-counter-position replace-name \
--rename-counter-start 1 --rename-counter-padding 4
# Template rename with dimensions
pixstrip process photos/ --rename-template "{name}_{width}x{height}.{ext}"
# Everything at once
pixstrip process photos/ -r --resize fit:1920x1080 --format webp \
--quality high --strip-metadata --rename-case lower \
--watermark "(c) 2026" --overwrite skip -o output/
```
### Preset and history management
```bash
# List available presets
pixstrip preset list
# Show preset details
pixstrip preset show "Blog Photos"
# View processing history
pixstrip history
# Undo the last batch (restores from trash)
pixstrip undo
```
### Watch folders
Monitor directories and auto-process new images as they appear.
```bash
# Add a watched directory with a preset
pixstrip watch add ~/incoming --preset "Web Optimization"
# List active watches
pixstrip watch list
# Start the watcher (runs until stopped)
pixstrip watch start
# Remove a watch
pixstrip watch remove ~/incoming
```
## 📁 File manager integration
Right-click any image (or selection of images) in your file manager to process them directly with Pixstrip. A submenu lists all your presets for one-click processing, plus an "Open in Pixstrip" option for the full wizard.
| File Manager | How it works |
|---|---|
| **Nautilus** (GNOME Files) | Python extension - dynamically reads your presets |
| **Nemo** (Cinnamon) | Python extension - dynamically reads your presets |
| **Thunar** (Xfce) | Custom action - regenerated when presets change |
| **Dolphin** (KDE) | Service menu - regenerated when presets change |
Toggle each integration on or off in Settings. The extensions are installed into the standard locations for each file manager.
## 🏗️ Building from source
Pixstrip is written in Rust and builds with standard Cargo tooling.
**Dependencies:** Rust 1.85+, GTK4 4.16+ dev libraries, libadwaita 1.6+ dev libraries.
```bash
# Install build dependencies (Ubuntu/Debian)
sudo apt install libgtk-4-dev libadwaita-1-dev
# Clone, build, run
git clone https://git.lashman.live/lashman/pixstrip.git
cd pixstrip
cargo build --release
# The two binaries
./target/release/pixstrip-gtk # GUI
./target/release/pixstrip-cli # CLI
```
### Building an AppImage
The included script downloads `linuxdeploy` with its GTK plugin, bundles all shared libraries, and produces a self-contained AppImage.
```bash
./build-appimage.sh
# Output: Pixstrip-x86_64.AppImage (~39 MB)
```
## 🗂️ Project structure
Pixstrip is a Cargo workspace with three crates that share a common core.
```
pixstrip/
pixstrip-core/ Processing engine, operations, presets, pipeline, watcher
pixstrip-gtk/ GTK4/libadwaita graphical interface
pixstrip-cli/ Command-line interface with clap
data/ Desktop file, AppStream metainfo, icons
```
The architecture is deliberately simple. `pixstrip-core` contains all the image processing logic and has no GUI dependencies. Both the GTK app and the CLI are thin frontends that build jobs and hand them to the core's pipeline executor. If you wanted to build a different frontend - a web UI, a TUI, a bot - you'd just depend on `pixstrip-core` and wire up the interface.
## 🤝 Contributing
Pixstrip is built in the open and contributions are welcome. The project uses [CC0](https://creativecommons.org/publicdomain/zero/1.0/) - there are no copyright assignments, no CLAs, no legal barriers. Your contributions enter the public domain just like the rest of the code.
File bugs, suggest features, or submit patches through the [issue tracker](https://git.lashman.live/lashman/pixstrip/issues). The codebase is straightforward Rust with GTK4 bindings - if you can read Rust, you can contribute.
## 🔒 Privacy and trust
Pixstrip processes everything on your machine. It never phones home, never uploads your images, never tracks what you do with it. There is no analytics, no crash reporting, no update checker, no account system. The binary does exactly what you tell it to and nothing else.
Your images are yours. Your metadata is yours. Your workflow is yours. The tool is yours too - CC0 means nobody owns it, including us. Fork it, modify it, redistribute it, sell it, give it away. No permission needed.
## ⚖️ License
**CC0 1.0 - Public Domain.** No rights reserved. This work is dedicated to the public domain worldwide. You can copy, modify, distribute, and use it for any purpose, commercial or otherwise, without asking permission and without owing anyone anything.
Good tools should be freely available to everyone who needs them.

View File

@@ -84,14 +84,15 @@ fn shell_safe(s: &str) -> String {
.collect()
}
/// Path to the GTK binary for "Open in Pixstrip" actions.
fn pixstrip_gtk_bin() -> String {
// When running from AppImage, use the AppImage path directly
if let Ok(appimage) = std::env::var("APPIMAGE") {
return appimage;
}
fn pixstrip_bin() -> String {
// Try to find the pixstrip binary path
if let Ok(exe) = std::env::current_exe() {
// If running from the GTK app, find the CLI sibling
let dir = exe.parent().unwrap_or(Path::new("/usr/bin"));
let cli_path = dir.join("pixstrip");
if cli_path.exists() {
return cli_path.display().to_string();
}
let gtk_path = dir.join("pixstrip-gtk");
if gtk_path.exists() {
return gtk_path.display().to_string();
@@ -101,22 +102,6 @@ fn pixstrip_gtk_bin() -> String {
"pixstrip-gtk".into()
}
/// Path to the CLI binary for preset processing actions.
fn pixstrip_cli_bin() -> String {
// When running from AppImage, use the AppImage path directly
if let Ok(appimage) = std::env::var("APPIMAGE") {
return appimage;
}
if let Ok(exe) = std::env::current_exe() {
let dir = exe.parent().unwrap_or(Path::new("/usr/bin"));
let cli_path = dir.join("pixstrip");
if cli_path.exists() {
return cli_path.display().to_string();
}
}
"pixstrip".into()
}
fn get_preset_names() -> Vec<String> {
let mut names: Vec<String> = Preset::all_builtins()
.into_iter()
@@ -153,8 +138,7 @@ fn install_nautilus() -> Result<()> {
let dir = nautilus_extension_dir();
std::fs::create_dir_all(&dir)?;
let gtk_bin = pixstrip_gtk_bin();
let cli_bin = pixstrip_cli_bin();
let bin = pixstrip_bin();
let presets = get_preset_names();
let mut preset_items = String::new();
@@ -172,8 +156,7 @@ fn install_nautilus() -> Result<()> {
));
}
let escaped_gtk_bin = gtk_bin.replace('\\', "\\\\").replace('\'', "\\'");
let escaped_cli_bin = cli_bin.replace('\\', "\\\\").replace('\'', "\\'");
let escaped_bin = bin.replace('\\', "\\\\").replace('\'', "\\'");
let script = format!(
r#"import subprocess
from gi.repository import Nautilus, GObject
@@ -221,15 +204,14 @@ class PixstripExtension(GObject.GObject, Nautilus.MenuProvider):
def _on_open(self, menu, files):
paths = [f.get_location().get_path() for f in files if f.get_location()]
subprocess.Popen(['{gtk_bin}'] + paths)
subprocess.Popen(['{bin}', '--files'] + paths)
def _on_preset(self, menu, preset_name, files):
paths = [f.get_location().get_path() for f in files if f.get_location()]
subprocess.Popen(['{cli_bin}', 'process', '--preset', preset_name] + paths)
subprocess.Popen(['{bin}', '--preset', preset_name, '--files'] + paths)
"#,
preset_items = preset_items,
gtk_bin = escaped_gtk_bin,
cli_bin = escaped_cli_bin,
bin = escaped_bin,
);
atomic_write(&nautilus_extension_path(), &script)?;
@@ -263,20 +245,19 @@ fn install_nemo() -> Result<()> {
let dir = nemo_action_dir();
std::fs::create_dir_all(&dir)?;
let gtk_bin = pixstrip_gtk_bin();
let cli_bin = pixstrip_cli_bin();
let bin = pixstrip_bin();
// Main "Open in Pixstrip" action
let open_action = format!(
"[Nemo Action]\n\
Name=Open in Pixstrip...\n\
Comment=Process images with Pixstrip\n\
Exec={gtk_bin} %F\n\
Exec={bin} --files %F\n\
Icon-Name=applications-graphics-symbolic\n\
Selection=Any\n\
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
Mimetypes=image/*;\n",
gtk_bin = gtk_bin,
bin = bin,
);
atomic_write(&nemo_action_path(), &open_action)?;
@@ -289,14 +270,14 @@ fn install_nemo() -> Result<()> {
"[Nemo Action]\n\
Name=Pixstrip: {name}\n\
Comment=Process with {name} preset\n\
Exec={cli_bin} process --preset \"{safe_label}\" %F\n\
Exec={bin} --preset \"{safe_label}\" --files %F\n\
Icon-Name=applications-graphics-symbolic\n\
Selection=Any\n\
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
Mimetypes=image/*;\n",
name = name,
safe_label = shell_safe(name),
cli_bin = cli_bin,
bin = bin,
);
atomic_write(&action_path, &action)?;
}
@@ -343,8 +324,7 @@ fn install_thunar() -> Result<()> {
let dir = thunar_action_dir();
std::fs::create_dir_all(&dir)?;
let gtk_bin = pixstrip_gtk_bin();
let cli_bin = pixstrip_cli_bin();
let bin = pixstrip_bin();
let presets = get_preset_names();
let mut actions = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<actions>\n");
@@ -354,13 +334,13 @@ fn install_thunar() -> Result<()> {
" <action>\n\
\x20 <icon>applications-graphics-symbolic</icon>\n\
\x20 <name>Open in Pixstrip...</name>\n\
\x20 <command>{gtk_bin} %F</command>\n\
\x20 <command>{bin} --files %F</command>\n\
\x20 <description>Process images with Pixstrip</description>\n\
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
\x20 <image-files/>\n\
\x20 <directories/>\n\
</action>\n",
gtk_bin = gtk_bin,
bin = bin,
));
for name in &presets {
@@ -368,7 +348,7 @@ fn install_thunar() -> Result<()> {
" <action>\n\
\x20 <icon>applications-graphics-symbolic</icon>\n\
\x20 <name>Pixstrip: {xml_name}</name>\n\
\x20 <command>{cli_bin} process --preset \"{safe_label}\" %F</command>\n\
\x20 <command>{bin} --preset \"{safe_label}\" --files %F</command>\n\
\x20 <description>Process with {xml_name} preset</description>\n\
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
\x20 <image-files/>\n\
@@ -376,7 +356,7 @@ fn install_thunar() -> Result<()> {
</action>\n",
xml_name = name.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;"),
safe_label = shell_safe(name),
cli_bin = cli_bin,
bin = bin,
));
}
@@ -412,8 +392,7 @@ fn install_dolphin() -> Result<()> {
let dir = dolphin_service_dir();
std::fs::create_dir_all(&dir)?;
let gtk_bin = pixstrip_gtk_bin();
let cli_bin = pixstrip_cli_bin();
let bin = pixstrip_bin();
let presets = get_preset_names();
let mut desktop = format!(
@@ -427,8 +406,8 @@ fn install_dolphin() -> Result<()> {
[Desktop Action Open]\n\
Name=Open in Pixstrip...\n\
Icon=applications-graphics-symbolic\n\
Exec={gtk_bin} %F\n\n",
gtk_bin = gtk_bin,
Exec={bin} --files %F\n\n",
bin = bin,
preset_actions = presets
.iter()
.enumerate()
@@ -442,11 +421,11 @@ fn install_dolphin() -> Result<()> {
"[Desktop Action Preset{i}]\n\
Name={name}\n\
Icon=applications-graphics-symbolic\n\
Exec={cli_bin} process --preset \"{safe_label}\" %F\n\n",
Exec={bin} --preset \"{safe_label}\" --files %F\n\n",
i = i,
name = name,
safe_label = shell_safe(name),
cli_bin = cli_bin,
bin = bin,
));
}

View File

@@ -30,10 +30,8 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
let subfolder_choice: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None));
// Set up drag-and-drop on the entire page
// Accept both FileList (from file managers) and single File
let drop_target = gtk::DropTarget::new(gtk::gdk::FileList::static_type(), gtk::gdk::DragAction::COPY | gtk::gdk::DragAction::MOVE);
drop_target.set_types(&[gtk::gdk::FileList::static_type(), gtk::gio::File::static_type(), glib::GString::static_type()]);
drop_target.set_preload(true);
let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY);
drop_target.set_types(&[gtk::gio::File::static_type()]);
{
let loaded_files = state.loaded_files.clone();
@@ -42,97 +40,40 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
let stack_ref = stack.clone();
let subfolder_choice = subfolder_choice.clone();
drop_target.connect_drop(move |target, value, _x, _y| {
// Collect paths from FileList, single File, or URI text
let mut paths: Vec<PathBuf> = Vec::new();
if let Ok(file_list) = value.get::<gtk::gdk::FileList>() {
for file in file_list.files() {
if let Some(path) = file.path() {
paths.push(path);
}
}
} else if let Ok(file) = value.get::<gtk::gio::File>() {
if let Some(path) = file.path() {
paths.push(path);
}
} else if let Ok(text) = value.get::<glib::GString>() {
// Handle URI text drops (from web browsers)
let text = text.trim().to_string();
let lower = text.to_lowercase();
let is_image_url = (lower.starts_with("http://") || lower.starts_with("https://"))
&& (lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| lower.ends_with(".png")
|| lower.ends_with(".webp")
|| lower.ends_with(".gif")
|| lower.ends_with(".avif")
|| lower.ends_with(".tiff")
|| lower.ends_with(".bmp")
|| lower.contains(".jpg?")
|| lower.contains(".jpeg?")
|| lower.contains(".png?")
|| lower.contains(".webp?"));
if is_image_url {
let loaded = loaded_files.clone();
let excl = excluded.clone();
let sz = sizes.clone();
let sr = stack_ref.clone();
let (tx, rx) = std::sync::mpsc::channel::<Option<std::path::PathBuf>>();
let url = text.clone();
std::thread::spawn(move || {
let result = download_image_url(&url);
let _ = tx.send(result);
});
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
match rx.try_recv() {
Ok(Some(path)) => {
let mut files = loaded.borrow_mut();
if !files.contains(&path) {
files.push(path);
}
let count = files.len();
drop(files);
refresh_grid(&sr, &loaded, &excl, &sz, count);
glib::ControlFlow::Break
}
Ok(None) => glib::ControlFlow::Break,
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
Err(_) => glib::ControlFlow::Break,
}
});
return true;
}
return false;
}
if paths.is_empty() {
return false;
}
for path in paths {
if let Ok(file) = value.get::<gtk::gio::File>()
&& let Some(path) = file.path()
{
if path.is_dir() {
let has_subdirs = has_subfolders(&path);
if !has_subdirs {
let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
} else {
let choice = *subfolder_choice.borrow();
match choice {
Some(true) => {
let mut files = loaded_files.borrow_mut();
add_images_from_dir(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
}
Some(false) => {
let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
}
None => {
let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
let loaded_files = loaded_files.clone();
let excluded = excluded.clone();
@@ -157,23 +98,88 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
}
}
}
return true;
} else if is_image_file(&path) {
let mut files = loaded_files.borrow_mut();
if !files.contains(&path) {
files.push(path);
}
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
return true;
}
}
let count = loaded_files.borrow().len();
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
true
false
});
}
stack.add_controller(drop_target);
// Also accept URI text drops (from web browsers)
let uri_drop = gtk::DropTarget::new(glib::GString::static_type(), gtk::gdk::DragAction::COPY);
{
let loaded_files = state.loaded_files.clone();
let excluded = state.excluded_files.clone();
let sizes = state.file_sizes.clone();
let stack_ref = stack.clone();
uri_drop.connect_drop(move |_target, value, _x, _y| {
if let Ok(text) = value.get::<glib::GString>() {
let text = text.trim().to_string();
// Check if it looks like an image URL
let lower = text.to_lowercase();
let is_image_url = (lower.starts_with("http://") || lower.starts_with("https://"))
&& (lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| lower.ends_with(".png")
|| lower.ends_with(".webp")
|| lower.ends_with(".gif")
|| lower.ends_with(".avif")
|| lower.ends_with(".tiff")
|| lower.ends_with(".bmp")
|| lower.contains(".jpg?")
|| lower.contains(".jpeg?")
|| lower.contains(".png?")
|| lower.contains(".webp?"));
if is_image_url {
let loaded = loaded_files.clone();
let excl = excluded.clone();
let sz = sizes.clone();
let sr = stack_ref.clone();
// Download in background thread
let (tx, rx) = std::sync::mpsc::channel::<Option<std::path::PathBuf>>();
let url = text.clone();
std::thread::spawn(move || {
let result = download_image_url(&url);
let _ = tx.send(result);
});
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
match rx.try_recv() {
Ok(Some(path)) => {
let mut files = loaded.borrow_mut();
if !files.contains(&path) {
files.push(path);
}
let count = files.len();
drop(files);
refresh_grid(&sr, &loaded, &excl, &sz, count);
glib::ControlFlow::Break
}
Ok(None) => glib::ControlFlow::Break,
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
Err(_) => glib::ControlFlow::Break,
}
});
return true;
}
}
false
});
}
stack.add_controller(uri_drop);
adw::NavigationPage::builder()
.title("Add Images")
.tag("step-images")