Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2435a06b2 |
No files matched your search
+51
-40
@@ -1,41 +1,52 @@
|
|||||||
## 0.1.7
|
## 0.1.8
|
||||||
|
|
||||||
* Add `DesktopDropX` + `DropTarget` — OS-level drag & drop of files and
|
* Linux, debug builds only: a Flutter hot restart no longer crashes apps that
|
||||||
images into the app, powered by a vendored desktop_drop engine (v0.7.1,
|
use media_kit with live players. Hot restart frees the old isolate's
|
||||||
Apache-2.0): GTK uri-list drag destination on Linux, OLE `IDropTarget`
|
NativeCallables while the mpv handles live on; media_kit then sends the
|
||||||
(`CF_HDROP`) on Windows, dead code on mobile and web. `DesktopDropX`
|
leftovers "quit" and mpv calls a dead wakeup callback (SIGSEGV on mpv's
|
||||||
provides `pathsOf` (dropped file paths) and `readImage` (magic-byte
|
"core" thread). The plugin clears every live handle's wakeup callback on
|
||||||
image sniffing, same table as `ClipboardX.pasteImage`).
|
the engine's `on-pre-engine-restart` signal (handles found through
|
||||||
* The Linux/Windows plugin entry is now a combined `FoxUtilsPlugin`
|
media_kit's NativeReferenceHolder file). Automatic: depending on fox_utils
|
||||||
registration that wires the vendored pasteboard and desktop_drop engines
|
is enough. Relies on media_kit internals (checked against 1.2.6).
|
||||||
from one native entry per platform (the registered plugin class changed
|
|
||||||
from `PasteboardPlugin` to `FoxUtilsPlugin` — generated registrants
|
## 0.1.7
|
||||||
regenerate on the next build).
|
|
||||||
* New dependencies: `cross_file` (drop items are `XFile`s) and
|
* Add `DesktopDropX` + `DropTarget` — OS-level drag & drop of files and
|
||||||
`universal_platform` (web-safe platform checks), both required by the
|
images into the app, powered by a vendored desktop_drop engine (v0.7.1,
|
||||||
vendored engine.
|
Apache-2.0): GTK uri-list drag destination on Linux, OLE `IDropTarget`
|
||||||
|
(`CF_HDROP`) on Windows, dead code on mobile and web. `DesktopDropX`
|
||||||
## 0.1.6
|
provides `pathsOf` (dropped file paths) and `readImage` (magic-byte
|
||||||
|
image sniffing, same table as `ClipboardX.pasteImage`).
|
||||||
* Add `ClipboardX.pasteImage` — reads an image from the clipboard whether it
|
* The Linux/Windows plugin entry is now a combined `FoxUtilsPlugin`
|
||||||
was copied as image data (browser) or as a file reference (file manager),
|
registration that wires the vendored pasteboard and desktop_drop engines
|
||||||
on Windows, Linux and Android. Adds a `readFileBytes` channel method for
|
from one native entry per platform (the registered plugin class changed
|
||||||
resolving Android content URIs.
|
from `PasteboardPlugin` to `FoxUtilsPlugin` — generated registrants
|
||||||
|
regenerate on the next build).
|
||||||
## 0.1.5
|
* New dependencies: `cross_file` (drop items are `XFile`s) and
|
||||||
|
`universal_platform` (web-safe platform checks), both required by the
|
||||||
* Add `ClipboardX` — clipboard image/files/text copy via a vendored
|
vendored engine.
|
||||||
pasteboard engine (MixinNetwork, Apache-2.0) with modernized builds:
|
|
||||||
the Android channel is Java (no KGP), the FileProvider is merged from
|
## 0.1.6
|
||||||
the plugin manifest (zero per-project setup), image writes are byte-faithful
|
|
||||||
with magic-byte MIME sniffing, and two upstream bugs are fixed.
|
* Add `ClipboardX.pasteImage` — reads an image from the clipboard whether it
|
||||||
|
was copied as image data (browser) or as a file reference (file manager),
|
||||||
## 0.1.4
|
on Windows, Linux and Android. Adds a `readFileBytes` channel method for
|
||||||
|
resolving Android content URIs.
|
||||||
* Add `AndroidFiles.save` — permission-free file saving to public Android
|
|
||||||
directories (Pictures/Movies/Music/Download) with an optional media scan.
|
## 0.1.5
|
||||||
Android platform channel, no storage permissions required.
|
|
||||||
|
* Add `ClipboardX` — clipboard image/files/text copy via a vendored
|
||||||
## 0.0.1
|
pasteboard engine (MixinNetwork, Apache-2.0) with modernized builds:
|
||||||
|
the Android channel is Java (no KGP), the FileProvider is merged from
|
||||||
|
the plugin manifest (zero per-project setup), image writes are byte-faithful
|
||||||
|
with magic-byte MIME sniffing, and two upstream bugs are fixed.
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
* Add `AndroidFiles.save` — permission-free file saving to public Android
|
||||||
|
directories (Pictures/Movies/Music/Download) with an optional media scan.
|
||||||
|
Android platform channel, no storage permissions required.
|
||||||
|
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
* TODO: Describe initial release.
|
* TODO: Describe initial release.
|
||||||
@@ -1,183 +1,193 @@
|
|||||||
# fox_utils
|
# fox_utils
|
||||||
|
|
||||||
Reusable Flutter utilities shared across projects: permission-free Android
|
Reusable Flutter utilities shared across projects: permission-free Android
|
||||||
file saving, a cross-platform clipboard wrapper, and desktop drag & drop.
|
file saving, a cross-platform clipboard wrapper, and desktop drag & drop.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### `AndroidFiles.save` — save files to public Android storage
|
### `AndroidFiles.save` — save files to public Android storage
|
||||||
|
|
||||||
Writes any bytes to a public Android directory (`Pictures`, `Movies`,
|
Writes any bytes to a public Android directory (`Pictures`, `Movies`,
|
||||||
`Music`, `Download`) **without any storage permission**, with an optional
|
`Music`, `Download`) **without any storage permission**, with an optional
|
||||||
media scan so the file shows up in gallery apps.
|
media scan so the file shows up in gallery apps.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final path = await AndroidFiles.save(
|
final path = await AndroidFiles.save(
|
||||||
bytes, // any file type
|
bytes, // any file type
|
||||||
fileName: 'image.jpg',
|
fileName: 'image.jpg',
|
||||||
location: AndroidPublicLocation.pictures, // pictures | movies | music | downloads
|
location: AndroidPublicLocation.pictures, // pictures | movies | music | downloads
|
||||||
subPath: 'Imagen', // folder structure under the location
|
subPath: 'Imagen', // folder structure under the location
|
||||||
scan: true, // false = saved but not indexed (no gallery)
|
scan: true, // false = saved but not indexed (no gallery)
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Requires **no permissions** on Android 10+ (API 29+): creating new files
|
- Requires **no permissions** on Android 10+ (API 29+): creating new files
|
||||||
inside media collections is permission-free. Legacy devices (< API 29) may
|
inside media collections is permission-free. Legacy devices (< API 29) may
|
||||||
fail — the exception carries the native message.
|
fail — the exception carries the native message.
|
||||||
- `scan: false` writes the file without indexing it. On modern Android the
|
- `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
|
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
|
"never in gallery" guarantee, save outside a media collection or add a
|
||||||
`.nomedia` file to the folder.
|
`.nomedia` file to the folder.
|
||||||
- Throws `FileSaveException` on failure (logged with stack trace).
|
- Throws `FileSaveException` on failure (logged with stack trace).
|
||||||
|
|
||||||
### `ClipboardX` — clipboard copy
|
### `ClipboardX` — clipboard copy
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
await ClipboardX.copyImage(bytes); // image bytes as-is, MIME sniffed
|
await ClipboardX.copyImage(bytes); // image bytes as-is, MIME sniffed
|
||||||
await ClipboardX.copyFiles(['/path/a.png']); // desktop: files
|
await ClipboardX.copyFiles(['/path/a.png']); // desktop: files
|
||||||
ClipboardX.copyText('hello'); // text (Flutter built-in)
|
ClipboardX.copyText('hello'); // text (Flutter built-in)
|
||||||
final text = await ClipboardX.readText();
|
final text = await ClipboardX.readText();
|
||||||
final image = await ClipboardX.readImage();
|
final image = await ClipboardX.readImage();
|
||||||
final files = await ClipboardX.readFiles();
|
final files = await ClipboardX.readFiles();
|
||||||
```
|
```
|
||||||
|
|
||||||
Platform matrix:
|
Platform matrix:
|
||||||
|
|
||||||
| Operation | Android | Windows | Linux | macOS | iOS |
|
| Operation | Android | Windows | Linux | macOS | iOS |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| copy image | ✓ (FileProvider) | ✓ (GDI+) | ✓ (file reference) | ✓ | ✓ |
|
| copy image | ✓ (FileProvider) | ✓ (GDI+) | ✓ (file reference) | ✓ | ✓ |
|
||||||
| copy files | – | ✓ (CF_HDROP) | ✓ (GNOME) | ✓ | – |
|
| copy files | – | ✓ (CF_HDROP) | ✓ (GNOME) | ✓ | – |
|
||||||
| copy/read text | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| copy/read text | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| read image | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| read image | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| read files | ✓ (URIs) | ✓ | ✓ | ✓ | – |
|
| read files | ✓ (URIs) | ✓ | ✓ | ✓ | – |
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Image bytes are placed **as-is** (no re-encode); the extension is sniffed
|
- 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.
|
from magic bytes (PNG/JPEG/GIF/WebP/BMP), so animated GIFs survive.
|
||||||
- **Linux cannot hold raw image bytes** on the clipboard (GTK limitation), so
|
- **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
|
`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
|
file reference — pasted apps receive the image file. The staging file is
|
||||||
replaced on each copy.
|
replaced on each copy.
|
||||||
- **Android requires no manifest setup**: the FileProvider is declared in
|
- **Android requires no manifest setup**: the FileProvider is declared in
|
||||||
this plugin's manifest and merged automatically (`${applicationId}.provider`).
|
this plugin's manifest and merged automatically (`${applicationId}.provider`).
|
||||||
Images are staged in the app cache dir.
|
Images are staged in the app cache dir.
|
||||||
- Throws `ClipboardCopyException` on failure.
|
- Throws `ClipboardCopyException` on failure.
|
||||||
|
|
||||||
### `ClipboardX.pasteImage` — paste an image, whatever the source
|
### `ClipboardX.pasteImage` — paste an image, whatever the source
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final Uint8List? image = await ClipboardX.pasteImage();
|
final Uint8List? image = await ClipboardX.pasteImage();
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
// send to server, display, whatever
|
// send to server, display, whatever
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Resolution order: image data on the clipboard first (browser "copy image"),
|
Resolution order: image data on the clipboard first (browser "copy image"),
|
||||||
then the first file reference (file manager "copy"). The file's first bytes
|
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
|
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.
|
files are never loaded. Returns `null` when the clipboard holds no image.
|
||||||
|
|
||||||
Source matrix:
|
Source matrix:
|
||||||
|
|
||||||
| Copied from | Windows | Linux | Android |
|
| Copied from | Windows | Linux | Android |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Browser, "copy image" | CF_DIB → image bytes | `image/png` → image bytes | content URI (image mime) → image bytes |
|
| 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 |
|
| File manager, "copy" | CF_HDROP → first file's bytes | `text/uri-list` → first file's bytes | content URI → resolved bytes |
|
||||||
|
|
||||||
Android notes:
|
Android notes:
|
||||||
|
|
||||||
- Reading the clipboard works **only while the app is in the foreground**
|
- Reading the clipboard works **only while the app is in the foreground**
|
||||||
(Android 10+); background reads return `null`.
|
(Android 10+); background reads return `null`.
|
||||||
- Android 12+ shows a system toast ("app pasted from your clipboard") on
|
- Android 12+ shows a system toast ("app pasted from your clipboard") on
|
||||||
read — cosmetic.
|
read — cosmetic.
|
||||||
- File references come back as `content://` URIs; `pasteImage` resolves them
|
- File references come back as `content://` URIs; `pasteImage` resolves them
|
||||||
through the platform channel (`readFileBytes`), so no manifest setup or
|
through the platform channel (`readFileBytes`), so no manifest setup or
|
||||||
permission is needed.
|
permission is needed.
|
||||||
|
|
||||||
### `DesktopDropX` + `DropTarget` — drag & drop files and images
|
### `DesktopDropX` + `DropTarget` — drag & drop files and images
|
||||||
|
|
||||||
OS-level drag & drop into the app (desktop only). Wrap any widget in
|
OS-level drag & drop into the app (desktop only). Wrap any widget in
|
||||||
`DropTarget` to get drag enter/exit/update/drop callbacks; `DesktopDropX`
|
`DropTarget` to get drag enter/exit/update/drop callbacks; `DesktopDropX`
|
||||||
turns the dropped items into plain paths or image bytes:
|
turns the dropped items into plain paths or image bytes:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
DropTarget(
|
DropTarget(
|
||||||
onDragEntered: (d) => setState(() => dragging = true),
|
onDragEntered: (d) => setState(() => dragging = true),
|
||||||
onDragExited: (d) => setState(() => dragging = false),
|
onDragExited: (d) => setState(() => dragging = false),
|
||||||
onDragDone: (details) async {
|
onDragDone: (details) async {
|
||||||
final paths = DesktopDropX.pathsOf(details.files); // absolute paths
|
final paths = DesktopDropX.pathsOf(details.files); // absolute paths
|
||||||
final Uint8List? image = await DesktopDropX.readImage(details.files);
|
final Uint8List? image = await DesktopDropX.readImage(details.files);
|
||||||
// image == null when the drop contains no readable image
|
// image == null when the drop contains no readable image
|
||||||
},
|
},
|
||||||
child: myWidget,
|
child: myWidget,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Platform notes:
|
Platform notes:
|
||||||
|
|
||||||
- **Linux**: GTK drag destination on the whole window (`text/uri-list` from
|
- **Linux**: GTK drag destination on the whole window (`text/uri-list` from
|
||||||
GNOME/KDE file managers, browsers, …). Items are absolute paths, delivered
|
GNOME/KDE file managers, browsers, …). Items are absolute paths, delivered
|
||||||
in the order dropped.
|
in the order dropped.
|
||||||
- **Windows**: OLE `IDropTarget` on the window (`CF_HDROP` from Explorer,
|
- **Windows**: OLE `IDropTarget` on the window (`CF_HDROP` from Explorer,
|
||||||
browsers, …). Same path semantics; `DropDoneDetails.localPosition` is
|
browsers, …). Same path semantics; `DropDoneDetails.localPosition` is
|
||||||
already DPI-corrected.
|
already DPI-corrected.
|
||||||
- **Android/iOS/web**: no native handler — the widget renders its child and
|
- **Android/iOS/web**: no native handler — the widget renders its child and
|
||||||
no drop events ever arrive. **macOS** is not vendored (no macOS target);
|
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.
|
re-adding it is a copy of upstream's `macos/` folder behind the same API.
|
||||||
- `DropItem` extends `XFile`: `item.path`, `item.name`, `item.readAsBytes()`,
|
- `DropItem` extends `XFile`: `item.path`, `item.name`, `item.readAsBytes()`,
|
||||||
`item.openRead()`, … are available directly.
|
`item.openRead()`, … are available directly.
|
||||||
- Drop targets receive events for the **whole window**; an invisible
|
- Drop targets receive events for the **whole window**; an invisible
|
||||||
`DropTarget` keeps receiving them, so set `enable: false` when pushing a
|
`DropTarget` keeps receiving them, so set `enable: false` when pushing a
|
||||||
page over it. Coordinate-based `DropTarget` bounds filtering is handled by
|
page over it. Coordinate-based `DropTarget` bounds filtering is handled by
|
||||||
the widget.
|
the widget.
|
||||||
- `readImage` never throws: unreadable files are logged with a stack trace
|
- `readImage` never throws: unreadable files are logged with a stack trace
|
||||||
and skipped; `null` means "no image in this drop".
|
and skipped; `null` means "no image in this drop".
|
||||||
|
|
||||||
## Vendored engines
|
### media_kit hot-restart guard (Linux, debug)
|
||||||
|
|
||||||
The clipboard channels (`pasteboard` method channel) are **vendored from
|
Apps using media_kit crash on a hot restart (`R`) while any `Player` is
|
||||||
MixinNetwork's `pasteboard` package (v0.5.0, Apache-2.0)** because upstream's
|
alive: mpv calls a wakeup callback that belonged to the discarded isolate.
|
||||||
build applies the legacy Kotlin Gradle Plugin (KGP), which future Flutter
|
fox_utils' Linux plugin clears those callbacks just before the engine
|
||||||
versions will reject. The vendored copy lives in `lib/vendor/` (Dart) and the
|
restarts, so hot restart works. Nothing to call; release builds compile it
|
||||||
native platform folders, with these changes:
|
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
|
||||||
- **Android**: the Kotlin plugin was ported to Java and merged into
|
media_kit upgrades.
|
||||||
`FoxUtilsPlugin` (one plugin class, no KGP, no Kotlin in the module).
|
|
||||||
- **Byte-faithful image writes**: upstream decodes and re-encodes images as
|
## Vendored engines
|
||||||
PNG (destroying GIF animation and metadata); ours writes the original bytes
|
|
||||||
with magic-byte MIME sniffing.
|
The clipboard channels (`pasteboard` method channel) are **vendored from
|
||||||
- **Merged FileProvider**: declared in the plugin manifest — zero per-project
|
MixinNetwork's `pasteboard` package (v0.5.0, Apache-2.0)** because upstream's
|
||||||
setup (upstream requires a manual manifest entry per app).
|
build applies the legacy Kotlin Gradle Plugin (KGP), which future Flutter
|
||||||
- **Two upstream bugs fixed**: clipboard image reads ran synchronously on the
|
versions will reject. The vendored copy lives in `lib/vendor/` (Dart) and the
|
||||||
calling thread (now on the executor), and `writeFiles` dropped the first
|
native platform folders, with these changes:
|
||||||
clip item (now assembles correctly).
|
|
||||||
- **Linux/Windows**: C++ sources byte-identical, include paths and CMake
|
- **Android**: the Kotlin plugin was ported to Java and merged into
|
||||||
targets renamed to the `fox_utils` package name.
|
`FoxUtilsPlugin` (one plugin class, no KGP, no Kotlin in the module).
|
||||||
|
- **Byte-faithful image writes**: upstream decodes and re-encodes images as
|
||||||
To diff against upstream: `git diff` the folders against
|
PNG (destroying GIF animation and metadata); ours writes the original bytes
|
||||||
`packages/pasteboard/` at tag `v0.5.0` of
|
with magic-byte MIME sniffing.
|
||||||
`https://github.com/MixinNetwork/flutter-plugins`. If upstream ever
|
- **Merged FileProvider**: declared in the plugin manifest — zero per-project
|
||||||
modernizes their build, un-vendoring is a swap behind the same `ClipboardX`
|
setup (upstream requires a manual manifest entry per app).
|
||||||
API.
|
- **Two upstream bugs fixed**: clipboard image reads ran synchronously on the
|
||||||
|
calling thread (now on the executor), and `writeFiles` dropped the first
|
||||||
The drag & drop channels (`desktop_drop` method channel) are **vendored from
|
clip item (now assembles correctly).
|
||||||
MixinNetwork's `desktop_drop` package (v0.7.1, Apache-2.0)** for the same
|
- **Linux/Windows**: C++ sources byte-identical, include paths and CMake
|
||||||
reason: its `android/` folder applies KGP. Only the Dart sources and the
|
targets renamed to the `fox_utils` package name.
|
||||||
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
|
To diff against upstream: `git diff` the folders against
|
||||||
target in the app). Linux/Windows native sources are byte-identical to
|
`packages/pasteboard/` at tag `v0.5.0` of
|
||||||
upstream except the include path rename (`include/fox_utils/`). Both engines
|
`https://github.com/MixinNetwork/flutter-plugins`. If upstream ever
|
||||||
are registered from one native entry per platform:
|
modernizes their build, un-vendoring is a swap behind the same `ClipboardX`
|
||||||
`fox_utils_plugin_register_with_registrar` (Linux) /
|
API.
|
||||||
`FoxUtilsPluginRegisterWithRegistrar` (Windows) — the plugin class in
|
|
||||||
`pubspec.yaml` is no longer `PasteboardPlugin`. To diff against upstream, use
|
The drag & drop channels (`desktop_drop` method channel) are **vendored from
|
||||||
tag `desktop_drop-v0.7.1` of the same monorepo; un-vendoring is a swap
|
MixinNetwork's `desktop_drop` package (v0.7.1, Apache-2.0)** for the same
|
||||||
behind the same `DesktopDropX` API.
|
reason: its `android/` folder applies KGP. Only the Dart sources and the
|
||||||
|
Linux/Windows native folders are vendored — the Android/Kotlin, macOS and
|
||||||
## License
|
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
|
||||||
MIT for this package's own code; the vendored engines are Apache-2.0
|
upstream except the include path rename (`include/fox_utils/`). Both engines
|
||||||
© MixinNetwork (see `lib/vendor/` and the native platform folders).
|
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).
|
||||||
@@ -10,6 +10,7 @@ add_library(${PLUGIN_NAME} SHARED
|
|||||||
"fox_utils_plugin.cc"
|
"fox_utils_plugin.cc"
|
||||||
"pasteboard_plugin.cc"
|
"pasteboard_plugin.cc"
|
||||||
"desktop_drop_plugin.cc"
|
"desktop_drop_plugin.cc"
|
||||||
|
"media_kit_restart_guard.cc"
|
||||||
)
|
)
|
||||||
apply_standard_settings(${PLUGIN_NAME})
|
apply_standard_settings(${PLUGIN_NAME})
|
||||||
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
||||||
@@ -19,6 +20,7 @@ target_include_directories(${PLUGIN_NAME} INTERFACE
|
|||||||
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||||
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
|
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
|
||||||
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
|
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE ${CMAKE_DL_LIBS})
|
||||||
|
|
||||||
# List of absolute paths to libraries that should be bundled with the plugin
|
# List of absolute paths to libraries that should be bundled with the plugin
|
||||||
set(fox_utils_bundled_libraries
|
set(fox_utils_bundled_libraries
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#include "include/fox_utils/fox_utils_plugin.h"
|
#include "include/fox_utils/fox_utils_plugin.h"
|
||||||
#include "include/fox_utils/pasteboard_plugin.h"
|
#include "include/fox_utils/pasteboard_plugin.h"
|
||||||
#include "include/fox_utils/desktop_drop_plugin.h"
|
#include "include/fox_utils/desktop_drop_plugin.h"
|
||||||
|
#include "include/fox_utils/media_kit_restart_guard.h"
|
||||||
|
|
||||||
void fox_utils_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
|
void fox_utils_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
|
||||||
pasteboard_plugin_register_with_registrar(registrar);
|
pasteboard_plugin_register_with_registrar(registrar);
|
||||||
desktop_drop_plugin_register_with_registrar(registrar);
|
desktop_drop_plugin_register_with_registrar(registrar);
|
||||||
|
media_kit_restart_guard_register_with_registrar(registrar);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Debug builds only: keeps a Flutter hot restart from crashing apps that use
|
||||||
|
// media_kit on Linux. Hot restart frees the old isolate's NativeCallables
|
||||||
|
// while mpv handles stay alive; media_kit then "quit"s the leftovers and mpv
|
||||||
|
// calls a dead wakeup callback (SIGSEGV on mpv's "core" thread). On the
|
||||||
|
// engine's on-pre-engine-restart signal this clears every live handle's
|
||||||
|
// wakeup callback (found through media_kit's NativeReferenceHolder file).
|
||||||
|
// Does nothing without media_kit/libmpv, and nothing in release builds.
|
||||||
|
#ifndef FOX_UTILS_MEDIA_KIT_RESTART_GUARD_H_
|
||||||
|
#define FOX_UTILS_MEDIA_KIT_RESTART_GUARD_H_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
void media_kit_restart_guard_register_with_registrar(FlPluginRegistrar* registrar);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "include/fox_utils/media_kit_restart_guard.h"
|
||||||
|
|
||||||
|
#ifdef NDEBUG
|
||||||
|
|
||||||
|
void media_kit_restart_guard_register_with_registrar(FlPluginRegistrar* registrar) {}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Mirrors media_kit's NativeReferenceHolder (debug only): a calloc'd buffer
|
||||||
|
// of kReferenceBufferSize intptr slots holding live mpv_handle addresses,
|
||||||
|
// its address written in decimal to <tmp>/<kFilePrefix><pid>.
|
||||||
|
constexpr int kReferenceBufferSize = 512;
|
||||||
|
constexpr char kFilePrefix[] = "com.alexmercerind.media_kit.NativeReferenceHolder.";
|
||||||
|
|
||||||
|
using SetWakeupCallback = void (*)(void* ctx, void (*cb)(void*), void* d);
|
||||||
|
|
||||||
|
SetWakeupCallback find_set_wakeup_callback() {
|
||||||
|
static const char* const kNames[] = {"libmpv.so.2", "libmpv.so", "libmpv.so.1"};
|
||||||
|
for (const char* name : kNames) {
|
||||||
|
void* lib = dlopen(name, RTLD_NOW | RTLD_NOLOAD);
|
||||||
|
if (lib == nullptr) continue;
|
||||||
|
auto fn = reinterpret_cast<SetWakeupCallback>(
|
||||||
|
dlsym(lib, "mpv_set_wakeup_callback"));
|
||||||
|
dlclose(lib); // Drops only the reference NOLOAD added.
|
||||||
|
if (fn != nullptr) return fn;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs on the engine's UI thread before the old isolate shuts down, so the
|
||||||
|
// buffer is not being written concurrently. Clearing the callback takes the
|
||||||
|
// same lock mpv calls it under: once this returns, mpv never calls it again.
|
||||||
|
// The handles stay in the buffer: media_kit's startup in the new isolate
|
||||||
|
// still sends them "quit".
|
||||||
|
void on_pre_engine_restart(FlEngine* engine, gpointer user_data) {
|
||||||
|
g_autofree gchar* name = g_strdup_printf("%s%d", kFilePrefix, getpid());
|
||||||
|
g_autofree gchar* path = g_build_filename(g_get_tmp_dir(), name, nullptr);
|
||||||
|
g_autofree gchar* contents = nullptr;
|
||||||
|
if (!g_file_get_contents(path, &contents, nullptr, nullptr)) return;
|
||||||
|
const guint64 address = g_ascii_strtoull(contents, nullptr, 10);
|
||||||
|
if (address == 0) return;
|
||||||
|
const SetWakeupCallback set_wakeup = find_set_wakeup_callback();
|
||||||
|
if (set_wakeup == nullptr) return;
|
||||||
|
const auto* slots = reinterpret_cast<const intptr_t*>(address);
|
||||||
|
int cleared = 0;
|
||||||
|
for (int i = 0; i < kReferenceBufferSize; i++) {
|
||||||
|
if (slots[i] == 0) continue;
|
||||||
|
set_wakeup(reinterpret_cast<void*>(slots[i]), nullptr, nullptr);
|
||||||
|
cleared++;
|
||||||
|
}
|
||||||
|
if (cleared > 0) {
|
||||||
|
g_message("hot restart: unhooked %d mpv wakeup callback(s)", cleared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void media_kit_restart_guard_register_with_registrar(FlPluginRegistrar* registrar) {
|
||||||
|
FlView* view = fl_plugin_registrar_get_view(registrar);
|
||||||
|
if (view == nullptr) return; // headless: no hot restart to guard
|
||||||
|
g_signal_connect(fl_view_get_engine(view), "on-pre-engine-restart",
|
||||||
|
G_CALLBACK(on_pre_engine_restart), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name: fox_utils
|
name: fox_utils
|
||||||
description: A collection of reusable Flutter widgets and extensions.
|
description: A collection of reusable Flutter widgets and extensions.
|
||||||
version: 0.1.7
|
version: 0.1.8
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
Reference in new issue
Block a user