Files
fox_utils/README.md
T

184 lines
8.3 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fox_utils
Reusable Flutter utilities shared across projects: permission-free Android
file saving, a cross-platform clipboard wrapper, and desktop drag & drop.
## Features
### `AndroidFiles.save` — save files to public Android storage
Writes any bytes to a public Android directory (`Pictures`, `Movies`,
`Music`, `Download`) **without any storage permission**, with an optional
media scan so the file shows up in gallery apps.
```dart
final path = await AndroidFiles.save(
bytes, // any file type
fileName: 'image.jpg',
location: AndroidPublicLocation.pictures, // pictures | movies | music | downloads
subPath: 'Imagen', // folder structure under the location
scan: true, // false = saved but not indexed (no gallery)
);
```
Notes:
- Requires **no permissions** on Android 10+ (API 29+): creating new files
inside media collections is permission-free. Legacy devices (< API 29) may
fail — the exception carries the native message.
- `scan: false` writes the file without indexing it. On modern Android the
file can still be picked up by a later system rescan; for a hard
"never in gallery" guarantee, save outside a media collection or add a
`.nomedia` file to the folder.
- Throws `FileSaveException` on failure (logged with stack trace).
### `ClipboardX` — clipboard copy
```dart
await ClipboardX.copyImage(bytes); // image bytes as-is, MIME sniffed
await ClipboardX.copyFiles(['/path/a.png']); // desktop: files
ClipboardX.copyText('hello'); // text (Flutter built-in)
final text = await ClipboardX.readText();
final image = await ClipboardX.readImage();
final files = await ClipboardX.readFiles();
```
Platform matrix:
| Operation | Android | Windows | Linux | macOS | iOS |
|---|---|---|---|---|---|
| copy image | ✓ (FileProvider) | ✓ (GDI+) | ✓ (file reference) | ✓ | ✓ |
| copy files | – | ✓ (CF_HDROP) | ✓ (GNOME) | ✓ | – |
| copy/read text | ✓ | ✓ | ✓ | ✓ | ✓ |
| read image | ✓ | ✓ | ✓ | ✓ | ✓ |
| read files | ✓ (URIs) | ✓ | ✓ | ✓ | – |
Notes:
- Image bytes are placed **as-is** (no re-encode); the extension is sniffed
from magic bytes (PNG/JPEG/GIF/WebP/BMP), so animated GIFs survive.
- **Linux cannot hold raw image bytes** on the clipboard (GTK limitation), so
`copyImage` stages the bytes to a rotating temp file and copies it as a
file reference — pasted apps receive the image file. The staging file is
replaced on each copy.
- **Android requires no manifest setup**: the FileProvider is declared in
this plugin's manifest and merged automatically (`${applicationId}.provider`).
Images are staged in the app cache dir.
- Throws `ClipboardCopyException` on failure.
### `ClipboardX.pasteImage` — paste an image, whatever the source
```dart
final Uint8List? image = await ClipboardX.pasteImage();
if (image != null) {
// send to server, display, whatever
}
```
Resolution order: image data on the clipboard first (browser "copy image"),
then the first file reference (file manager "copy"). The file's first bytes
are sniffed (PNG/JPEG/GIF/WebP/BMP) before the full read, so huge non-image
files are never loaded. Returns `null` when the clipboard holds no image.
Source matrix:
| Copied from | Windows | Linux | Android |
|---|---|---|---|
| Browser, "copy image" | CF_DIB → image bytes | `image/png` → image bytes | content URI (image mime) → image bytes |
| File manager, "copy" | CF_HDROP → first file's bytes | `text/uri-list` → first file's bytes | content URI → resolved bytes |
Android notes:
- Reading the clipboard works **only while the app is in the foreground**
(Android 10+); background reads return `null`.
- Android 12+ shows a system toast ("app pasted from your clipboard") on
read — cosmetic.
- File references come back as `content://` URIs; `pasteImage` resolves them
through the platform channel (`readFileBytes`), so no manifest setup or
permission is needed.
### `DesktopDropX` + `DropTarget` — drag & drop files and images
OS-level drag & drop into the app (desktop only). Wrap any widget in
`DropTarget` to get drag enter/exit/update/drop callbacks; `DesktopDropX`
turns the dropped items into plain paths or image bytes:
```dart
DropTarget(
onDragEntered: (d) => setState(() => dragging = true),
onDragExited: (d) => setState(() => dragging = false),
onDragDone: (details) async {
final paths = DesktopDropX.pathsOf(details.files); // absolute paths
final Uint8List? image = await DesktopDropX.readImage(details.files);
// image == null when the drop contains no readable image
},
child: myWidget,
)
```
Platform notes:
- **Linux**: GTK drag destination on the whole window (`text/uri-list` from
GNOME/KDE file managers, browsers, …). Items are absolute paths, delivered
in the order dropped.
- **Windows**: OLE `IDropTarget` on the window (`CF_HDROP` from Explorer,
browsers, …). Same path semantics; `DropDoneDetails.localPosition` is
already DPI-corrected.
- **Android/iOS/web**: no native handler — the widget renders its child and
no drop events ever arrive. **macOS** is not vendored (no macOS target);
re-adding it is a copy of upstream's `macos/` folder behind the same API.
- `DropItem` extends `XFile`: `item.path`, `item.name`, `item.readAsBytes()`,
`item.openRead()`, … are available directly.
- Drop targets receive events for the **whole window**; an invisible
`DropTarget` keeps receiving them, so set `enable: false` when pushing a
page over it. Coordinate-based `DropTarget` bounds filtering is handled by
the widget.
- `readImage` never throws: unreadable files are logged with a stack trace
and skipped; `null` means "no image in this drop".
## Vendored engines
The clipboard channels (`pasteboard` method channel) are **vendored from
MixinNetwork's `pasteboard` package (v0.5.0, Apache-2.0)** because upstream's
build applies the legacy Kotlin Gradle Plugin (KGP), which future Flutter
versions will reject. The vendored copy lives in `lib/vendor/` (Dart) and the
native platform folders, with these changes:
- **Android**: the Kotlin plugin was ported to Java and merged into
`FoxUtilsPlugin` (one plugin class, no KGP, no Kotlin in the module).
- **Byte-faithful image writes**: upstream decodes and re-encodes images as
PNG (destroying GIF animation and metadata); ours writes the original bytes
with magic-byte MIME sniffing.
- **Merged FileProvider**: declared in the plugin manifest — zero per-project
setup (upstream requires a manual manifest entry per app).
- **Two upstream bugs fixed**: clipboard image reads ran synchronously on the
calling thread (now on the executor), and `writeFiles` dropped the first
clip item (now assembles correctly).
- **Linux/Windows**: C++ sources byte-identical, include paths and CMake
targets renamed to the `fox_utils` package name.
To diff against upstream: `git diff` the folders against
`packages/pasteboard/` at tag `v0.5.0` of
`https://github.com/MixinNetwork/flutter-plugins`. If upstream ever
modernizes their build, un-vendoring is a swap behind the same `ClipboardX`
API.
The drag & drop channels (`desktop_drop` method channel) are **vendored from
MixinNetwork's `desktop_drop` package (v0.7.1, Apache-2.0)** for the same
reason: its `android/` folder applies KGP. Only the Dart sources and the
Linux/Windows native folders are vendored — the Android/Kotlin, macOS and
web bindings are omitted (Android DnD is out of scope, and there is no macOS
target in the app). Linux/Windows native sources are byte-identical to
upstream except the include path rename (`include/fox_utils/`). Both engines
are registered from one native entry per platform:
`fox_utils_plugin_register_with_registrar` (Linux) /
`FoxUtilsPluginRegisterWithRegistrar` (Windows) — the plugin class in
`pubspec.yaml` is no longer `PasteboardPlugin`. To diff against upstream, use
tag `desktop_drop-v0.7.1` of the same monorepo; un-vendoring is a swap
behind the same `DesktopDropX` API.
## License
MIT for this package's own code; the vendored engines are Apache-2.0
© MixinNetwork (see `lib/vendor/` and the native platform folders).