├── screenshots ├── sc1.png ├── sc2.png ├── sc3.png └── sc4.png ├── example ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-512.png │ ├── manifest.json │ └── index.html ├── macos │ ├── Runner │ │ ├── Configs │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ ├── Warnings.xcconfig │ │ │ └── AppInfo.xcconfig │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_64.png │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── Contents.json │ │ ├── Release.entitlements │ │ ├── AppDelegate.swift │ │ ├── DebugProfile.entitlements │ │ ├── MainFlutterWindow.swift │ │ ├── Info.plist │ │ ├── ClipboardPlugin.swift │ │ └── Base.lproj │ │ │ └── MainMenu.xib │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── Podfile.lock │ └── Podfile ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── ephemeral │ │ │ ├── flutter_lldbinit │ │ │ └── flutter_lldb_helper.py │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── ClipboardChannelHandler.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── AppDelegate.h │ │ ├── ClipboardPluginRegistrant.m │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── ClipboardPlugin.swift │ │ ├── AppDelegate.m │ │ └── ClipboardChannelHandler.m │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile.lock │ ├── .gitignore │ └── Podfile ├── android │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── xml │ │ │ │ │ │ └── file_paths.xml │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── net │ │ │ │ │ │ └── cubiclab │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ │ └── net │ │ │ │ │ └── cubiclab │ │ │ │ │ └── clipboard │ │ │ │ │ └── ClipboardChannelHandler.java │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle.kts │ └── settings.gradle.kts ├── .gitignore ├── .metadata ├── test │ └── widget_test.dart ├── analysis_options.yaml ├── README.md ├── pubspec.yaml └── pubspec.lock ├── .metadata ├── pubspec.yaml ├── lib └── src │ ├── clipboard_web_stub.dart │ └── clipboard_web.dart ├── LICENSE ├── .gitignore ├── pubspec.lock ├── CHANGELOG.md ├── test └── clipboard_test.dart └── README.md /screenshots/sc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/screenshots/sc1.png -------------------------------------------------------------------------------- /screenshots/sc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/screenshots/sc2.png -------------------------------------------------------------------------------- /screenshots/sc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/screenshots/sc3.png -------------------------------------------------------------------------------- /screenshots/sc4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/screenshots/sc4.png -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/ephemeral/flutter_lldbinit: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | command script import --relative-to-command-file flutter_lldb_helper.py 6 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner/ClipboardChannelHandler.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ClipboardChannelHandler : NSObject 4 | @end 5 | 6 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelezedi/flutter_clipboard/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip 6 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 2a7bc389f28d83c581f7ddd4601588a22e12512e 8 | channel: beta 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_selector_macos 9 | 10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 11 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 12 | } 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: clipboard 2 | description: Flutter clipboard with text, HTML, and image support. 3 | version: 3.0.8 4 | homepage: https://github.com/samuelezedi/flutter_clipboard 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | web: ^1.1.1 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | @property (nonatomic, strong) FlutterMethodChannel* clipboardMethodChannel; 6 | @property (nonatomic, strong) FlutterEventChannel* clipboardEventChannel; 7 | @property (nonatomic, strong) FlutterEventSink eventSink; 8 | @end 9 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/ClipboardPluginRegistrant.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "ClipboardPlugin-Swift.h" 3 | 4 | @interface ClipboardPluginRegistrant : NSObject 5 | @end 6 | 7 | @implementation ClipboardPluginRegistrant 8 | 9 | + (void)registerWithRegistry:(NSObject*)registry { 10 | [ClipboardPlugin registerWithRegistrar:[registry registrarForPlugin:@"ClipboardPlugin"]]; 11 | } 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - image_picker_ios (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | image_picker_ios: 14 | :path: ".symlinks/plugins/image_picker_ios/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 18 | image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 19 | 20 | PODFILE CHECKSUM: dd5737010dea5257fcf7df36f343d7c3cfd7d787 21 | 22 | COCOAPODS: 1.16.2 23 | -------------------------------------------------------------------------------- /example/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | // Register the clipboard plugin 14 | ClipboardPlugin.register(with: flutterViewController.registrar(forPlugin: "ClipboardPlugin")) 15 | 16 | super.awakeFromNib() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = net.cubiclab.example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 net.cubiclab. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "minimal-ui", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter application.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - file_selector_macos (0.0.1): 3 | - FlutterMacOS 4 | - FlutterMacOS (1.0.0) 5 | 6 | DEPENDENCIES: 7 | - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) 8 | - FlutterMacOS (from `Flutter/ephemeral`) 9 | 10 | EXTERNAL SOURCES: 11 | file_selector_macos: 12 | :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos 13 | FlutterMacOS: 14 | :path: Flutter/ephemeral 15 | 16 | SPEC CHECKSUMS: 17 | file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d 18 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 19 | 20 | PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 21 | 22 | COCOAPODS: 1.16.2 23 | -------------------------------------------------------------------------------- /lib/src/clipboard_web_stub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | // Stub implementation for non-web platforms 4 | Future copyImageWebImpl(Uint8List imageBytes) async { 5 | throw UnsupportedError('Web clipboard operations are only supported on web'); 6 | } 7 | 8 | Future pasteImageWebImpl() async { 9 | throw UnsupportedError('Web clipboard operations are only supported on web'); 10 | } 11 | 12 | Future pasteTextWebImpl() async { 13 | throw UnsupportedError('Web clipboard operations are only supported on web'); 14 | } 15 | 16 | // Note: This needs to return EnhancedClipboardData, but we can't import it here 17 | // So we'll use dynamic and cast it in the main file 18 | Future pasteRichTextWebImpl() async { 19 | throw UnsupportedError('Web clipboard operations are only supported on web'); 20 | } 21 | -------------------------------------------------------------------------------- /example/android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.3" apply false 22 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Exceptions to above rules. 43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 44 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "6fba2447e95c451518584c35e25f5433f14d888c" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 6fba2447e95c451518584c35e25f5433f14d888c 17 | base_revision: 6fba2447e95c451518584c35e25f5433f14d888c 18 | - platform: android 19 | create_revision: 6fba2447e95c451518584c35e25f5433f14d888c 20 | base_revision: 6fba2447e95c451518584c35e25f5433f14d888c 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | example 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/ios/Flutter/ephemeral/flutter_lldb_helper.py: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | import lldb 6 | 7 | def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): 8 | """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" 9 | base = frame.register["x0"].GetValueAsAddress() 10 | page_len = frame.register["x1"].GetValueAsUnsigned() 11 | 12 | # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the 13 | # first page to see if handled it correctly. This makes diagnosing 14 | # misconfiguration (e.g. missing breakpoint) easier. 15 | data = bytearray(page_len) 16 | data[0:8] = b'IHELPED!' 17 | 18 | error = lldb.SBError() 19 | frame.GetThread().GetProcess().WriteMemory(base, data, error) 20 | if not error.Success(): 21 | print(f'Failed to write into {base}[+{page_len}]', error) 22 | return 23 | 24 | def __lldb_init_module(debugger: lldb.SBDebugger, _): 25 | target = debugger.GetDummyTarget() 26 | # Caveat: must use BreakpointCreateByRegEx here and not 27 | # BreakpointCreateByName. For some reasons callback function does not 28 | # get carried over from dummy target for the later. 29 | bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") 30 | bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) 31 | bp.SetAutoContinue(True) 32 | print("-- LLDB integration loaded --") 33 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/net/cubiclab/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.cubiclab.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | import io.flutter.embedding.engine.FlutterEngine 5 | import io.flutter.plugin.common.MethodChannel 6 | import io.flutter.plugin.common.EventChannel 7 | import net.cubiclab.clipboard.ClipboardChannelHandler 8 | 9 | class MainActivity : FlutterActivity() { 10 | private var clipboardHandler: ClipboardChannelHandler? = null 11 | private var methodChannel: MethodChannel? = null 12 | private var eventChannel: EventChannel? = null 13 | 14 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 15 | super.configureFlutterEngine(flutterEngine) 16 | clipboardHandler = ClipboardChannelHandler(applicationContext) 17 | methodChannel = MethodChannel( 18 | flutterEngine.dartExecutor.binaryMessenger, 19 | "net.cubiclab.clipboard/methods" 20 | ).apply { setMethodCallHandler(clipboardHandler) } 21 | 22 | eventChannel = EventChannel( 23 | flutterEngine.dartExecutor.binaryMessenger, 24 | "net.cubiclab.clipboard/events" 25 | ).apply { setStreamHandler(clipboardHandler) } 26 | } 27 | 28 | override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) { 29 | methodChannel?.setMethodCallHandler(null) 30 | eventChannel?.setStreamHandler(null) 31 | clipboardHandler = null 32 | super.cleanUpFlutterEngine(flutterEngine) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | 32 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 33 | target 'RunnerTests' do 34 | inherit! :search_paths 35 | end 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_macos_build_settings(target) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | # include: package:flutter_lints/flutter.yaml # Commented out - flutter_lints not in dependencies 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /example/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "net.cubiclab.example" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11.toString() 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "net.cubiclab.example" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.getByName("debug") 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | # RunnerTests target removed - doesn't exist in project 33 | # target 'RunnerTests' do 34 | # inherit! :search_paths 35 | # end 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_ios_build_settings(target) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright 2020, the Clipboard project authors. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | * Neither the name of the Clipboard project authors nor the 16 | names of its contributors may be used to endorse or promote 17 | products derived from this software without specific prior 18 | written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | 14 | 15 | 24 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example App - Enhanced Clipboard Demo 2 | 3 | This example app demonstrates all features of the Enhanced Flutter Clipboard package, including the new image copy/paste functionality. 4 | 5 | ## Features Demonstrated 6 | 7 | ### Basic Features 8 | - ✅ Text copy/paste 9 | - ✅ Rich text (HTML) copy/paste 10 | - ✅ Multiple format copy 11 | - ✅ Callback support 12 | - ✅ Clipboard monitoring 13 | - ✅ Utility methods (hasData, isEmpty, getContentType, etc.) 14 | - ✅ Debug information 15 | 16 | ### New Image Features (v3.0.0) 17 | - ✅ **Image Selection**: Pick images from gallery 18 | - ✅ **Image Copy**: Copy images to clipboard 19 | - ✅ **Image Paste**: Paste images from clipboard 20 | - ✅ **Multiple Format Copy**: Copy text + HTML + image simultaneously 21 | - ✅ **Rich Paste**: Paste all formats including images 22 | - ✅ **Image Display**: View selected and pasted images 23 | - ✅ **Image History**: Clipboard history shows image entries 24 | 25 | ## Setup 26 | 27 | 1. Install dependencies: 28 | ```bash 29 | cd example 30 | flutter pub get 31 | ``` 32 | 33 | 2. Run the app: 34 | ```bash 35 | flutter run 36 | ``` 37 | 38 | ## Image Testing Workflow 39 | 40 | 1. **Select Image**: Tap "Pick Image" to select an image from your gallery 41 | 2. **Copy Image**: Tap "Copy Image" to copy the selected image to clipboard 42 | 3. **Paste Image**: Tap "Paste Image" to retrieve the image from clipboard 43 | 4. **Copy Multiple**: Use "Copy Multiple (Text + Image)" to copy text and image together 44 | 5. **Paste All**: Use "Paste All Formats" to get text, HTML, and image if available 45 | 46 | ## Platform Notes 47 | 48 | ### Android 49 | - Requires `READ_EXTERNAL_STORAGE` permission for image picking (handled by image_picker) 50 | - Image clipboard works via platform channels 51 | - May need FileProvider setup for Android 7.0+ (see main package README) 52 | 53 | ### iOS 54 | - Requires photo library access permission (handled by image_picker) 55 | - Image clipboard works via platform channels 56 | - Full native support 57 | 58 | ### Web 59 | - Image picking works via file input 60 | - Image clipboard has limited browser support 61 | 62 | ## UI Sections 63 | 64 | 1. **Clipboard Status**: Shows current status and monitoring controls 65 | 2. **Input Section**: Text, HTML, and image selection 66 | 3. **Copy Operations**: All copy methods including image 67 | 4. **Paste Operations**: All paste methods including image 68 | 5. **Pasted Content**: Displays text and images 69 | 6. **Clipboard History**: Shows recent clipboard changes 70 | 7. **Debug Information**: Technical details for debugging 71 | 72 | ## Testing Tips 73 | 74 | - Copy an image, then paste it to verify it works 75 | - Copy text + image together, then paste to see both 76 | - Monitor clipboard changes to see image entries 77 | - Check debug info to see if native monitoring is active 78 | - Test on both Android and iOS for platform differences 79 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=3.0.0 <4.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | 28 | # The following adds the Cupertino Icons font to your application. 29 | # Use with the CupertinoIcons class for iOS style icons. 30 | cupertino_icons: ^1.0.2 31 | clipboard: 32 | path: ../ 33 | image_picker: ^1.0.0 34 | 35 | dev_dependencies: 36 | flutter_test: 37 | sdk: flutter 38 | 39 | # For information on the generic Dart part of this file, see the 40 | # following page: https://dart.dev/tools/pub/pubspec 41 | 42 | # The following section is specific to Flutter. 43 | flutter: 44 | 45 | # The following line ensures that the Material Icons font is 46 | # included with your application, so that you can use the icons in 47 | # the material Icons class. 48 | uses-material-design: true 49 | 50 | # To add assets to your application, add an assets section, like this: 51 | # assets: 52 | # - images/a_dot_burr.jpeg 53 | # - images/a_dot_ham.jpeg 54 | 55 | # An image asset can refer to one or more resolution-specific "variants", see 56 | # https://flutter.dev/assets-and-images/#resolution-aware. 57 | 58 | # For details regarding adding assets from package dependencies, see 59 | # https://flutter.dev/assets-and-images/#from-packages 60 | 61 | # To add custom fonts to your application, add a fonts section here, 62 | # in this "flutter" section. Each entry in this list should have a 63 | # "family" key with the font family name, and a "fonts" key with a 64 | # list giving the asset and other descriptors for the font. For 65 | # example: 66 | # fonts: 67 | # - family: Schyler 68 | # fonts: 69 | # - asset: fonts/Schyler-Regular.ttf 70 | # - asset: fonts/Schyler-Italic.ttf 71 | # style: italic 72 | # - family: Trajan Pro 73 | # fonts: 74 | # - asset: fonts/TrajanPro.ttf 75 | # - asset: fonts/TrajanPro_Bold.ttf 76 | # weight: 700 77 | # 78 | # For details regarding fonts from package dependencies, 79 | # see https://flutter.dev/custom-fonts/#from-packages 80 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 57 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /lib/src/clipboard_web.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | import 'package:web/web.dart' as web; 4 | import 'dart:js_interop'; 5 | 6 | // Web implementation using browser Clipboard API (requires user gesture) 7 | Future copyImageWebImpl(Uint8List imageBytes) async { 8 | final clipboard = web.window.navigator.clipboard; 9 | 10 | try { 11 | // Create Blob from image bytes 12 | final blobParts = [imageBytes.toJS].toJS; 13 | final blobOptions = web.BlobPropertyBag(type: 'image/png'); 14 | final blob = web.Blob(blobParts, blobOptions); 15 | 16 | // Construct ClipboardItem with image/png type 17 | final clipboardItemData = {'image/png': blob}.jsify()!; 18 | final clipboardItem = web.ClipboardItem(clipboardItemData as JSObject); 19 | 20 | // Call navigator.clipboard.write([clipboardItem]) 21 | await clipboard.write([clipboardItem].toJS).toDart; 22 | } catch (e) { 23 | throw Exception('Failed to copy image to clipboard: $e'); 24 | } 25 | } 26 | 27 | Future pasteImageWebImpl() async { 28 | final clipboard = web.window.navigator.clipboard; 29 | 30 | try { 31 | // navigator.clipboard.read() returns JSPromise 32 | final items = await clipboard.read().toDart; 33 | final itemsList = items.toDart; 34 | 35 | for (var i = 0; i < itemsList.length; i++) { 36 | final item = itemsList[i]; 37 | 38 | // Check if item has image/png type 39 | final types = item.types.toDart; 40 | if (!types.contains('image/png'.toJS)) continue; 41 | 42 | // item.getType('image/png') -> Promise 43 | final blob = await item.getType('image/png').toDart; 44 | 45 | // blob.arrayBuffer() -> Promise 46 | final arrayBuffer = await blob.arrayBuffer().toDart; 47 | 48 | // Convert ArrayBuffer to Uint8List 49 | final bytes = arrayBuffer.toDart.asUint8List(); 50 | return bytes; 51 | } 52 | return null; 53 | } catch (e) { 54 | return null; 55 | } 56 | } 57 | 58 | // Web implementation for pasting text 59 | Future pasteTextWebImpl() async { 60 | final clipboard = web.window.navigator.clipboard; 61 | 62 | try { 63 | // navigator.clipboard.readText() returns JSPromise 64 | final text = await clipboard.readText().toDart; 65 | return text.toDart; 66 | } catch (e) { 67 | throw Exception('Failed to paste text from clipboard: $e'); 68 | } 69 | } 70 | 71 | // Web implementation for pasting rich text (text + HTML) 72 | Future> pasteRichTextWebImpl() async { 73 | final clipboard = web.window.navigator.clipboard; 74 | 75 | try { 76 | String? text; 77 | String? htmlText; 78 | 79 | // Try to read text 80 | try { 81 | final textResult = await clipboard.readText().toDart; 82 | text = textResult.toDart; 83 | } catch (e) { 84 | // Text might not be available 85 | } 86 | 87 | // Try to read HTML from clipboard items 88 | try { 89 | final items = await clipboard.read().toDart; 90 | final itemsList = items.toDart; 91 | 92 | for (var i = 0; i < itemsList.length; i++) { 93 | final item = itemsList[i]; 94 | final types = item.types.toDart; 95 | 96 | // Check for HTML 97 | if (types.contains('text/html'.toJS)) { 98 | // item.getType('text/html') -> Promise 99 | final blob = await item.getType('text/html').toDart; 100 | 101 | // blob.text() -> Promise 102 | final htmlResult = await blob.text().toDart; 103 | htmlText = htmlResult.toDart; 104 | break; 105 | } 106 | } 107 | } catch (e) { 108 | // HTML might not be available 109 | } 110 | 111 | return { 112 | 'text': text, 113 | 'html': htmlText, 114 | }; 115 | } catch (e) { 116 | throw Exception('Failed to paste rich text from clipboard: $e'); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.13.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.2" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.4.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.2" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.1" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.3" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | leak_tracker: 63 | dependency: transitive 64 | description: 65 | name: leak_tracker 66 | sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "10.0.9" 70 | leak_tracker_flutter_testing: 71 | dependency: transitive 72 | description: 73 | name: leak_tracker_flutter_testing 74 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "3.0.9" 78 | leak_tracker_testing: 79 | dependency: transitive 80 | description: 81 | name: leak_tracker_testing 82 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "3.0.1" 86 | matcher: 87 | dependency: transitive 88 | description: 89 | name: matcher 90 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "0.12.17" 94 | material_color_utilities: 95 | dependency: transitive 96 | description: 97 | name: material_color_utilities 98 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "0.11.1" 102 | meta: 103 | dependency: transitive 104 | description: 105 | name: meta 106 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "1.16.0" 110 | path: 111 | dependency: transitive 112 | description: 113 | name: path 114 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.9.1" 118 | sky_engine: 119 | dependency: transitive 120 | description: flutter 121 | source: sdk 122 | version: "0.0.0" 123 | source_span: 124 | dependency: transitive 125 | description: 126 | name: source_span 127 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.10.1" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.12.1" 139 | stream_channel: 140 | dependency: transitive 141 | description: 142 | name: stream_channel 143 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "2.1.4" 147 | string_scanner: 148 | dependency: transitive 149 | description: 150 | name: string_scanner 151 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "1.4.1" 155 | term_glyph: 156 | dependency: transitive 157 | description: 158 | name: term_glyph 159 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.2.2" 163 | test_api: 164 | dependency: transitive 165 | description: 166 | name: test_api 167 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "0.7.4" 171 | vector_math: 172 | dependency: transitive 173 | description: 174 | name: vector_math 175 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "2.1.4" 179 | vm_service: 180 | dependency: transitive 181 | description: 182 | name: vm_service 183 | sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "15.0.0" 187 | web: 188 | dependency: "direct main" 189 | description: 190 | name: web 191 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "1.1.1" 195 | sdks: 196 | dart: ">=3.7.0-0 <4.0.0" 197 | flutter: ">=3.18.0-18.0.pre.54" 198 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.8 2 | 3 | * **Code Formatting**: Improved code formatting and readability with better line breaks and consistent formatting. 4 | * **Code Quality**: Removed trailing whitespace and fixed indentation issues for better code maintainability. 5 | 6 | ## 3.0.7 7 | 8 | * **Fixed JSString Type Issues**: Fixed type compatibility issues in web clipboard implementation by properly converting Dart Strings to JSString for type comparisons. 9 | * **Analyzer Fixes**: Resolved all static analysis warnings and errors for pub.dev compliance. 10 | * **Code Cleanup**: Removed unnecessary imports and fixed analysis configuration issues. 11 | 12 | ## 3.0.6 13 | 14 | * **Web Package Migration**: Migrated from `dart:html` and `dart:js_util` to the new `package:web` package for better compatibility and modern JS interop. 15 | * **Conditional Import Update**: Changed conditional import from `dart.library.html` to `dart.library.js` to align with the new web package. 16 | * **JS Interop Improvements**: Updated web clipboard implementation to use modern JS interop APIs (`dart:js_interop`) for better type safety and performance. 17 | 18 | ## 3.0.5 19 | 20 | * **Web Clipboard Fixes**: Added full web support for copy/paste of text, rich text (HTML), and images using the browser Clipboard API with conditional imports. 21 | * **Web Image Copy/Paste**: Implemented web-side image handling with `ClipboardItem`/`navigator.clipboard` for copy and `read`/`getType` for paste. 22 | * **Web Text/Rich Text Paste**: Added `readText` and HTML extraction via `read()` + `getType('text/html')`. 23 | * **Example App**: Updated to avoid early clipboard reads; paste dialogs no longer show on launch. 24 | * **Lint/Build**: Resolved analyzer issues after web additions. 25 | 26 | ## 3.0.0 27 | 28 | * **Major Release**: Complete rewrite with platform channel support for true multi-format clipboard 29 | * **Platform Channels**: Added native Android (Kotlin) and iOS (Objective-C) implementations for enhanced features 30 | * **True Rich Text Support**: HTML clipboard support now works via platform channels, not just in-memory storage 31 | * **Image Support**: Full image copy/paste support for PNG images on Android and iOS 32 | * **Native Clipboard Monitoring**: Real-time clipboard change detection using platform APIs (Android clipboard listeners, iOS notification center) 33 | * **Memory Management**: Fixed listener memory leaks by using Set instead of List and adding cleanup mechanisms 34 | * **Enhanced Error Handling**: Improved error handling with graceful fallbacks to Flutter's Clipboard API 35 | * **Better Listener Management**: `addListener()` now returns a cleanup function, added `removeAllListeners()` method 36 | * **New Methods**: Added `copyImage()` and `pasteImage()` for direct image clipboard operations 37 | * **Production Ready**: Comprehensive fallback mechanisms ensure reliability across all platforms 38 | * **Documentation**: Updated README with accurate feature descriptions, image examples, and platform setup instructions 39 | * **Code Quality**: Improved code structure, better separation of concerns, and comprehensive error handling 40 | * **Breaking Changes**: This is a major version bump due to significant architectural improvements and platform channel integration 41 | 42 | ## 2.0.2 43 | 44 | * **Major Improvements**: Complete rewrite with platform channel support for true multi-format clipboard 45 | * **Platform Channels**: Added native Android (Kotlin) and iOS (Swift) implementations for enhanced features 46 | * **True Rich Text Support**: HTML clipboard support now works via platform channels, not just in-memory storage 47 | * **Image Support**: Full image copy/paste support for PNG images on Android and iOS 48 | * **Native Clipboard Monitoring**: Real-time clipboard change detection using platform APIs (Android clipboard listeners) 49 | * **Memory Management**: Fixed listener memory leaks by using Set instead of List and adding cleanup mechanisms 50 | * **Enhanced Error Handling**: Improved error handling with graceful fallbacks to Flutter's Clipboard API 51 | * **Better Listener Management**: `addListener()` now returns a cleanup function, added `removeAllListeners()` method 52 | * **New Methods**: Added `copyImage()` and `pasteImage()` for direct image clipboard operations 53 | * **Production Ready**: Comprehensive fallback mechanisms ensure reliability across all platforms 54 | * **Documentation**: Updated README with accurate feature descriptions, image examples, and platform setup instructions 55 | * **Code Quality**: Improved code structure, better separation of concerns, and comprehensive error handling 56 | 57 | ## 2.0.0 58 | 59 | * **Major Release**: Fixed naming conflicts and improved API consistency 60 | * **Class Renaming**: Renamed `ClipboardData` to `EnhancedClipboardData` to avoid conflicts with Flutter's built-in `ClipboardData` 61 | * **API Improvements**: Better separation between Flutter's native clipboard API and enhanced features 62 | * **Bug Fixes**: Fixed all compilation errors and linter issues 63 | 64 | ## 1.0.0 65 | 66 | * **Major Release**: Complete rewrite with enhanced functionality 67 | * **SDK Update**: Updated to Dart SDK >=3.0.0 <4.0.0 68 | * **Rich Text Support**: Added `copyRichText()` and `pasteRichText()` methods 69 | * **Multiple Format Copy**: Added `copyMultiple()` for copying multiple data formats 70 | * **Clipboard Monitoring**: Added real-time clipboard change detection with `addListener()`, `startMonitoring()`, and `stopMonitoring()` 71 | * **Enhanced Error Handling**: Custom `ClipboardException` class with error codes 72 | * **Utility Methods**: Added `hasData()`, `isEmpty()`, `getContentType()`, `getDataSize()`, `isValidInput()`, and `clear()` 73 | * **Callback Support**: Added `copyWithCallback()` with success and error callbacks 74 | * **Debug Information**: Added `getDebugInfo()` for comprehensive debugging 75 | * **Testing Support**: Added `setMockData()` for testing scenarios 76 | * **ClipboardData Class**: New data class with rich information about clipboard content 77 | * **ClipboardContentType Enum**: Enum for identifying clipboard content types 78 | * **Backward Compatibility**: All original methods (`copy`, `paste`, `controlC`, `controlV`) still supported 79 | * **Improved Documentation**: Comprehensive README with examples for all features 80 | * **Enhanced Example App**: Complete demo showcasing all new features 81 | 82 | ## 0.1.3 83 | 84 | * Added null safety 85 | 86 | ## 0.1.2+8 87 | 88 | * Fixed bug in clipboard class 89 | * No major changes, edited readme, changelog, pubspec.yaml. 90 | 91 | ## 0.1.2+7 92 | 93 | * Fixed bug in clipboard class 94 | * No major changes, edited readme, changelog, pubspec.yaml. 95 | 96 | ## 0.1.2+6 97 | 98 | * No major changes, edited readme, changelog, pubspec.yaml. 99 | 100 | ## 0.1.2+5 101 | 102 | * No major changes, edited readme, changelog, pubspec.yaml. 103 | 104 | ## 0.1.2+4 105 | 106 | * No major changes, edited readme, changelog, pubspec.yaml. 107 | 108 | ## 0.1.2+3 109 | 110 | * No major changes, edited readme, changelog, pubspec.yaml. 111 | 112 | ## 0.1.2+2 113 | 114 | * No major changes, edited readme, changelog, pubspec.yaml. 115 | 116 | ## 0.1.2+1 117 | 118 | * No major changes, just a few tweaks in readme, changelog. 119 | 120 | ## 0.1.2 121 | 122 | * No major changes, just a few tweaks in readme, changelog, example director etc. 123 | 124 | ## 0.1.1 125 | 126 | * First release 127 | * User can Copy from clipboard 128 | * User can Paste from cliboard, -------------------------------------------------------------------------------- /test/clipboard_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:clipboard/clipboard.dart'; 3 | 4 | void main() { 5 | group('FlutterClipboard Tests', () { 6 | setUp(() async { 7 | // Clear any existing listeners before each test 8 | await FlutterClipboard.stopMonitoring(); 9 | FlutterClipboard.removeAllListeners(); 10 | }); 11 | 12 | tearDown(() async { 13 | // Clean up after each test 14 | await FlutterClipboard.stopMonitoring(); 15 | FlutterClipboard.removeAllListeners(); 16 | }); 17 | 18 | group('Basic Copy/Paste Operations', () { 19 | test('copy should throw exception for empty text', () async { 20 | expect( 21 | () => FlutterClipboard.copy(''), 22 | throwsA(isA()), 23 | ); 24 | }); 25 | 26 | test('copy should accept valid text', () async { 27 | expect(() => FlutterClipboard.copy('Hello World'), returnsNormally); 28 | }); 29 | 30 | test('paste should return string', () async { 31 | final result = await FlutterClipboard.paste(); 32 | expect(result, isA()); 33 | }); 34 | 35 | test('controlC should return boolean', () async { 36 | final result = await FlutterClipboard.controlC('Test'); 37 | expect(result, isA()); 38 | }); 39 | 40 | test('controlV should return dynamic data', () async { 41 | final result = await FlutterClipboard.controlV(); 42 | expect(result, isA()); 43 | }); 44 | }); 45 | 46 | group('Rich Text Operations', () { 47 | test('copyRichText should accept text and html', () async { 48 | expect( 49 | () => FlutterClipboard.copyRichText( 50 | text: 'Hello', 51 | html: 'Hello', 52 | ), 53 | returnsNormally, 54 | ); 55 | }); 56 | 57 | test('copyRichText should throw for empty content', () async { 58 | expect( 59 | () => FlutterClipboard.copyRichText(text: '', html: ''), 60 | throwsA(isA()), 61 | ); 62 | }); 63 | 64 | test('pasteRichText should return EnhancedClipboardData', () async { 65 | final result = await FlutterClipboard.pasteRichText(); 66 | expect(result, isA()); 67 | }); 68 | }); 69 | 70 | group('Multiple Format Operations', () { 71 | test('copyMultiple should accept formats map', () async { 72 | final formats = { 73 | 'text/plain': 'Hello', 74 | 'text/html': 'Hello', 75 | }; 76 | expect(() => FlutterClipboard.copyMultiple(formats), returnsNormally); 77 | }); 78 | 79 | test('copyMultiple should throw for empty formats', () async { 80 | expect( 81 | () => FlutterClipboard.copyMultiple({}), 82 | throwsA(isA()), 83 | ); 84 | }); 85 | }); 86 | 87 | group('Callback Operations', () { 88 | test('copyWithCallback should call success callback', () async { 89 | bool successCalled = false; 90 | await FlutterClipboard.copyWithCallback( 91 | text: 'Test', 92 | onSuccess: () => successCalled = true, 93 | ); 94 | expect(successCalled, isTrue); 95 | }); 96 | 97 | test('copyWithCallback should call error callback on failure', () async { 98 | String? errorMessage; 99 | try { 100 | await FlutterClipboard.copyWithCallback( 101 | text: '', 102 | onError: (error) => errorMessage = error, 103 | ); 104 | } catch (e) { 105 | // Expected to throw 106 | } 107 | expect(errorMessage, isNotNull); 108 | }); 109 | }); 110 | 111 | group('Utility Methods', () { 112 | test('isValidInput should validate text correctly', () { 113 | expect(FlutterClipboard.isValidInput(''), isFalse); 114 | expect(FlutterClipboard.isValidInput(' '), isFalse); 115 | expect(FlutterClipboard.isValidInput('Hello'), isTrue); 116 | }); 117 | 118 | test('hasData should return boolean', () async { 119 | final result = await FlutterClipboard.hasData(); 120 | expect(result, isA()); 121 | }); 122 | 123 | test('isEmpty should return boolean', () async { 124 | final result = await FlutterClipboard.isEmpty(); 125 | expect(result, isA()); 126 | }); 127 | 128 | test('getDataSize should return integer', () async { 129 | final result = await FlutterClipboard.getDataSize(); 130 | expect(result, isA()); 131 | }); 132 | 133 | test('getContentType should return ClipboardContentType', () async { 134 | final result = await FlutterClipboard.getContentType(); 135 | expect(result, isA()); 136 | }); 137 | }); 138 | 139 | group('Clipboard Monitoring', () { 140 | test('addListener should add listener and return cleanup function', () { 141 | void testListener(EnhancedClipboardData data) {} 142 | final removeListener = FlutterClipboard.addListener(testListener); 143 | expect(removeListener, isA()); 144 | expect(() => removeListener(), returnsNormally); 145 | }); 146 | 147 | test('removeListener should remove listener', () { 148 | void testListener(EnhancedClipboardData data) {} 149 | FlutterClipboard.addListener(testListener); 150 | expect(() => FlutterClipboard.removeListener(testListener), 151 | returnsNormally); 152 | }); 153 | 154 | test('removeAllListeners should remove all listeners', () { 155 | void testListener1(EnhancedClipboardData data) {} 156 | void testListener2(EnhancedClipboardData data) {} 157 | FlutterClipboard.addListener(testListener1); 158 | FlutterClipboard.addListener(testListener2); 159 | expect(() => FlutterClipboard.removeAllListeners(), returnsNormally); 160 | }); 161 | 162 | test('startMonitoring should start monitoring', () async { 163 | expect(() => FlutterClipboard.startMonitoring(), returnsNormally); 164 | await FlutterClipboard.stopMonitoring(); 165 | }); 166 | 167 | test('stopMonitoring should stop monitoring', () async { 168 | await FlutterClipboard.startMonitoring(); 169 | expect(() => FlutterClipboard.stopMonitoring(), returnsNormally); 170 | }); 171 | }); 172 | 173 | group('Debug and Testing', () { 174 | test('getDebugInfo should return map with monitoring info', () async { 175 | final result = await FlutterClipboard.getDebugInfo(); 176 | expect(result, isA>()); 177 | expect(result.containsKey('listenersCount'), isTrue); 178 | expect(result.containsKey('isMonitoring'), isTrue); 179 | expect(result.containsKey('hasNativeMonitoring'), isTrue); 180 | }); 181 | 182 | test('setMockData should set mock data', () async { 183 | expect(() => FlutterClipboard.setMockData('Test'), returnsNormally); 184 | }); 185 | }); 186 | 187 | group('EnhancedClipboardData Class', () { 188 | test('EnhancedClipboardData should have correct properties', () { 189 | final data = EnhancedClipboardData( 190 | text: 'Hello', 191 | html: 'Hello', 192 | timestamp: DateTime.now(), 193 | ); 194 | expect(data.text, equals('Hello')); 195 | expect(data.html, equals('Hello')); 196 | expect(data.timestamp, isNotNull); 197 | }); 198 | 199 | test('EnhancedClipboardData isEmpty should work correctly', () { 200 | final emptyData = EnhancedClipboardData(); 201 | final nonEmptyData = EnhancedClipboardData(text: 'Hello'); 202 | 203 | expect(emptyData.isEmpty, isTrue); 204 | expect(nonEmptyData.isEmpty, isFalse); 205 | }); 206 | 207 | test('EnhancedClipboardData hasText should work correctly', () { 208 | final emptyData = EnhancedClipboardData(); 209 | final textData = EnhancedClipboardData(text: 'Hello'); 210 | 211 | expect(emptyData.hasText, isFalse); 212 | expect(textData.hasText, isTrue); 213 | }); 214 | }); 215 | 216 | group('ClipboardException Class', () { 217 | test('ClipboardException should have message and code', () { 218 | final exception = ClipboardException('Test error', 'TEST_CODE'); 219 | expect(exception.message, equals('Test error')); 220 | expect(exception.code, equals('TEST_CODE')); 221 | }); 222 | 223 | test('ClipboardException toString should include message and code', () { 224 | final exception = ClipboardException('Test error', 'TEST_CODE'); 225 | final string = exception.toString(); 226 | expect(string, contains('Test error')); 227 | expect(string, contains('TEST_CODE')); 228 | }); 229 | }); 230 | }); 231 | } 232 | -------------------------------------------------------------------------------- /example/ios/Runner/ClipboardPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class ClipboardPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { 5 | private var eventSink: FlutterEventSink? 6 | private var clipboardChangeObserver: NSObjectProtocol? 7 | 8 | public static func register(with registrar: FlutterPluginRegistrar) { 9 | let methodChannel = FlutterMethodChannel( 10 | name: "net.cubiclab.clipboard/methods", 11 | binaryMessenger: registrar.messenger() 12 | ) 13 | let eventChannel = FlutterEventChannel( 14 | name: "net.cubiclab.clipboard/events", 15 | binaryMessenger: registrar.messenger() 16 | ) 17 | 18 | let instance = ClipboardPlugin() 19 | registrar.addMethodCallDelegate(instance, channel: methodChannel) 20 | eventChannel.setStreamHandler(instance) 21 | } 22 | 23 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 24 | switch call.method { 25 | case "copy": 26 | guard let args = call.arguments as? [String: Any], 27 | let text = args["text"] as? String else { 28 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Text is required", details: nil)) 29 | return 30 | } 31 | if text.isEmpty { 32 | result(FlutterError(code: "EMPTY_TEXT", message: "Text cannot be empty", details: nil)) 33 | return 34 | } 35 | UIPasteboard.general.string = text 36 | result(true) 37 | 38 | case "copyRichText": 39 | guard let args = call.arguments as? [String: Any] else { 40 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil)) 41 | return 42 | } 43 | let text = args["text"] as? String ?? "" 44 | let html = args["html"] as? String 45 | 46 | if text.isEmpty && (html == nil || html!.isEmpty) { 47 | result(FlutterError(code: "EMPTY_CONTENT", message: "Either text or html must be provided", details: nil)) 48 | return 49 | } 50 | 51 | if let html = html, !html.isEmpty { 52 | UIPasteboard.general.setValue(html, forPasteboardType: "public.html") 53 | if !text.isEmpty { 54 | UIPasteboard.general.string = text 55 | } 56 | } else { 57 | UIPasteboard.general.string = text 58 | } 59 | result(true) 60 | 61 | case "copyMultiple": 62 | guard let args = call.arguments as? [String: Any], 63 | let formats = args["formats"] as? [String: Any] else { 64 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil)) 65 | return 66 | } 67 | if formats.isEmpty { 68 | result(FlutterError(code: "EMPTY_FORMATS", message: "At least one format must be provided", details: nil)) 69 | return 70 | } 71 | 72 | // Handle image first (highest priority) 73 | if let imageBytes = formats["image/png"] as? [Int], !imageBytes.isEmpty { 74 | let data = Data(imageBytes.map { UInt8($0 & 0xFF) }) 75 | if let image = UIImage(data: data) { 76 | UIPasteboard.general.image = image 77 | if let text = formats["text/plain"] as? String, !text.isEmpty { 78 | UIPasteboard.general.string = text 79 | } 80 | result(true) 81 | return 82 | } 83 | } 84 | 85 | // Fallback to HTML or text 86 | if let text = formats["text/plain"] as? String { 87 | UIPasteboard.general.string = text 88 | } 89 | if let html = formats["text/html"] as? String { 90 | UIPasteboard.general.setValue(html, forPasteboardType: "public.html") 91 | } 92 | result(true) 93 | 94 | case "copyImage": 95 | guard let args = call.arguments as? [String: Any], 96 | let imageBytes = args["imageBytes"] as? [Int] else { 97 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Image bytes are required", details: nil)) 98 | return 99 | } 100 | if imageBytes.isEmpty { 101 | result(FlutterError(code: "EMPTY_IMAGE", message: "Image bytes cannot be empty", details: nil)) 102 | return 103 | } 104 | let data = Data(imageBytes.map { UInt8($0 & 0xFF) }) 105 | guard let image = UIImage(data: data) else { 106 | result(FlutterError(code: "INVALID_IMAGE", message: "Failed to decode image", details: nil)) 107 | return 108 | } 109 | UIPasteboard.general.image = image 110 | result(true) 111 | 112 | case "paste": 113 | let text = UIPasteboard.general.string ?? "" 114 | result(["text": text]) 115 | 116 | case "pasteRichText": 117 | let text = UIPasteboard.general.string ?? "" 118 | let html = UIPasteboard.general.value(forPasteboardType: "public.html") as? String 119 | let imageBytes = getImageBytesFromClipboard() 120 | result([ 121 | "text": text, 122 | "html": html ?? NSNull(), 123 | "imageBytes": imageBytes ?? NSNull(), 124 | "timestamp": Int64(Date().timeIntervalSince1970 * 1000) 125 | ]) 126 | 127 | case "pasteImage": 128 | let imageBytes = getImageBytesFromClipboard() 129 | if let bytes = imageBytes { 130 | result(["imageBytes": bytes]) 131 | } else { 132 | result(["imageBytes": NSNull()]) 133 | } 134 | 135 | case "getContentType": 136 | let text = UIPasteboard.general.string ?? "" 137 | let html = UIPasteboard.general.value(forPasteboardType: "public.html") as? String 138 | let hasImage = UIPasteboard.general.image != nil 139 | 140 | if hasImage && (!text.isEmpty || (html != nil && !html!.isEmpty)) { 141 | result("mixed") 142 | } else if hasImage { 143 | result("image") 144 | } else if text.isEmpty && (html == nil || html!.isEmpty) { 145 | result("empty") 146 | } else if !text.isEmpty && html != nil && !html!.isEmpty { 147 | result("mixed") 148 | } else if html != nil && !html!.isEmpty { 149 | result("html") 150 | } else { 151 | result("text") 152 | } 153 | 154 | case "hasData": 155 | let text = UIPasteboard.general.string ?? "" 156 | result(!text.isEmpty) 157 | 158 | case "clear": 159 | UIPasteboard.general.string = "" 160 | result(true) 161 | 162 | case "getDataSize": 163 | let text = UIPasteboard.general.string ?? "" 164 | result(text.count) 165 | 166 | case "startMonitoring": 167 | startMonitoring() 168 | result(true) 169 | 170 | case "stopMonitoring": 171 | stopMonitoring() 172 | result(true) 173 | 174 | default: 175 | result(FlutterMethodNotImplemented) 176 | } 177 | } 178 | 179 | private func startMonitoring() { 180 | if clipboardChangeObserver != nil { 181 | return 182 | } 183 | 184 | // iOS doesn't have native clipboard change notifications 185 | // We'll use a timer-based approach as fallback 186 | // The Dart side will handle the actual polling 187 | NotificationCenter.default.addObserver( 188 | forName: UIApplication.didBecomeActiveNotification, 189 | object: nil, 190 | queue: .main 191 | ) { [weak self] _ in 192 | self?.checkClipboardChange() 193 | } 194 | } 195 | 196 | private func stopMonitoring() { 197 | if let observer = clipboardChangeObserver { 198 | NotificationCenter.default.removeObserver(observer) 199 | clipboardChangeObserver = nil 200 | } 201 | } 202 | 203 | private func checkClipboardChange() { 204 | let text = UIPasteboard.general.string ?? "" 205 | let html = UIPasteboard.general.value(forPasteboardType: "public.html") as? String 206 | 207 | eventSink?([ 208 | "text": text, 209 | "html": html ?? NSNull(), 210 | "timestamp": Int64(Date().timeIntervalSince1970 * 1000) 211 | ]) 212 | } 213 | 214 | public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 215 | self.eventSink = events 216 | startMonitoring() 217 | return nil 218 | } 219 | 220 | public func onCancel(withArguments arguments: Any?) -> FlutterError? { 221 | eventSink = nil 222 | stopMonitoring() 223 | return nil 224 | } 225 | 226 | private func getImageBytesFromClipboard() -> [Int]? { 227 | guard let image = UIPasteboard.general.image else { 228 | return nil 229 | } 230 | guard let imageData = image.pngData() else { 231 | return nil 232 | } 233 | return Array(imageData.map { Int($0) }) 234 | } 235 | 236 | deinit { 237 | stopMonitoring() 238 | } 239 | } 240 | 241 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate { 5 | id _clipboardChangeObserver; 6 | } 7 | 8 | - (BOOL)application:(UIApplication *)application 9 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 10 | [GeneratedPluginRegistrant registerWithRegistry:self]; 11 | 12 | // Register clipboard channels using registrar 13 | id registrar = [self registrarForPlugin:@"ClipboardPlugin"]; 14 | id messenger = [registrar messenger]; 15 | 16 | // Method channel 17 | _clipboardMethodChannel = [FlutterMethodChannel 18 | methodChannelWithName:@"net.cubiclab.clipboard/methods" 19 | binaryMessenger:messenger]; 20 | [_clipboardMethodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { 21 | [self handleClipboardMethodCall:call result:result]; 22 | }]; 23 | 24 | // Event channel 25 | _clipboardEventChannel = [FlutterEventChannel 26 | eventChannelWithName:@"net.cubiclab.clipboard/events" 27 | binaryMessenger:messenger]; 28 | [_clipboardEventChannel setStreamHandler:self]; 29 | 30 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 31 | } 32 | 33 | - (void)handleClipboardMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 34 | if ([@"copy" isEqualToString:call.method]) { 35 | NSString* text = call.arguments[@"text"]; 36 | if (text == nil || text.length == 0) { 37 | result([FlutterError errorWithCode:@"EMPTY_TEXT" message:@"Text cannot be empty" details:nil]); 38 | return; 39 | } 40 | [UIPasteboard generalPasteboard].string = text; 41 | result(@YES); 42 | } else if ([@"paste" isEqualToString:call.method]) { 43 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 44 | result(@{@"text": text}); 45 | } else if ([@"copyRichText" isEqualToString:call.method]) { 46 | NSString* text = call.arguments[@"text"] ?: @""; 47 | NSString* html = call.arguments[@"html"]; 48 | if (text.length == 0 && (html == nil || html.length == 0)) { 49 | result([FlutterError errorWithCode:@"EMPTY_CONTENT" message:@"Either text or html must be provided" details:nil]); 50 | return; 51 | } 52 | if (html != nil && html.length > 0) { 53 | [[UIPasteboard generalPasteboard] setValue:html forPasteboardType:@"public.html"]; 54 | if (text.length > 0) { 55 | [UIPasteboard generalPasteboard].string = text; 56 | } 57 | } else { 58 | [UIPasteboard generalPasteboard].string = text; 59 | } 60 | result(@YES); 61 | } else if ([@"pasteRichText" isEqualToString:call.method]) { 62 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 63 | NSString* html = [[UIPasteboard generalPasteboard] valueForPasteboardType:@"public.html"]; 64 | UIImage* image = [UIPasteboard generalPasteboard].image; 65 | NSArray* imageBytes = nil; 66 | if (image != nil) { 67 | NSData* imageData = UIImagePNGRepresentation(image); 68 | if (imageData != nil) { 69 | NSMutableArray* bytes = [NSMutableArray arrayWithCapacity:imageData.length]; 70 | const uint8_t* dataBytes = imageData.bytes; 71 | for (NSUInteger i = 0; i < imageData.length; i++) { 72 | [bytes addObject:@(dataBytes[i])]; 73 | } 74 | imageBytes = bytes; 75 | } 76 | } 77 | result(@{ 78 | @"text": text, 79 | @"html": html ?: [NSNull null], 80 | @"imageBytes": imageBytes ?: [NSNull null], 81 | @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000)) 82 | }); 83 | } else if ([@"copyImage" isEqualToString:call.method]) { 84 | NSArray* imageBytes = call.arguments[@"imageBytes"]; 85 | if (imageBytes == nil || imageBytes.count == 0) { 86 | result([FlutterError errorWithCode:@"EMPTY_IMAGE" message:@"Image bytes cannot be empty" details:nil]); 87 | return; 88 | } 89 | NSMutableData* data = [NSMutableData dataWithCapacity:imageBytes.count]; 90 | for (NSNumber* byte in imageBytes) { 91 | uint8_t b = [byte unsignedCharValue]; 92 | [data appendBytes:&b length:1]; 93 | } 94 | UIImage* image = [UIImage imageWithData:data]; 95 | if (image == nil) { 96 | result([FlutterError errorWithCode:@"INVALID_IMAGE" message:@"Failed to decode image" details:nil]); 97 | return; 98 | } 99 | [UIPasteboard generalPasteboard].image = image; 100 | result(@YES); 101 | } else if ([@"pasteImage" isEqualToString:call.method]) { 102 | UIImage* image = [UIPasteboard generalPasteboard].image; 103 | if (image != nil) { 104 | NSData* imageData = UIImagePNGRepresentation(image); 105 | if (imageData != nil) { 106 | NSMutableArray* bytes = [NSMutableArray arrayWithCapacity:imageData.length]; 107 | const uint8_t* dataBytes = imageData.bytes; 108 | for (NSUInteger i = 0; i < imageData.length; i++) { 109 | [bytes addObject:@(dataBytes[i])]; 110 | } 111 | result(@{@"imageBytes": bytes}); 112 | return; 113 | } 114 | } 115 | result(@{@"imageBytes": [NSNull null]}); 116 | } else if ([@"copyMultiple" isEqualToString:call.method]) { 117 | NSDictionary* formats = call.arguments[@"formats"]; 118 | if (formats == nil || formats.count == 0) { 119 | result([FlutterError errorWithCode:@"EMPTY_FORMATS" message:@"At least one format must be provided" details:nil]); 120 | return; 121 | } 122 | if (formats[@"image/png"] != nil) { 123 | NSArray* imageBytes = formats[@"image/png"]; 124 | if (imageBytes != nil && imageBytes.count > 0) { 125 | NSMutableData* data = [NSMutableData dataWithCapacity:imageBytes.count]; 126 | for (NSNumber* byte in imageBytes) { 127 | uint8_t b = [byte unsignedCharValue]; 128 | [data appendBytes:&b length:1]; 129 | } 130 | UIImage* image = [UIImage imageWithData:data]; 131 | if (image != nil) { 132 | [UIPasteboard generalPasteboard].image = image; 133 | if (formats[@"text/plain"] != nil) { 134 | [UIPasteboard generalPasteboard].string = [formats[@"text/plain"] description]; 135 | } 136 | result(@YES); 137 | return; 138 | } 139 | } 140 | } 141 | if (formats[@"text/plain"] != nil) { 142 | [UIPasteboard generalPasteboard].string = [formats[@"text/plain"] description]; 143 | } 144 | if (formats[@"text/html"] != nil) { 145 | [[UIPasteboard generalPasteboard] setValue:[formats[@"text/html"] description] 146 | forPasteboardType:@"public.html"]; 147 | } 148 | result(@YES); 149 | } else if ([@"getContentType" isEqualToString:call.method]) { 150 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 151 | NSString* html = [[UIPasteboard generalPasteboard] valueForPasteboardType:@"public.html"]; 152 | BOOL hasImage = [UIPasteboard generalPasteboard].image != nil; 153 | if (hasImage && (text.length > 0 || (html != nil && html.length > 0))) { 154 | result(@"mixed"); 155 | } else if (hasImage) { 156 | result(@"image"); 157 | } else if (text.length == 0 && (html == nil || html.length == 0)) { 158 | result(@"empty"); 159 | } else if (text.length > 0 && html != nil && html.length > 0) { 160 | result(@"mixed"); 161 | } else if (html != nil && html.length > 0) { 162 | result(@"html"); 163 | } else { 164 | result(@"text"); 165 | } 166 | } else if ([@"hasData" isEqualToString:call.method]) { 167 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 168 | result(@(text.length > 0)); 169 | } else if ([@"clear" isEqualToString:call.method]) { 170 | [UIPasteboard generalPasteboard].string = @""; 171 | result(@YES); 172 | } else if ([@"getDataSize" isEqualToString:call.method]) { 173 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 174 | result(@(text.length)); 175 | } else if ([@"startMonitoring" isEqualToString:call.method]) { 176 | [self startClipboardMonitoring]; 177 | result(@YES); 178 | } else if ([@"stopMonitoring" isEqualToString:call.method]) { 179 | [self stopClipboardMonitoring]; 180 | result(@YES); 181 | } else { 182 | result(FlutterMethodNotImplemented); 183 | } 184 | } 185 | 186 | - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)events { 187 | _eventSink = events; 188 | [self startClipboardMonitoring]; 189 | return nil; 190 | } 191 | 192 | - (FlutterError*)onCancelWithArguments:(id)arguments { 193 | _eventSink = nil; 194 | [self stopClipboardMonitoring]; 195 | return nil; 196 | } 197 | 198 | - (void)startClipboardMonitoring { 199 | if (_clipboardChangeObserver != nil) { 200 | return; 201 | } 202 | __weak typeof(self) weakSelf = self; 203 | _clipboardChangeObserver = [[NSNotificationCenter defaultCenter] 204 | addObserverForName:UIApplicationDidBecomeActiveNotification 205 | object:nil 206 | queue:[NSOperationQueue mainQueue] 207 | usingBlock:^(NSNotification* note) { 208 | [weakSelf checkClipboardChange]; 209 | }]; 210 | } 211 | 212 | - (void)stopClipboardMonitoring { 213 | if (_clipboardChangeObserver != nil) { 214 | [[NSNotificationCenter defaultCenter] removeObserver:_clipboardChangeObserver]; 215 | _clipboardChangeObserver = nil; 216 | } 217 | } 218 | 219 | - (void)checkClipboardChange { 220 | if (_eventSink == nil) { 221 | return; 222 | } 223 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 224 | NSString* html = [[UIPasteboard generalPasteboard] valueForPasteboardType:@"public.html"]; 225 | _eventSink(@{ 226 | @"text": text, 227 | @"html": html ?: [NSNull null], 228 | @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000)) 229 | }); 230 | } 231 | 232 | @end 233 | -------------------------------------------------------------------------------- /example/ios/Runner/ClipboardChannelHandler.m: -------------------------------------------------------------------------------- 1 | #import "ClipboardChannelHandler.h" 2 | 3 | @implementation ClipboardChannelHandler { 4 | FlutterEventSink _eventSink; 5 | NSObject *_clipboardChangeObserver; 6 | } 7 | 8 | + (void)registerWithRegistrar:(NSObject*)registrar { 9 | ClipboardChannelHandler* instance = [[ClipboardChannelHandler alloc] init]; 10 | 11 | FlutterMethodChannel* methodChannel = [FlutterMethodChannel 12 | methodChannelWithName:@"net.cubiclab.clipboard/methods" 13 | binaryMessenger:[registrar messenger]]; 14 | [registrar addMethodCallDelegate:instance channel:methodChannel]; 15 | 16 | FlutterEventChannel* eventChannel = [FlutterEventChannel 17 | eventChannelWithName:@"net.cubiclab.clipboard/events" 18 | binaryMessenger:[registrar messenger]]; 19 | [eventChannel setStreamHandler:instance]; 20 | } 21 | 22 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 23 | if ([@"copy" isEqualToString:call.method]) { 24 | NSString* text = call.arguments[@"text"]; 25 | if (text == nil || text.length == 0) { 26 | result([FlutterError errorWithCode:@"EMPTY_TEXT" 27 | message:@"Text cannot be empty" 28 | details:nil]); 29 | return; 30 | } 31 | [UIPasteboard generalPasteboard].string = text; 32 | result(@YES); 33 | } else if ([@"paste" isEqualToString:call.method]) { 34 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 35 | result(@{@"text": text}); 36 | } else if ([@"copyRichText" isEqualToString:call.method]) { 37 | NSString* text = call.arguments[@"text"] ?: @""; 38 | NSString* html = call.arguments[@"html"]; 39 | if (text.length == 0 && (html == nil || html.length == 0)) { 40 | result([FlutterError errorWithCode:@"EMPTY_CONTENT" 41 | message:@"Either text or html must be provided" 42 | details:nil]); 43 | return; 44 | } 45 | if (html != nil && html.length > 0) { 46 | [[UIPasteboard generalPasteboard] setValue:html forPasteboardType:@"public.html"]; 47 | if (text.length > 0) { 48 | [UIPasteboard generalPasteboard].string = text; 49 | } 50 | } else { 51 | [UIPasteboard generalPasteboard].string = text; 52 | } 53 | result(@YES); 54 | } else if ([@"pasteRichText" isEqualToString:call.method]) { 55 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 56 | NSString* html = [[UIPasteboard generalPasteboard] valueForPasteboardType:@"public.html"]; 57 | UIImage* image = [UIPasteboard generalPasteboard].image; 58 | NSArray* imageBytes = nil; 59 | if (image != nil) { 60 | NSData* imageData = UIImagePNGRepresentation(image); 61 | if (imageData != nil) { 62 | NSMutableArray* bytes = [NSMutableArray arrayWithCapacity:imageData.length]; 63 | const uint8_t* dataBytes = imageData.bytes; 64 | for (NSUInteger i = 0; i < imageData.length; i++) { 65 | [bytes addObject:@(dataBytes[i])]; 66 | } 67 | imageBytes = bytes; 68 | } 69 | } 70 | result(@{ 71 | @"text": text, 72 | @"html": html ?: [NSNull null], 73 | @"imageBytes": imageBytes ?: [NSNull null], 74 | @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000)) 75 | }); 76 | } else if ([@"copyImage" isEqualToString:call.method]) { 77 | NSArray* imageBytes = call.arguments[@"imageBytes"]; 78 | if (imageBytes == nil || imageBytes.count == 0) { 79 | result([FlutterError errorWithCode:@"EMPTY_IMAGE" 80 | message:@"Image bytes cannot be empty" 81 | details:nil]); 82 | return; 83 | } 84 | NSMutableData* data = [NSMutableData dataWithCapacity:imageBytes.count]; 85 | for (NSNumber* byte in imageBytes) { 86 | uint8_t b = [byte unsignedCharValue]; 87 | [data appendBytes:&b length:1]; 88 | } 89 | UIImage* image = [UIImage imageWithData:data]; 90 | if (image == nil) { 91 | result([FlutterError errorWithCode:@"INVALID_IMAGE" 92 | message:@"Failed to decode image" 93 | details:nil]); 94 | return; 95 | } 96 | [UIPasteboard generalPasteboard].image = image; 97 | result(@YES); 98 | } else if ([@"pasteImage" isEqualToString:call.method]) { 99 | UIImage* image = [UIPasteboard generalPasteboard].image; 100 | if (image != nil) { 101 | NSData* imageData = UIImagePNGRepresentation(image); 102 | if (imageData != nil) { 103 | NSMutableArray* bytes = [NSMutableArray arrayWithCapacity:imageData.length]; 104 | const uint8_t* dataBytes = imageData.bytes; 105 | for (NSUInteger i = 0; i < imageData.length; i++) { 106 | [bytes addObject:@(dataBytes[i])]; 107 | } 108 | result(@{@"imageBytes": bytes}); 109 | return; 110 | } 111 | } 112 | result(@{@"imageBytes": [NSNull null]}); 113 | } else if ([@"copyMultiple" isEqualToString:call.method]) { 114 | NSDictionary* formats = call.arguments[@"formats"]; 115 | if (formats == nil || formats.count == 0) { 116 | result([FlutterError errorWithCode:@"EMPTY_FORMATS" 117 | message:@"At least one format must be provided" 118 | details:nil]); 119 | return; 120 | } 121 | if (formats[@"image/png"] != nil) { 122 | NSArray* imageBytes = formats[@"image/png"]; 123 | if (imageBytes != nil && imageBytes.count > 0) { 124 | NSMutableData* data = [NSMutableData dataWithCapacity:imageBytes.count]; 125 | for (NSNumber* byte in imageBytes) { 126 | uint8_t b = [byte unsignedCharValue]; 127 | [data appendBytes:&b length:1]; 128 | } 129 | UIImage* image = [UIImage imageWithData:data]; 130 | if (image != nil) { 131 | [UIPasteboard generalPasteboard].image = image; 132 | if (formats[@"text/plain"] != nil) { 133 | [UIPasteboard generalPasteboard].string = [formats[@"text/plain"] description]; 134 | } 135 | result(@YES); 136 | return; 137 | } 138 | } 139 | } 140 | if (formats[@"text/plain"] != nil) { 141 | [UIPasteboard generalPasteboard].string = [formats[@"text/plain"] description]; 142 | } 143 | if (formats[@"text/html"] != nil) { 144 | [[UIPasteboard generalPasteboard] setValue:[formats[@"text/html"] description] 145 | forPasteboardType:@"public.html"]; 146 | } 147 | result(@YES); 148 | } else if ([@"getContentType" isEqualToString:call.method]) { 149 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 150 | NSString* html = [[UIPasteboard generalPasteboard] valueForPasteboardType:@"public.html"]; 151 | BOOL hasImage = [UIPasteboard generalPasteboard].image != nil; 152 | if (hasImage && (text.length > 0 || (html != nil && html.length > 0))) { 153 | result(@"mixed"); 154 | } else if (hasImage) { 155 | result(@"image"); 156 | } else if (text.length == 0 && (html == nil || html.length == 0)) { 157 | result(@"empty"); 158 | } else if (text.length > 0 && html != nil && html.length > 0) { 159 | result(@"mixed"); 160 | } else if (html != nil && html.length > 0) { 161 | result(@"html"); 162 | } else { 163 | result(@"text"); 164 | } 165 | } else if ([@"hasData" isEqualToString:call.method]) { 166 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 167 | result(@(text.length > 0)); 168 | } else if ([@"clear" isEqualToString:call.method]) { 169 | [UIPasteboard generalPasteboard].string = @""; 170 | result(@YES); 171 | } else if ([@"getDataSize" isEqualToString:call.method]) { 172 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 173 | result(@(text.length)); 174 | } else if ([@"startMonitoring" isEqualToString:call.method]) { 175 | [self startMonitoring]; 176 | result(@YES); 177 | } else if ([@"stopMonitoring" isEqualToString:call.method]) { 178 | [self stopMonitoring]; 179 | result(@YES); 180 | } else { 181 | result(FlutterMethodNotImplemented); 182 | } 183 | } 184 | 185 | - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)events { 186 | _eventSink = events; 187 | [self startMonitoring]; 188 | return nil; 189 | } 190 | 191 | - (FlutterError*)onCancelWithArguments:(id)arguments { 192 | _eventSink = nil; 193 | [self stopMonitoring]; 194 | return nil; 195 | } 196 | 197 | - (void)startMonitoring { 198 | if (_clipboardChangeObserver != nil) { 199 | return; 200 | } 201 | __weak typeof(self) weakSelf = self; 202 | _clipboardChangeObserver = [[NSNotificationCenter defaultCenter] 203 | addObserverForName:UIApplicationDidBecomeActiveNotification 204 | object:nil 205 | queue:[NSOperationQueue mainQueue] 206 | usingBlock:^(NSNotification* note) { 207 | [weakSelf checkClipboardChange]; 208 | }]; 209 | } 210 | 211 | - (void)stopMonitoring { 212 | if (_clipboardChangeObserver != nil) { 213 | [[NSNotificationCenter defaultCenter] removeObserver:_clipboardChangeObserver]; 214 | _clipboardChangeObserver = nil; 215 | } 216 | } 217 | 218 | - (void)checkClipboardChange { 219 | if (_eventSink == nil) { 220 | return; 221 | } 222 | NSString* text = [UIPasteboard generalPasteboard].string ?: @""; 223 | NSString* html = [[UIPasteboard generalPasteboard] valueForPasteboardType:@"public.html"]; 224 | _eventSink(@{ 225 | @"text": text, 226 | @"html": html ?: [NSNull null], 227 | @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000)) 228 | }); 229 | } 230 | 231 | @end 232 | 233 | -------------------------------------------------------------------------------- /example/macos/Runner/ClipboardPlugin.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | public class ClipboardPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { 5 | private var eventSink: FlutterEventSink? 6 | private var clipboardChangeObserver: NSObjectProtocol? 7 | private var monitoringTimer: Timer? 8 | private var lastChangeCount: Int = 0 9 | 10 | public static func register(with registrar: FlutterPluginRegistrar) { 11 | let methodChannel = FlutterMethodChannel( 12 | name: "net.cubiclab.clipboard/methods", 13 | binaryMessenger: registrar.messenger 14 | ) 15 | let eventChannel = FlutterEventChannel( 16 | name: "net.cubiclab.clipboard/events", 17 | binaryMessenger: registrar.messenger 18 | ) 19 | 20 | let instance = ClipboardPlugin() 21 | registrar.addMethodCallDelegate(instance, channel: methodChannel) 22 | eventChannel.setStreamHandler(instance) 23 | } 24 | 25 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 26 | let pasteboard = NSPasteboard.general 27 | 28 | switch call.method { 29 | case "copy": 30 | guard let args = call.arguments as? [String: Any], 31 | let text = args["text"] as? String else { 32 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Text is required", details: nil)) 33 | return 34 | } 35 | if text.isEmpty { 36 | result(FlutterError(code: "EMPTY_TEXT", message: "Text cannot be empty", details: nil)) 37 | return 38 | } 39 | pasteboard.clearContents() 40 | pasteboard.setString(text, forType: .string) 41 | result(true) 42 | 43 | case "copyRichText": 44 | guard let args = call.arguments as? [String: Any] else { 45 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil)) 46 | return 47 | } 48 | let text = args["text"] as? String ?? "" 49 | let html = args["html"] as? String 50 | 51 | if text.isEmpty && (html == nil || html!.isEmpty) { 52 | result(FlutterError(code: "EMPTY_CONTENT", message: "Either text or html must be provided", details: nil)) 53 | return 54 | } 55 | 56 | pasteboard.clearContents() 57 | if let html = html, !html.isEmpty { 58 | pasteboard.setString(html, forType: .html) 59 | if !text.isEmpty { 60 | pasteboard.setString(text, forType: .string) 61 | } 62 | } else { 63 | pasteboard.setString(text, forType: .string) 64 | } 65 | result(true) 66 | 67 | case "copyMultiple": 68 | guard let args = call.arguments as? [String: Any], 69 | let formats = args["formats"] as? [String: Any] else { 70 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Invalid arguments", details: nil)) 71 | return 72 | } 73 | if formats.isEmpty { 74 | result(FlutterError(code: "EMPTY_FORMATS", message: "At least one format must be provided", details: nil)) 75 | return 76 | } 77 | 78 | pasteboard.clearContents() 79 | 80 | // Handle image first (highest priority) 81 | if let imageBytes = formats["image/png"] as? [Int], !imageBytes.isEmpty { 82 | let data = Data(imageBytes.map { UInt8($0 & 0xFF) }) 83 | if let image = NSImage(data: data) { 84 | pasteboard.writeObjects([image]) 85 | if let text = formats["text/plain"] as? String, !text.isEmpty { 86 | pasteboard.setString(text, forType: .string) 87 | } 88 | result(true) 89 | return 90 | } 91 | } 92 | 93 | // Fallback to HTML or text 94 | if let text = formats["text/plain"] as? String { 95 | pasteboard.setString(text, forType: .string) 96 | } 97 | if let html = formats["text/html"] as? String { 98 | pasteboard.setString(html, forType: .html) 99 | } 100 | result(true) 101 | 102 | case "copyImage": 103 | guard let args = call.arguments as? [String: Any], 104 | let imageBytes = args["imageBytes"] as? [Int] else { 105 | result(FlutterError(code: "INVALID_ARGUMENT", message: "Image bytes are required", details: nil)) 106 | return 107 | } 108 | if imageBytes.isEmpty { 109 | result(FlutterError(code: "EMPTY_IMAGE", message: "Image bytes cannot be empty", details: nil)) 110 | return 111 | } 112 | let data = Data(imageBytes.map { UInt8($0 & 0xFF) }) 113 | guard let image = NSImage(data: data) else { 114 | result(FlutterError(code: "INVALID_IMAGE", message: "Failed to decode image", details: nil)) 115 | return 116 | } 117 | pasteboard.clearContents() 118 | pasteboard.writeObjects([image]) 119 | result(true) 120 | 121 | case "paste": 122 | let text = pasteboard.string(forType: .string) ?? "" 123 | result(["text": text]) 124 | 125 | case "pasteRichText": 126 | let text = pasteboard.string(forType: .string) ?? "" 127 | let html = pasteboard.string(forType: .html) 128 | let imageBytes = getImageBytesFromClipboard() 129 | result([ 130 | "text": text, 131 | "html": (html ?? NSNull()) as Any, 132 | "imageBytes": (imageBytes ?? NSNull()) as Any, 133 | "timestamp": Int64(Date().timeIntervalSince1970 * 1000) 134 | ]) 135 | 136 | case "pasteImage": 137 | let imageBytes = getImageBytesFromClipboard() 138 | if let bytes = imageBytes { 139 | result(["imageBytes": bytes]) 140 | } else { 141 | result(["imageBytes": NSNull()]) 142 | } 143 | 144 | case "getContentType": 145 | let text = pasteboard.string(forType: .string) ?? "" 146 | let html = pasteboard.string(forType: .html) 147 | let hasImage = pasteboard.canReadObject(forClasses: [NSImage.self], options: nil) 148 | 149 | if hasImage && (!text.isEmpty || (html != nil && !html!.isEmpty)) { 150 | result("mixed") 151 | } else if hasImage { 152 | result("image") 153 | } else if text.isEmpty && (html == nil || html!.isEmpty) { 154 | result("empty") 155 | } else if !text.isEmpty && html != nil && !html!.isEmpty { 156 | result("mixed") 157 | } else if html != nil && !html!.isEmpty { 158 | result("html") 159 | } else { 160 | result("text") 161 | } 162 | 163 | case "hasData": 164 | let text = pasteboard.string(forType: .string) ?? "" 165 | result(!text.isEmpty) 166 | 167 | case "clear": 168 | pasteboard.clearContents() 169 | result(true) 170 | 171 | case "getDataSize": 172 | let text = pasteboard.string(forType: .string) ?? "" 173 | result(text.count) 174 | 175 | case "startMonitoring": 176 | startMonitoring() 177 | result(true) 178 | 179 | case "stopMonitoring": 180 | stopMonitoring() 181 | result(true) 182 | 183 | default: 184 | result(FlutterMethodNotImplemented) 185 | } 186 | } 187 | 188 | private func startMonitoring() { 189 | if monitoringTimer != nil { 190 | return 191 | } 192 | 193 | let pasteboard = NSPasteboard.general 194 | lastChangeCount = pasteboard.changeCount 195 | 196 | // macOS doesn't have native clipboard change notifications 197 | // Use a timer-based polling approach 198 | monitoringTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in 199 | self?.checkClipboardChange() 200 | } 201 | } 202 | 203 | private func stopMonitoring() { 204 | monitoringTimer?.invalidate() 205 | monitoringTimer = nil 206 | if let observer = clipboardChangeObserver { 207 | NotificationCenter.default.removeObserver(observer) 208 | clipboardChangeObserver = nil 209 | } 210 | } 211 | 212 | private func checkClipboardChange() { 213 | let pasteboard = NSPasteboard.general 214 | let currentChangeCount = pasteboard.changeCount 215 | 216 | // Only notify if clipboard actually changed 217 | if currentChangeCount != lastChangeCount { 218 | lastChangeCount = currentChangeCount 219 | 220 | let text = pasteboard.string(forType: .string) ?? "" 221 | let html = pasteboard.string(forType: .html) 222 | 223 | eventSink?([ 224 | "text": text, 225 | "html": (html ?? NSNull()) as Any, 226 | "timestamp": Int64(Date().timeIntervalSince1970 * 1000) 227 | ]) 228 | } 229 | } 230 | 231 | public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 232 | self.eventSink = events 233 | startMonitoring() 234 | return nil 235 | } 236 | 237 | public func onCancel(withArguments arguments: Any?) -> FlutterError? { 238 | eventSink = nil 239 | stopMonitoring() 240 | return nil 241 | } 242 | 243 | private func getImageBytesFromClipboard() -> [Int]? { 244 | let pasteboard = NSPasteboard.general 245 | 246 | // Check if clipboard has image data 247 | guard pasteboard.canReadObject(forClasses: [NSImage.self], options: nil) else { 248 | return nil 249 | } 250 | 251 | guard let image = pasteboard.readObjects(forClasses: [NSImage.self], options: nil)?.first as? NSImage else { 252 | return nil 253 | } 254 | 255 | // Convert NSImage to PNG data 256 | guard let tiffData = image.tiffRepresentation, 257 | let bitmapImage = NSBitmapImageRep(data: tiffData), 258 | let pngData = bitmapImage.representation(using: .png, properties: [:]) else { 259 | return nil 260 | } 261 | 262 | return Array(pngData.map { Int($0) }) 263 | } 264 | 265 | deinit { 266 | stopMonitoring() 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enhanced Flutter Clipboard 2 | 3 | [![pub package](https://img.shields.io/badge/3.0.0-brightgreen)](https://github.com/samuelezedi/flutter_clipboard) 4 | 5 | A super-power clipboard package for Flutter, with text, HTML, and image support. 6 | 7 | [GitHub](https://github.com/samuelezedi/flutter_clipboard) 8 | 9 | ## Screenshots 10 | 11 |
12 | Screenshot 1 13 | Screenshot 2 14 | Screenshot 3 15 | Screenshot 4 16 |
17 | 18 | ## Features 19 | 20 | - ✅ **Basic Copy/Paste**: Simple text copying and pasting 21 | - ✅ **True Rich Text Support**: Native HTML clipboard support via platform channels 22 | - ✅ **Image Support**: Copy and paste images to/from clipboard (PNG format) 23 | - ✅ **Multiple Formats**: Copy multiple data formats simultaneously (text, HTML, images) 24 | - ✅ **Native Clipboard Monitoring**: Real-time clipboard change detection using platform APIs 25 | - ✅ **Error Handling**: Comprehensive error handling with custom exceptions and error codes 26 | - ✅ **Utility Methods**: Check clipboard status, size, and content type 27 | - ✅ **Callback Support**: Success and error callbacks for operations 28 | - ✅ **Debug Information**: Get detailed clipboard debugging info 29 | - ✅ **Cross-Platform**: Works on Android, iOS, and Web with graceful fallbacks 30 | - ✅ **Null Safety**: Full null safety support 31 | - ✅ **Memory Safe**: Proper listener management with cleanup mechanisms 32 | - ✅ **Production Ready**: Battle-tested with comprehensive error handling 33 | 34 | ## Installation 35 | 36 | Add this to your package's `pubspec.yaml` file: 37 | 38 | ```yaml 39 | dependencies: 40 | clipboard: ^3.0.8 41 | ``` 42 | 43 | ## Platform Setup 44 | 45 | ### Android 46 | The package uses platform channels for enhanced features. No additional setup required - the package automatically falls back to Flutter's built-in Clipboard API if platform channels are unavailable. 47 | 48 | ### iOS 49 | No additional setup required. The package automatically handles platform channel registration. 50 | 51 | ### Web 52 | Fully supported with automatic fallback to browser Clipboard API. 53 | 54 | ## Basic Usage 55 | 56 | ```dart 57 | import 'package:clipboard/clipboard.dart'; 58 | ``` 59 | 60 | ### Copy to clipboard 61 | 62 | ```dart 63 | // Basic copy 64 | await FlutterClipboard.copy('Hello Flutter friends'); 65 | 66 | // Copy with error handling 67 | try { 68 | await FlutterClipboard.copy('Hello World'); 69 | print('Text copied successfully!'); 70 | } on ClipboardException catch (e) { 71 | print('Copy failed: ${e.message}'); 72 | } 73 | ``` 74 | 75 | ### Paste from clipboard 76 | 77 | ```dart 78 | // Basic paste 79 | String text = await FlutterClipboard.paste(); 80 | 81 | // Paste with error handling 82 | try { 83 | String text = await FlutterClipboard.paste(); 84 | setState(() { 85 | myTextField.text = text; 86 | }); 87 | } on ClipboardException catch (e) { 88 | print('Paste failed: ${e.message}'); 89 | } 90 | ``` 91 | 92 | ## Advanced Features 93 | 94 | ### Rich Text Support 95 | 96 | ```dart 97 | // Copy rich text with HTML 98 | await FlutterClipboard.copyRichText( 99 | text: 'Hello World', 100 | html: 'Hello World', 101 | ); 102 | 103 | // Paste rich text 104 | EnhancedClipboardData data = await FlutterClipboard.pasteRichText(); 105 | print('Text: ${data.text}'); 106 | print('HTML: ${data.html}'); 107 | ``` 108 | 109 | ### Multiple Format Copy 110 | 111 | ```dart 112 | // Copy multiple formats simultaneously 113 | await FlutterClipboard.copyMultiple({ 114 | 'text/plain': 'Hello World', 115 | 'text/html': 'Hello World', 116 | 'image/png': imageBytes, // Uint8List of PNG image 117 | 'custom/format': 'Custom data', 118 | }); 119 | ``` 120 | 121 | ### Image Copy/Paste 122 | 123 | ```dart 124 | import 'dart:typed_data'; 125 | import 'package:flutter/services.dart'; 126 | 127 | // Copy image to clipboard 128 | final ByteData imageData = await rootBundle.load('assets/image.png'); 129 | final Uint8List imageBytes = imageData.buffer.asUint8List(); 130 | await FlutterClipboard.copyImage(imageBytes); 131 | 132 | // Paste image from clipboard 133 | final Uint8List? pastedImage = await FlutterClipboard.pasteImage(); 134 | if (pastedImage != null) { 135 | // Use the image bytes (e.g., display in Image.memory) 136 | Image.memory(pastedImage); 137 | } 138 | 139 | // Images are also included in pasteRichText() 140 | final data = await FlutterClipboard.pasteRichText(); 141 | if (data.hasImage) { 142 | print('Clipboard contains image: ${data.imageBytes?.length} bytes'); 143 | } 144 | ``` 145 | 146 | ### Callback Support 147 | 148 | ```dart 149 | // Copy with success/error callbacks 150 | await FlutterClipboard.copyWithCallback( 151 | text: 'Hello World', 152 | onSuccess: () { 153 | print('Copy successful!'); 154 | showSnackBar('Text copied to clipboard'); 155 | }, 156 | onError: (error) { 157 | print('Copy failed: $error'); 158 | showSnackBar('Copy failed: $error'); 159 | }, 160 | ); 161 | ``` 162 | 163 | ### Clipboard Monitoring 164 | 165 | ```dart 166 | // Add clipboard change listener (returns cleanup function) 167 | void onClipboardChanged(EnhancedClipboardData data) { 168 | print('Clipboard changed: ${data.text}'); 169 | } 170 | 171 | // Add listener and get cleanup function 172 | final removeListener = FlutterClipboard.addListener(onClipboardChanged); 173 | 174 | // Start native monitoring (uses platform APIs when available) 175 | await FlutterClipboard.startMonitoring(interval: Duration(milliseconds: 500)); 176 | 177 | // Stop monitoring 178 | await FlutterClipboard.stopMonitoring(); 179 | 180 | // Remove listener (or use the returned cleanup function) 181 | FlutterClipboard.removeListener(onClipboardChanged); 182 | // Or: removeListener(); 183 | 184 | // Remove all listeners at once 185 | FlutterClipboard.removeAllListeners(); 186 | ``` 187 | 188 | ### Utility Methods 189 | 190 | ```dart 191 | // Check if clipboard has content 192 | bool hasData = await FlutterClipboard.hasData(); 193 | 194 | // Check if clipboard is empty 195 | bool isEmpty = await FlutterClipboard.isEmpty(); 196 | 197 | // Get clipboard content type 198 | ClipboardContentType type = await FlutterClipboard.getContentType(); 199 | 200 | // Get clipboard data size 201 | int size = await FlutterClipboard.getDataSize(); 202 | 203 | // Validate input before copying 204 | bool isValid = FlutterClipboard.isValidInput('Hello World'); 205 | 206 | // Clear clipboard 207 | await FlutterClipboard.clear(); 208 | ``` 209 | 210 | ### Debug Information 211 | 212 | ```dart 213 | // Get comprehensive debug information 214 | Map debugInfo = await FlutterClipboard.getDebugInfo(); 215 | print(debugInfo); 216 | // Output: { 217 | // 'hasData': true, 218 | // 'contentType': 'ClipboardContentType.text', 219 | // 'dataSize': 11, 220 | // 'listenersCount': 2, 221 | // 'isMonitoring': true, 222 | // 'lastData': 'Hello World' 223 | // } 224 | ``` 225 | 226 | ## EnhancedClipboardData Class 227 | 228 | The `EnhancedClipboardData` class provides rich information about clipboard content: 229 | 230 | ```dart 231 | EnhancedClipboardData data = await FlutterClipboard.pasteRichText(); 232 | 233 | // Check content types 234 | if (data.hasText) print('Has text: ${data.text}'); 235 | if (data.hasHtml) print('Has HTML: ${data.html}'); 236 | if (data.hasImage) print('Has image: ${data.imageBytes?.length} bytes'); 237 | if (data.hasFiles) print('Has file paths: ${data.filePaths}'); 238 | 239 | // Check if completely empty 240 | if (data.isEmpty) print('Clipboard is empty'); 241 | 242 | // Get timestamp 243 | print('Copied at: ${data.timestamp}'); 244 | ``` 245 | 246 | ## Error Handling 247 | 248 | The package provides custom exceptions for better error handling: 249 | 250 | ```dart 251 | try { 252 | await FlutterClipboard.copy(''); 253 | } on ClipboardException catch (e) { 254 | print('Error: ${e.message}'); 255 | print('Error code: ${e.code}'); 256 | } 257 | ``` 258 | 259 | Common error codes: 260 | - `EMPTY_TEXT`: Attempted to copy empty text 261 | - `COPY_ERROR`: General copy operation failed 262 | - `PASTE_ERROR`: General paste operation failed 263 | - `EMPTY_CONTENT`: No content provided for rich text copy 264 | - `EMPTY_FORMATS`: No formats provided for multiple format copy 265 | 266 | ## Content Types 267 | 268 | The `ClipboardContentType` enum provides information about clipboard content: 269 | 270 | ```dart 271 | ClipboardContentType type = await FlutterClipboard.getContentType(); 272 | 273 | switch (type) { 274 | case ClipboardContentType.text: 275 | print('Plain text content'); 276 | break; 277 | case ClipboardContentType.html: 278 | print('HTML content'); 279 | break; 280 | case ClipboardContentType.mixed: 281 | print('Mixed content (text + HTML)'); 282 | break; 283 | case ClipboardContentType.empty: 284 | print('Empty clipboard'); 285 | break; 286 | case ClipboardContentType.unknown: 287 | print('Unknown content type'); 288 | break; 289 | } 290 | ``` 291 | 292 | ## Legacy Methods 293 | 294 | For backward compatibility, the original methods are still available: 295 | 296 | ```dart 297 | // Legacy methods (still supported) 298 | bool success = await FlutterClipboard.controlC('Hello World'); 299 | dynamic data = await FlutterClipboard.controlV(); 300 | ``` 301 | 302 | ## Testing 303 | 304 | The package includes comprehensive testing utilities: 305 | 306 | ```dart 307 | // Set mock data for testing 308 | await FlutterClipboard.setMockData('Test data'); 309 | 310 | // Get debug information for testing 311 | Map info = await FlutterClipboard.getDebugInfo(); 312 | ``` 313 | 314 | ## Why This Enhanced Package? 315 | 316 | I originally built this package 4 years ago for basic clipboard functionality. Over time, I realized developers needed more advanced features: 317 | 318 | - **True Rich Text Support**: Native HTML clipboard support via platform channels (not just in-memory storage) 319 | - **Native Monitoring**: Real-time clipboard change detection using platform APIs (Android clipboard listeners, iOS notifications) 320 | - **Better Error Handling**: Proper exceptions with error codes for better debugging 321 | - **Utility Methods**: Check clipboard status, content type, and data size 322 | - **Debug Support**: Comprehensive debugging information 323 | - **Memory Safe**: Proper listener management prevents memory leaks 324 | - **Production Ready**: Graceful fallbacks ensure reliability across all platforms 325 | - **Modern API**: Updated to latest Dart/Flutter standards with platform channel support 326 | 327 | This enhanced version maintains backward compatibility while adding powerful new features that modern Flutter apps need. The package now uses platform channels for true multi-format support and native clipboard monitoring, with automatic fallbacks to Flutter's built-in Clipboard API when needed. 328 | 329 | ## Contributing 330 | 331 | Contributions are welcome! Please feel free to submit a Pull Request. 332 | 333 | ## License 334 | 335 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 336 | 337 | ## Follow Me 338 | 339 | - [GitHub](https://github.com/samuelezedi) 340 | - [Medium](https://medium.com/@samuelezedi) 341 | - [Instagram](https://instagram.com/_zedempire) -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.13.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.2" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.4.0" 28 | clipboard: 29 | dependency: "direct main" 30 | description: 31 | path: ".." 32 | relative: true 33 | source: path 34 | version: "3.0.5" 35 | clock: 36 | dependency: transitive 37 | description: 38 | name: clock 39 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 40 | url: "https://pub.dev" 41 | source: hosted 42 | version: "1.1.2" 43 | collection: 44 | dependency: transitive 45 | description: 46 | name: collection 47 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 48 | url: "https://pub.dev" 49 | source: hosted 50 | version: "1.19.1" 51 | cross_file: 52 | dependency: transitive 53 | description: 54 | name: cross_file 55 | sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" 56 | url: "https://pub.dev" 57 | source: hosted 58 | version: "0.3.5+1" 59 | cupertino_icons: 60 | dependency: "direct main" 61 | description: 62 | name: cupertino_icons 63 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 64 | url: "https://pub.dev" 65 | source: hosted 66 | version: "1.0.8" 67 | fake_async: 68 | dependency: transitive 69 | description: 70 | name: fake_async 71 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 72 | url: "https://pub.dev" 73 | source: hosted 74 | version: "1.3.3" 75 | file_selector_linux: 76 | dependency: transitive 77 | description: 78 | name: file_selector_linux 79 | sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" 80 | url: "https://pub.dev" 81 | source: hosted 82 | version: "0.9.4" 83 | file_selector_macos: 84 | dependency: transitive 85 | description: 86 | name: file_selector_macos 87 | sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" 88 | url: "https://pub.dev" 89 | source: hosted 90 | version: "0.9.4+4" 91 | file_selector_platform_interface: 92 | dependency: transitive 93 | description: 94 | name: file_selector_platform_interface 95 | sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" 96 | url: "https://pub.dev" 97 | source: hosted 98 | version: "2.7.0" 99 | file_selector_windows: 100 | dependency: transitive 101 | description: 102 | name: file_selector_windows 103 | sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" 104 | url: "https://pub.dev" 105 | source: hosted 106 | version: "0.9.3+5" 107 | flutter: 108 | dependency: "direct main" 109 | description: flutter 110 | source: sdk 111 | version: "0.0.0" 112 | flutter_plugin_android_lifecycle: 113 | dependency: transitive 114 | description: 115 | name: flutter_plugin_android_lifecycle 116 | sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 117 | url: "https://pub.dev" 118 | source: hosted 119 | version: "2.0.31" 120 | flutter_test: 121 | dependency: "direct dev" 122 | description: flutter 123 | source: sdk 124 | version: "0.0.0" 125 | flutter_web_plugins: 126 | dependency: transitive 127 | description: flutter 128 | source: sdk 129 | version: "0.0.0" 130 | http: 131 | dependency: transitive 132 | description: 133 | name: http 134 | sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" 135 | url: "https://pub.dev" 136 | source: hosted 137 | version: "1.6.0" 138 | http_parser: 139 | dependency: transitive 140 | description: 141 | name: http_parser 142 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "4.1.2" 146 | image_picker: 147 | dependency: "direct main" 148 | description: 149 | name: image_picker 150 | sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "1.2.1" 154 | image_picker_android: 155 | dependency: transitive 156 | description: 157 | name: image_picker_android 158 | sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "0.8.13+1" 162 | image_picker_for_web: 163 | dependency: transitive 164 | description: 165 | name: image_picker_for_web 166 | sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "3.1.1" 170 | image_picker_ios: 171 | dependency: transitive 172 | description: 173 | name: image_picker_ios 174 | sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "0.8.13" 178 | image_picker_linux: 179 | dependency: transitive 180 | description: 181 | name: image_picker_linux 182 | sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "0.2.2" 186 | image_picker_macos: 187 | dependency: transitive 188 | description: 189 | name: image_picker_macos 190 | sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "0.2.2" 194 | image_picker_platform_interface: 195 | dependency: transitive 196 | description: 197 | name: image_picker_platform_interface 198 | sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "2.11.1" 202 | image_picker_windows: 203 | dependency: transitive 204 | description: 205 | name: image_picker_windows 206 | sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "0.2.2" 210 | leak_tracker: 211 | dependency: transitive 212 | description: 213 | name: leak_tracker 214 | sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" 215 | url: "https://pub.dev" 216 | source: hosted 217 | version: "10.0.9" 218 | leak_tracker_flutter_testing: 219 | dependency: transitive 220 | description: 221 | name: leak_tracker_flutter_testing 222 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 223 | url: "https://pub.dev" 224 | source: hosted 225 | version: "3.0.9" 226 | leak_tracker_testing: 227 | dependency: transitive 228 | description: 229 | name: leak_tracker_testing 230 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 231 | url: "https://pub.dev" 232 | source: hosted 233 | version: "3.0.1" 234 | matcher: 235 | dependency: transitive 236 | description: 237 | name: matcher 238 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 239 | url: "https://pub.dev" 240 | source: hosted 241 | version: "0.12.17" 242 | material_color_utilities: 243 | dependency: transitive 244 | description: 245 | name: material_color_utilities 246 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 247 | url: "https://pub.dev" 248 | source: hosted 249 | version: "0.11.1" 250 | meta: 251 | dependency: transitive 252 | description: 253 | name: meta 254 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 255 | url: "https://pub.dev" 256 | source: hosted 257 | version: "1.16.0" 258 | mime: 259 | dependency: transitive 260 | description: 261 | name: mime 262 | sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" 263 | url: "https://pub.dev" 264 | source: hosted 265 | version: "2.0.0" 266 | path: 267 | dependency: transitive 268 | description: 269 | name: path 270 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 271 | url: "https://pub.dev" 272 | source: hosted 273 | version: "1.9.1" 274 | plugin_platform_interface: 275 | dependency: transitive 276 | description: 277 | name: plugin_platform_interface 278 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 279 | url: "https://pub.dev" 280 | source: hosted 281 | version: "2.1.8" 282 | sky_engine: 283 | dependency: transitive 284 | description: flutter 285 | source: sdk 286 | version: "0.0.0" 287 | source_span: 288 | dependency: transitive 289 | description: 290 | name: source_span 291 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 292 | url: "https://pub.dev" 293 | source: hosted 294 | version: "1.10.1" 295 | stack_trace: 296 | dependency: transitive 297 | description: 298 | name: stack_trace 299 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 300 | url: "https://pub.dev" 301 | source: hosted 302 | version: "1.12.1" 303 | stream_channel: 304 | dependency: transitive 305 | description: 306 | name: stream_channel 307 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 308 | url: "https://pub.dev" 309 | source: hosted 310 | version: "2.1.4" 311 | string_scanner: 312 | dependency: transitive 313 | description: 314 | name: string_scanner 315 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 316 | url: "https://pub.dev" 317 | source: hosted 318 | version: "1.4.1" 319 | term_glyph: 320 | dependency: transitive 321 | description: 322 | name: term_glyph 323 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 324 | url: "https://pub.dev" 325 | source: hosted 326 | version: "1.2.2" 327 | test_api: 328 | dependency: transitive 329 | description: 330 | name: test_api 331 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 332 | url: "https://pub.dev" 333 | source: hosted 334 | version: "0.7.4" 335 | typed_data: 336 | dependency: transitive 337 | description: 338 | name: typed_data 339 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 340 | url: "https://pub.dev" 341 | source: hosted 342 | version: "1.4.0" 343 | vector_math: 344 | dependency: transitive 345 | description: 346 | name: vector_math 347 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 348 | url: "https://pub.dev" 349 | source: hosted 350 | version: "2.1.4" 351 | vm_service: 352 | dependency: transitive 353 | description: 354 | name: vm_service 355 | sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 356 | url: "https://pub.dev" 357 | source: hosted 358 | version: "15.0.0" 359 | web: 360 | dependency: transitive 361 | description: 362 | name: web 363 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 364 | url: "https://pub.dev" 365 | source: hosted 366 | version: "1.1.1" 367 | sdks: 368 | dart: ">=3.8.0 <4.0.0" 369 | flutter: ">=3.32.0" 370 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/net/cubiclab/clipboard/ClipboardChannelHandler.java: -------------------------------------------------------------------------------- 1 | package net.cubiclab.clipboard; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import androidx.core.content.FileProvider; 11 | import io.flutter.plugin.common.MethodCall; 12 | import io.flutter.plugin.common.MethodChannel; 13 | import io.flutter.plugin.common.EventChannel; 14 | import java.io.ByteArrayOutputStream; 15 | import java.io.File; 16 | import java.io.FileOutputStream; 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | public class ClipboardChannelHandler implements MethodChannel.MethodCallHandler, EventChannel.StreamHandler { 23 | private final Context context; 24 | private final ClipboardManager clipboardManager; 25 | private EventChannel.EventSink eventSink; 26 | private ClipboardManager.OnPrimaryClipChangedListener clipboardChangeListener; 27 | 28 | public ClipboardChannelHandler(Context context) { 29 | this.context = context; 30 | this.clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 31 | } 32 | 33 | @Override 34 | public void onMethodCall(MethodCall call, MethodChannel.Result result) { 35 | switch (call.method) { 36 | case "copy": 37 | handleCopy(call, result); 38 | break; 39 | case "copyRichText": 40 | handleCopyRichText(call, result); 41 | break; 42 | case "copyMultiple": 43 | handleCopyMultiple(call, result); 44 | break; 45 | case "copyImage": 46 | handleCopyImage(call, result); 47 | break; 48 | case "paste": 49 | handlePaste(call, result); 50 | break; 51 | case "pasteRichText": 52 | handlePasteRichText(call, result); 53 | break; 54 | case "pasteImage": 55 | handlePasteImage(call, result); 56 | break; 57 | case "getContentType": 58 | handleGetContentType(call, result); 59 | break; 60 | case "hasData": 61 | handleHasData(call, result); 62 | break; 63 | case "clear": 64 | handleClear(call, result); 65 | break; 66 | case "getDataSize": 67 | handleGetDataSize(call, result); 68 | break; 69 | case "startMonitoring": 70 | startMonitoring(); 71 | result.success(true); 72 | break; 73 | case "stopMonitoring": 74 | stopMonitoring(); 75 | result.success(true); 76 | break; 77 | default: 78 | result.notImplemented(); 79 | } 80 | } 81 | 82 | private void handleCopy(MethodCall call, MethodChannel.Result result) { 83 | String text = call.argument("text"); 84 | if (text == null || text.isEmpty()) { 85 | result.error("EMPTY_TEXT", "Text cannot be empty", null); 86 | return; 87 | } 88 | try { 89 | ClipData clip = ClipData.newPlainText("text", text); 90 | clipboardManager.setPrimaryClip(clip); 91 | result.success(true); 92 | } catch (Exception e) { 93 | result.error("COPY_ERROR", e.getMessage(), null); 94 | } 95 | } 96 | 97 | private void handleCopyRichText(MethodCall call, MethodChannel.Result result) { 98 | String text = call.argument("text"); 99 | String html = call.argument("html"); 100 | if ((text == null || text.isEmpty()) && (html == null || html.isEmpty())) { 101 | result.error("EMPTY_CONTENT", "Either text or html must be provided", null); 102 | return; 103 | } 104 | try { 105 | if (html != null && !html.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 106 | ClipData clip = ClipData.newHtmlText("html", text != null ? text : "", html); 107 | clipboardManager.setPrimaryClip(clip); 108 | } else { 109 | ClipData clip = ClipData.newPlainText("text", text != null ? text : ""); 110 | clipboardManager.setPrimaryClip(clip); 111 | } 112 | result.success(true); 113 | } catch (Exception e) { 114 | result.error("COPY_RICH_ERROR", e.getMessage(), null); 115 | } 116 | } 117 | 118 | @SuppressWarnings("unchecked") 119 | private void handleCopyMultiple(MethodCall call, MethodChannel.Result result) { 120 | Map formats = call.argument("formats"); 121 | if (formats == null || formats.isEmpty()) { 122 | result.error("EMPTY_FORMATS", "At least one format must be provided", null); 123 | return; 124 | } 125 | try { 126 | String text = formats.get("text/plain") != null ? formats.get("text/plain").toString() : ""; 127 | String html = formats.get("text/html") != null ? formats.get("text/html").toString() : null; 128 | List imageBytes = null; 129 | if (formats.get("image/png") instanceof List) { 130 | imageBytes = (List) formats.get("image/png"); 131 | } 132 | 133 | if (imageBytes != null && !imageBytes.isEmpty()) { 134 | byte[] byteArray = new byte[imageBytes.size()]; 135 | for (int i = 0; i < imageBytes.size(); i++) { 136 | byteArray[i] = imageBytes.get(i).byteValue(); 137 | } 138 | Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); 139 | if (bitmap != null) { 140 | Uri imageUri = saveBitmapToCache(bitmap); 141 | if (imageUri != null) { 142 | ClipData clip = ClipData.newUri(context.getContentResolver(), "image", imageUri); 143 | if (text != null && !text.isEmpty()) { 144 | clip.addItem(new ClipData.Item(text)); 145 | } 146 | clipboardManager.setPrimaryClip(clip); 147 | result.success(true); 148 | return; 149 | } 150 | } 151 | } 152 | 153 | if (html != null && !html.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 154 | ClipData clip = ClipData.newHtmlText("html", text, html); 155 | clipboardManager.setPrimaryClip(clip); 156 | } else if (text != null && !text.isEmpty()) { 157 | ClipData clip = ClipData.newPlainText("text", text); 158 | clipboardManager.setPrimaryClip(clip); 159 | } 160 | result.success(true); 161 | } catch (Exception e) { 162 | result.error("COPY_MULTIPLE_ERROR", e.getMessage(), null); 163 | } 164 | } 165 | 166 | private void handleCopyImage(MethodCall call, MethodChannel.Result result) { 167 | List imageBytes = call.argument("imageBytes"); 168 | if (imageBytes == null || imageBytes.isEmpty()) { 169 | result.error("EMPTY_IMAGE", "Image bytes cannot be empty", null); 170 | return; 171 | } 172 | try { 173 | byte[] byteArray = new byte[imageBytes.size()]; 174 | for (int i = 0; i < imageBytes.size(); i++) { 175 | byteArray[i] = imageBytes.get(i).byteValue(); 176 | } 177 | Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); 178 | if (bitmap == null) { 179 | result.error("INVALID_IMAGE", "Failed to decode image", null); 180 | return; 181 | } 182 | Uri imageUri = saveBitmapToCache(bitmap); 183 | if (imageUri == null) { 184 | result.error("SAVE_ERROR", "Failed to save image to cache", null); 185 | return; 186 | } 187 | ClipData clip = ClipData.newUri(context.getContentResolver(), "image", imageUri); 188 | clipboardManager.setPrimaryClip(clip); 189 | result.success(true); 190 | } catch (Exception e) { 191 | result.error("COPY_IMAGE_ERROR", e.getMessage(), null); 192 | } 193 | } 194 | 195 | private void handlePaste(MethodCall call, MethodChannel.Result result) { 196 | try { 197 | ClipData clipData = clipboardManager.getPrimaryClip(); 198 | String text = clipData != null && clipData.getItemCount() > 0 199 | ? clipData.getItemAt(0).getText().toString() 200 | : ""; 201 | Map resultMap = new HashMap<>(); 202 | resultMap.put("text", text); 203 | result.success(resultMap); 204 | } catch (Exception e) { 205 | result.error("PASTE_ERROR", e.getMessage(), null); 206 | } 207 | } 208 | 209 | private void handlePasteRichText(MethodCall call, MethodChannel.Result result) { 210 | try { 211 | ClipData clipData = clipboardManager.getPrimaryClip(); 212 | ClipData.Item item = clipData != null && clipData.getItemCount() > 0 213 | ? clipData.getItemAt(0) 214 | : null; 215 | String text = item != null && item.getText() != null ? item.getText().toString() : ""; 216 | String html = null; 217 | if (item != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 218 | html = item.getHtmlText(); 219 | } 220 | List imageBytes = getImageFromClipboard(item); 221 | 222 | Map resultMap = new HashMap<>(); 223 | resultMap.put("text", text); 224 | resultMap.put("html", html); 225 | resultMap.put("imageBytes", imageBytes); 226 | resultMap.put("timestamp", System.currentTimeMillis()); 227 | result.success(resultMap); 228 | } catch (Exception e) { 229 | result.error("PASTE_RICH_ERROR", e.getMessage(), null); 230 | } 231 | } 232 | 233 | private void handlePasteImage(MethodCall call, MethodChannel.Result result) { 234 | try { 235 | ClipData clipData = clipboardManager.getPrimaryClip(); 236 | ClipData.Item item = clipData != null && clipData.getItemCount() > 0 237 | ? clipData.getItemAt(0) 238 | : null; 239 | List imageBytes = getImageFromClipboard(item); 240 | Map resultMap = new HashMap<>(); 241 | resultMap.put("imageBytes", imageBytes); 242 | result.success(resultMap); 243 | } catch (Exception e) { 244 | result.error("PASTE_IMAGE_ERROR", e.getMessage(), null); 245 | } 246 | } 247 | 248 | private void handleGetContentType(MethodCall call, MethodChannel.Result result) { 249 | try { 250 | ClipData clipData = clipboardManager.getPrimaryClip(); 251 | if (clipData == null || clipData.getItemCount() == 0) { 252 | result.success("empty"); 253 | return; 254 | } 255 | ClipData.Item item = clipData.getItemAt(0); 256 | boolean hasText = item.getText() != null; 257 | boolean hasHtml = false; 258 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 259 | hasHtml = item.getHtmlText() != null; 260 | } 261 | boolean hasImage = item.getUri() != null && isImageUri(item.getUri()); 262 | 263 | if (hasImage && (hasText || hasHtml)) { 264 | result.success("mixed"); 265 | } else if (hasImage) { 266 | result.success("image"); 267 | } else if (hasText && hasHtml) { 268 | result.success("mixed"); 269 | } else if (hasHtml) { 270 | result.success("html"); 271 | } else if (hasText) { 272 | result.success("text"); 273 | } else { 274 | result.success("empty"); 275 | } 276 | } catch (Exception e) { 277 | result.success("unknown"); 278 | } 279 | } 280 | 281 | private void handleHasData(MethodCall call, MethodChannel.Result result) { 282 | try { 283 | ClipData clipData = clipboardManager.getPrimaryClip(); 284 | boolean hasData = clipData != null && clipData.getItemCount() > 0; 285 | result.success(hasData); 286 | } catch (Exception e) { 287 | result.success(false); 288 | } 289 | } 290 | 291 | private void handleClear(MethodCall call, MethodChannel.Result result) { 292 | try { 293 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 294 | clipboardManager.clearPrimaryClip(); 295 | } else { 296 | ClipData clip = ClipData.newPlainText("", ""); 297 | clipboardManager.setPrimaryClip(clip); 298 | } 299 | result.success(true); 300 | } catch (Exception e) { 301 | result.error("CLEAR_ERROR", e.getMessage(), null); 302 | } 303 | } 304 | 305 | private void handleGetDataSize(MethodCall call, MethodChannel.Result result) { 306 | try { 307 | ClipData clipData = clipboardManager.getPrimaryClip(); 308 | String text = clipData != null && clipData.getItemCount() > 0 309 | ? clipData.getItemAt(0).getText().toString() 310 | : ""; 311 | result.success(text.length()); 312 | } catch (Exception e) { 313 | result.success(0); 314 | } 315 | } 316 | 317 | private Uri saveBitmapToCache(Bitmap bitmap) { 318 | try { 319 | File cacheDir = context.getCacheDir(); 320 | File imageFile = new File(cacheDir, "clipboard_image_" + System.currentTimeMillis() + ".png"); 321 | FileOutputStream out = new FileOutputStream(imageFile); 322 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 323 | out.close(); 324 | 325 | // Use FileProvider for N+ 326 | return FileProvider.getUriForFile( 327 | context, 328 | context.getPackageName() + ".provider", 329 | imageFile 330 | ); 331 | } catch (Exception e) { 332 | return null; 333 | } 334 | } 335 | 336 | private List getImageFromClipboard(ClipData.Item item) { 337 | if (item == null || item.getUri() == null) { 338 | return null; 339 | } 340 | if (!isImageUri(item.getUri())) { 341 | return null; 342 | } 343 | try { 344 | android.content.ContentResolver resolver = context.getContentResolver(); 345 | java.io.InputStream inputStream = resolver.openInputStream(item.getUri()); 346 | if (inputStream == null) { 347 | return null; 348 | } 349 | Bitmap bitmap = BitmapFactory.decodeStream(inputStream); 350 | inputStream.close(); 351 | if (bitmap == null) { 352 | return null; 353 | } 354 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 355 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); 356 | byte[] byteArray = outputStream.toByteArray(); 357 | outputStream.close(); 358 | 359 | List result = new ArrayList<>(); 360 | for (byte b : byteArray) { 361 | result.add((int) b & 0xFF); 362 | } 363 | return result; 364 | } catch (Exception e) { 365 | return null; 366 | } 367 | } 368 | 369 | private boolean isImageUri(Uri uri) { 370 | String mimeType = context.getContentResolver().getType(uri); 371 | return mimeType != null && mimeType.startsWith("image/"); 372 | } 373 | 374 | @Override 375 | public void onListen(Object arguments, EventChannel.EventSink events) { 376 | this.eventSink = events; 377 | startMonitoring(); 378 | } 379 | 380 | @Override 381 | public void onCancel(Object arguments) { 382 | this.eventSink = null; 383 | stopMonitoring(); 384 | } 385 | 386 | private void startMonitoring() { 387 | if (clipboardChangeListener != null) { 388 | return; 389 | } 390 | clipboardChangeListener = () -> { 391 | if (eventSink != null) { 392 | try { 393 | ClipData clipData = clipboardManager.getPrimaryClip(); 394 | ClipData.Item item = clipData != null && clipData.getItemCount() > 0 395 | ? clipData.getItemAt(0) 396 | : null; 397 | String text = item != null && item.getText() != null 398 | ? item.getText().toString() 399 | : ""; 400 | String html = null; 401 | if (item != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 402 | html = item.getHtmlText(); 403 | } 404 | Map eventMap = new HashMap<>(); 405 | eventMap.put("text", text); 406 | eventMap.put("html", html); 407 | eventMap.put("timestamp", System.currentTimeMillis()); 408 | eventSink.success(eventMap); 409 | } catch (Exception e) { 410 | // Ignore errors 411 | } 412 | } 413 | }; 414 | clipboardManager.addPrimaryClipChangedListener(clipboardChangeListener); 415 | } 416 | 417 | private void stopMonitoring() { 418 | if (clipboardChangeListener != null) { 419 | clipboardManager.removePrimaryClipChangedListener(clipboardChangeListener); 420 | clipboardChangeListener = null; 421 | } 422 | } 423 | } 424 | 425 | -------------------------------------------------------------------------------- /example/macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | --------------------------------------------------------------------------------