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.

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

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

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:

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".

media_kit hot-restart guard (Linux, debug)

Apps using media_kit crash on a hot restart (R) while any Player is alive: mpv calls a wakeup callback that belonged to the discarded isolate. fox_utils' Linux plugin clears those callbacks just before the engine restarts, so hot restart works. Nothing to call; release builds compile it out; it does nothing without media_kit. It reads media_kit's private NativeReferenceHolder file (checked against media_kit 1.2.6): re-check after media_kit upgrades.

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).

S
Description
No description provided
Readme
94 KiB
0 Stars 1 Watchers 0 Forks
Languages
C++ 49.7%
Dart 34.5%
Java 9.9%
C 3.9%
CMake 2%