import 'dart:io' show File; import 'dart:typed_data'; import 'package:flutter/foundation.dart' show debugPrint; import 'vendor/desktop_drop.dart' show DropItem, DropItemDirectory; // The widget API and drop item model come from the vendored engine: // DropTarget (drag-enter/exit/updated/done callbacks), DropItem / DropItemFile / // DropItemDirectory, DropDoneDetails and DropEventDetails. export 'vendor/desktop_drop.dart' show DropTarget, DropItem, DropItemFile, DropItemDirectory, DropDoneDetails, DropEventDetails; /// Drag & drop of files and images from the OS into the app. /// /// The widget side of this API is [DropTarget] (re-exported from this /// library). Wrap any widget in a [DropTarget] to receive drag-enter, /// drag-exit, drag-update and drop callbacks for OS-level drags — anything /// that can produce file URIs or file lists: file managers, browsers, /// terminals, image editors. /// /// [DesktopDropX] provides the ergonomics the app needs on top of the raw /// [DropItem] list delivered by [DropDoneDetails.files]: extracting plain /// file paths and loading the first image among the dropped items. /// /// Platform behavior: /// - **Linux**: the native side installs a GTK drag destination on the whole /// Flutter window and delivers `text/uri-list` (GNOME/KDE file managers, /// browsers, etc.). Dropped items are absolute filesystem paths. /// - **Windows**: the native side registers an OLE `IDropTarget` on the /// window and reads `CF_HDROP`, so drops work from Explorer and any /// browser. Dropped items are absolute filesystem paths. /// - **Android/iOS**: not implemented — this package does not vendor the /// desktop_drop mobile bindings. [DropTarget] compiles and renders its /// child, but no native handler answers the channel, so no drop events /// ever arrive. /// - **macOS**: not vendored (no macOS target in the app); re-adding it is /// a copy of upstream's `macos/` folder behind the same API. /// /// Errors: nothing here throws. A drop that contains no readable image is an /// expected outcome (returns `null`), and file reads that fail (stale drop, /// permissions) are logged with a stack trace and skipped. class DesktopDropX { DesktopDropX._(); /// Returns one filesystem path per top-level dropped item. /// /// Each item in [items] maps to its path — files and directories alike; a /// dropped folder yields its own path, its contents are not expanded /// ([DropItemDirectory.children] is only populated on macOS, which is not /// vendored). Items that carry no path (web data drops) yield `''`. /// /// Pass [DropDoneDetails.files] from a drop callback to get the paths the /// app can hand to file I/O: /// /// ```dart /// DropTarget( /// onDragDone: (details) { /// final paths = DesktopDropX.pathsOf(details.files); /// // read, copy, upload… /// }, /// child: …, /// ) /// ``` static List pathsOf(List items) => items.map((item) => item.path).toList(); /// Reads the first image among [items] and returns its bytes. /// /// Resolution order follows [items]: for each item the first 16 bytes are /// sniffed for image magic bytes (PNG/JPEG/GIF/WebP/BMP — same table as /// `ClipboardX.pasteImage`); the first match is loaded in full. Huge /// non-image files are never fully read. Returns `null` when none of the /// dropped items is an image, or when none could be read. /// /// Directories are skipped ([DropItemDirectory] carries no file bytes). /// Files that fail to open or read (deleted between drop and read, /// permission denied) are logged with a stack trace and skipped — a later /// item is still tried, and the method returns `null` only if nothing /// readable remains. /// /// The returned bytes are as-dropped (no re-encode); use the bytes with /// `Image.memory` or upload them as-is. /// /// ```dart /// DropTarget( /// onDragDone: (details) async { /// final Uint8List? bytes = await DesktopDropX.readImage(details.files); /// if (bytes == null) return; // drop contained no image /// // display or upload /// }, /// child: …, /// ) /// ``` static Future readImage(List items) async { for (final item in items) { if (item is DropItemDirectory) continue; final path = item.path; if (path.isEmpty) continue; final head = await _readHead(path); if (head == null || !_looksLikeImage(head)) continue; try { final bytes = await item.readAsBytes(); if (bytes == null || !_looksLikeImage(bytes)) continue; return bytes; } on Exception catch (e, st) { debugPrint( 'DesktopDropX.readImage: failed reading ${item.path}: $e\n$st'); } } return null; } static Future _readHead(String path) async { try { final raf = File(path).openSync(); final head = raf.readSync(16); raf.closeSync(); return head; } on Exception catch (e, st) { debugPrint('DesktopDropX.readImage: could not read $path: $e\n$st'); return null; } } /// Sniffs common image magic bytes (PNG/JPEG/GIF/WebP/BMP). /// /// Shares the table used by `ClipboardX._looksLikeImage`; kept as a local /// copy so the two APIs stay independent. static bool _looksLikeImage(Uint8List b) { if (b.length < 4) return false; if (b[0] == 0xFF && b[1] == 0xD8 && b[2] == 0xFF) return true; // jpeg if (b[0] == 0x89 && b[1] == 0x50 && b[2] == 0x4E && b[3] == 0x47) return true; // png if (b[0] == 0x47 && b[1] == 0x49 && b[2] == 0x46 && b[3] == 0x38) return true; // gif if (b.length >= 12 && b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F' && b[4] == 'W' && b[5] == 'E' && b[6] == 'B' && b[7] == 'P') { return true; // webp } if (b[0] == 'B' && b[1] == 'M') return true; // bmp return false; } }