diff --git a/packages/desktop_drop/.metadata b/packages/desktop_drop/.metadata deleted file mode 100644 index 98dfe94d..00000000 --- a/packages/desktop_drop/.metadata +++ /dev/null @@ -1,42 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "2524052335ec76bb03e04ede244b071f1b86d190" - channel: "stable" - -project_type: plugin - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - - platform: android - create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - - platform: linux - create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - - platform: macos - create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - - platform: web - create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - - platform: windows - create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/desktop_drop/CHANGELOG.md b/packages/desktop_drop/CHANGELOG.md index b2d92f8c..cceb99ca 100644 --- a/packages/desktop_drop/CHANGELOG.md +++ b/packages/desktop_drop/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.8.0 + +* [macOS] Add app-wide drops from Dock, Finder, and Open With via + `DropTarget.catchAppWideDrops`. +* [macOS] Add Dock text/link drops through Services, delivered as + memory-backed `DropItem`s with text and URI helpers. +* Document and demonstrate the required macOS `Info.plist` and `AppDelegate` + setup for global file, folder, text, and link drops. + ## 0.7.1 * update worksapce diff --git a/packages/desktop_drop/README.md b/packages/desktop_drop/README.md index ee00af53..19b0b976 100644 --- a/packages/desktop_drop/README.md +++ b/packages/desktop_drop/README.md @@ -67,6 +67,153 @@ class _ExampleDragTargetState extends State { ``` +## macOS: Global Drops + +On macOS there are two ways users can drop content into your app: + +- In-window drag & drop over your UI (`DropTarget`). +- Drop on the app's Dock icon, or use Open With from Finder, which is an + application-level open. + +The application-level path needs small macOS app configuration in addition to +the Dart `DropTarget`. + +### Files and folders via Dock or Finder + +Add document types to your macOS `Info.plist` so Finder can route files and +folders to the app: + +```xml + +CFBundleDocumentTypes + + + CFBundleTypeRole + Viewer + LSItemContentTypes + + public.data + public.folder + + + +``` + +Initialize the channel early. You can observe application-level drops with a raw +listener: + +```dart +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + DesktopDrop.instance.addRawDropEventListener((event) async { + if (event is DropDoneEvent && event.location == Offset.zero) { + // Process files and directories in event.files. + } + }); + + DesktopDrop.instance.init(); + runApp(const MyApp()); +} +``` + +You can also opt a primary `DropTarget` into app-wide drops: + +```dart +DropTarget( + catchAppWideDrops: true, + onDragDone: (details) { + // Handles normal in-window drops and app-wide macOS drops. + }, + child: child, +) +``` + +If multiple `DropTarget`s set `catchAppWideDrops: true`, each target can receive +the same app-wide drop. In most apps, enable it only on the primary drop area. + +### Text and links via Dock Services + +macOS delivers selected text and links dropped on the Dock icon through +Services. To accept those drops, configure both `Info.plist` and +`AppDelegate.swift`. + +1. Add an `NSServices` entry to your macOS `Info.plist`: + +```xml +NSServices + + + NSMenuItem + + default + Drop Text into My App + + NSMessage + desktopDropAcceptDroppedText + NSSendTypes + + NSStringPboardType + public.text + public.plain-text + public.utf8-plain-text + public.utf16-plain-text + public.utf16-external-plain-text + public.html + public.rtf + public.url + + + +``` + +2. Install the Services provider in `AppDelegate.swift` before the app finishes + launching: + +```swift +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationWillFinishLaunching(_ notification: Notification) { + if NSApp.servicesProvider == nil, + let cls = NSClassFromString("DesktopDropServicesProvider") as? NSObject.Type { + NSApp.servicesProvider = cls.init() + } + super.applicationWillFinishLaunching(notification) + } +} +``` + +Dock text and link drops are delivered as memory-backed `DropItem`s. The package +exports helpers for reading those values: + +```dart +import 'package:desktop_drop/desktop_drop.dart'; + +onDragDone: (details) async { + for (final item in details.files) { + if (item.isMemoryBacked && item.isTextLike) { + final uris = await item.readAsUris(); + if (uris.isNotEmpty) { + // Handle text/uri-list links. + continue; + } + + final text = await item.readAsText(); + // Handle plain text, HTML, or raw RTF content. + continue; + } + + // Handle real files and directories as before. + } +} +``` + +The example app includes the required `Info.plist` and `AppDelegate.swift` +setup, plus a `TextDropDemo` that displays dropped text and links. + ## LICENSE see LICENSE file diff --git a/packages/desktop_drop/example/.metadata b/packages/desktop_drop/example/.metadata deleted file mode 100644 index 878648b2..00000000 --- a/packages/desktop_drop/example/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "ea121f8859e4b13e47a8f845e4586164519588bc" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - - platform: android - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/desktop_drop/example/README.md b/packages/desktop_drop/example/README.md index b906d761..74c4b0cc 100644 --- a/packages/desktop_drop/example/README.md +++ b/packages/desktop_drop/example/README.md @@ -1,6 +1,6 @@ # desktop_drop_example -Demonstrates how to use the desktop_drop plugin. +A new Flutter project. ## Getting Started @@ -8,59 +8,9 @@ This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a -full API reference. - -## Example - -```dart -class ExampleDragTarget extends StatefulWidget { - const ExampleDragTarget({Key? key}) : super(key: key); - - @override - _ExampleDragTargetState createState() => _ExampleDragTargetState(); -} - -class _ExampleDragTargetState extends State { - final List _list = []; - - bool _dragging = false; - - @override - Widget build(BuildContext context) { - return DropTarget( - onDragDone: (urls) { - setState(() { - for (final uri in urls) { - debugPrint("uri: ${uri.toFilePath()} " - "${File(uri.toFilePath()).existsSync()}"); - } - _list.addAll(urls); - }); - }, - onDragEntered: () { - setState(() { - _dragging = true; - }); - }, - onDragExited: () { - setState(() { - _dragging = false; - }); - }, - child: Container( - height: 200, - width: 200, - color: _dragging ? Colors.blue.withOpacity(0.4) : Colors.black26, - child: _list.isEmpty - ? const Center(child: Text("Drop here")) - : Text(_list.join("\n")), - ), - ); - } -} -``` \ No newline at end of file +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/desktop_drop/example/analysis_options.yaml b/packages/desktop_drop/example/analysis_options.yaml index 61b6c4de..0d290213 100644 --- a/packages/desktop_drop/example/analysis_options.yaml +++ b/packages/desktop_drop/example/analysis_options.yaml @@ -13,8 +13,7 @@ linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. + # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code diff --git a/packages/desktop_drop/example/android/app/build.gradle.kts b/packages/desktop_drop/example/android/app/build.gradle.kts index 51da201b..1859ca56 100644 --- a/packages/desktop_drop/example/android/app/build.gradle.kts +++ b/packages/desktop_drop/example/android/app/build.gradle.kts @@ -11,12 +11,12 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { diff --git a/packages/desktop_drop/example/android/build.gradle.kts b/packages/desktop_drop/example/android/build.gradle.kts index 89176ef4..dbee657b 100644 --- a/packages/desktop_drop/example/android/build.gradle.kts +++ b/packages/desktop_drop/example/android/build.gradle.kts @@ -5,7 +5,10 @@ allprojects { } } -val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() rootProject.layout.buildDirectory.value(newBuildDir) subprojects { diff --git a/packages/desktop_drop/example/android/gradle.properties b/packages/desktop_drop/example/android/gradle.properties index f018a618..fbee1d8c 100644 --- a/packages/desktop_drop/example/android/gradle.properties +++ b/packages/desktop_drop/example/android/gradle.properties @@ -1,3 +1,2 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/desktop_drop/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/desktop_drop/example/android/gradle/wrapper/gradle-wrapper.properties index afa1e8eb..e4ef43fb 100644 --- a/packages/desktop_drop/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/desktop_drop/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/desktop_drop/example/android/settings.gradle.kts b/packages/desktop_drop/example/android/settings.gradle.kts index a439442c..ca7fe065 100644 --- a/packages/desktop_drop/example/android/settings.gradle.kts +++ b/packages/desktop_drop/example/android/settings.gradle.kts @@ -1,11 +1,12 @@ pluginManagement { - val flutterSdkPath = run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") @@ -18,8 +19,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false } include(":app") diff --git a/packages/desktop_drop/example/ios/.gitignore b/packages/desktop_drop/example/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/packages/desktop_drop/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/desktop_drop/example/ios/Flutter/AppFrameworkInfo.plist b/packages/desktop_drop/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..1dc6cf76 --- /dev/null +++ b/packages/desktop_drop/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/packages/desktop_drop/example/ios/Flutter/Debug.xcconfig b/packages/desktop_drop/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/packages/desktop_drop/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/desktop_drop/example/ios/Flutter/Release.xcconfig b/packages/desktop_drop/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/packages/desktop_drop/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/desktop_drop/example/ios/Podfile b/packages/desktop_drop/example/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/packages/desktop_drop/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/desktop_drop/example/ios/Runner.xcodeproj/project.pbxproj b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..6b29efdc --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,619 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = U6XCBBN2GK; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = U6XCBBN2GK; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = U6XCBBN2GK; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/desktop_drop/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/desktop_drop/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3773d42 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/desktop_drop/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/desktop_drop/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/desktop_drop/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/desktop_drop/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/desktop_drop/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/desktop_drop/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/desktop_drop/example/ios/Runner/AppDelegate.swift b/packages/desktop_drop/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..62666446 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/packages/desktop_drop/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/desktop_drop/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/desktop_drop/example/ios/Runner/Base.lproj/Main.storyboard b/packages/desktop_drop/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/desktop_drop/example/ios/Runner/Info.plist b/packages/desktop_drop/example/ios/Runner/Info.plist new file mode 100644 index 00000000..73dfbe33 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Desktop Drop Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + desktop_drop_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/desktop_drop/example/ios/Runner/Runner-Bridging-Header.h b/packages/desktop_drop/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/packages/desktop_drop/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/desktop_drop/example/ios/RunnerTests/RunnerTests.swift b/packages/desktop_drop/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/packages/desktop_drop/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/desktop_drop/example/lib/debug_logger.dart b/packages/desktop_drop/example/lib/debug_logger.dart new file mode 100644 index 00000000..106529c8 --- /dev/null +++ b/packages/desktop_drop/example/lib/debug_logger.dart @@ -0,0 +1,50 @@ +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; + +/// Logs a [DropDoneEvent] with full recursive details of directories and files. +void logDropEvent(DropDoneEvent event, {String source = 'Unknown'}) { + _logDropGeneric(event.location, event.files, source); +} + +/// Logs a [DropDoneDetails] with full recursive details of directories and files. +void logDropDetails(DropDoneDetails details, {String source = 'Unknown'}) { + _logDropGeneric(details.globalPosition, details.files, source); +} + +void _logDropGeneric(Offset location, List files, String source) { + debugPrint( + '==================================================================', + ); + debugPrint('🛠️ DEBUG [$source]: Drop received'); + debugPrint('📍 Location: $location'); + debugPrint('📦 Items count: ${files.length}'); + + for (var i = 0; i < files.length; i++) { + _logDropItem(files[i], 0, index: i); + } + debugPrint( + '==================================================================', + ); +} + +void _logDropItem(DropItem item, int depth, {int? index}) { + final indent = ' ' * depth; + final prefix = index != null ? '$index. ' : '- '; + final typeIcon = item is DropItemDirectory ? '📁' : '📄'; + final typeName = item is DropItemDirectory ? 'Directory' : 'File'; + + debugPrint('$indent$prefix$typeIcon $typeName: "${item.name}"'); + debugPrint('$indent Path: ${item.path}'); + debugPrint('$indent MIME: ${item.mimeType}'); + + if (item is DropItemDirectory) { + if (item.children.isEmpty) { + debugPrint('$indent (Empty Directory)'); + } else { + for (var i = 0; i < item.children.length; i++) { + _logDropItem(item.children[i], depth + 1, index: i); + } + } + } +} diff --git a/packages/desktop_drop/example/lib/main.dart b/packages/desktop_drop/example/lib/main.dart index 5c1cd392..dbb63202 100644 --- a/packages/desktop_drop/example/lib/main.dart +++ b/packages/desktop_drop/example/lib/main.dart @@ -8,12 +8,46 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_platform/universal_platform.dart'; -void main() { +import 'debug_logger.dart'; + +final GlobalKey _scaffoldMessengerKey = + GlobalKey(); + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Set up the global listener before init so queued events are delivered. + DesktopDrop.instance.addRawDropEventListener((event) async { + if (event is DropDoneEvent && event.files.isNotEmpty) { + logDropEvent(event, source: 'Main/GlobalListener'); + // Dock/Finder open events arrive without prior hover; location defaults to Offset.zero. + final fromDockOrFinder = event.location == Offset.zero; + + if (fromDockOrFinder) { + final names = event.files.map((e) => e.path.split('/').last).join("\n"); + // Show notification after first frame to ensure context is ready + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _scaffoldMessengerKey.currentContext; + if (ctx != null) { + _scaffoldMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text('Dock/Finder drop opened:\n$names'), + duration: const Duration(seconds: 5), + ), + ); + } + }); + } + } + }); + // Initialize channel and signal readiness + DesktopDrop.instance.init(); + runApp(const MyApp()); } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); void loadFile(BuildContext context, bool bookmarkEnable) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -25,85 +59,78 @@ class MyApp extends StatelessWidget { String appleBookmarkStr = data["apple-bookmark"]! as String; Uint8List appleBookmark = base64.decode(appleBookmarkStr); - // var file = XFile(path); - // var fileSize = await file.length(); - try { if (bookmarkEnable) { bool grantedPermission = await DesktopDrop.instance .startAccessingSecurityScopedResource(bookmark: appleBookmark); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - "file permission :" + grantedPermission.toString(), - ))); + _scaffoldMessengerKey.currentState?.showSnackBar( + SnackBar(content: Text("file permission :$grantedPermission")), + ); } var file = File(path); - var contents = await file.readAsBytes(); var fileSize = contents.length; if (bookmarkEnable) { - await DesktopDrop.instance - .stopAccessingSecurityScopedResource(bookmark: appleBookmark); + await DesktopDrop.instance.stopAccessingSecurityScopedResource( + bookmark: appleBookmark, + ); } - final snackBar = - SnackBar(content: Text('file size:' + fileSize.toString())); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + final snackBar = SnackBar(content: Text('file size:$fileSize')); + _scaffoldMessengerKey.currentState?.showSnackBar(snackBar); } catch (e) { - final snackBar = SnackBar(content: Text('error:' + e.toString())); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + final snackBar = SnackBar(content: Text('error:$e')); + _scaffoldMessengerKey.currentState?.showSnackBar(snackBar); } } @override Widget build(BuildContext context) { return MaterialApp( + scaffoldMessengerKey: _scaffoldMessengerKey, home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), + appBar: AppBar(title: const Text('Desktop Drop Example')), body: Wrap( direction: Axis.horizontal, runSpacing: 8, spacing: 8, children: [ - const ExampleDragTarget(), - const ExampleDragTarget(), - const ExampleDragTarget(), - const ExampleDragTarget(), - const ExampleDragTarget(), - const ExampleDragTarget(), + const TextDropDemo(), + const ExampleDragTarget(catchAppWideDrops: true), + const ExampleDragTarget(catchAppWideDrops: false), + const ExampleDragTarget(catchAppWideDrops: false), + const ExampleDragTarget(catchAppWideDrops: false), + const ExampleDragTarget(catchAppWideDrops: false), + const ExampleDragTarget(catchAppWideDrops: false), if (UniversalPlatform.isMacOS) - StatefulBuilder(builder: (context, setState) { - return Column( - children: [ - const Text( - "Test Apple Bookmark\n1 drag file \n2 save the bookmark,\n3 restart app\n4 choice test button", - ), - TextButton( - onPressed: () async { - loadFile(context, true); - return; - }, - child: const Text( - "with applemark, suc", + StatefulBuilder( + builder: (context, setState) { + return Column( + children: [ + const Text( + "Test Apple Bookmark\n1 drag file \n2 save the bookmark,\n3 restart app\n4 choice test button", ), - ), - TextButton( - onPressed: () async { - loadFile(context, false); - return; - }, - child: const Text( - "without applemark, err", + TextButton( + onPressed: () async { + loadFile(context, true); + return; + }, + child: const Text("with applemark, suc"), ), - ), - ], - ); - }), + TextButton( + onPressed: () async { + loadFile(context, false); + return; + }, + child: const Text("without applemark, err"), + ), + ], + ); + }, + ), ], ), ), @@ -111,11 +138,81 @@ class MyApp extends StatelessWidget { } } +class TextDropDemo extends StatefulWidget { + const TextDropDemo({super.key}); + + @override + State createState() => _TextDropDemoState(); +} + +class _TextDropDemoState extends State { + String? _lastText; + String? _lastLabel; + + Future _handleDrop(List items) async { + for (final item in items) { + if (!item.isMemoryBacked || !item.isTextLike) continue; + + final uris = await item.readAsUris(); + final text = uris.isNotEmpty + ? uris.map((uri) => uri.toString()).join('\n') + : await item.readAsText(); + if (text == null || !mounted) return; + + setState(() { + _lastText = text.length > 2000 ? '${text.substring(0, 2000)}...' : text; + _lastLabel = item.name; + }); + return; + } + } + + @override + Widget build(BuildContext context) { + return DropTarget( + catchAppWideDrops: true, + onDragDone: (details) { + logDropDetails(details, source: 'TabShell/DropTarget'); + _handleDrop(details.files); + }, + child: Container( + height: 200, + width: 300, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.15), + border: Border.all(color: Colors.green.shade400), + borderRadius: BorderRadius.circular(8), + ), + child: _lastText == null + ? const Center(child: Text('Drop text or links here')) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _lastLabel ?? 'Text', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Expanded( + child: SingleChildScrollView( + child: SelectableText(_lastText!), + ), + ), + ], + ), + ), + ); + } +} + class ExampleDragTarget extends StatefulWidget { - const ExampleDragTarget({Key? key}) : super(key: key); + const ExampleDragTarget({super.key, this.catchAppWideDrops = false}); + + final bool catchAppWideDrops; @override - _ExampleDragTargetState createState() => _ExampleDragTargetState(); + State createState() => _ExampleDragTargetState(); } class _ExampleDragTargetState extends State { @@ -123,17 +220,10 @@ class _ExampleDragTargetState extends State { final List dropFiles = []; bool _dragging = false; - Offset? offset; Future printFiles(List files, [int depth = 0]) async { - debugPrint(' |' * depth); for (final file in files) { - debugPrint(' |' * depth + - '> ${file.path} ${file.name}' - ' ${await file.lastModified()}' - ' ${await file.length()}' - ' ${file.mimeType}'); if (file is DropItemDirectory) { printFiles(file.children, depth + 1); } @@ -143,13 +233,12 @@ class _ExampleDragTargetState extends State { @override Widget build(BuildContext context) { return DropTarget( + catchAppWideDrops: widget.catchAppWideDrops, onDragDone: (detail) async { setState(() { _list.addAll(detail.files); dropFiles.addAll(detail.files); }); - - debugPrint('onDragDone:'); await printFiles(detail.files); }, onDragUpdated: (details) { @@ -178,7 +267,25 @@ class _ExampleDragTargetState extends State { if (_list.isEmpty) const Center(child: Text("Drop here")) else - Text(_list.map((e) => e.path).join("\n")), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + _list.map((e) => e.path.split('/').last).join("\n"), + style: const TextStyle(fontSize: 10), + overflow: TextOverflow.ellipsis, + ), + ), + if (widget.catchAppWideDrops) + const Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: EdgeInsets.all(4), + child: Text( + 'Primary (catches Dock)', + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + ), + ), if (offset != null) Align( alignment: Alignment.topRight, @@ -192,12 +299,17 @@ class _ExampleDragTargetState extends State { alignment: Alignment.bottomRight, child: TextButton( onPressed: () async { + if (dropFiles.isEmpty) return; + Map data = {}; data["path"] = dropFiles[0].path; - String bookmark = - base64.encode(dropFiles[0].extraAppleBookmark!); - data["apple-bookmark"] = bookmark; + if (dropFiles[0].extraAppleBookmark != null) { + String bookmark = base64.encode( + dropFiles[0].extraAppleBookmark!, + ); + data["apple-bookmark"] = bookmark; + } String jsonStr = json.encode(data); debugPrint(jsonStr); @@ -205,16 +317,20 @@ class _ExampleDragTargetState extends State { await SharedPreferences.getInstance(); prefs.setString("apple-bookmark", jsonStr); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + _scaffoldMessengerKey.currentState?.showSnackBar( + const SnackBar( content: Text( - 'Save Suc, restart app, and Test Apple Bookmark'))); + 'Save Suc, restart app, and Test Apple Bookmark', + ), + ), + ); }, child: const Text( 'save bookmark', style: TextStyle(fontSize: 14), ), ), - ) + ), ], ), ), diff --git a/packages/desktop_drop/example/linux/.gitignore b/packages/desktop_drop/example/linux/.gitignore index b11f5d1a..d3896c98 100644 --- a/packages/desktop_drop/example/linux/.gitignore +++ b/packages/desktop_drop/example/linux/.gitignore @@ -1,2 +1 @@ flutter/ephemeral -cmake-build-debug \ No newline at end of file diff --git a/packages/desktop_drop/example/linux/CMakeLists.txt b/packages/desktop_drop/example/linux/CMakeLists.txt index 6f758042..98a0ce82 100644 --- a/packages/desktop_drop/example/linux/CMakeLists.txt +++ b/packages/desktop_drop/example/linux/CMakeLists.txt @@ -1,11 +1,19 @@ -cmake_minimum_required(VERSION 3.10) +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) project(runner LANGUAGES CXX) +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. set(BINARY_NAME "desktop_drop_example") -set(APPLICATION_ID "com.example.desktop_drop") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.desktop_drop_example") +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. cmake_policy(SET CMP0063 NEW) +# Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. @@ -18,7 +26,7 @@ if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() -# Configure build options. +# Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) @@ -27,6 +35,10 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) endif() # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) @@ -34,27 +46,20 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") -# Application build -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) -apply_standard_settings(${BINARY_NAME}) -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) + # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of @@ -64,6 +69,7 @@ set_target_properties(${BINARY_NAME} RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -94,11 +100,17 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR} install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -endif() +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. diff --git a/packages/desktop_drop/example/linux/flutter/CMakeLists.txt b/packages/desktop_drop/example/linux/flutter/CMakeLists.txt index 33fd5801..d5bd0164 100644 --- a/packages/desktop_drop/example/linux/flutter/CMakeLists.txt +++ b/packages/desktop_drop/example/linux/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") diff --git a/packages/desktop_drop/example/linux/runner/CMakeLists.txt b/packages/desktop_drop/example/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/packages/desktop_drop/example/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/packages/desktop_drop/example/linux/main.cc b/packages/desktop_drop/example/linux/runner/main.cc similarity index 100% rename from packages/desktop_drop/example/linux/main.cc rename to packages/desktop_drop/example/linux/runner/main.cc diff --git a/packages/desktop_drop/example/linux/my_application.cc b/packages/desktop_drop/example/linux/runner/my_application.cc similarity index 59% rename from packages/desktop_drop/example/linux/my_application.cc rename to packages/desktop_drop/example/linux/runner/my_application.cc index 5e15587d..35c333ba 100644 --- a/packages/desktop_drop/example/linux/my_application.cc +++ b/packages/desktop_drop/example/linux/runner/my_application.cc @@ -14,6 +14,11 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -48,32 +53,44 @@ static void my_application_activate(GApplication* application) { } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); - } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); @@ -82,6 +99,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch return TRUE; } +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); @@ -91,15 +126,23 @@ static void my_application_dispose(GObject* object) { static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); } diff --git a/packages/desktop_drop/example/linux/my_application.h b/packages/desktop_drop/example/linux/runner/my_application.h similarity index 70% rename from packages/desktop_drop/example/linux/my_application.h rename to packages/desktop_drop/example/linux/runner/my_application.h index 72271d5e..db16367a 100644 --- a/packages/desktop_drop/example/linux/my_application.h +++ b/packages/desktop_drop/example/linux/runner/my_application.h @@ -3,7 +3,10 @@ #include -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, GtkApplication) /** diff --git a/packages/desktop_drop/example/macos/.gitignore b/packages/desktop_drop/example/macos/.gitignore index d2fd3772..746adbb6 100644 --- a/packages/desktop_drop/example/macos/.gitignore +++ b/packages/desktop_drop/example/macos/.gitignore @@ -3,4 +3,5 @@ **/Pods/ # Xcode-related +**/dgph **/xcuserdata/ diff --git a/packages/desktop_drop/example/macos/Podfile b/packages/desktop_drop/example/macos/Podfile index ff5ddb3b..a46f7f23 100644 --- a/packages/desktop_drop/example/macos/Podfile +++ b/packages/desktop_drop/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/desktop_drop/example/macos/Podfile.lock b/packages/desktop_drop/example/macos/Podfile.lock new file mode 100644 index 00000000..0d9fe5c2 --- /dev/null +++ b/packages/desktop_drop/example/macos/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - desktop_drop (0.5.0): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos + FlutterMacOS: + :path: Flutter/ephemeral + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + +SPEC CHECKSUMS: + desktop_drop: bf6b0bcd7841e1b4cca34886e7ad05ced635d338 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + +PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f + +COCOAPODS: 1.16.2 diff --git a/packages/desktop_drop/example/macos/Runner.xcodeproj/project.pbxproj b/packages/desktop_drop/example/macos/Runner.xcodeproj/project.pbxproj index 9831d2e6..d8a8ebdb 100644 --- a/packages/desktop_drop/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/desktop_drop/example/macos/Runner.xcodeproj/project.pbxproj @@ -21,15 +21,24 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 4173BD2914EB6F097D0F5206 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AC4FAA22606A16810BE4313 /* Pods_Runner.framework */; }; + 7D18DD7A7DC87D516C6F3491 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D18B1FAAF44469D19027B4 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; @@ -53,6 +62,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* desktop_drop_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop_drop_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -67,23 +78,60 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 41F1C1798AF4BC2426AD0005 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 4AC4FAA22606A16810BE4313 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5CE29643F3CA1A5D7B825656 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 9AEE8AB4AA7897F87B483C9E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + ABCAFFF295DA059A64EC8F71 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C87322658DE929B1C8896963 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + E5D18B1FAAF44469D19027B4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E9DBF4C11B5717399B3CC475 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D18DD7A7DC87D516C6F3491 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 4173BD2914EB6F097D0F5206 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 229399E4D29671637BFAE251 /* Pods */ = { + isa = PBXGroup; + children = ( + 9AEE8AB4AA7897F87B483C9E /* Pods-Runner.debug.xcconfig */, + 5CE29643F3CA1A5D7B825656 /* Pods-Runner.release.xcconfig */, + ABCAFFF295DA059A64EC8F71 /* Pods-Runner.profile.xcconfig */, + E9DBF4C11B5717399B3CC475 /* Pods-RunnerTests.debug.xcconfig */, + C87322658DE929B1C8896963 /* Pods-RunnerTests.release.xcconfig */, + 41F1C1798AF4BC2426AD0005 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -100,7 +148,10 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 229399E4D29671637BFAE251 /* Pods */, ); sourceTree = ""; }; @@ -108,6 +159,7 @@ isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* desktop_drop_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -126,7 +178,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -148,18 +199,48 @@ path = Runner; sourceTree = ""; }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4AC4FAA22606A16810BE4313 /* Pods_Runner.framework */, + E5D18B1FAAF44469D19027B4 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + D6881685BA3071E8082FD06A /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 56AD47E3DF18488C7D31EEE1 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + A942A8BF099A790F91C1E46E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -167,9 +248,6 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* desktop_drop_example.app */; productType = "com.apple.product-type.application"; @@ -180,10 +258,15 @@ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; @@ -209,20 +292,25 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, - ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -273,9 +361,78 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 56AD47E3DF18488C7D31EEE1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A942A8BF099A790F91C1E46E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D6881685BA3071E8082FD06A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -289,6 +446,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; @@ -309,11 +471,57 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E9DBF4C11B5717399B3CC475 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desktop_drop_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/desktop_drop_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C87322658DE929B1C8896963 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desktop_drop_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/desktop_drop_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 41F1C1798AF4BC2426AD0005 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desktop_drop_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/desktop_drop_example"; + }; + name = Profile; + }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -337,9 +545,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -347,7 +557,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -387,6 +597,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -410,9 +621,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -426,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -440,6 +653,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -463,9 +677,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -473,7 +689,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -539,6 +755,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -570,20 +796,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = FlutterGeneratedPluginSwiftPackage; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/packages/desktop_drop/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/desktop_drop/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5271036b..32e61af4 100644 --- a/packages/desktop_drop/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/desktop_drop/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,24 +5,6 @@ - - - - - - - - - - - - + + + + + + - - Bool { return true } diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7..82b6f9d9 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc164..13b35eba 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 483be613..0a3f5fa4 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df..bdb57226 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 9c0a6528..f083318e 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a7261..326c0e72 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 8a31fe2d..2f1632cf 100644 Binary files a/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/packages/desktop_drop/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/desktop_drop/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/desktop_drop/example/macos/Runner/Base.lproj/MainMenu.xib index 537341ab..80e867a4 100644 --- a/packages/desktop_drop/example/macos/Runner/Base.lproj/MainMenu.xib +++ b/packages/desktop_drop/example/macos/Runner/Base.lproj/MainMenu.xib @@ -323,6 +323,10 @@ + + + + diff --git a/packages/desktop_drop/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/desktop_drop/example/macos/Runner/Configs/AppInfo.xcconfig index 6f51f209..53967007 100644 --- a/packages/desktop_drop/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/desktop_drop/example/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = desktop_drop_example PRODUCT_BUNDLE_IDENTIFIER = com.example.desktopDropExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/packages/desktop_drop/example/macos/Runner/DebugProfile.entitlements b/packages/desktop_drop/example/macos/Runner/DebugProfile.entitlements index 0ceee8df..dddb8a30 100644 --- a/packages/desktop_drop/example/macos/Runner/DebugProfile.entitlements +++ b/packages/desktop_drop/example/macos/Runner/DebugProfile.entitlements @@ -8,7 +8,5 @@ com.apple.security.network.server - com.apple.security.files.user-selected.read-only - diff --git a/packages/desktop_drop/example/macos/Runner/Info.plist b/packages/desktop_drop/example/macos/Runner/Info.plist index 4789daa6..3e045a79 100644 --- a/packages/desktop_drop/example/macos/Runner/Info.plist +++ b/packages/desktop_drop/example/macos/Runner/Info.plist @@ -28,5 +28,42 @@ MainMenu NSPrincipalClass NSApplication + +CFBundleDocumentTypes + + + CFBundleTypeRole + Viewer + LSItemContentTypes + + public.data + public.folder + + + +NSServices + + + NSMenuItem + + default + Drop Text into Desktop Drop + + NSMessage + desktopDropAcceptDroppedText + NSSendTypes + + NSStringPboardType + public.text + public.plain-text + public.utf8-plain-text + public.utf16-plain-text + public.utf16-external-plain-text + public.html + public.rtf + public.url + + + diff --git a/packages/desktop_drop/example/macos/Runner/MainFlutterWindow.swift b/packages/desktop_drop/example/macos/Runner/MainFlutterWindow.swift index 2722837e..3cc05eb2 100644 --- a/packages/desktop_drop/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/desktop_drop/example/macos/Runner/MainFlutterWindow.swift @@ -3,7 +3,7 @@ import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() + let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) diff --git a/packages/desktop_drop/example/macos/RunnerTests/RunnerTests.swift b/packages/desktop_drop/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/packages/desktop_drop/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/desktop_drop/example/pubspec.lock b/packages/desktop_drop/example/pubspec.lock index ad03841b..bceaff2f 100644 --- a/packages/desktop_drop/example/pubspec.lock +++ b/packages/desktop_drop/example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" url: "https://pub.dev" source: hosted - version: "0.3.4+2" + version: "0.3.5+1" cupertino_icons: dependency: "direct main" description: @@ -63,7 +63,7 @@ packages: path: ".." relative: true source: path - version: "0.7.1" + version: "0.8.0" fake_async: dependency: transitive description: @@ -97,10 +97,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -139,10 +139,10 @@ packages: dependency: transitive description: name: lints - sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "6.0.0" matcher: dependency: transitive description: @@ -219,26 +219,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.18" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -344,10 +344,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.2" web: dependency: transitive description: @@ -366,4 +366,4 @@ packages: version: "1.1.0" sdks: dart: ">=3.10.0-0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.35.0" diff --git a/packages/desktop_drop/example/pubspec.yaml b/packages/desktop_drop/example/pubspec.yaml index 5ca82dd7..b2634736 100644 --- a/packages/desktop_drop/example/pubspec.yaml +++ b/packages/desktop_drop/example/pubspec.yaml @@ -7,8 +7,8 @@ publish_to: 'none' # sdk: ">=2.12.0 <3.0.0" environment: - sdk: ">=2.13.0 <3.0.0" - flutter: ">=1.20.0" + sdk: '>=3.8.1 <4.0.0' + flutter: '>=3.32.1' dependencies: flutter: @@ -16,9 +16,9 @@ dependencies: desktop_drop: path: ../ - cross_file: ^0.3.2 - cupertino_icons: ^1.0.2 - shared_preferences: ^2.2.3 + cross_file: ^0.3.5+1 + cupertino_icons: ^1.0.8 + shared_preferences: ^2.5.3 universal_platform: dev_dependencies: @@ -30,7 +30,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^1.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/desktop_drop/example/web/index.html b/packages/desktop_drop/example/web/index.html index e64971d4..75f318ca 100644 --- a/packages/desktop_drop/example/web/index.html +++ b/packages/desktop_drop/example/web/index.html @@ -18,7 +18,7 @@ - + diff --git a/packages/desktop_drop/example/web/manifest.json b/packages/desktop_drop/example/web/manifest.json index 063ef71c..bbf0c42e 100644 --- a/packages/desktop_drop/example/web/manifest.json +++ b/packages/desktop_drop/example/web/manifest.json @@ -5,7 +5,7 @@ "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "Demonstrates how to use the desktop_drop plugin.", + "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ diff --git a/packages/desktop_drop/example/windows/.gitignore b/packages/desktop_drop/example/windows/.gitignore index e18ceda1..d492d0d9 100644 --- a/packages/desktop_drop/example/windows/.gitignore +++ b/packages/desktop_drop/example/windows/.gitignore @@ -15,5 +15,3 @@ x86/ *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ - -cmake-build-debug \ No newline at end of file diff --git a/packages/desktop_drop/example/windows/CMakeLists.txt b/packages/desktop_drop/example/windows/CMakeLists.txt index df971c1d..3a8d2966 100644 --- a/packages/desktop_drop/example/windows/CMakeLists.txt +++ b/packages/desktop_drop/example/windows/CMakeLists.txt @@ -1,13 +1,16 @@ -cmake_minimum_required(VERSION 3.15) +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) project(desktop_drop_example LANGUAGES CXX) +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. set(BINARY_NAME "desktop_drop_example") -cmake_policy(SET CMP0063 NEW) +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -30,6 +33,10 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,14 +45,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -80,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/packages/desktop_drop/example/windows/flutter/CMakeLists.txt b/packages/desktop_drop/example/windows/flutter/CMakeLists.txt index b02c5485..903f4899 100644 --- a/packages/desktop_drop/example/windows/flutter/CMakeLists.txt +++ b/packages/desktop_drop/example/windows/flutter/CMakeLists.txt @@ -1,4 +1,5 @@ -cmake_minimum_required(VERSION 3.15) +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/packages/desktop_drop/example/windows/runner/CMakeLists.txt b/packages/desktop_drop/example/windows/runner/CMakeLists.txt index 0b899a0b..394917c0 100644 --- a/packages/desktop_drop/example/windows/runner/CMakeLists.txt +++ b/packages/desktop_drop/example/windows/runner/CMakeLists.txt @@ -1,6 +1,11 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" @@ -10,8 +15,26 @@ add_executable(${BINARY_NAME} WIN32 "Runner.rc" "runner.exe.manifest" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/desktop_drop/example/windows/runner/Runner.rc b/packages/desktop_drop/example/windows/runner/Runner.rc index e234503d..cc80df46 100644 --- a/packages/desktop_drop/example/windows/runner/Runner.rc +++ b/packages/desktop_drop/example/windows/runner/Runner.rc @@ -90,10 +90,10 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "Demonstrates how to use the desktop_drop plugin." "\0" + VALUE "FileDescription", "desktop_drop_example" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "desktop_drop_example" "\0" - VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "desktop_drop_example.exe" "\0" VALUE "ProductName", "desktop_drop_example" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/packages/desktop_drop/example/windows/runner/flutter_window.cpp b/packages/desktop_drop/example/windows/runner/flutter_window.cpp index 39fab1f2..955ee303 100644 --- a/packages/desktop_drop/example/windows/runner/flutter_window.cpp +++ b/packages/desktop_drop/example/windows/runner/flutter_window.cpp @@ -27,6 +27,15 @@ bool FlutterWindow::OnCreate() { RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } diff --git a/packages/desktop_drop/example/windows/runner/main.cpp b/packages/desktop_drop/example/windows/runner/main.cpp index bb138453..325c44e0 100644 --- a/packages/desktop_drop/example/windows/runner/main.cpp +++ b/packages/desktop_drop/example/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"desktop_drop_example", origin, size)) { + if (!window.Create(L"desktop_drop_example", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/packages/desktop_drop/example/windows/runner/runner.exe.manifest b/packages/desktop_drop/example/windows/runner/runner.exe.manifest index c977c4a4..153653e8 100644 --- a/packages/desktop_drop/example/windows/runner/runner.exe.manifest +++ b/packages/desktop_drop/example/windows/runner/runner.exe.manifest @@ -7,14 +7,8 @@ - + - - - - - - diff --git a/packages/desktop_drop/example/windows/runner/utils.cpp b/packages/desktop_drop/example/windows/runner/utils.cpp index d19bdbbc..3a0b4651 100644 --- a/packages/desktop_drop/example/windows/runner/utils.cpp +++ b/packages/desktop_drop/example/windows/runner/utils.cpp @@ -45,18 +45,19 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } - int target_length = ::WideCharToMultiByte( + unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/packages/desktop_drop/example/windows/runner/win32_window.cpp b/packages/desktop_drop/example/windows/runner/win32_window.cpp index c10f08dc..60608d0f 100644 --- a/packages/desktop_drop/example/windows/runner/win32_window.cpp +++ b/packages/desktop_drop/example/windows/runner/win32_window.cpp @@ -1,13 +1,31 @@ #include "win32_window.h" +#include #include #include "resource.h" namespace { +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; @@ -31,8 +49,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); } + FreeLibrary(user32_module); } } // namespace @@ -42,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); @@ -102,9 +120,9 @@ Win32Window::~Win32Window() { Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { Destroy(); const wchar_t* window_class = @@ -117,7 +135,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); @@ -126,9 +144,15 @@ bool Win32Window::CreateAndShow(const std::wstring& title, return false; } + UpdateTheme(window); + return OnCreate(); } +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, @@ -188,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); @@ -243,3 +271,18 @@ bool Win32Window::OnCreate() { void Win32Window::OnDestroy() { // No-op; provided for subclasses. } + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/desktop_drop/example/windows/runner/win32_window.h b/packages/desktop_drop/example/windows/runner/win32_window.h index 17ba4311..e901dde6 100644 --- a/packages/desktop_drop/example/windows/runner/win32_window.h +++ b/packages/desktop_drop/example/windows/runner/win32_window.h @@ -28,15 +28,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); // Release OS resources associated with window. void Destroy(); @@ -76,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -86,6 +87,9 @@ class Win32Window { // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + bool quit_on_close_ = false; // window handle for top level window. diff --git a/packages/desktop_drop/lib/desktop_drop.dart b/packages/desktop_drop/lib/desktop_drop.dart index b0d30e1e..7aa37485 100644 --- a/packages/desktop_drop/lib/desktop_drop.dart +++ b/packages/desktop_drop/lib/desktop_drop.dart @@ -1,3 +1,5 @@ -export 'src/drop_target.dart'; -export 'src/drop_item.dart'; export 'src/channel.dart'; +export 'src/drop_item.dart'; +export 'src/drop_target.dart'; +export 'src/events.dart' show DropDoneEvent; +export 'src/text_drop_extensions.dart'; diff --git a/packages/desktop_drop/lib/desktop_drop_web.dart b/packages/desktop_drop/lib/desktop_drop_web.dart index e628572e..e02d9fe1 100644 --- a/packages/desktop_drop/lib/desktop_drop_web.dart +++ b/packages/desktop_drop/lib/desktop_drop_web.dart @@ -33,13 +33,16 @@ class DesktopDropWeb { // Calling it only once silently truncates directories with >100 files. // See: https://wicg.github.io/entries-api/#dom-filesystemdirectoryreader-readentries Future> _readAllEntries( - web.FileSystemDirectoryReader reader) async { + web.FileSystemDirectoryReader reader, + ) async { final allEntries = []; while (true) { final completer = Completer>(); - reader.readEntries((JSArray batch) { - completer.complete(batch.toDart); - }.toJS); + reader.readEntries( + (JSArray batch) { + completer.complete(batch.toDart); + }.toJS, + ); final batch = await completer.future; if (batch.isEmpty) break; allEntries.addAll(batch); @@ -54,11 +57,11 @@ class DesktopDropWeb { final List entries = await _readAllEntries(reader); - final children = await Future.wait( - entries.map((e) => _entryToWebDropItem(e)), - ) - ..removeWhere( - (element) => element.name == '.DS_Store' && element.type == ''); + final children = + await Future.wait(entries.map((e) => _entryToWebDropItem(e))) + ..removeWhere( + (element) => element.name == '.DS_Store' && element.type == '', + ); return WebDropItem( uri: web.URL.createObjectURL(web.Blob().slice(0, 0, 'directory')), @@ -100,11 +103,13 @@ class DesktopDropWeb { final items = event.dataTransfer!.items; - Future.wait(List.generate(items.length, (index) { - final item = items[index]; - final entry = item.webkitGetAsEntry()!; - return _entryToWebDropItem(entry); - })).then((webItems) { + Future.wait( + List.generate(items.length, (index) { + final item = items[index]; + final entry = item.webkitGetAsEntry()!; + return _entryToWebDropItem(entry); + }), + ).then((webItems) { channel.invokeMethod( "performOperation_web", webItems.map((e) => e.toJson()).toList(), diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart index 88e4fbca..1d17cba9 100644 --- a/packages/desktop_drop/lib/src/channel.dart +++ b/packages/desktop_drop/lib/src/channel.dart @@ -19,6 +19,11 @@ class DesktopDrop { final _listeners = {}; + // Buffer for app-wide drops (e.g., Dock/Finder on macOS) that may arrive + // before any widget listeners are registered. Delivered once to the next + // listener that registers, then cleared. + final List _pendingAppWideDrops = []; + var _inited = false; Offset? _offset; @@ -35,6 +40,17 @@ class DesktopDrop { debugPrint('_handleMethodChannel: $e $s'); } }); + + // Inform the macOS side that Dart is ready to receive global drop events. + // This allows the plugin to flush any queued Dock/Finder open events that + // may have arrived before the handler was installed. + if (UniversalPlatform.isMacOS) { + _channel.invokeMethod('readyForGlobalDrops').catchError((_) {}); + // Retry once on next frame in case the plugin registered after this call. + WidgetsBinding.instance.addPostFrameCallback((_) { + _channel.invokeMethod('readyForGlobalDrops').catchError((_) {}); + }); + } } /// macOS: Attempt to start security-scoped access for a bookmarked URL. @@ -45,13 +61,16 @@ class DesktopDrop { /// If [bookmark] is empty, this function returns `false` and does not /// invoke the platform call. Promise files written under your container do /// not require security-scoped access. - Future startAccessingSecurityScopedResource( - {required Uint8List bookmark}) async { + Future startAccessingSecurityScopedResource({ + required Uint8List bookmark, + }) async { if (bookmark.isEmpty) return false; Map resultMap = {}; resultMap["apple-bookmark"] = bookmark; final bool? result = await _channel.invokeMethod( - "startAccessingSecurityScopedResource", resultMap); + "startAccessingSecurityScopedResource", + resultMap, + ); if (result == null) return false; return result; } @@ -60,13 +79,16 @@ class DesktopDrop { /// /// If [bookmark] is empty, this function returns `true` and does not /// invoke the platform call, acting as a no-op. - Future stopAccessingSecurityScopedResource( - {required Uint8List bookmark}) async { + Future stopAccessingSecurityScopedResource({ + required Uint8List bookmark, + }) async { if (bookmark.isEmpty) return true; Map resultMap = {}; resultMap["apple-bookmark"] = bookmark; final bool result = await _channel.invokeMethod( - "stopAccessingSecurityScopedResource", resultMap); + "stopAccessingSecurityScopedResource", + resultMap, + ); return result; } @@ -104,30 +126,55 @@ class DesktopDrop { break; case "performOperation_macos": final items = (call.arguments as List).cast(); - _notifyEvent( - DropDoneEvent( - location: _offset ?? Offset.zero, - files: items.map((raw) { - final path = raw["path"] as String; - final bookmark = raw["apple-bookmark"] as Uint8List?; - final isDir = (raw["isDirectory"] as bool?) ?? false; - final fromPromise = (raw["fromPromise"] as bool?) ?? false; - if (isDir) { - return DropItemDirectory( - path, - const [], - extraAppleBookmark: bookmark, - fromPromise: fromPromise, - ); - } - return DropItemFile( + final event = DropDoneEvent( + location: _offset ?? Offset.zero, + files: items.map((raw) { + final data = raw["data"]; + if (data is Uint8List) { + final mime = + (raw["mimeType"] as String?) ?? 'application/octet-stream'; + final name = (raw["name"] as String?) ?? 'Dropped.data'; + final pseudoPath = + 'memory://drop/${DateTime.now().microsecondsSinceEpoch}/$name'; + return DropItemFile.fromData( + data, + name: name, + mimeType: mime, + length: data.lengthInBytes, + lastModified: DateTime.now(), + path: pseudoPath, + fromPromise: (raw["fromPromise"] as bool?) ?? false, + ); + } + + final path = raw["path"] as String; + final bookmark = raw["apple-bookmark"] as Uint8List?; + final isDir = (raw["isDirectory"] as bool?) ?? false; + final fromPromise = (raw["fromPromise"] as bool?) ?? false; + if (isDir) { + return DropItemDirectory( path, + const [], extraAppleBookmark: bookmark, fromPromise: fromPromise, ); - }).toList(), - ), + } + return DropItemFile( + path, + extraAppleBookmark: bookmark, + fromPromise: fromPromise, + ); + }).toList(), ); + _notifyEvent(event); + // If this was an application-wide drop (e.g., Dock/Finder on macOS) + // and no widget listeners were yet registered, buffer it so the first + // listener added can still receive it. + if (UniversalPlatform.isMacOS && event.location == Offset.zero) { + _pendingAppWideDrops + ..clear() + ..add(event); + } _offset = null; break; @@ -151,10 +198,12 @@ class DesktopDrop { } return ''; }).where((e) => e.isNotEmpty); - _notifyEvent(DropDoneEvent( - location: Offset(offset[0], offset[1]), - files: paths.map((e) => DropItemFile(e)).toList(), - )); + _notifyEvent( + DropDoneEvent( + location: Offset(offset[0], offset[1]), + files: paths.map((e) => DropItemFile(e)).toList(), + ), + ); break; case "performOperation_web": final results = (call.arguments as List) @@ -181,6 +230,16 @@ class DesktopDrop { void addRawDropEventListener(RawDropListener listener) { assert(!_listeners.contains(listener)); _listeners.add(listener); + // If there is a pending app-wide drop (e.g., Dock/Finder) that arrived + // before any widget registered, deliver it once to the newly added + // listener, then clear. This makes `catchAppWideDrops` widgets receive + // a launch-time drop even if they register slightly later. + if (UniversalPlatform.isMacOS && _pendingAppWideDrops.isNotEmpty) { + for (final e in _pendingAppWideDrops) { + listener(e); + } + _pendingAppWideDrops.clear(); + } } void removeRawDropEventListener(RawDropListener listener) { diff --git a/packages/desktop_drop/lib/src/drop_target.dart b/packages/desktop_drop/lib/src/drop_target.dart index 1ebf4675..8d337324 100644 --- a/packages/desktop_drop/lib/src/drop_target.dart +++ b/packages/desktop_drop/lib/src/drop_target.dart @@ -19,10 +19,7 @@ class DropDoneDetails { } class DropEventDetails { - DropEventDetails({ - required this.localPosition, - required this.globalPosition, - }); + DropEventDetails({required this.localPosition, required this.globalPosition}); final Offset localPosition; @@ -43,6 +40,7 @@ class DropTarget extends StatefulWidget { this.onDragDone, this.onDragUpdated, this.enable = true, + this.catchAppWideDrops = false, }); final Widget child; @@ -67,18 +65,22 @@ class DropTarget extends StatefulWidget { /// https://github.com/MixinNetwork/flutter-plugins/issues/2 final bool enable; + /// When true, this drop target will also receive application-wide drops + /// that did not hover the widget (e.g., drops on the app's Dock icon on macOS). + /// + /// If multiple `DropTarget`s set this to true, each will receive the drop. + /// Consider enabling it on a single primary target. + final bool catchAppWideDrops; + @override State createState() => _DropTargetState(); } -enum _DragTargetStatus { - enter, - update, - idle, -} +enum _DragTargetStatus { enter, update, idle } class _DropTargetState extends State { _DragTargetStatus _status = _DragTargetStatus.idle; + DropDoneEvent? _queuedAppWideDrop; @override void initState() { @@ -109,6 +111,18 @@ class _DropTargetState extends State { void _onDropEvent(DropEvent event) { final renderBox = context.findRenderObject() as RenderBox?; if (renderBox == null) { + // If a launch-time app-wide drop arrives before first layout, queue it + // and deliver right after the first frame so the widget can process it. + final isDockOrAppWideDrop = !UniversalPlatform.isLinux && + event is DropDoneEvent && + event.location == Offset.zero && + widget.catchAppWideDrops; + if (isDockOrAppWideDrop) { + _queuedAppWideDrop = event; + WidgetsBinding.instance.addPostFrameCallback( + (_) => _tryDeliverQueuedDrop(), + ); + } return; } final globalPosition = _scaleHoverPoint(context, event.location); @@ -153,21 +167,70 @@ class _DropTargetState extends State { globalLocation: globalPosition, localLocation: position, ); - } else if (event is DropDoneEvent && - (_status != _DragTargetStatus.idle || UniversalPlatform.isLinux) && - inBounds) { - _updateStatus( - _DragTargetStatus.idle, - debugRequiredStatus: false, - globalLocation: globalPosition, - localLocation: position, + } else if (event is DropDoneEvent) { + // Normal path: only deliver when hovered and released inside this widget. + final hoveredDrop = + (_status != _DragTargetStatus.idle || UniversalPlatform.isLinux) && + inBounds; + + // App-wide path (e.g., macOS Dock/Finder): no hover events were sent, + // so event.location is Offset.zero and _status remains idle. + final isDockOrAppWideDrop = + !UniversalPlatform.isLinux && event.location == Offset.zero; + + final shouldDeliver = + hoveredDrop || (widget.catchAppWideDrops && isDockOrAppWideDrop); + + if (shouldDeliver) { + // If not hovered/inBounds, synthesize a reasonable position: center. + final local = inBounds ? position : (renderBox.paintBounds.center); + final global = + inBounds ? globalPosition : renderBox.localToGlobal(local); + + _updateStatus( + _DragTargetStatus.idle, + debugRequiredStatus: false, + globalLocation: global, + localLocation: local, + ); + widget.onDragDone?.call( + DropDoneDetails( + files: event.files, + localPosition: local, + globalPosition: global, + ), + ); + } + } + } + + void _tryDeliverQueuedDrop() { + if (_queuedAppWideDrop == null) return; + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) { + // Still not laid out — try next frame. + WidgetsBinding.instance.addPostFrameCallback( + (_) => _tryDeliverQueuedDrop(), ); - widget.onDragDone?.call(DropDoneDetails( - files: event.files, - localPosition: position, - globalPosition: globalPosition, - )); + return; } + final local = renderBox.paintBounds.center; + final global = renderBox.localToGlobal(local); + final event = _queuedAppWideDrop!; + _queuedAppWideDrop = null; + _updateStatus( + _DragTargetStatus.idle, + debugRequiredStatus: false, + globalLocation: global, + localLocation: local, + ); + widget.onDragDone?.call( + DropDoneDetails( + files: event.files, + localPosition: local, + globalPosition: global, + ), + ); } void _updateStatus( diff --git a/packages/desktop_drop/lib/src/events.dart b/packages/desktop_drop/lib/src/events.dart index 193f6e2c..4eadb85a 100644 --- a/packages/desktop_drop/lib/src/events.dart +++ b/packages/desktop_drop/lib/src/events.dart @@ -27,10 +27,8 @@ class DropUpdateEvent extends DropEvent { class DropDoneEvent extends DropEvent { final List files; - DropDoneEvent({ - required Offset location, - required this.files, - }) : super(location); + DropDoneEvent({required Offset location, required this.files}) + : super(location); @override String toString() { diff --git a/packages/desktop_drop/lib/src/text_drop_extensions.dart b/packages/desktop_drop/lib/src/text_drop_extensions.dart new file mode 100644 index 00000000..27fbf5bf --- /dev/null +++ b/packages/desktop_drop/lib/src/text_drop_extensions.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'drop_item.dart'; + +/// Helpers for handling memory-backed text/link drops on macOS. +/// +/// Dock text/links are delivered as in-memory [DropItem]s with a pseudo-path +/// like `memory://...` and a text-like MIME type (e.g., `text/plain`, +/// `text/html`, `application/rtf`, or `text/uri-list`). +extension DropItemTextExtensions on DropItem { + /// True when this item is memory-backed (not a real filesystem path). + bool get isMemoryBacked => path.startsWith('memory://'); + + /// True when this item carries a text-like payload. + bool get isTextLike { + final m = mimeType ?? ''; + return m.startsWith('text/') || + m == 'text/uri-list' || + m == 'application/rtf'; + } + + /// Reads the item as a UTF-8 string if it appears to be text-like. + /// + /// For `text/uri-list`, the returned string may contain multiple URIs + /// separated by newlines. For `application/rtf`, this returns the raw + /// RTF content as text (no conversion to plain text is attempted). + Future readAsText({bool allowMalformed = true}) async { + if (!isMemoryBacked || !isTextLike) return null; + final bytes = await readAsBytes(); + return Utf8Decoder(allowMalformed: allowMalformed).convert(bytes); + } + + /// Parses a `text/uri-list` payload into URIs. Returns an empty list for + /// non-URI items. + Future> readAsUris() async { + if (mimeType != 'text/uri-list') return const []; + final s = await readAsText() ?? ''; + return s + .split('\n') + .map((line) => line.trim()) + .where((l) => l.isNotEmpty && !l.startsWith('#')) + .map(Uri.parse) + .toList(); + } +} diff --git a/packages/desktop_drop/macos/desktop_drop.podspec b/packages/desktop_drop/macos/desktop_drop.podspec index 5711b24b..abff2f9d 100644 --- a/packages/desktop_drop/macos/desktop_drop.podspec +++ b/packages/desktop_drop/macos/desktop_drop.podspec @@ -4,19 +4,20 @@ # Pod::Spec.new do |s| s.name = 'desktop_drop' - s.version = '0.0.1' - s.summary = 'A new flutter plugin project.' + s.version = '0.5.0' + s.summary = 'A plugin which allows user dragging files to your flutter desktop applications.' s.description = <<-DESC -A new flutter plugin project. +A plugin which allows user dragging files to your flutter desktop applications. +Supports files, folders, text, and URLs from Finder, Dock, and Chromium-based apps. DESC - s.homepage = 'http://example.com' + s.homepage = 'https://github.com/omar-hanafy/desktop_drop' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'MixinNetwork' => 'https://github.com/omar-hanafy' } s.source = { :path => '.' } s.source_files = 'desktop_drop/Sources/desktop_drop/**/*.{h,m,swift}' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.13' + s.platform = :osx, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/desktop_drop/macos/desktop_drop/Package.swift b/packages/desktop_drop/macos/desktop_drop/Package.swift index c5da455b..0bef624a 100644 --- a/packages/desktop_drop/macos/desktop_drop/Package.swift +++ b/packages/desktop_drop/macos/desktop_drop/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "desktop_drop", platforms: [ - .macOS("10.13") + .macOS(.v11) ], products: [ .library(name: "desktop-drop", targets: ["desktop_drop"]) diff --git a/packages/desktop_drop/macos/desktop_drop/Sources/desktop_drop/DesktopDropPlugin.swift b/packages/desktop_drop/macos/desktop_drop/Sources/desktop_drop/DesktopDropPlugin.swift index 5efe9e3f..6a6ee8b3 100644 --- a/packages/desktop_drop/macos/desktop_drop/Sources/desktop_drop/DesktopDropPlugin.swift +++ b/packages/desktop_drop/macos/desktop_drop/Sources/desktop_drop/DesktopDropPlugin.swift @@ -1,202 +1,620 @@ import Cocoa import FlutterMacOS +import Carbon -private func findFlutterViewController(_ viewController: NSViewController?) -> FlutterViewController? { - guard let vc = viewController else { - return nil - } - if let fvc = vc as? FlutterViewController { - return fvc - } - for child in vc.children { - let fvc = findFlutterViewController(child) - if fvc != nil { - return fvc - } - } - return nil -} +// ============================================================================= +// MARK: - DesktopDropServicesProvider (Dock Text/URL via NSServices) +// ============================================================================= + +/// Accepts text/link drops on the Dock icon via macOS Services. +/// +/// Host app should install this in `applicationWillFinishLaunching`: +/// ```swift +/// if NSApp.servicesProvider == nil, +/// let cls = NSClassFromString("DesktopDropServicesProvider") as? NSObject.Type { +/// NSApp.servicesProvider = cls.init() +/// } +/// ``` +@objc(DesktopDropServicesProvider) +public class DesktopDropServicesProvider: NSObject { + private static var pending: [[String: Any]] = [] + + private func enqueueAndPost(_ dict: [String: Any]) { + DesktopDropServicesProvider.pending.append(dict) + NotificationCenter.default.post( + name: .desktopDropServicePayload, + object: nil, + userInfo: ["items": [dict]] + ) + } + + /// Queried by the plugin to drain any pre-launch payloads. + @objc public func desktopDropFetchPendingServicePayloads() -> [Any] { + let copy = DesktopDropServicesProvider.pending + DesktopDropServicesProvider.pending.removeAll() + return copy + } -public class DesktopDropPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - guard let flutterView = registrar.view else { return } - guard let flutterWindow = flutterView.window else { return } - guard let vc = findFlutterViewController(flutterWindow.contentViewController) else { return } - - let channel = FlutterMethodChannel(name: "desktop_drop", binaryMessenger: registrar.messenger) - - let instance = DesktopDropPlugin() - - channel.setMethodCallHandler(instance.handle(_:result:)) - - let d = DropTarget(frame: vc.view.bounds, channel: channel) - d.autoresizingMask = [.width, .height] - - // Register for all relevant types (promises, URLs, and legacy filename arrays) - var types = NSFilePromiseReceiver.readableDraggedTypes.map { NSPasteboard.PasteboardType($0) } - types.append(.fileURL) // public.file-url - types.append(NSPasteboard.PasteboardType("NSFilenamesPboardType")) // legacy multi-file array - d.registerForDraggedTypes(types) - - vc.view.addSubview(d) - - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult){ - - if call.method == "startAccessingSecurityScopedResource"{ - let map = call.arguments as! NSDictionary - var isStale: Bool = false - - let bookmarkByte = map["apple-bookmark"] as! FlutterStandardTypedData - let bookmark = bookmarkByte.data - - let url = try? URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale) - let suc = url?.startAccessingSecurityScopedResource() - result(suc) + /// NSServices entry point (Info.plist: NSMessage = desktopDropAcceptDroppedText). + @objc public func desktopDropAcceptDroppedText( + _ pboard: NSPasteboard, + userData: String, + error: AutoreleasingUnsafeMutablePointer? + ) { + // Priority: HTML → RTF → URL → plain text + if let html = pboard.string(forType: .html), let data = html.data(using: .utf8) { + enqueueAndPost(DropUtils.memoryItem(data: data, mimeType: "text/html; charset=utf-8", name: "Dock Dropped Text.html")) + return + } + if let rtf = pboard.data(forType: .rtf) { + enqueueAndPost(DropUtils.memoryItem(data: rtf, mimeType: "application/rtf", name: "Dock Dropped Text.rtf")) + return + } + if let urlString = pboard.string(forType: .URL), let data = urlString.data(using: .utf8) { + enqueueAndPost(DropUtils.memoryItem(data: data, mimeType: "text/uri-list", name: "Dock Dropped URL.txt")) + return + } + if let s = pboard.string(forType: .string), let data = s.data(using: .utf8) { + enqueueAndPost(DropUtils.memoryItem(data: data, mimeType: "text/plain; charset=utf-8", name: "Dock Dropped Text.txt")) return - } - - if call.method == "stopAccessingSecurityScopedResource"{ - let map = call.arguments as! NSDictionary - var isStale: Bool = false - let bookmarkByte = map["apple-bookmark"] as! FlutterStandardTypedData - let bookmark = bookmarkByte.data - let url = try? URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale) + } + } +} + +// ============================================================================= +// MARK: - DropUtils (Centralized Helpers) +// ============================================================================= + +enum DropUtils { + + /// Shared queue for asynchronous file promise operations. + static let workQueue: OperationQueue = { + let queue = OperationQueue() + queue.qualityOfService = .userInitiated + return queue + }() + + /// Prepares a standardized dictionary for a file/directory item. + static func fileItem(for url: URL, fromPromise: Bool, seen: inout Set) -> [String: Any]? { + let path = url.path + + // De-duplicate by path + guard seen.insert(path).inserted else { return nil } + + let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false + let bookmark = createBookmarkIfNeeded(for: url, fromPromise: fromPromise) + + return [ + "path": path, + "apple-bookmark": bookmark, + "isDirectory": isDirectory, + "fromPromise": fromPromise, + ] + } + + /// Creates a standardized dictionary for memory-backed items (text, URLs). + static func memoryItem(data: Data, mimeType: String, name: String) -> [String: Any] { + return [ + "data": FlutterStandardTypedData(bytes: data), + "mimeType": mimeType, + "name": name, + "fromPromise": false, + ] + } + + /// Creates a security-scoped bookmark if needed (for files outside container). + private static func createBookmarkIfNeeded(for url: URL, fromPromise: Bool) -> Any { + // Promise files are written into our container/temp, no bookmark needed + if fromPromise { return NSNull() } + + // Files in temp directory are always accessible + if url.path.hasPrefix(FileManager.default.temporaryDirectory.path) { + return NSNull() + } + + // In sandboxed apps, NSHomeDirectory() points to container root. + // Files inside the container don't need bookmarks. + if url.path.hasPrefix(NSHomeDirectory()) { + return NSNull() + } + + // External files need security-scoped bookmarks + do { + let bookmark = try url.bookmarkData( + options: [.withSecurityScope], + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + return bookmark + } catch { + return NSNull() + } + } + + /// Generates a unique timestamped directory for promised files. + static func uniqueDropDestination() -> URL { + let base = FileManager.default.temporaryDirectory.appendingPathComponent("Drops", isDirectory: true) + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyyMMdd_HHmmss_SSS'Z'" + let stamp = formatter.string(from: Date()) + let dest = base.appendingPathComponent(stamp, isDirectory: true) + try? FileManager.default.createDirectory(at: dest, withIntermediateDirectories: true, attributes: nil) + return dest + } +} + +// ============================================================================= +// MARK: - Notification Name Extension +// ============================================================================= + +extension Notification.Name { + static let desktopDropServicePayload = Notification.Name("desktop_drop.servicePayload") +} + +// ============================================================================= +// MARK: - DesktopDropPlugin (Core Plugin) +// ============================================================================= + +public class DesktopDropPlugin: NSObject, FlutterPlugin, FlutterAppLifecycleDelegate { + + private var channel: FlutterMethodChannel! + private var pendingOpenItems: [[String: Any]] = [] + private var didFinishLaunching = false + private var dartReady = false + private var dropTargetInstalled = false + + // MARK: - Plugin Registration + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "desktop_drop", binaryMessenger: registrar.messenger) + let instance = DesktopDropPlugin() + instance.channel = channel + + channel.setMethodCallHandler(instance.handle(_:result:)) + registrar.addMethodCallDelegate(instance, channel: channel) + registrar.addApplicationDelegate(instance) + + instance.setup() + } + + private func setup() { + // Try to install drop target immediately + tryInstallDropTarget() + + // Observe app activation to install drop target if view wasn't ready + NotificationCenter.default.addObserver( + forName: NSApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.tryInstallDropTarget() + } + + // Observe window activation for multi-window scenarios + NotificationCenter.default.addObserver( + forName: NSWindow.didBecomeKeyNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.tryInstallDropTarget() + } + + // Flutter macOS registers plugins after launch, so assume launched + didFinishLaunching = true + drainPendingServicePayloads() + + // Observe runtime service payloads (Dock text drops) + NotificationCenter.default.addObserver( + forName: .desktopDropServicePayload, + object: nil, + queue: .main + ) { [weak self] note in + guard let self = self, + let items = note.userInfo?["items"] as? [[String: Any]] else { return } + self.pendingOpenItems.append(contentsOf: items) + self.flushPendingIfReady() + } + + // Handle AppleEvent for text dropped on Dock icon + NSAppleEventManager.shared().setEventHandler( + self, + andSelector: #selector(handleOpenContentsEvent(_:withReplyEvent:)), + forEventClass: AEEventClass(kCoreEventClass), + andEventID: AEEventID(kAEOpenContents) + ) + } + + // MARK: - Drop Target Installation + + private func tryInstallDropTarget() { + guard !dropTargetInstalled else { return } + guard let vc = findFlutterViewController() else { return } + + let dropTarget = DropTargetView(frame: vc.view.bounds, channel: channel) + dropTarget.autoresizingMask = [.width, .height] + dropTarget.registerForDrags() + vc.view.addSubview(dropTarget) + dropTargetInstalled = true + } + + private func findFlutterViewController() -> FlutterViewController? { + // Search all windows for a FlutterViewController + for window in NSApp.windows { + if let fvc = findFlutterVC(in: window.contentViewController) { + return fvc + } + } + // Fallback to key/main windows + if let fvc = findFlutterVC(in: NSApp.keyWindow?.contentViewController) { return fvc } + if let fvc = findFlutterVC(in: NSApp.mainWindow?.contentViewController) { return fvc } + return nil + } + + private func findFlutterVC(in viewController: NSViewController?) -> FlutterViewController? { + guard let vc = viewController else { return nil } + if let fvc = vc as? FlutterViewController { return fvc } + for child in vc.children { + if let fvc = findFlutterVC(in: child) { return fvc } + } + return nil + } + + // MARK: - Method Channel Handler + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "readyForGlobalDrops": + drainPendingServicePayloads() + dartReady = true + flushPendingIfReady() + result(true) + + case "startAccessingSecurityScopedResource": + guard let args = call.arguments as? [String: Any], + let bookmarkData = (args["apple-bookmark"] as? FlutterStandardTypedData)?.data else { + result(false) + return + } + var isStale = false + let url = try? URL( + resolvingBookmarkData: bookmarkData, + options: [.withSecurityScope], + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) + let success = url?.startAccessingSecurityScopedResource() ?? false + result(success) + + case "stopAccessingSecurityScopedResource": + guard let args = call.arguments as? [String: Any], + let bookmarkData = (args["apple-bookmark"] as? FlutterStandardTypedData)?.data else { + result(true) // No-op if missing + return + } + var isStale = false + let url = try? URL( + resolvingBookmarkData: bookmarkData, + options: [.withSecurityScope], + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) url?.stopAccessingSecurityScopedResource() result(true) - return - } - Swift.print("method not found: \(call.method)") - result(FlutterMethodNotImplemented) - return - } + default: + result(FlutterMethodNotImplemented) + } + } + + // MARK: - Pending Items Management + + private func flushPendingIfReady() { + guard didFinishLaunching, dartReady, !pendingOpenItems.isEmpty else { return } + channel.invokeMethod("performOperation_macos", arguments: pendingOpenItems) + pendingOpenItems.removeAll() + } + + // MARK: - Lifecycle (Dock/Finder Opens) + + public func handleDidFinishLaunching(_ notification: Notification) { + didFinishLaunching = true + drainPendingServicePayloads() + flushPendingIfReady() + } - + public func handleOpen(_ urls: [URL]) -> Bool { + var items: [[String: Any]] = [] + var seen = Set() + + for url in urls { + if let item = DropUtils.fileItem(for: url, fromPromise: false, seen: &seen) { + items.append(item) + } + } + + guard !items.isEmpty else { return false } + pendingOpenItems.append(contentsOf: items) + flushPendingIfReady() + return true + } + + // MARK: - Services & Apple Events + + private func drainPendingServicePayloads() { + let selector = #selector(DesktopDropServicesProvider.desktopDropFetchPendingServicePayloads) + + func fetch(from obj: Any?) { + guard let o = obj as? NSObject, + o.responds(to: selector), + let unmanaged = o.perform(selector), + let payloads = unmanaged.takeUnretainedValue() as? [[String: Any]] else { + return + } + pendingOpenItems.append(contentsOf: payloads) + } + + // Primary: Check the installed services provider (set by host app in AppDelegate) + fetch(from: NSApp.servicesProvider) + + // Fallback: Check NSApp itself (in case provider was attached differently) + fetch(from: NSApp) + } + + @objc private func handleOpenContentsEvent( + _ event: NSAppleEventDescriptor, + withReplyEvent reply: NSAppleEventDescriptor + ) { + guard let desc = event.paramDescriptor(forKeyword: keyDirectObject) else { return } + var items: [[String: Any]] = [] + + func processDescriptor(_ d: NSAppleEventDescriptor) { + if let s = d.stringValue, let data = s.data(using: .utf8) { + items.append(DropUtils.memoryItem( + data: data, + mimeType: "text/plain; charset=utf-8", + name: "Dock Dropped Text.txt" + )) + } + } + + if desc.descriptorType == typeAEList { + for i in 1...desc.numberOfItems { + if let item = desc.atIndex(i) { + processDescriptor(item) + } + } + } else { + processDescriptor(desc) + } + + guard !items.isEmpty else { return } + pendingOpenItems.append(contentsOf: items) + flushPendingIfReady() + } } -class DropTarget: NSView { - private let channel: FlutterMethodChannel - private let itemsLock = NSLock() - - init(frame frameRect: NSRect, channel: FlutterMethodChannel) { - self.channel = channel - super.init(frame: frameRect) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - channel.invokeMethod("entered", arguments: convertPoint(sender.draggingLocation)) - return .copy - } - - override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { - channel.invokeMethod("updated", arguments: convertPoint(sender.draggingLocation)) - return .copy - } - - override func draggingExited(_ sender: NSDraggingInfo?) { - channel.invokeMethod("exited", arguments: nil) - } - - /// Create a per-drop destination for promised files (avoids name collisions). - private func uniqueDropDestination() -> URL { - let base = FileManager.default.temporaryDirectory.appendingPathComponent("Drops", isDirectory: true) - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "yyyyMMdd_HHmmss_SSS'Z'" - let stamp = formatter.string(from: Date()) - let dest = base.appendingPathComponent(stamp, isDirectory: true) - try? FileManager.default.createDirectory(at: dest, withIntermediateDirectories: true, attributes: nil) - return dest - } - - /// Queue used for reading and writing file promises. - private lazy var workQueue: OperationQueue = { - let providerQueue = OperationQueue() - providerQueue.qualityOfService = .userInitiated - return providerQueue - }() - - override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { - let pb = sender.draggingPasteboard - let dest = uniqueDropDestination() - var items: [[String: Any]] = [] - var seen = Set() - let group = DispatchGroup() - - func push(url: URL, fromPromise: Bool) { - let path = url.path - itemsLock.lock(); defer { itemsLock.unlock() } - - // de-dupe safely under lock - if !seen.insert(path).inserted { return } - - let values = try? url.resourceValues(forKeys: [.isDirectoryKey]) - let isDirectory: Bool = values?.isDirectory ?? false - - // Only create a security-scoped bookmark for items outside our container. - let bundleID = Bundle.main.bundleIdentifier ?? "" - let containerRoot = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/Containers/\(bundleID)", isDirectory: true) - .path - let tmpPath = FileManager.default.temporaryDirectory.path - let isInsideContainer = path.hasPrefix(containerRoot) || path.hasPrefix(tmpPath) - - let bmData: Any - if isInsideContainer { - bmData = NSNull() - } else { - let bm = try? url.bookmarkData(options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil) - bmData = bm ?? NSNull() - } - items.append([ - "path": path, - "apple-bookmark": bmData, - "isDirectory": isDirectory, - "fromPromise": fromPromise, - ]) - } - - // Prefer real file URLs if they exist; only fall back to promises - let urls = (pb.readObjects(forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) as? [URL]) ?? [] - let legacyList = (pb.propertyList(forType: NSPasteboard.PasteboardType("NSFilenamesPboardType")) as? [String]) ?? [] - - if !urls.isEmpty || !legacyList.isEmpty { - // 1) Modern file URLs - urls.forEach { push(url: $0, fromPromise: false) } - // 2) Legacy filename array used by some apps - legacyList.forEach { push(url: URL(fileURLWithPath: $0), fromPromise: false) } - } else { - // 3) Handle file promises (e.g., VS Code, browsers, Mail) - if let receivers = pb.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver], - !receivers.isEmpty { - for r in receivers { - group.enter() - r.receivePromisedFiles(atDestination: dest, options: [:], operationQueue: self.workQueue) { url, error in - defer { group.leave() } - if let error = error { - debugPrint("NSFilePromiseReceiver error: \(error)") - return +// ============================================================================= +// MARK: - DropTargetView (In-Window Drag Handling) +// ============================================================================= + +class DropTargetView: NSView { + + private let channel: FlutterMethodChannel + + init(frame frameRect: NSRect, channel: FlutterMethodChannel) { + self.channel = channel + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Drag Registration + + func registerForDrags() { + var types: [NSPasteboard.PasteboardType] = [] + + // File promises + types.append(contentsOf: NSFilePromiseReceiver.readableDraggedTypes.map { NSPasteboard.PasteboardType($0) }) + + // File URLs (modern + legacy) + types.append(.fileURL) + types.append(NSPasteboard.PasteboardType("NSFilenamesPboardType")) + + // Text and links (for Chromium compatibility) + types.append(contentsOf: [.string, .html, .rtf, .URL]) + + registerForDraggedTypes(types) + } + + // MARK: - NSDraggingDestination + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + channel.invokeMethod("entered", arguments: convertPointForFlutter(sender.draggingLocation)) + return .copy + } + + override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { + channel.invokeMethod("updated", arguments: convertPointForFlutter(sender.draggingLocation)) + return .copy + } + + override func draggingExited(_ sender: NSDraggingInfo?) { + channel.invokeMethod("exited", arguments: nil) + } + + // MARK: - Perform Drag Operation + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + let pb = sender.draggingPasteboard + var items: [[String: Any]] = [] + var seen = Set() + + // ───────────────────────────────────────────────────────────────────── + // PRIORITY 1: Standard file URLs (public.file-url, NSFilenamesPboardType) + // ───────────────────────────────────────────────────────────────────── + let standardURLs = (pb.readObjects(forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) as? [URL]) ?? [] + let legacyPaths = (pb.propertyList(forType: NSPasteboard.PasteboardType("NSFilenamesPboardType")) as? [String]) ?? [] + + for url in standardURLs { + if let item = DropUtils.fileItem(for: url, fromPromise: false, seen: &seen) { + items.append(item) + } + } + + for path in legacyPaths { + let url = URL(fileURLWithPath: path) + if let item = DropUtils.fileItem(for: url, fromPromise: false, seen: &seen) { + items.append(item) } - push(url: url, fromPromise: true) - } } - } + + // ───────────────────────────────────────────────────────────────────── + // PRIORITY 1.5: Chromium/Electron workarounds (VS Code, Cursor, etc.) + // ───────────────────────────────────────────────────────────────────── + if items.isEmpty { + let chromiumURLs = extractChromiumPaths(from: pb) + + for url in chromiumURLs { + // Verify file exists and is accessible + if FileManager.default.fileExists(atPath: url.path), + let item = DropUtils.fileItem(for: url, fromPromise: false, seen: &seen) { + items.append(item) + } + } + } + + // Deliver if we found items in Priority 1 or 1.5 + if !items.isEmpty { + channel.invokeMethod("performOperation_macos", arguments: items) + return true + } + + // ───────────────────────────────────────────────────────────────────── + // PRIORITY 2: File promises (async fallback) + // ───────────────────────────────────────────────────────────────────── + if let receivers = pb.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver], + !receivers.isEmpty { + handleFilePromises(receivers: receivers, seen: seen) + return true + } + + // ───────────────────────────────────────────────────────────────────── + // PRIORITY 3 & 4: Non-file content (URLs, text) + // ───────────────────────────────────────────────────────────────────── + items.append(contentsOf: extractNonFileContent(from: pb)) + + if !items.isEmpty { + channel.invokeMethod("performOperation_macos", arguments: items) + return true + } + + return false + } + + // MARK: - Chromium Path Extraction + + private func extractChromiumPaths(from pb: NSPasteboard) -> [URL] { + var urls: [URL] = [] + + // 1. Check plain text for newline-separated paths (multi-file support) + if let plainText = pb.string(forType: .string) { + let lines = plainText + .components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + + for line in lines { + if line.hasPrefix("/") { + urls.append(URL(fileURLWithPath: line)) + } else if line.hasPrefix("file://"), + let url = URL(string: line), + url.isFileURL { + urls.append(url) + } + } + } + + // 2. Fallback: public.url (single file) + if urls.isEmpty, + let urlString = pb.string(forType: .URL), + let url = URL(string: urlString), + url.isFileURL { + urls.append(url) + } + + // 3. Fallback: promised-file-url + if urls.isEmpty, + let promisedURLString = pb.string(forType: NSPasteboard.PasteboardType("com.apple.pasteboard.promised-file-url")), + let url = URL(string: promisedURLString), + url.isFileURL { + urls.append(url) + } + + return urls + } + + // MARK: - File Promise Handling + + private func handleFilePromises(receivers: [NSFilePromiseReceiver], seen: Set) { + let group = DispatchGroup() + let dest = DropUtils.uniqueDropDestination() + var items: [[String: Any]] = [] + let lock = NSLock() + var mutableSeen = seen + + for receiver in receivers { + group.enter() + receiver.receivePromisedFiles(atDestination: dest, options: [:], operationQueue: DropUtils.workQueue) { url, error in + defer { group.leave() } + + if error != nil { return } + + lock.lock() + if let item = DropUtils.fileItem(for: url, fromPromise: true, seen: &mutableSeen) { + items.append(item) + } + lock.unlock() + } + } + + group.notify(queue: .main) { [weak self] in + self?.channel.invokeMethod("performOperation_macos", arguments: items) + } } - group.notify(queue: .main) { - self.channel.invokeMethod("performOperation_macos", arguments: items) + // MARK: - Non-File Content Extraction + + private func extractNonFileContent(from pb: NSPasteboard) -> [[String: Any]] { + var items: [[String: Any]] = [] + + // Non-file URLs + let anyURLs = (pb.readObjects(forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: false]) as? [URL]) ?? [] + for url in anyURLs where !url.isFileURL { + if let data = url.absoluteString.data(using: .utf8) { + items.append(DropUtils.memoryItem(data: data, mimeType: "text/uri-list", name: "Dropped URL.txt")) + } + } + + // Text formats (only if no URLs found) + if items.isEmpty { + if let html = pb.string(forType: .html), let data = html.data(using: .utf8) { + items.append(DropUtils.memoryItem(data: data, mimeType: "text/html; charset=utf-8", name: "Dropped Text.html")) + } else if let rtf = pb.data(forType: .rtf) { + items.append(DropUtils.memoryItem(data: rtf, mimeType: "application/rtf", name: "Dropped Text.rtf")) + } else if let s = pb.string(forType: .string), let data = s.data(using: .utf8) { + items.append(DropUtils.memoryItem(data: data, mimeType: "text/plain; charset=utf-8", name: "Dropped Text.txt")) + } + } + + return items } - return true - } - func convertPoint(_ location: NSPoint) -> [CGFloat] { - return [location.x, bounds.height - location.y] - } + // MARK: - Coordinate Conversion + + private func convertPointForFlutter(_ location: NSPoint) -> [CGFloat] { + return [location.x, bounds.height - location.y] + } } diff --git a/packages/desktop_drop/pubspec.yaml b/packages/desktop_drop/pubspec.yaml index 73d48ae5..4d67a4ae 100644 --- a/packages/desktop_drop/pubspec.yaml +++ b/packages/desktop_drop/pubspec.yaml @@ -1,7 +1,7 @@ name: desktop_drop resolution: workspace description: A plugin which allows user dragging files to your flutter desktop applications. -version: 0.7.1 +version: 0.8.0 homepage: https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_drop environment: