livemd_flutter_clipboard 0.1.0
livemd_flutter_clipboard: ^0.1.0 copied to clipboard
Optional native and browser media clipboard adapter for the LiveMD Markdown editor.
LiveMD media clipboard adapter #
Native and browser media clipboard access for livemd_flutter. The editor owns
paste targets, atomic insertion and undo. This package reads clipboard media;
the application owns persistence, uploads, attachment metadata and players.
Version 0.1.0 supports livemd_flutter 0.6.0 and later compatible releases.
flutter pub add livemd_flutter_clipboard:^0.1.0
Integration #
import 'package:livemd_flutter/livemd_flutter.dart';
import 'package:livemd_flutter_clipboard/livemd_flutter_clipboard.dart';
LiveMarkdownEditor(
controller: controller,
pasteConfiguration: LiveMarkdownPasteConfiguration(
clipboard: const SuperClipboardAdapter(),
onImportMedia: (request) async {
// Application service: save the stream and its MIME metadata.
// itemId is unique; operationId is shared by the entire batch.
final saved = await mediaStore.importMedia(request);
if (saved == null) return null;
return LiveMarkdownImportedMedia(destination: saved.uri, alt: saved.name);
},
onResult: handlePasteResult,
),
mediaTypeResolver: mediaStore.typeOf,
imageResolver: resolveImage,
videoBuilder: buildVideoPlayer,
audioBuilder: buildAudioPlayer,
)
The storage service, result handler, resolvers and players above belong to the application. The complete example persists files in an app support directory (IndexedDB on Web), restores document/attachment metadata, and plays media through application-owned media_kit/video_player bindings. It uses stable extensionless attachment IDs and a 20 MiB per-file limit. The example also exposes an upload-service interface; all storage/player dependencies remain confined to the example.
Each item exposes a single-use openRead() stream, MIME, suggested name and an
optional length. Consume the stream before returning from the importer and honor
request.cancellation. The request also has itemId, itemIndex and itemCount.
Do not use only the batch operationId to name files, or multiple items will
collide. Per-item IDs are unique for the running engine; persistent stores should
use their own durable IDs.
All items must import successfully before the editor inserts the batch. It keeps
clipboard order despite concurrent completion and records one undo step. A
rejection, error or cancellation leaves the original text intact. Inspect
result.items and result.importedMedia to release unused imports through your
storage service, accounting for shared/deduplicated attachments. Undo does not
delete files or repeat uploads. Native readers close on completion/cancellation,
and URI streams support backpressure and awaited cleanup.
mediaTypeResolver returns image/video/audio, or null for filename-extension
fallback. Persist this metadata when URLs or attachment IDs have no extension.
The renderer does not infer type from a transient upload cache or fetch URL
headers. Core API types expose neither this plugin's native types nor File.
Formats and routing #
- PNG/JPEG/GIF/WebP/BMP, audio and video are accepted by the default core policy.
allowedMimeTypes,maxItems(default 32), andmaxConcurrentImports(default- are configurable. Additional image formats exposed by the adapter require explicit admission and an application renderer that supports them.
- One representation is selected per item. Actual audio/video and original GIF data take priority over image thumbnails. TIFF/DIB may be exposed as PNG by the native plugin. Native file references use the platform reader, with an owned file-URI stream fallback where necessary.
- Unsupported files do not become their thumbnail. Plain strings and URLs stay text. On Web, only real File objects are opened; URI-list strings are text alternatives. Files without MIME may use a recognized filename. A text file is not eagerly loaded and mistaken for a plain-text clipboard representation.
- Browser and iOS native paste events are claimed once by the focused editor, before asynchronous work. Unrelated inputs retain default paste. The captured snapshot supplies text fallback without a second clipboard read.
- A browser menu/button may use the asynchronous clipboard API where available; that API can require permission. Keyboard/browser-native paste events are the primary route, including browsers without asynchronous clipboard reads.
- Android IME input uses Flutter's content insertion callback on every editor
field.
contentInsertionMimeTypesdefaults to PNG/JPEG/GIF/WebP; empty disables it. Flutter delivers materialized bytes here, not a lazy native file stream. Missing bytes report readFailed; a content URI is never opened as a file path. - The core IME route also works without this adapter: omit
clipboardfromLiveMarkdownPasteConfigurationto retain SDK text paste and keyboard media. - Text/HTML/RTF with text representations retain text fallback. HTML conversion, dragging files, copying rendered media back out, and codec/player selection are separate from this paste feature.
Verified environments #
All checks use Flutter 3.47.1 / Dart 3.13.1 and super_clipboard 0.9.1.
| Environment | Verification |
|---|---|
| macOS 26.6.2 | PNG/JPEG/TIFF/GIF, audio/video, ordered files, external AppKit TIFF/file URLs, text fallback and undo |
| Windows hosted CI | Native image/GIF/audio/video clipboard, ordered files, text fallback and undo |
| Linux hosted CI / Xvfb (X11) | Same native matrix, plus browser-event tests |
| iOS 26.5 simulator | UIKit paste selector and native event bridge with an app-private named pasteboard; image/GIF/audio/video, files, text and undo |
| Android API 36 emulator | Native clipboard with content-provider media; real commitContent into document/table/Callout fields |
| Chrome | Browser File/text/URI-list paste events, ordering, untyped files, atomic insertion, undo and focus ownership |
Desktop CI workflow. Safari/Firefox, Linux Wayland, and physical-mobile permission dialogs have not been separately exercised. These are validation boundaries, not claims of identical behavior on every OS/browser version.
Build and run #
cd packages/livemd_flutter_clipboard/example
flutter pub get
flutter run -d macos # or chrome, an Android/iOS device, Windows or Linux
The checked-in development overrides point to the local core package. The demo persists its document and attachments locally. Host apps adapt the example's storage/upload service and appropriate file access; the macOS example enables user-selected read-only access for sandboxed file imports.
Apple native dependencies currently use CocoaPods. The Android example uses
Gradle 9.3.1 / AGP 9.1.0 / Kotlin 2.4.0 with Flutter 3.47.1's legacy Android
DSL/Kotlin compatibility flags (android.newDsl=false, android.builtInKotlin=false).
The adapter keeps stable super_clipboard 0.9.1; its Android minimum remains API 23
(the current Flutter example itself requires API 24).
For a Gradle 9 host, add this line at the top of android/build.gradle.kts,
before any subprojects blocks (run flutter pub get first):
apply(from = project(":livemd_flutter_clipboard").file("cargokit_compat.gradle"))
Groovy android/build.gradle equivalent:
apply from: project(':livemd_flutter_clipboard').file('cargokit_compat.gradle')
The package supplies this compatibility script. It raises the two old upstream
plugins' compile SDK to at least 35 before AGP finalizes their configuration, and
runs their legacy Cargokit tasks through Gradle's supported ExecOperations API.
It preserves native build modes, ABIs, output locations and process failures.
No pub-cache edits, forks or pre-release dependencies are needed. Applying the
script is harmless on Gradle 8; CI also builds Gradle 8.14.3 / AGP 8.11.1.
Core-only Android IME users do not need this native-plugin setup.
The native plugin builds Rust or obtains precompiled artifacts. If Homebrew rustup exists but rustc is missing from PATH, use the existing toolchain for the command without changing global settings:
taskRustCompiler=$(rustup which rustc)
RUSTC="$taskRustCompiler" PATH="$(dirname "$taskRustCompiler"):$PATH" flutter run -d macos
Tests #
# In this package:
flutter test test/adapter_test.dart
flutter test --platform chrome test/browser
# In example/:
flutter test test
swift tool/native_clipboard_test.swift
The macOS wrapper retains the previous clipboard in memory and restores it before its native test provider exits. Do not forcibly terminate it mid-test. The other native suite is explicitly gated for disposable emulators/CI desktops:
flutter test integration_test/isolated_native_paste_test.dart -d DEVICE_ID \
--dart-define=LIVEMD_ISOLATED_CLIPBOARD=true
Use a dedicated Android emulator or CI desktop, not a user's everyday clipboard. iOS debug hooks isolate the test in a named pasteboard and exercise UIKit's paste selector; Android debug hooks exercise an actual input connection/content provider. The hooks are absent from release builds. No test changes StrictMode or relaxes file-URI protections. Android multi-file tests use content providers.
Publication #
When releasing both packages, publish core before the adapter. Dart applies the root package's
.pubignore to nested publication; stage the committed adapter outside the repo:
taskStageDir=$(bash tool/stage_clipboard_package.sh)
dart pub publish --directory "$taskStageDir" --dry-run
The root script exports HEAD, removes development overrides and never publishes.
Before core is published, use --local-core for a local validation override.
After publishing core, validate again without that flag.
API references #
Flutter keyboard content insertion is Android-specific. KeyboardInsertedContent may contain bytes and a content URI. Native/browser clipboard behavior comes from super_clipboard; its 0.9.1 native event implementation also supports iOS paste selectors.