Add UxGallery.libraryChanges — a Stream<void> that emits whenever the
underlying photo library reports a change, so picker UIs can drop
their cached asset lists and reload reactively.
- iOS: GalleryPlugin conforms to PHPhotoLibraryChangeObserver +
FlutterStreamHandler; on photoLibraryDidChange the fetchCache is
cleared and a void event is pushed over ux/gallery/changes. The
observer is registered lazily on the first granted/limited
authorization so plugin init doesn't trigger iOS's permission
evaluation at app launch.
- macOS: near-verbatim port of the iOS shape (same Photos.framework,
same fetchCache staleness, same fix).
- Android: registers a MediaStore ContentObserver on
Files.getContentUri(VOLUME_EXTERNAL) with notifyForDescendants =
true; observer lifecycle tracks the Dart subscription (registered
on onListen, unregistered on onCancel / plugin detach). No native
cache to invalidate today, but the pipeline is wired for the
upcoming READ_MEDIA_VISUAL_USER_SELECTED limited-access work.
- iOS presentLimitedLibraryPicker switched to the iOS 15+ completion-
handler variant so the Dart await resolves on dismissal, not on
presentation. The actual reload is now driven by the change
observer (which fires after iOS commits the new subset),
side-stepping the completion-vs-commit race that produced an
off-by-one in the picker on consecutive MANAGE taps.
- FakeUxGalleryBackend exposes emitLibraryChange() so tests can
drive the reactive-reload wiring without going through the real
method channel.
Flutter's Clipboard API only exposes text shapes. Banlu's chat composer
needs image bytes from the system clipboard for desktop paste, so add
a UxClipboard facade backed by per-platform native plugins:
* iOS: prefer raw PNG/JPEG bytes off the pasteboard, fall back to
re-encoding `UIPasteboard.image` as PNG.
* macOS: prefer NSPasteboard `.png`, fall back to TIFF transcoded
through NSBitmapImageRep so screenshots / Preview hand-offs still
work.
* Android: read primary ClipData's first item URI and stream the bytes
through ContentResolver — don't trust the clip description's MIME,
copy whatever the resolver returns.
Returns null (never throws) when the clipboard has no image — callers
treat null as "fall through to text paste".
Two new methods on `UxFile`, both designed to keep large file content
out of the platform-channel buffer (the failure mode of file_selector
on Android: a ~200 MB video PUT through the Pigeon codec OOM'd the
JVM via `byte[size]` allocation in `FileSelectorApiImpl`).
`UxFile.pick({mimeTypes})` returns `UxPickedFile?` with `path`, `name`,
`mimeType`, `size`. The platform channel reply carries only the
metadata; bytes never cross.
- Android: `ACTION_OPEN_DOCUMENT` + `EXTRA_MIME_TYPES`, registered
as `ActivityResultListener`. On result, stream-copies the SAF
content URI to `cacheDir/ux_pick/<ts>_<safeName>` via an 8 KB
buffer (no full-file allocation in JVM heap), returns the cache
path.
- iOS: `UIDocumentPickerViewController(documentTypes:in: .import)`
— `.import` mode copies the picked file into the app's
Documents/Inbox so the URL is stable. Strong-retained delegate
(the picker's delegate ref is weak).
- macOS: `NSOpenPanel` with `allowedFileTypes`. Sheet-modal when a
Flutter window exists; free-modal otherwise.
`UxFile.videoThumbnail({path, atMs, maxWidth})` returns
`UxVideoThumbnail?` (PNG bytes + dims).
- Android: `MediaMetadataRetriever.getFrameAtTime(..., OPTION_CLOSEST_SYNC)`,
`Bitmap.createScaledBitmap` to maxWidth, PNG-encode via
`ByteArrayOutputStream`, recycle bitmaps in `finally`, release
retriever in `finally`.
- iOS: `AVAssetImageGenerator` with `appliesPreferredTrackTransform = true`,
`maximumSize = (maxWidth, 0)` (preserve aspect), ±500 ms tolerance
for keyframe alignment, decode on `userInitiated` queue.
- macOS: same generator, encoded via `NSBitmapImageRep`.
Compatible with the package's existing iOS 13 / macOS 10.15 deployment
targets — uses legacy `kUTType*` + `UTTypeCreatePreferredIdentifierForTag`
instead of `UTType` (iOS 14 / macOS 11).