Compare commits

..

131 Commits

Author SHA1 Message Date
7677e05b20 Fix title case in README headings 2026-03-13 19:32:49 +02:00
e3b391b18a Fix drag-and-drop and file manager integration 2026-03-08 16:53:30 +02:00
bb75a7764f Use fediverse platforms in README examples 2026-03-08 15:06:10 +02:00
834bf5fbb6 Rewrite README with detailed feature documentation 2026-03-08 15:04:06 +02:00
b94626036d Add README 2026-03-08 14:59:32 +02:00
2b1c328c93 Add screenshots, update metainfo, and fix AppImage build script 2026-03-08 14:50:59 +02:00
28253f009d Improve UX, add popover tour, metadata, and hicolor icons 2026-03-08 14:18:15 +02:00
388670964a Clean up minor code quality issues 2026-03-08 00:22:24 +02:00
96f6973d39 Fix bugs across all crates 2026-03-07 23:35:32 +02:00
72c519bebc Fix performance and UX issues 2026-03-07 23:11:00 +02:00
0ef3893bce Fix bugs in CLI, core, and GTK 2026-03-07 23:02:57 +02:00
6392b7952f Fix CLI/GTK history timestamp format mismatch 2026-03-07 22:35:25 +02:00
5ed5f3a1d5 Fix cleanup, template, and watermark bugs 2026-03-07 22:27:37 +02:00
f471d22767 Fix overflow, race condition, and format bugs 2026-03-07 22:14:48 +02:00
150d483fbe Fix path traversal, encoding, and edge case bugs 2026-03-07 20:49:10 +02:00
9e1562c4c4 Fix edge cases and consistency issues 2026-03-07 19:47:23 +02:00
6bf9d60430 Fix step indicator layout warnings on narrow windows 2026-03-06 18:44:41 +02:00
780ac75e7b Add AppImage packaging, app icon, and AppStream metainfo 2026-03-06 18:31:18 +02:00
213e3d1de8 Add EXIF auto-orient and aspect ratio lock toggle 2026-03-06 18:18:04 +02:00
7feb8583cd Implement watermark rotation for text and image watermarks 2026-03-06 18:14:53 +02:00
62ad99d254 Fix pipeline order, add selective metadata stripping, rename case/regex 2026-03-06 18:12:18 +02:00
8edaab0e2b Add EXIF-based rename template variables 2026-03-06 18:03:12 +02:00
3fd602652a Add thumbnail re-render preview to resize step 2026-03-06 17:56:44 +02:00
6cf5a6e80c Integrate watch folder monitoring into GTK app 2026-03-06 17:53:41 +02:00
f301f1a78b Persist advanced options expand/collapse state per section 2026-03-06 17:49:07 +02:00
646a698ac1 Add watermark tiling, rotation types, margin/scale controls 2026-03-06 17:36:07 +02:00
3e6a539749 Add collapsible watch folders panel in main window 2026-03-06 17:29:02 +02:00
3d200fc0a9 Add fixed output folder option in settings 2026-03-06 17:23:57 +02:00
ebf9f2ecfc Add PNG DPI support via pHYs chunk and fix font picker 2026-03-06 17:16:43 +02:00
727056f385 Add font family selector for watermark text 2026-03-06 17:12:23 +02:00
81e244e069 Add overwrite confirmation dialog and preset file drop import 2026-03-06 17:07:53 +02:00
f2b1f0be2e Add drag-and-drop import for .pixstrip-preset files 2026-03-06 17:06:02 +02:00
21e17d0c50 Add preset export/delete buttons and single-instance support 2026-03-06 17:04:18 +02:00
7e54ff1c72 Improve screen reader support for wizard navigation 2026-03-06 17:02:04 +02:00
230410e09e Add single-instance file handling via GIO HANDLES_OPEN 2026-03-06 16:58:23 +02:00
e81698a367 Enhance history dialog with expandable details and DPI support 2026-03-06 16:56:23 +02:00
8b39b1a678 Apply output DPI to JPEG files via mozjpeg pixel density 2026-03-06 16:53:32 +02:00
35b1ae4e59 Add sequential batch queue processing 2026-03-06 16:17:25 +02:00
fd0fbeb385 Add Watch Folders settings page with full CRUD 2026-03-06 16:13:14 +02:00
47d6813d6f Add rename template presets and watermark color picker 2026-03-06 16:10:05 +02:00
f3060479f5 Add URL drag-and-drop support for images from web browsers 2026-03-06 16:02:39 +02:00
84bf287851 Add thumbnail selection for compression and watermark previews 2026-03-06 15:59:44 +02:00
1dc3abf691 Improve output step with individual operation summary rows 2026-03-06 15:57:06 +02:00
5582aa21bd Add batch queue with slide-out side panel 2026-03-06 15:52:18 +02:00
d94ea19a0d Add tutorial overlay tour after welcome wizard 2026-03-06 15:48:06 +02:00
4a328010d7 Enhance save preset dialog with settings summary and update option 2026-03-06 15:43:45 +02:00
d74fd0abc0 Wire DPI setting from resize step through to processing job 2026-03-06 15:41:25 +02:00
322f6440dd Add live watermark preview with position and opacity updates 2026-03-06 15:39:10 +02:00
4409e596d8 Add file manager integration install/uninstall logic 2026-03-06 15:37:25 +02:00
88ee177467 Wire skill level and accessibility settings to UI 2026-03-06 15:28:02 +02:00
135bbe80f8 Initialize overwrite behavior from app settings 2026-03-06 15:24:51 +02:00
245adec076 Add completion sound, preserve directory structure support 2026-03-06 15:24:04 +02:00
acb60e409a Add history pruning with configurable max entries and max days 2026-03-06 15:22:17 +02:00
2ad0538a13 Add --algorithm and --overwrite flags to CLI 2026-03-06 15:19:34 +02:00
e871d47a80 Wire resize algorithm selection, overwrite behavior, and fix rotation/flip scope 2026-03-06 15:17:59 +02:00
0388d3a510 Fix imported presets not persisting, remove duplicate help handler 2026-03-06 15:12:06 +02:00
60c5a45c20 Restore session settings for convert format, quality preset, and metadata mode 2026-03-06 15:10:28 +02:00
cd286a23de Wire progressive JPEG, AVIF speed, and custom per-format quality to encoder 2026-03-06 15:07:54 +02:00
3bc3d89ead Wire help button to show contextual step help dialog 2026-03-06 14:59:29 +02:00
105058d767 Guard output summary adjustments behind adjustments_enabled flag 2026-03-06 14:57:38 +02:00
6838fd3b7e Auto-show What's New dialog on first launch after update 2026-03-06 14:54:43 +02:00
5b47d68d5c Add wizard step-skipping for disabled operations 2026-03-06 14:53:04 +02:00
4eccbb1804 Support per-format conversion mapping in processing pipeline 2026-03-06 14:51:17 +02:00
1feb9805bb Wire missing UI controls to job config 2026-03-06 14:50:12 +02:00
c31b30eed2 Replace custom shortcuts dialog with GtkShortcutsWindow, fix Process More button 2026-03-06 14:46:54 +02:00
d581640d0c Add squoosh-style compression preview with draggable divider 2026-03-06 14:29:01 +02:00
f3dc164018 Add image adjustments pipeline (brightness, contrast, crop, effects) 2026-03-06 14:24:14 +02:00
600b36279e Wire all missing operations into pipeline executor 2026-03-06 14:20:47 +02:00
2b977b833a Add accessible labels to thumbnail grid and toolbar 2026-03-06 14:08:52 +02:00
4a5114e6f9 Add visual size preview to resize step 2026-03-06 14:05:52 +02:00
0b635f6d81 Add CSS provider for thumbnail grid styling 2026-03-06 14:02:49 +02:00
f8a23af851 Replace image list with GtkGridView thumbnail grid 2026-03-06 14:01:52 +02:00
a87a976b6e Implement parallel image processing with rayon thread pool 2026-03-06 13:57:14 +02:00
ce726a7838 Add accessible value updates to processing progress bar 2026-03-06 13:53:32 +02:00
ded1880711 Wire thread count and error behavior settings into executor 2026-03-06 13:52:08 +02:00
b54e0545b0 Add accessible labels to sliders and watermark position grid 2026-03-06 13:51:01 +02:00
f5d55167fb Add per-step contextual help button in header bar 2026-03-06 13:48:13 +02:00
324e121f43 Add Ctrl+V clipboard paste for images 2026-03-06 13:46:20 +02:00
ea596e01fe Add Escape key shortcut and subfolder prompt for folder drops 2026-03-06 13:45:06 +02:00
5428214d6c Add accessible labels to processing progress bar, results icon, drop zone 2026-03-06 13:41:30 +02:00
31b12478b6 Replace watermark position dropdown with visual 3x3 grid 2026-03-06 13:38:12 +02:00
ec81b059b2 Add Ctrl+Z undo last batch and register select/deselect shortcuts 2026-03-06 13:35:45 +02:00
42e3bfcec1 Add Ctrl+A and Ctrl+Shift+A keyboard shortcuts for select/deselect all 2026-03-06 13:33:06 +02:00
543368ee45 Respect excluded files in output step counts and processing validation 2026-03-06 13:31:35 +02:00
81da76310a Add file manager integration toggles and reset button to Settings 2026-03-06 13:29:25 +02:00
9e99012a88 Add per-image checkboxes and wire Select All / Deselect All buttons 2026-03-06 13:24:18 +02:00
9dabd89a55 Add WebP effort, AVIF speed controls, case conversion and regex rename 2026-03-06 13:19:11 +02:00
0c1dd2cd3b Add File Manager Integration page to welcome wizard 2026-03-06 13:16:11 +02:00
dbc215f0ad Add What's New dialog, accessibility labels, focus management 2026-03-06 13:13:39 +02:00
36e291e486 Add format mapping to convert step, orientation controls to resize step 2026-03-06 13:11:00 +02:00
ac6f305bcc Add crop/trim/canvas padding to adjustments, wire all sliders to config 2026-03-06 13:09:45 +02:00
6ef0f14804 Add Photographer metadata preset and improve quality descriptions 2026-03-06 13:05:07 +02:00
7531de5332 Add custom workflow operation toggles, improve output step summary 2026-03-06 13:00:52 +02:00
fde0fbdf9f Add visual format cards, per-image remove, shortcuts dialog, wire threads 2026-03-06 12:58:43 +02:00
0eebeabd42 Enhance rename preview to show 5 files, add watermark advanced expander 2026-03-06 12:54:35 +02:00
08c287843d Enhance adjustments with sliders/effects, add undo toast, compress AVIF/progressive 2026-03-06 12:47:12 +02:00
24459ef9ac Add session memory, resize mode tabs, improved output summary 2026-03-06 12:44:16 +02:00
3d4ee6cac9 Add real pause, desktop notifications, undo, accessibility, CLI watch 2026-03-06 12:38:16 +02:00
d1bc450d7d Add rotate, flip, watermark, and rename CLI flags with helper functions 2026-03-06 12:33:21 +02:00
3e63761b43 Add window persistence, clickable step indicator, file list improvements 2026-03-06 12:27:19 +02:00
283206c411 Improve Images, Compress, Output, Workflow steps 2026-03-06 12:22:15 +02:00
457f132f51 Add ETA calculation, activity log, keyboard shortcuts, expand format options 2026-03-06 12:20:04 +02:00
6235d3d686 Add Adjustments, Watermark, Rename wizard steps; expand to 10-step wizard 2026-03-06 12:15:02 +02:00
662270d7b9 Wire remaining UI elements: presets, drag-drop, import/save, output summary 2026-03-06 12:01:50 +02:00
86480545b7 Wire all wizard step controls to shared JobConfig state 2026-03-06 11:51:01 +02:00
609a682105 Wire settings dialog to persist config via ConfigStore 2026-03-06 11:44:16 +02:00
a1df353521 Wire welcome dialog with navigation and first-run detection 2026-03-06 11:43:25 +02:00
c4bf5cbdda Wire up step buttons: Browse, preset cards, output directory picker 2026-03-06 11:41:46 +02:00
4e03cc389d Wire up all GTK UI actions to real functionality 2026-03-06 11:37:32 +02:00
cfd2660b95 Add welcome wizard, desktop entry, and Nautilus extension 2026-03-06 11:18:28 +02:00
9ad70c960b Add file watcher for watch folder functionality 2026-03-06 11:17:02 +02:00
47e019dbb9 Implement fully functional CLI with process, preset, history, and undo commands 2026-03-06 11:13:33 +02:00
86307aeefc Add processing, results, and settings UI screens 2026-03-06 11:10:38 +02:00
d36f90565e Add all wizard step UIs: workflow, images, resize, convert, compress, metadata, output 2026-03-06 11:08:38 +02:00
9fe680dad5 Add GTK app shell with wizard navigation, step indicator, and actions 2026-03-06 11:03:11 +02:00
0bc39542ee Add storage module for presets, config, session, and history persistence 2026-03-06 02:14:57 +02:00
9b5cfd1212 Add pipeline executor with progress reporting and cancellation 2026-03-06 02:08:11 +02:00
c81e2364af Add metadata stripping, watermark positioning, and rename template modules 2026-03-06 02:06:01 +02:00
56f0a7172e Add metadata, watermark, and rename dependencies: little_exif, imageproc, ab_glyph 2026-03-06 02:03:04 +02:00
90807f5632 Add output encoders (mozjpeg, oxipng, webp, avif) and integration tests 2026-03-06 02:02:27 +02:00
4e34c77d46 Add output encoder dependencies: mozjpeg, oxipng, webp 2026-03-06 01:59:52 +02:00
9a65a034d1 Fix clippy: collapse nested if in discovery module 2026-03-06 01:59:00 +02:00
2747cb9b4a Add CLI skeleton with clap: process, preset, history, and undo subcommands 2026-03-06 01:57:43 +02:00
f10ee90c0a Add minimal GTK4/libadwaita window with header bar and status page 2026-03-06 01:57:03 +02:00
4b354b79c4 Add SIMD-accelerated resize operation using fast_image_resize 2026-03-06 01:54:57 +02:00
1fc958f068 Add ImageLoader and file discovery modules 2026-03-06 01:50:46 +02:00
34cac53eb8 Add image processing dependencies: magick_rust, fast_image_resize, image, rayon, walkdir 2026-03-06 01:49:12 +02:00
0f539875bb Add AppConfig with overwrite behavior, skill level, thread count settings 2026-03-06 01:43:22 +02:00
7967e86a59 Add Preset type with 8 built-in presets and JSON serialization 2026-03-06 01:42:35 +02:00
f503c81df8 Add ProcessingJob type with source management and output path resolution 2026-03-06 01:41:23 +02:00
f6cccda7bf Add operation configuration types: resize, convert, compress, metadata, watermark, rename 2026-03-06 01:40:24 +02:00
3 changed files with 469 additions and 109 deletions

345
README.md Normal file
View File

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

View File

@@ -30,8 +30,10 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
let subfolder_choice: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None)); let subfolder_choice: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None));
// Set up drag-and-drop on the entire page // Set up drag-and-drop on the entire page
let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY); // Accept both FileList (from file managers) and single File
drop_target.set_types(&[gtk::gio::File::static_type()]); 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 loaded_files = state.loaded_files.clone(); let loaded_files = state.loaded_files.clone();
@@ -40,40 +42,97 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
let stack_ref = stack.clone(); let stack_ref = stack.clone();
let subfolder_choice = subfolder_choice.clone(); let subfolder_choice = subfolder_choice.clone();
drop_target.connect_drop(move |target, value, _x, _y| { drop_target.connect_drop(move |target, value, _x, _y| {
if let Ok(file) = value.get::<gtk::gio::File>() // Collect paths from FileList, single File, or URI text
&& let Some(path) = file.path() 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 path.is_dir() { if path.is_dir() {
let has_subdirs = has_subfolders(&path); let has_subdirs = has_subfolders(&path);
if !has_subdirs { if !has_subdirs {
let mut files = loaded_files.borrow_mut(); let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files); add_images_flat(&path, &mut files);
let count = files.len();
drop(files); drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
} else { } else {
let choice = *subfolder_choice.borrow(); let choice = *subfolder_choice.borrow();
match choice { match choice {
Some(true) => { Some(true) => {
let mut files = loaded_files.borrow_mut(); let mut files = loaded_files.borrow_mut();
add_images_from_dir(&path, &mut files); add_images_from_dir(&path, &mut files);
let count = files.len();
drop(files); drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
} }
Some(false) => { Some(false) => {
let mut files = loaded_files.borrow_mut(); let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files); add_images_flat(&path, &mut files);
let count = files.len();
drop(files); drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
} }
None => { None => {
let mut files = loaded_files.borrow_mut(); let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files); add_images_flat(&path, &mut files);
let count = files.len();
drop(files); drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
let loaded_files = loaded_files.clone(); let loaded_files = loaded_files.clone();
let excluded = excluded.clone(); let excluded = excluded.clone();
@@ -98,88 +157,23 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
} }
} }
} }
return true;
} else if is_image_file(&path) { } else if is_image_file(&path) {
let mut files = loaded_files.borrow_mut(); let mut files = loaded_files.borrow_mut();
if !files.contains(&path) { if !files.contains(&path) {
files.push(path); files.push(path);
} }
let count = files.len();
drop(files); drop(files);
}
}
let count = loaded_files.borrow().len();
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count); refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
return true; true
}
}
false
}); });
} }
stack.add_controller(drop_target); 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() adw::NavigationPage::builder()
.title("Add Images") .title("Add Images")
.tag("step-images") .tag("step-images")