diff --git a/packages/share_plus/share_plus/README.md b/packages/share_plus/share_plus/README.md index 282bea03a7..17211bd542 100644 --- a/packages/share_plus/share_plus/README.md +++ b/packages/share_plus/share_plus/README.md @@ -14,11 +14,11 @@ on iOS, or equivalent platform content sharing methods. ## Platform Support -| Method | Android | iOS | MacOS | Web | Linux | Windows | -| :-----------: | :-----: | :-: | :---: | :-: | :---: | :----: | -| `share` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `shareUri` | ✅ | ✅ | | | | | -| `shareXFiles` | ✅ | ✅ | ✅ | ✅ | | ✅ | +| Shared content | Android | iOS | MacOS | Web | Linux | Windows | +| :------------: | :-----: | :-: | :---: | :-: | :---: | :-----: | +| Text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| URI | ✅ | ✅ | ✅ | As text | As text | As text | +| Files | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | Also compatible with Windows and Linux by using "mailto" to share text via Email. @@ -47,23 +47,30 @@ import 'package:share_plus/share_plus.dart'; ### Share Text -Invoke the static `share()` method anywhere in your Dart code. +Access the `SharePlus` instance via `SharePlus.instance`. +Then, invoke the `share()` method anywhere in your Dart code. ```dart -Share.share('check out my website https://example.com'); +SharePlus.instance.share( + ShareParams(text: 'check out my website https://example.com') +); ``` -The `share` method also takes an optional `subject` that will be used when -sharing to email. +The `share()` method requires the `ShareParams` object, +which contains the content to share. -```dart -Share.share('check out my website https://example.com', subject: 'Look what I made!'); -``` +These are some of the accepted parameters of the `ShareParams` class: + +- `text`: text to share. +- `title`: content or share-sheet title (if supported). +- `subject`: email subject (if supported). + +Check the class documentation for more details. `share()` returns `status` object that allows to check the result of user action in the share sheet. ```dart -final result = await Share.share('check out my website https://example.com'); +final result = await SharePlus.instance.share(params); if (result.status == ShareResultStatus.success) { print('Thank you for sharing my website!'); @@ -72,10 +79,16 @@ if (result.status == ShareResultStatus.success) { ### Share Files -To share one or multiple files, invoke the static `shareXFiles` method anywhere in your Dart code. The method returns a `ShareResult`. Optionally, you can pass `subject`, `text` and `sharePositionOrigin`. +To share one or multiple files, provide the `files` list in `ShareParams`. +Optionally, you can pass `title`, `text` and `sharePositionOrigin`. ```dart -final result = await Share.shareXFiles([XFile('${directory.path}/image.jpg')], text: 'Great picture'); +final params = ShareParams( + text: 'Great picture', + files: [XFile('${directory.path}/image.jpg')], +); + +final result = await SharePlus.instance.share(params); if (result.status == ShareResultStatus.success) { print('Thank you for sharing the picture!'); @@ -83,7 +96,14 @@ if (result.status == ShareResultStatus.success) { ``` ```dart -final result = await Share.shareXFiles([XFile('${directory.path}/image1.jpg'), XFile('${directory.path}/image2.jpg')]); +final params = ShareParams( + files: [ + XFile('${directory.path}/image1.jpg'), + XFile('${directory.path}/image2.jpg'), + ], +); + +final result = await SharePlus.instance.share(params); if (result.status == ShareResultStatus.dismissed) { print('Did you not like the pictures?'); @@ -96,15 +116,13 @@ See [Can I Use - Web Share API](https://caniuse.com/web-share) to understand which browsers are supported. This builds on the [`cross_file`](https://pub.dev/packages/cross_file) package. - -```dart -Share.shareXFiles([XFile('assets/hello.txt')], text: 'Great picture'); -``` - File downloading fallback mechanism for web can be disabled by setting: ```dart -Share.downloadFallbackEnabled = false; +ShareParams( + // rest of params + downloadFallbackEnabled: false, +) ``` #### Share Data @@ -114,7 +132,12 @@ You can also share files that you dynamically generate from its data using [`XFi To set the name of such files, use the `fileNameOverrides` parameter, otherwise the file name will be a random UUID string. ```dart -Share.shareXFiles([XFile.fromData(utf8.encode(text), mimeType: 'text/plain')], fileNameOverrides: ['myfile.txt']); +final params = ShareParams( + files: [XFile.fromData(utf8.encode(text), mimeType: 'text/plain')], + fileNameOverrides: ['myfile.txt'] +); + +SharePlus.instance.share(params); ``` > [!CAUTION] @@ -123,10 +146,13 @@ Share.shareXFiles([XFile.fromData(utf8.encode(text), mimeType: 'text/plain')], f ### Share URI iOS supports fetching metadata from a URI when shared using `UIActivityViewController`. -This special method is only properly supported on iOS. +This special functionality is only properly supported on iOS. +On other platforms, the URI will be shared as plain text. ```dart -Share.shareUri(uri: uri); +final params = ShareParams(uri: uri); + +SharePlus.instance.share(params); ``` ### Share Results @@ -201,15 +227,52 @@ Builder( // _onShare method: final box = context.findRenderObject() as RenderBox?; -await Share.share( - text, - subject: subject, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, +await SharePlus.instance.share( + ShareParams( + text: text, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ) ); ``` See the `main.dart` in the `example` for a complete example. +## Migrating from `Share.share()` to `SharePlus.instance.share()` + +The static methods `Share.share()`, `Share.shareUri()` and `Share.shareXFiles()` +have been deprecated in favor of the `SharePlus.instance.share(params)`. + +To convert code using `Share.share()` to the new `SharePlus` class: + +1. Wrap the current parameters in a `ShareParams` object. +2. Change the call to `SharePlus.instance.share()`. + +e.g. + +```dart +Share.share("Shared text"); + +Share.shareUri("http://example.com"); + +Share.shareXFiles(files); +``` + +Becomes: + +```dart +SharePlus.instance.share( + ShareParams(text: "Shared text"), +); + +SharePlus.instance.share( + ShareParams(uri: "http://example.com"), +); + +SharePlus.instance.share( + ShareParams(files: files), +); +``` + ## Learn more - [API Documentation](https://pub.dev/documentation/share_plus/latest/share_plus/share_plus-library.html) diff --git a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt index d4488922e5..9b3341c8be 100644 --- a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt +++ b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt @@ -1,10 +1,8 @@ package dev.fluttercommunity.plus.share import android.os.Build -import io.flutter.BuildConfig import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import java.io.IOException /** Handles the method calls for the plugin. */ internal class MethodCallHandler( @@ -24,35 +22,13 @@ internal class MethodCallHandler( try { when (call.method) { - "shareUri" -> { - share.share( - call.argument("uri") as String, - subject = null, - withResult = isWithResult, - ) - success(isWithResult, result) - } - "share" -> { share.share( - call.argument("text") as String, - call.argument("subject") as String?, - isWithResult, - ) - success(isWithResult, result) - } - - "shareFiles" -> { - share.shareFiles( - call.argument>("paths")!!, - call.argument?>("mimeTypes"), - call.argument("text"), - call.argument("subject"), - isWithResult, + arguments = call.arguments>()!!, + withResult = isWithResult, ) success(isWithResult, result) } - else -> result.notImplemented() } } catch (e: Throwable) { diff --git a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt index 59bdb1e914..b56a76f8f8 100644 --- a/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt +++ b/packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt @@ -55,83 +55,71 @@ internal class Share( this.activity = activity } - fun share(text: String, subject: String?, withResult: Boolean) { - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, text) - if (subject != null) { - putExtra(Intent.EXTRA_SUBJECT, subject) - } - } - // If we dont want the result we use the old 'createChooser' - val chooserIntent = - if (withResult && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - // Build chooserIntent with broadcast to ShareSuccessManager on success - Intent.createChooser( - shareIntent, - null, // dialog title optional - PendingIntent.getBroadcast( - context, - 0, - Intent(context, SharePlusPendingIntent::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or immutabilityIntentFlags - ).intentSender - ) - } else { - Intent.createChooser(shareIntent, null /* dialog title optional */) - } - startActivity(chooserIntent, withResult) - } - @Throws(IOException::class) - fun shareFiles( - paths: List, - mimeTypes: List?, - text: String?, - subject: String?, - withResult: Boolean - ) { + fun share(arguments: Map, withResult: Boolean) { clearShareCacheFolder() - val fileUris = getUrisForPaths(paths) + + val text = arguments["text"] as String? + val uri = arguments["uri"] as String? + val subject = arguments["subject"] as String? + val title = arguments["title"] as String? + val paths = (arguments["paths"] as List<*>?)?.filterIsInstance() + val mimeTypes = (arguments["mimeTypes"] as List<*>?)?.filterIsInstance() + val fileUris = paths?.let { getUrisForPaths(paths) } + + // Create Share Intent val shareIntent = Intent() - when { - (fileUris.isEmpty() && !text.isNullOrBlank()) -> { - share(text, subject, withResult) - return + if (fileUris == null) { + shareIntent.apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, uri ?: text) + if (subject != null) putExtra(Intent.EXTRA_SUBJECT, subject) + if (title != null) putExtra(Intent.EXTRA_TITLE, title) } + } else { + when { + fileUris.isEmpty() -> { + throw IOException("Error sharing files: No files found") + } - fileUris.size == 1 -> { - val mimeType = if (!mimeTypes.isNullOrEmpty()) { - mimeTypes.first() - } else { - "*/*" + fileUris.size == 1 -> { + val mimeType = if (!mimeTypes.isNullOrEmpty()) { + mimeTypes.first() + } else { + "*/*" + } + shareIntent.apply { + action = Intent.ACTION_SEND + type = mimeType + putExtra(Intent.EXTRA_STREAM, fileUris.first()) + } } - shareIntent.apply { - action = Intent.ACTION_SEND - type = mimeType - putExtra(Intent.EXTRA_STREAM, fileUris.first()) + + else -> { + shareIntent.apply { + action = Intent.ACTION_SEND_MULTIPLE + type = reduceMimeTypes(mimeTypes) + putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris) + } } } - else -> { - shareIntent.apply { - action = Intent.ACTION_SEND_MULTIPLE - type = reduceMimeTypes(mimeTypes) - putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris) - } + shareIntent.apply { + if (text != null) putExtra(Intent.EXTRA_TEXT, text) + if (subject != null) putExtra(Intent.EXTRA_SUBJECT, subject) + if (title != null) putExtra(Intent.EXTRA_TITLE, title) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } } - if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text) - if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject) - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - // If we dont want the result we use the old 'createChooser' + + // Create the chooser intent val chooserIntent = if (withResult && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { // Build chooserIntent with broadcast to ShareSuccessManager on success Intent.createChooser( shareIntent, - null, // dialog title optional + title, PendingIntent.getBroadcast( context, 0, @@ -140,21 +128,27 @@ internal class Share( ).intentSender ) } else { - Intent.createChooser(shareIntent, null /* dialog title optional */) + Intent.createChooser(shareIntent, title) } - val resInfoList = getContext().packageManager.queryIntentActivities( - chooserIntent, PackageManager.MATCH_DEFAULT_ONLY - ) - resInfoList.forEach { resolveInfo -> - val packageName = resolveInfo.activityInfo.packageName - fileUris.forEach { fileUri -> - getContext().grantUriPermission( - packageName, - fileUri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION, - ) + + // Grant permissions to all apps that can handle the files shared + if (fileUris != null) { + val resInfoList = getContext().packageManager.queryIntentActivities( + chooserIntent, PackageManager.MATCH_DEFAULT_ONLY + ) + resInfoList.forEach { resolveInfo -> + val packageName = resolveInfo.activityInfo.packageName + fileUris.forEach { fileUri -> + getContext().grantUriPermission( + packageName, + fileUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION, + ) + } } } + + // Launch share intent startActivity(chooserIntent, withResult) } diff --git a/packages/share_plus/share_plus/example/integration_test/share_plus_test.dart b/packages/share_plus/share_plus/example/integration_test/share_plus_test.dart index 18caa757f1..129637cd6e 100644 --- a/packages/share_plus/share_plus/example/integration_test/share_plus_test.dart +++ b/packages/share_plus/share_plus/example/integration_test/share_plus_test.dart @@ -1,6 +1,7 @@ // Copyright 2019, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. + import 'dart:io'; import 'package:flutter/services.dart'; @@ -12,13 +13,20 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Can launch share', (WidgetTester tester) async { + final params = ShareParams( + text: 'message', + subject: 'title', + ); // Check isNotNull because we cannot wait for ShareResult - expect(Share.share('message', subject: 'title'), isNotNull); + expect(SharePlus.instance.share(params), isNotNull); }); testWidgets('Can launch shareUri', (WidgetTester tester) async { + final params = ShareParams( + uri: Uri.parse('https://example.com'), + ); // Check isNotNull because we cannot wait for ShareResult - expect(Share.shareUri(Uri.parse('https://example.com')), isNotNull); + expect(SharePlus.instance.share(params), isNotNull); }, skip: !Platform.isAndroid && !Platform.isIOS); testWidgets('Can shareXFile created using File.fromData()', @@ -27,6 +35,10 @@ void main() { final XFile file = XFile.fromData(bytes, name: 'image.jpg', mimeType: 'image/jpeg'); - expect(Share.shareXFiles([file], text: "example"), isNotNull); + final params = ShareParams( + files: [file], + text: 'message', + ); + expect(SharePlus.instance.share(params), isNotNull); }); } diff --git a/packages/share_plus/share_plus/example/ios/Runner/AppDelegate.swift b/packages/share_plus/share_plus/example/ios/Runner/AppDelegate.swift index 70693e4a8c..b636303481 100644 --- a/packages/share_plus/share_plus/example/ios/Runner/AppDelegate.swift +++ b/packages/share_plus/share_plus/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 913a6e33ce..e698caa98c 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -19,10 +19,6 @@ import 'package:share_plus/share_plus.dart'; import 'image_previews.dart'; void main() { - // Set `downloadFallbackEnabled` to `false` - // to disable downloading files if `shareXFiles` fails on web. - Share.downloadFallbackEnabled = true; - runApp(const DemoApp()); } @@ -36,6 +32,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; + String title = ''; String uri = ''; String fileName = ''; List imageNames = []; @@ -84,6 +81,18 @@ class DemoAppState extends State { }), ), const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Share title', + hintText: 'Enter title to share (optional)', + ), + maxLines: null, + onChanged: (String value) => setState(() { + title = value; + }), + ), + const SizedBox(height: 16), TextField( decoration: const InputDecoration( border: OutlineInputBorder(), @@ -221,22 +230,32 @@ class DemoAppState extends State { for (var i = 0; i < imagePaths.length; i++) { files.add(XFile(imagePaths[i], name: imageNames[i])); } - shareResult = await Share.shareXFiles( - files, - text: text, - subject: subject, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + shareResult = await SharePlus.instance.share( + ShareParams( + text: text.isEmpty ? null : text, + subject: subject.isEmpty ? null : subject, + title: title.isEmpty ? null : title, + files: files, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); } else if (uri.isNotEmpty) { - shareResult = await Share.shareUri( - Uri.parse(uri), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + shareResult = await SharePlus.instance.share( + ShareParams( + uri: Uri.parse(uri), + subject: subject.isEmpty ? null : subject, + title: title.isEmpty ? null : title, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); } else { - shareResult = await Share.share( - text, - subject: subject, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + shareResult = await SharePlus.instance.share( + ShareParams( + text: text.isEmpty ? null : text, + subject: subject.isEmpty ? null : subject, + title: title.isEmpty ? null : title, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); } scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); @@ -248,15 +267,18 @@ class DemoAppState extends State { try { final data = await rootBundle.load('assets/flutter_logo.png'); final buffer = data.buffer; - final shareResult = await Share.shareXFiles( - [ - XFile.fromData( - buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), - name: 'flutter_logo.png', - mimeType: 'image/png', - ), - ], - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + final shareResult = await SharePlus.instance.share( + ShareParams( + files: [ + XFile.fromData( + buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), + name: 'flutter_logo.png', + mimeType: 'image/png', + ), + ], + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + downloadFallbackEnabled: true, + ), ); scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); } catch (e) { @@ -271,16 +293,19 @@ class DemoAppState extends State { final scaffoldMessenger = ScaffoldMessenger.of(context); try { - final shareResult = await Share.shareXFiles( - [ - XFile.fromData( - utf8.encode(text), - // name: fileName, // Notice, how setting the name here does not work. - mimeType: 'text/plain', - ), - ], - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - fileNameOverrides: [fileName], + final shareResult = await SharePlus.instance.share( + ShareParams( + files: [ + XFile.fromData( + utf8.encode(text), + // name: fileName, // Notice, how setting the name here does not work. + mimeType: 'text/plain', + ), + ], + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + fileNameOverrides: [fileName], + downloadFallbackEnabled: true, + ), ); scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); diff --git a/packages/share_plus/share_plus/example/windows/flutter/CMakeLists.txt b/packages/share_plus/share_plus/example/windows/flutter/CMakeLists.txt index 744f08a938..0a91777227 100644 --- a/packages/share_plus/share_plus/example/windows/flutter/CMakeLists.txt +++ b/packages/share_plus/share_plus/example/windows/flutter/CMakeLists.txt @@ -9,6 +9,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") @@ -90,7 +95,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/share_plus/share_plus/example/windows/runner/Runner.rc b/packages/share_plus/share_plus/example/windows/runner/Runner.rc index 9d72c23ad7..df5874e2c5 100644 --- a/packages/share_plus/share_plus/example/windows/runner/Runner.rc +++ b/packages/share_plus/share_plus/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif diff --git a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m index a3afbfa5c3..035abe65a8 100644 --- a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m +++ b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m @@ -247,113 +247,127 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [FlutterMethodChannel methodChannelWithName:PLATFORM_CHANNEL binaryMessenger:registrar.messenger]; - [shareChannel - setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - NSDictionary *arguments = [call arguments]; - NSNumber *originX = arguments[@"originX"]; - NSNumber *originY = arguments[@"originY"]; - NSNumber *originWidth = arguments[@"originWidth"]; - NSNumber *originHeight = arguments[@"originHeight"]; - - CGRect originRect = CGRectZero; - if (originX && originY && originWidth && originHeight) { - originRect = - CGRectMake([originX doubleValue], [originY doubleValue], - [originWidth doubleValue], [originHeight doubleValue]); - } + [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, + FlutterResult result) { + NSDictionary *arguments = [call arguments]; + NSNumber *originX = arguments[@"originX"]; + NSNumber *originY = arguments[@"originY"]; + NSNumber *originWidth = arguments[@"originWidth"]; + NSNumber *originHeight = arguments[@"originHeight"]; + + CGRect originRect = CGRectZero; + if (originX && originY && originWidth && originHeight) { + originRect = + CGRectMake([originX doubleValue], [originY doubleValue], + [originWidth doubleValue], [originHeight doubleValue]); + } - if ([@"share" isEqualToString:call.method]) { - NSString *shareText = arguments[@"text"]; - NSString *shareSubject = arguments[@"subject"]; + if ([@"share" isEqualToString:call.method]) { + NSString *shareText = arguments[@"text"]; + NSArray *paths = arguments[@"paths"]; + NSArray *mimeTypes = arguments[@"mimeTypes"]; + NSString *uri = arguments[@"uri"]; + + // Use title field for consistency with Android. + // Subject field should only be used on email subjects. + NSString *shareTitle = arguments[@"title"]; + if (!shareTitle) { + // fallback to be backwards compatible with the subject field. + shareTitle = arguments[@"subject"]; + } - if (shareText.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty text expected" - details:nil]); - return; - } + // Check if text provided is valid + if (shareText && shareText.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty text expected" + details:nil]); + return; + } - UIViewController *rootViewController = RootViewController(); - if (!rootViewController) { - result([FlutterError errorWithCode:@"error" - message:@"No root view controller found" - details:nil]); - return; - } - UIViewController *topViewController = - TopViewControllerForViewController(rootViewController); - - [self shareText:shareText - subject:shareSubject - withController:topViewController - atSource:originRect - toResult:result]; - } else if ([@"shareFiles" isEqualToString:call.method]) { - NSArray *paths = arguments[@"paths"]; - NSArray *mimeTypes = arguments[@"mimeTypes"]; - NSString *subject = arguments[@"subject"]; - NSString *text = arguments[@"text"]; - - if (paths.count == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty paths expected" - details:nil]); - return; - } + // Check if title provided is valid + if (shareTitle && shareTitle.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty title expected" + details:nil]); + return; + } - for (NSString *path in paths) { - if (path.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Each path must not be empty" - details:nil]); - return; - } - } + // Check if uri provided is valid + if (uri && uri.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty uri expected" + details:nil]); + return; + } - UIViewController *rootViewController = RootViewController(); - if (!rootViewController) { - result([FlutterError errorWithCode:@"error" - message:@"No root view controller found" - details:nil]); - return; - } - UIViewController *topViewController = - TopViewControllerForViewController(rootViewController); - [self shareFiles:paths - withMimeType:mimeTypes - withSubject:subject - withText:text - withController:topViewController - atSource:originRect - toResult:result]; - } else if ([@"shareUri" isEqualToString:call.method]) { - NSString *uri = arguments[@"uri"]; - - if (uri.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty uri expected" - details:nil]); - return; - } + // Check if files provided are valid + if (paths) { + // If paths provided, it should not be empty + if (paths.count == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty paths expected" + details:nil]); + return; + } - UIViewController *rootViewController = RootViewController(); - if (!rootViewController) { + // If paths provided, paths should not be empty + for (NSString *path in paths) { + if (path.length == 0) { result([FlutterError errorWithCode:@"error" - message:@"No root view controller found" + message:@"Each path must not be empty" details:nil]); return; } - UIViewController *topViewController = - TopViewControllerForViewController(rootViewController); - - [self shareUri:uri - withController:topViewController - atSource:originRect - toResult:result]; - } else { - result(FlutterMethodNotImplemented); } - }]; + + if (mimeTypes && mimeTypes.count != paths.count) { + result([FlutterError + errorWithCode:@"error" + message:@"Paths and mimeTypes should have same length" + details:nil]); + return; + } + } + + // Check if root view controller is valid + UIViewController *rootViewController = RootViewController(); + if (!rootViewController) { + result([FlutterError errorWithCode:@"error" + message:@"No root view controller found" + details:nil]); + return; + } + UIViewController *topViewController = + TopViewControllerForViewController(rootViewController); + + if (uri) { + [self shareUri:uri + withController:topViewController + atSource:originRect + toResult:result]; + } else if (paths) { + [self shareFiles:paths + withMimeType:mimeTypes + withSubject:shareTitle + withText:shareText + withController:rootViewController + atSource:originRect + toResult:result]; + } else if (shareText) { + [self shareText:shareText + subject:shareTitle + withController:rootViewController + atSource:originRect + toResult:result]; + } else { + result([FlutterError errorWithCode:@"error" + message:@"No share content provided" + details:nil]); + } + } else { + result(FlutterMethodNotImplemented); + } + }]; } + (void)share:(NSArray *)shareItems diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index e6e2f64a8b..904a548016 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -2,23 +2,106 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:ui'; +import 'package:meta/meta.dart'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; export 'package:share_plus_platform_interface/share_plus_platform_interface.dart' - show ShareResult, ShareResultStatus, XFile; + show ShareResult, ShareResultStatus, XFile, ShareParams; export 'src/share_plus_linux.dart'; export 'src/share_plus_windows.dart' if (dart.library.js_interop) 'src/share_plus_web.dart'; -/// Plugin for summoning a platform share sheet. -class Share { - static SharePlatform get _platform => SharePlatform.instance; +class SharePlus { + /// Use [SharePlus.instance] to access the [share] method. + SharePlus._(this._platform); + + /// Platform interface + final SharePlatform _platform; + + /// The default instance of [SharePlus]. + static final SharePlus instance = SharePlus._(SharePlatform.instance); + + /// Create a custom instance of [SharePlus]. + /// Use this constructor for testing purposes only. + @visibleForTesting + factory SharePlus.custom(SharePlatform platform) => SharePlus._(platform); + + /// Summons the platform's share sheet to share context. + /// + /// Wraps the platform's native share dialog. Can share a text and/or a URL. + /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` + /// on iOS. + /// + /// When no native share dialog is available, + /// it will fall back to using mailto to share the content as email. + /// + /// Returns [ShareResult] when the action completes. + /// + /// * [ShareResult.success] when the user selected a share action. + /// * [ShareResult.dismissed] when the user dismissed the share sheet. + /// * [ShareResult.unavailable] when the share result is not available. + /// + /// Providing result is only supported on Android, iOS and macOS. + /// + /// To avoid deadlocks on Android, + /// any new call to [share] when there is a call pending, + /// will cause the previous call to return a [ShareResult.unavailable]. + /// + /// Because IOS, Android and macOS provide different feedback on share-sheet + /// interaction, a result on IOS will be more specific than on Android or macOS. + /// While on IOS the selected action can inform its caller that it was completed + /// or dismissed midway (_actions are free to return whatever they want_), + /// Android and macOS only record if the user selected an action or outright + /// dismissed the share-sheet. It is not guaranteed that the user actually shared + /// something. + /// + /// Will gracefully fall back to the non result variant if not implemented + /// for the current environment and return [ShareResult.unavailable]. + /// + /// See [ShareParams] for more information on what can be shared. + /// Throws [ArgumentError] if [ShareParams] are invalid. + /// + /// Throws other types of exceptions if the share method fails. + Future share(ShareParams params) async { + if (params.uri == null && + (params.files == null || params.files!.isEmpty) && + params.text == null) { + throw ArgumentError( + 'At least one of uri, files or text must be provided', + ); + } + + if (params.uri != null && params.text != null) { + throw ArgumentError('uri and text cannot be provided at the same time'); + } + + if (params.text != null && params.text!.isEmpty) { + throw ArgumentError('text provided, but cannot be empty'); + } + + if (params.files != null && params.files!.isEmpty) { + throw ArgumentError('files provided, but cannot be empty'); + } + + if (params.fileNameOverrides != null && + (params.files == null || + params.files!.length != params.fileNameOverrides!.length)) { + throw ArgumentError( + 'fileNameOverrides must have the same length as files.', + ); + } + return _platform.share(params); + } +} + +@Deprecated('Use SharePlus instead') +class Share { /// Whether to fall back to downloading files if [shareXFiles] fails on web. + @Deprecated('Use ShareParams.downloadFallbackEnabled instead') static bool downloadFallbackEnabled = true; /// Summons the platform's share sheet to share uri. @@ -37,13 +120,17 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. + @Deprecated('Use SharePlus.instance.share() instead') static Future shareUri( Uri uri, { Rect? sharePositionOrigin, }) async { - return _platform.shareUri( - uri, - sharePositionOrigin: sharePositionOrigin, + return SharePlus.instance.share( + ShareParams( + uri: uri, + sharePositionOrigin: sharePositionOrigin, + downloadFallbackEnabled: downloadFallbackEnabled, + ), ); } @@ -82,16 +169,20 @@ class Share { /// /// Will gracefully fall back to the non result variant if not implemented /// for the current environment and return [ShareResult.unavailable]. + @Deprecated('Use SharePlus.instance.share() instead') static Future share( String text, { String? subject, Rect? sharePositionOrigin, }) async { assert(text.isNotEmpty); - return _platform.share( - text, - subject: subject, - sharePositionOrigin: sharePositionOrigin, + return SharePlus.instance.share( + ShareParams( + text: text, + subject: subject, + sharePositionOrigin: sharePositionOrigin, + downloadFallbackEnabled: downloadFallbackEnabled, + ), ); } @@ -123,6 +214,7 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. + @Deprecated('Use SharePlus.instance.share() instead') static Future shareXFiles( List files, { String? subject, @@ -131,12 +223,15 @@ class Share { List? fileNameOverrides, }) async { assert(files.isNotEmpty); - return _platform.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: sharePositionOrigin, - fileNameOverrides: fileNameOverrides, + return SharePlus.instance.share( + ShareParams( + files: files, + subject: subject, + text: text, + sharePositionOrigin: sharePositionOrigin, + fileNameOverrides: fileNameOverrides, + downloadFallbackEnabled: downloadFallbackEnabled, + ), ); } } diff --git a/packages/share_plus/share_plus/lib/src/share_plus_linux.dart b/packages/share_plus/share_plus/lib/src/share_plus_linux.dart index 98dd9d786f..0acfd2b471 100644 --- a/packages/share_plus/share_plus/lib/src/share_plus_linux.dart +++ b/packages/share_plus/share_plus/lib/src/share_plus_linux.dart @@ -1,8 +1,6 @@ /// The Linux implementation of `share_plus`. library; -import 'dart:ui'; - import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; import 'package:url_launcher_linux/url_launcher_linux.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; @@ -19,26 +17,15 @@ class SharePlusLinuxPlugin extends SharePlatform { } @override - Future shareUri( - Uri uri, { - String? subject, - String? text, - Rect? sharePositionOrigin, - }) async { - throw UnimplementedError( - 'shareUri() has not been implemented on Linux. Use share().'); - } + Future share(ShareParams params) async { + if (params.files?.isNotEmpty == true) { + throw UnimplementedError('Sharing files not supported on Linux'); + } - /// Share text. - @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { final queryParameters = { - if (subject != null) 'subject': subject, - 'body': text, + if (params.subject != null) 'subject': params.subject, + if (params.uri != null) 'body': params.uri.toString(), + if (params.text != null) 'body': params.text, }; // see https://github.com/dart-lang/sdk/issues/43838#issuecomment-823551891 @@ -46,7 +33,7 @@ class SharePlusLinuxPlugin extends SharePlatform { scheme: 'mailto', query: queryParameters.entries .map((e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value ?? '')}') .join('&'), ); @@ -60,18 +47,4 @@ class SharePlusLinuxPlugin extends SharePlatform { return ShareResult.unavailable; } - - /// Share [XFile] objects with Result. - @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) { - throw UnimplementedError( - 'shareXFiles() has not been implemented on Linux.', - ); - } } diff --git a/packages/share_plus/share_plus/lib/src/share_plus_web.dart b/packages/share_plus/share_plus/lib/src/share_plus_web.dart index d0456d327c..c5482a707a 100644 --- a/packages/share_plus/share_plus/lib/src/share_plus_web.dart +++ b/packages/share_plus/share_plus/lib/src/share_plus_web.dart @@ -1,12 +1,10 @@ import 'dart:developer' as developer; import 'dart:js_interop'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:meta/meta.dart'; import 'package:mime/mime.dart' show lookupMimeType; -import 'package:share_plus/share_plus.dart'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:url_launcher_web/url_launcher_web.dart'; @@ -30,14 +28,11 @@ class SharePlusWebPlugin extends SharePlatform { }) : _navigator = debugNavigator ?? window.navigator; @override - Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) async { - final data = ShareData( - url: uri.toString(), - ); + Future share(ShareParams params) async { + // Prepare share data params + final ShareData data = await prepareData(params); + // Check if can share final bool canShare; try { canShare = _navigator.canShare(data); @@ -47,11 +42,11 @@ class SharePlusWebPlugin extends SharePlatform { error: e, ); - throw Exception('Navigator.canShare() is unavailable'); + return _fallback(params, 'Navigator.canShare() is unavailable'); } if (!canShare) { - throw Exception('Navigator.canShare() is false'); + return _fallback(params, 'Navigator.canShare() is false'); } try { @@ -66,192 +61,130 @@ class SharePlusWebPlugin extends SharePlatform { error: '${e.name}: ${e.message}', ); - throw Exception('Navigator.share() failed: ${e.message}'); + return _fallback(params, 'Navigator.share() failed: ${e.message}'); } return ShareResult.unavailable; } - @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { - final ShareData data; - if (subject != null && subject.isNotEmpty) { - data = ShareData( - title: subject, - text: text, - ); - } else { - data = ShareData( - text: text, - ); - } - - final bool canShare; - try { - canShare = _navigator.canShare(data); - } on NoSuchMethodError catch (e) { - developer.log( - 'Share API is not supported in this User Agent.', - error: e, - ); - - // Navigator is not available or the webPage is not served on https - final queryParameters = { - if (subject != null) 'subject': subject, - 'body': text, - }; - - // see https://github.com/dart-lang/sdk/issues/43838#issuecomment-823551891 - final uri = Uri( - scheme: 'mailto', - query: queryParameters.entries - .map((e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') - .join('&'), - ); + Future prepareData(ShareParams params) async { + // Prepare share data params + final uri = params.uri?.toString(); + final text = params.text; + final title = params.subject ?? params.title; + ShareData data; - final launchResult = await urlLauncher.launchUrl( - uri.toString(), - const LaunchOptions(), - ); - if (!launchResult) { - throw Exception('Failed to launch $uri'); + // Prepare files + final webFiles = []; + if (params.files != null) { + final files = params.files; + if (files != null && files.isNotEmpty == true) { + for (var index = 0; index < files.length; index++) { + final xFile = files[index]; + final filename = params.fileNameOverrides?.elementAt(index); + webFiles.add(await _fromXFile(xFile, nameOverride: filename)); + } } - - return ShareResult.unavailable; - } - - if (!canShare) { - throw Exception('Navigator.canShare() is false'); } - try { - await _navigator.share(data).toDart; - - // actions is success, but can't get the action name - return ShareResult.unavailable; - } on DOMException catch (e) { - if (e.name case 'AbortError') { - return _resultDismissed; - } - - developer.log( - 'Failed to share text', - error: '${e.name}: ${e.message}', + if (uri == null && text == null && webFiles.isEmpty) { + throw ArgumentError( + 'At least one of uri, text, or files must be provided', ); - - throw Exception('Navigator.share() failed: ${e.message}'); } - } - /// Share [XFile] objects. - /// - /// Remarks for the web implementation: - /// This uses the [Web Share API](https://web.dev/web-share/) if it's - /// available. This builds on the - /// [`cross_file`](https://pub.dev/packages/cross_file) package. - @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) async { - assert( - fileNameOverrides == null || files.length == fileNameOverrides.length); - final webFiles = []; - for (var index = 0; index < files.length; index++) { - final xFile = files[index]; - final filename = fileNameOverrides?.elementAt(index); - webFiles.add(await _fromXFile(xFile, nameOverride: filename)); + if (uri != null && text != null) { + throw ArgumentError('Only one of uri or text can be provided'); } - final ShareData data; - if (text != null && text.isNotEmpty) { - if (subject != null && subject.isNotEmpty) { - data = ShareData( - files: webFiles.toJS, - text: text, - title: subject, - ); - } else { - data = ShareData( - files: webFiles.toJS, - text: text, - ); - } - } else if (subject != null && subject.isNotEmpty) { + if (uri != null) { data = ShareData( + url: uri, + ); + } else if (webFiles.isNotEmpty && text != null && title != null) { + data = ShareData( + text: text, + title: title, files: webFiles.toJS, - title: subject, ); - } else { + } else if (webFiles.isNotEmpty && text != null) { data = ShareData( + text: text, files: webFiles.toJS, ); - } - - final bool canShare; - try { - canShare = _navigator.canShare(data); - } on NoSuchMethodError catch (e) { - developer.log( - 'Share API is not supported in this User Agent.', - error: e, + } else if (webFiles.isNotEmpty && title != null) { + data = ShareData( + title: title, + files: webFiles.toJS, ); - - return _downloadIfFallbackEnabled( - files, - fileNameOverrides, - 'Navigator.canShare() is unavailable', + } else if (webFiles.isNotEmpty) { + data = ShareData( + files: webFiles.toJS, ); - } - - if (!canShare) { - return _downloadIfFallbackEnabled( - files, - fileNameOverrides, - 'Navigator.canShare() is false', + } else if (text != null && title != null) { + data = ShareData( + text: text, + title: title, + ); + } else { + data = ShareData( + text: text!, ); } - try { - await _navigator.share(data).toDart; - - // actions is success, but can't get the action name - return ShareResult.unavailable; - } on DOMException catch (e) { - final name = e.name; - final message = e.message; + return data; + } - if (name case 'AbortError') { - return _resultDismissed; + /// Fallback method to when sharing on web fails. + /// If [ShareParams.downloadFallbackEnabled] is true, it will attempt to download the files. + /// If [ShareParams.mailToFallbackEnabled] is true, it will attempt to share text as email. + /// Otherwise, it will throw an exception. + Future _fallback(ShareParams params, String error) async { + developer.log(error); + + final subject = params.subject; + final text = params.text ?? params.uri?.toString() ?? ''; + final files = params.files; + final fileNameOverrides = params.fileNameOverrides; + final downloadFallbackEnabled = params.downloadFallbackEnabled; + final mailToFallbackEnabled = params.mailToFallbackEnabled; + + if (files != null && files.isNotEmpty) { + if (downloadFallbackEnabled) { + return _download(files, fileNameOverrides); + } else { + throw Exception(error); } + } - return _downloadIfFallbackEnabled( - files, - fileNameOverrides, - 'Navigator.share() failed: $message', - ); + if (!mailToFallbackEnabled) { + throw Exception(error); } - } - Future _downloadIfFallbackEnabled( - List files, - List? fileNameOverrides, - String message, - ) { - developer.log(message); - if (Share.downloadFallbackEnabled) { - return _download(files, fileNameOverrides); - } else { - throw Exception(message); + final queryParameters = { + if (subject != null) 'subject': subject, + 'body': text, + }; + + // see https://github.com/dart-lang/sdk/issues/43838#issuecomment-823551891 + final uri = Uri( + scheme: 'mailto', + query: queryParameters.entries + .map((e) => + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'), + ); + + final launchResult = await urlLauncher.launchUrl( + uri.toString(), + const LaunchOptions(), + ); + + if (!launchResult) { + throw Exception(error); } + + return ShareResult.unavailable; } Future _download( diff --git a/packages/share_plus/share_plus/lib/src/share_plus_windows.dart b/packages/share_plus/share_plus/lib/src/share_plus_windows.dart index bafbc280ef..5afa595fe7 100644 --- a/packages/share_plus/share_plus/lib/src/share_plus_windows.dart +++ b/packages/share_plus/share_plus/lib/src/share_plus_windows.dart @@ -1,8 +1,6 @@ /// The Windows implementation of `share_plus`. library; -import 'dart:ui'; - import 'package:share_plus/src/windows_version_helper.dart'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; @@ -24,26 +22,17 @@ class SharePlusWindowsPlugin extends SharePlatform { } @override - Future shareUri( - Uri uri, { - String? subject, - String? text, - Rect? sharePositionOrigin, - }) async { - throw UnimplementedError( - 'shareUri() has not been implemented on Windows. Use share().'); - } + Future share(ShareParams params) async { + if (params.files?.isNotEmpty == true) { + throw UnimplementedError( + 'sharing files is only available for Windows versions higher than 10.0.${VersionHelper.kWindows10RS5BuildNumber}.', + ); + } - /// Share text. - @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { final queryParameters = { - if (subject != null) 'subject': subject, - 'body': text, + if (params.subject != null) 'subject': params.subject, + if (params.uri != null) 'body': params.uri.toString(), + if (params.text != null) 'body': params.text, }; // see https://github.com/dart-lang/sdk/issues/43838#issuecomment-823551891 @@ -51,7 +40,7 @@ class SharePlusWindowsPlugin extends SharePlatform { scheme: 'mailto', query: queryParameters.entries .map((e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value ?? '')}') .join('&'), ); @@ -65,18 +54,4 @@ class SharePlusWindowsPlugin extends SharePlatform { return ShareResult.unavailable; } - - /// Share [XFile] objects with Result. - @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) { - throw UnimplementedError( - 'shareXFiles() is only available for Windows versions higher than 10.0.${VersionHelper.kWindows10RS5BuildNumber}.', - ); - } } diff --git a/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift b/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift index 414467e1f3..b561fa89db 100644 --- a/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift +++ b/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift @@ -21,13 +21,26 @@ public class SharePlusMacosPlugin: NSObject, FlutterPlugin, NSSharingServicePick switch call.method { case "share": - let text = args["text"] as! String - let subject = args["subject"] as? String - shareItems([text], subject: subject, origin: origin, view: registrar.view!, callback: result) - case "shareFiles": - let paths = args["paths"] as! [String] - let urls = paths.map { NSURL.fileURL(withPath: $0) } - shareItems(urls, origin: origin, view: registrar.view!, callback: result) + let text = args["text"] as? String + let uri = args["uri"] as? String + let paths = args["paths"] as? [String] + + // Title takes preference over Subject + // Subject should only be used for email subjects + // But added for retrocompatibility + let title = args["title"] as? String + let subject = title ?? args["subject"] as? String + + if let uri = uri { + shareItems([uri], subject: subject, origin: origin, view: registrar.view!, callback: result) + } else if let paths = paths { + let urls = paths.map { NSURL.fileURL(withPath: $0) } + shareItems(urls, subject: subject, origin: origin, view: registrar.view!, callback: result) + } else if let text = text { + shareItems([text], subject: subject, origin: origin, view: registrar.view!, callback: result) + } else { + result(FlutterError.init(code: "error", message: "No content to share", details: nil)) + } default: result(FlutterMethodNotImplemented) } diff --git a/packages/share_plus/share_plus/test/share_plus_linux_test.dart b/packages/share_plus/share_plus/test/share_plus_linux_test.dart index 28fc82c00d..7a4c90743d 100644 --- a/packages/share_plus/share_plus/test/share_plus_linux_test.dart +++ b/packages/share_plus/share_plus/test/share_plus_linux_test.dart @@ -9,10 +9,13 @@ void main() { SharePlusLinuxPlugin.registerWith(); expect(SharePlatform.instance, isA()); }); + test('url encoding is correct for &', () async { final mock = MockUrlLauncherPlatform(); - await SharePlusLinuxPlugin(mock).share('foo&bar', subject: 'bar&foo'); + await SharePlusLinuxPlugin(mock).share( + ShareParams(text: 'foo&bar', subject: 'bar&foo'), + ); expect(mock.url, 'mailto:?subject=bar%26foo&body=foo%26bar'); }); @@ -21,16 +24,20 @@ void main() { test('url encoding is correct for spaces', () async { final mock = MockUrlLauncherPlatform(); - await SharePlusLinuxPlugin(mock).share('foo bar', subject: 'bar foo'); + await SharePlusLinuxPlugin(mock).share( + ShareParams(text: 'foo bar', subject: 'bar foo'), + ); expect(mock.url, 'mailto:?subject=bar%20foo&body=foo%20bar'); }); - test('throws when url_launcher can\'t launch uri', () async { + test('can share URI on Linux', () async { final mock = MockUrlLauncherPlatform(); - mock.canLaunchMockValue = false; - expect(() async => await SharePlusLinuxPlugin(mock).share('foo bar'), - throwsException); + await SharePlusLinuxPlugin(mock).share( + ShareParams(uri: Uri.parse('http://example.com')), + ); + + expect(mock.url, 'mailto:?body=http%3A%2F%2Fexample.com'); }); } diff --git a/packages/share_plus/share_plus/test/share_plus_test.dart b/packages/share_plus/share_plus/test/share_plus_test.dart new file mode 100644 index 0000000000..3f839fab18 --- /dev/null +++ b/packages/share_plus/share_plus/test/share_plus_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; +import 'package:share_plus/share_plus.dart'; + +void main() { + late FakeSharePlatform fakePlatform; + late SharePlus sharePlus; + + setUp(() { + fakePlatform = FakeSharePlatform(); + sharePlus = SharePlus.custom(fakePlatform); + }); + + group('SharePlus', () { + test('share throws ArgumentError if no params are provided', () async { + expect( + () => sharePlus.share(ShareParams()), + throwsA(isA()), + ); + }); + + test('share throws ArgumentError if both uri and text are provided', + () async { + expect( + () => sharePlus.share( + ShareParams( + uri: Uri.parse('https://example.com'), + text: 'text', + ), + ), + throwsA(isA()), + ); + }); + + test('share throws ArgumentError if text is empty', () async { + expect( + () => sharePlus.share(ShareParams(text: '')), + throwsA(isA()), + ); + }); + + test('share throws ArgumentError if files are empty', () async { + expect( + () => sharePlus.share(ShareParams(files: [])), + throwsA(isA()), + ); + }); + + test( + 'share throws ArgumentError if fileNameOverrides length does not match files length', + () async { + expect( + () => sharePlus.share(ShareParams( + files: [XFile('path')], fileNameOverrides: ['name1', 'name2'])), + throwsA(isA()), + ); + }); + + test('share calls platform share method with correct params', () async { + final params = ShareParams(text: 'text'); + final result = await sharePlus.share(params); + expect(result, ShareResult.unavailable); + expect(fakePlatform.lastParams?.text, params.text); + }); + }); +} + +class FakeSharePlatform implements SharePlatform { + ShareParams? lastParams; + @override + Future share(ShareParams params) { + lastParams = params; + return Future.value(ShareResult.unavailable); + } +} diff --git a/packages/share_plus/share_plus/test/share_plus_windows_test.dart b/packages/share_plus/share_plus/test/share_plus_windows_test.dart index 5701d93c54..e4038762fe 100644 --- a/packages/share_plus/share_plus/test/share_plus_windows_test.dart +++ b/packages/share_plus/share_plus/test/share_plus_windows_test.dart @@ -32,7 +32,9 @@ void main() { () async { final mock = MockUrlLauncherPlatform(); - await SharePlusWindowsPlugin(mock).share('foo&bar', subject: 'bar&foo'); + await SharePlusWindowsPlugin(mock).share( + ShareParams(text: 'foo&bar', subject: 'bar&foo'), + ); expect(mock.url, 'mailto:?subject=bar%26foo&body=foo%26bar'); }, @@ -45,7 +47,9 @@ void main() { () async { final mock = MockUrlLauncherPlatform(); - await SharePlusWindowsPlugin(mock).share('foo bar', subject: 'bar foo'); + await SharePlusWindowsPlugin(mock).share( + ShareParams(text: 'foo bar', subject: 'bar foo'), + ); expect(mock.url, 'mailto:?subject=bar%20foo&body=foo%20bar'); }, @@ -53,13 +57,15 @@ void main() { ); test( - 'throws when url_launcher can\'t launch uri', + 'can share URI on Windows', () async { final mock = MockUrlLauncherPlatform(); - mock.canLaunchMockValue = false; - expect(() async => await SharePlusWindowsPlugin(mock).share('foo bar'), - throwsException); + await SharePlusWindowsPlugin(mock).share( + ShareParams(uri: Uri.parse('http://example.com')), + ); + + expect(mock.url, 'mailto:?body=http%3A%2F%2Fexample.com'); }, skip: VersionHelper.instance.isWindows10RS5OrGreater, ); diff --git a/packages/share_plus/share_plus/windows/share_plus_plugin.cpp b/packages/share_plus/share_plus/windows/share_plus_plugin.cpp index 4fff212571..fed28d46cb 100644 --- a/packages/share_plus/share_plus/windows/share_plus_plugin.cpp +++ b/packages/share_plus/share_plus/windows/share_plus_plugin.cpp @@ -93,78 +93,44 @@ HRESULT SharePlusWindowsPlugin::GetStorageFileFromPath( void SharePlusWindowsPlugin::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { + // Handle the share method. if (method_call.method_name().compare(kShare) == 0) { auto data_transfer_manager = GetDataTransferManager(); auto args = std::get(*method_call.arguments()); - if (auto text_value = - std::get_if(&args[flutter::EncodableValue("text")])) { + + // Extract the text, subject, uri, title, paths and mimeTypes from the arguments + if (auto text_value = std::get_if( + &args[flutter::EncodableValue("text")])) { share_text_ = *text_value; } if (auto subject_value = std::get_if( &args[flutter::EncodableValue("subject")])) { share_subject_ = *subject_value; } - auto callback = WRL::Callback>( - [&](auto &&, DataTransfer::IDataRequestedEventArgs *e) { - using Microsoft::WRL::Wrappers::HStringReference; - WRL::ComPtr request; - e->get_Request(&request); - WRL::ComPtr data; - request->get_Data(&data); - WRL::ComPtr properties; - data->get_Properties(&properties); - // The title is mandatory for Windows. - // Using |share_text_| as title. - auto text = Utf16FromUtf8(share_text_); - properties->put_Title(HStringReference(text.c_str()).Get()); - // If |share_subject_| is available, then set it as text since - // |share_text_| is already set as title. - if (share_subject_ && !share_subject_.value_or("").empty()) { - auto subject = Utf16FromUtf8(share_subject_.value_or("")); - properties->put_Description( - HStringReference(subject.c_str()).Get()); - data->SetText(HStringReference(subject.c_str()).Get()); - } - // If |share_subject_| is not available, then use |share_text_| as - // text aswell. - else { - data->SetText(HStringReference(text.c_str()).Get()); - } - return S_OK; - }); - data_transfer_manager->add_DataRequested(callback.Get(), - &data_transfer_manager_token_); - if (data_transfer_manager_interop_ != nullptr) { - data_transfer_manager_interop_->ShowShareUIForWindow(GetWindow()); + if (auto uri_value = std::get_if( + &args[flutter::EncodableValue("uri")])) { + share_uri_ = *uri_value; } - result->Success(flutter::EncodableValue(kShareResultUnavailable)); - } else if (method_call.method_name().compare(kShareFiles) == 0) { - auto data_transfer_manager = GetDataTransferManager(); - auto args = std::get(*method_call.arguments()); - if (auto text_value = - std::get_if(&args[flutter::EncodableValue("text")])) { - share_text_ = *text_value; - } - if (auto subject_value = std::get_if( - &args[flutter::EncodableValue("subject")])) { - share_subject_ = *subject_value; + if (auto title_value = std::get_if( + &args[flutter::EncodableValue("title")])) { + share_title_ = *title_value; } if (auto paths = std::get_if( - &args[flutter::EncodableValue("paths")])) { + &args[flutter::EncodableValue("paths")])) { paths_.clear(); - for (auto &path : *paths) { + for (auto& path : *paths) { paths_.emplace_back(std::get(path)); } } if (auto mime_types = std::get_if( - &args[flutter::EncodableValue("mimeTypes")])) { + &args[flutter::EncodableValue("mimeTypes")])) { mime_types_.clear(); - for (auto &mime_type : *mime_types) { + for (auto& mime_type : *mime_types) { mime_types_.emplace_back(std::get(mime_type)); } } + + // Create the share callback auto callback = WRL::Callback>( @@ -176,53 +142,57 @@ void SharePlusWindowsPlugin::HandleMethodCall( request->get_Data(&data); WRL::ComPtr properties; data->get_Properties(&properties); - // The title is mandatory for Windows. - // Using |share_text_| as title if available. - if (!share_text_.empty()) { - auto text = Utf16FromUtf8(share_text_); - properties->put_Title(HStringReference(text.c_str()).Get()); + + // Set the title of the share dialog + // Prefer the title, then the subject, then the text + // Setting a title is mandatory for Windows + if (share_title_ && !share_title_.value_or("").empty()) { + auto title = Utf16FromUtf8(share_title_.value_or("")); + properties->put_Title(HStringReference(title.c_str()).Get()); + } + else if (share_subject_ && !share_subject_.value_or("").empty()) { + auto title = Utf16FromUtf8(share_subject_.value_or("")); + properties->put_Title(HStringReference(title.c_str()).Get()); } - // Or use the file count string as title if there are multiple - // files & use the file name if a single file is shared. - // Same behavior may be seen in File Explorer. else { - if (paths_.size() > 1) { - auto title = std::to_wstring(paths_.size()) + L" files"; - properties->put_Title(HStringReference(title.c_str()).Get()); - } else if (paths_.size() == 1) { - auto title = Utf16FromUtf8(paths_.front()); - properties->put_Title(HStringReference(title.c_str()).Get()); - } + auto title = Utf16FromUtf8(share_text_.value_or("")); + properties->put_Title(HStringReference(title.c_str()).Get()); } - // If |share_subject_| is available, then set it as text since - // |share_text_| is already set as title. - if (share_subject_ && !share_subject_.value_or("").empty()) { - auto subject = Utf16FromUtf8(share_subject_.value_or("")); + + // Set the text of the share dialog + if (share_text_ && !share_text_.value_or("").empty()) { + auto text = Utf16FromUtf8(share_text_.value_or("")); properties->put_Description( - HStringReference(subject.c_str()).Get()); - data->SetText(HStringReference(subject.c_str()).Get()); - } - // If |share_subject_| is not available, then use |share_text_| as - // text aswell. - else if (!share_text_.empty()) { - auto text = Utf16FromUtf8(share_text_); + HStringReference(text.c_str()).Get()); data->SetText(HStringReference(text.c_str()).Get()); } + + // If URI provided, set the URI to share + if (share_uri_ && !share_uri_.value_or("").empty()) { + auto uri = Utf16FromUtf8(share_uri_.value_or("")); + properties->put_Description( + HStringReference(uri.c_str()).Get()); + data->SetText(HStringReference(uri.c_str()).Get()); + } + // Add files to the data. - Vector storage_items; - for (const std::string &path : paths_) { + Vector storage_items; + for (const std::string& path : paths_) { auto str = Utf16FromUtf8(path); - wchar_t *ptr = const_cast(str.c_str()); - WindowsStorage::IStorageFile *file = nullptr; + wchar_t* ptr = const_cast(str.c_str()); + WindowsStorage::IStorageFile* file = nullptr; if (SUCCEEDED(GetStorageFileFromPath(ptr, &file)) && - file != nullptr) { + file != nullptr) { storage_items.Append( - reinterpret_cast(file)); + reinterpret_cast(file)); } } data->SetStorageItemsReadOnly(&storage_items); + return S_OK; }); + + // Add the callback to the data transfer manager data_transfer_manager->add_DataRequested(callback.Get(), &data_transfer_manager_token_); if (data_transfer_manager_interop_ != nullptr) { diff --git a/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h b/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h index 848be0091f..ca7d2aaa14 100644 --- a/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h +++ b/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h @@ -45,7 +45,7 @@ class SharePlusWindowsPlugin : public flutter::Plugin { "dev.fluttercommunity.plus/share/unavailable"; static constexpr auto kShare = "share"; - static constexpr auto kShareFiles = "shareFiles"; + //static constexpr auto kShareFiles = "shareFiles"; HWND GetWindow(); @@ -70,8 +70,10 @@ class SharePlusWindowsPlugin : public flutter::Plugin { // Present here to keep |std::string| in memory until data request callback // from |IDataTransferManager| takes place. // Subsequent calls on the platform channel will overwrite the existing value. - std::string share_text_ = ""; + std::optional share_text_ = std::nullopt; + std::optional share_uri_ = std::nullopt; std::optional share_subject_ = std::nullopt; + std::optional share_title_ = std::nullopt; std::vector paths_ = {}; std::vector mime_types_ = {}; }; diff --git a/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart b/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart index bb7fa696c6..e175d70286 100644 --- a/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart +++ b/packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart @@ -5,10 +5,6 @@ import 'dart:async'; import 'dart:io'; -// Keep dart:ui for retrocompatiblity with Flutter <3.3.0 -// ignore: unnecessary_import -import 'dart:ui'; - import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show visibleForTesting; import 'package:mime/mime.dart' show extensionFromMime, lookupMimeType; @@ -24,95 +20,54 @@ class MethodChannelShare extends SharePlatform { MethodChannel('dev.fluttercommunity.plus/share'); @override - Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) async { - final params = {'uri': uri.toString()}; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - final result = await channel.invokeMethod('shareUri', params) ?? - 'dev.fluttercommunity.plus/share/unavailable'; - - return ShareResult(result, _statusFromResult(result)); - } - - /// Summons the platform's share sheet to share text. - @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { - assert(text.isNotEmpty); - final params = { - 'text': text, - 'subject': subject, - }; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - final result = await channel.invokeMethod('share', params) ?? + Future share(ShareParams params) async { + final paramsMap = await _toPlatformMap(params); + final result = await channel.invokeMethod('share', paramsMap) ?? 'dev.fluttercommunity.plus/share/unavailable'; return ShareResult(result, _statusFromResult(result)); } - /// Summons the platform's share sheet to share multiple files. - @override - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) async { - assert(files.isNotEmpty); + Future> _toPlatformMap(ShareParams params) async { assert( - fileNameOverrides == null || files.length == fileNameOverrides.length, - "fileNameOverrides list must have the same length as files list.", + params.text != null || + params.uri != null || + (params.files != null && params.files!.isNotEmpty), + 'At least one of text, uri or files must be provided', ); - final filesWithPath = await _getFiles(files, fileNameOverrides); - assert(filesWithPath.every((element) => element.path.isNotEmpty)); - final mimeTypes = filesWithPath - .map((e) => e.mimeType ?? _mimeTypeForPath(e.path)) - .toList(); + final map = { + if (params.text != null) 'text': params.text, + if (params.subject != null) 'subject': params.subject, + if (params.title != null) 'title': params.title, + if (params.uri != null) 'uri': params.uri.toString(), + }; - final paths = filesWithPath.map((e) => e.path).toList(); - assert(paths.length == mimeTypes.length); - assert(mimeTypes.every((element) => element.isNotEmpty)); + if (params.sharePositionOrigin != null) { + map['originX'] = params.sharePositionOrigin!.left; + map['originY'] = params.sharePositionOrigin!.top; + map['originWidth'] = params.sharePositionOrigin!.width; + map['originHeight'] = params.sharePositionOrigin!.height; + } - final params = { - 'paths': paths, - 'mimeTypes': mimeTypes, - }; + if (params.files != null) { + final filesWithPath = + await _getFiles(params.files!, params.fileNameOverrides); + assert(filesWithPath.every((element) => element.path.isNotEmpty)); - if (subject != null) params['subject'] = subject; - if (text != null) params['text'] = text; + final mimeTypes = filesWithPath + .map((e) => e.mimeType ?? _mimeTypeForPath(e.path)) + .toList(); - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } + final paths = filesWithPath.map((e) => e.path).toList(); + assert(paths.length == mimeTypes.length); + assert(mimeTypes.every((element) => element.isNotEmpty)); - final result = await channel.invokeMethod('shareFiles', params) ?? - 'dev.fluttercommunity.plus/share/unavailable'; + map['paths'] = paths; + map['mimeTypes'] = mimeTypes; + } - return ShareResult(result, _statusFromResult(result)); + return map; } /// Ensure that a file is readable from the file system. Will create file on-demand under TemporaryDiectory and return the temporary file otherwise. diff --git a/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart b/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart index f1840624ef..b361526f4b 100644 --- a/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart +++ b/packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart @@ -31,46 +31,123 @@ class SharePlatform extends PlatformInterface { _instance = instance; } - /// Share uri. - Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) { - return _instance.shareUri( - uri, - sharePositionOrigin: sharePositionOrigin, - ); + Future share(ShareParams params) async { + return _instance.share(params); } +} - /// Share text with Result. - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { - return await _instance.share( - text, - subject: subject, - sharePositionOrigin: sharePositionOrigin, - ); - } +class ShareParams { + /// The text to share + /// + /// Cannot be provided at the same time as [uri], + /// as the share method will use one or the other. + /// + /// Can be used together with [files], + /// but it depends on the receiving app if they support + /// loading files and text from a share action. + /// Some apps only support one or the other. + /// + /// * Supported platforms: All + final String? text; - /// Share [XFile] objects with Result. - Future shareXFiles( - List files, { - String? subject, - String? text, - Rect? sharePositionOrigin, - List? fileNameOverrides, - }) async { - return _instance.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: sharePositionOrigin, - fileNameOverrides: fileNameOverrides, - ); - } + /// Used as share sheet title where supported + /// + /// Provided to Android Intent.createChooser as the title, + /// as well as, EXTRA_TITLE Intent extra. + /// + /// Provided to web Navigator Share API as title. + /// + /// * Supported platforms: All + final String? title; + + /// Used as email subject where supported (e.g. EXTRA_SUBJECT on Android) + /// + /// When using the email fallback, this will be the subject of the email. + /// + /// * Supported platforms: All + final String? subject; + + /// Preview thumbnail + /// + /// TODO: https://github.com/fluttercommunity/plus_plugins/pull/3372 + /// + /// * Supported platforms: Android + /// Parameter ignored on other platforms. + final XFile? previewThumbnail; + + /// The optional [sharePositionOrigin] parameter can be used to specify a global + /// origin rect for the share sheet to popover from on iPads and Macs. It has no effect + /// on other devices. + /// + /// * Supported platforms: iPad and Mac + /// Parameter ignored on other platforms. + final Rect? sharePositionOrigin; + + /// Share a URI. + /// + /// On iOS, it will trigger the iOS system to fetch the html page + /// (if available), and the website icon will be extracted and displayed on + /// the iOS share sheet. + /// + /// On other platforms it behaves like sharing text. + /// + /// Cannot be used in combination with [text]. + /// + /// * Supported platforms: iOS, Android + /// Fallsback to sharing the URI as text on other platforms. + final Uri? uri; + + /// Share multiple files, can be used in combination with [text] + /// + /// Android supports all natively available MIME types (wildcards like image/* + /// are also supported) and it's considered best practice to avoid mixing + /// unrelated file types (eg. image/jpg & application/pdf). If MIME types are + /// mixed the plugin attempts to find the lowest common denominator. Even + /// if MIME types are supplied the receiving app decides if those are used + /// or handled. + /// + /// On iOS image/jpg, image/jpeg and image/png are handled as images, while + /// every other MIME type is considered a normal file. + /// + /// + /// * Supported platforms: Android, iOS, Web, recent macOS and Windows versions + /// Throws an [UnimplementedError] on other platforms. + final List? files; + + /// Override the names of shared files. + /// + /// When set, the list length must match the number of [files] to share. + /// This is useful when sharing files that were created by [`XFile.fromData`](https://github.com/flutter/packages/blob/754de1918a339270b70971b6841cf1e04dd71050/packages/cross_file/lib/src/types/io.dart#L43), + /// because name property will be ignored by [`cross_file`](https://pub.dev/packages/cross_file) on all platforms except on web. + /// + /// * Supported platforms: Same as [files] + /// Ignored on platforms that don't support [files]. + final List? fileNameOverrides; + + /// Whether to fall back to downloading files if [share] fails on web. + /// + /// * Supported platforms: Web + /// Parameter ignored on other platforms. + final bool downloadFallbackEnabled; + + /// Whether to fall back to sending an email if [share] fails on web. + /// + /// * Supported platforms: Web + /// Parameter ignored on other platforms. + final bool mailToFallbackEnabled; + + ShareParams({ + this.text, + this.subject, + this.title, + this.previewThumbnail, + this.sharePositionOrigin, + this.uri, + this.files, + this.fileNameOverrides, + this.downloadFallbackEnabled = true, + this.mailToFallbackEnabled = true, + }); } /// The result of a share to determine what action the diff --git a/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart b/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart index 03c796bddc..c647df5c2c 100644 --- a/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart +++ b/packages/share_plus/share_plus_platform_interface/test/share_plus_platform_interface_test.dart @@ -51,22 +51,24 @@ void main() { test('sharing empty fails', () { expect( - () => sharePlatform.share(''), + () => sharePlatform.share(ShareParams()), throwsA(const TypeMatcher()), ); expect( - () => SharePlatform.instance.share(''), + () => SharePlatform.instance.share(ShareParams()), throwsA(const TypeMatcher()), ); verifyZeroInteractions(mockChannel); }); test('sharing origin sets the right params', () async { - await sharePlatform.shareUri( - Uri.parse('https://pub.dev/packages/share_plus'), - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + await sharePlatform.share( + ShareParams( + uri: Uri.parse('https://pub.dev/packages/share_plus'), + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + ), ); - verify(mockChannel.invokeMethod('shareUri', { + verify(mockChannel.invokeMethod('share', { 'uri': 'https://pub.dev/packages/share_plus', 'originX': 1.0, 'originY': 2.0, @@ -75,9 +77,11 @@ void main() { })); await sharePlatform.share( - 'some text to share', - subject: 'some subject to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + ShareParams( + text: 'some text to share', + subject: 'some subject to share', + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + ), ); verify(mockChannel.invokeMethod('share', { 'text': 'some text to share', @@ -89,14 +93,16 @@ void main() { })); await withFile('tempfile-83649a.png', (File fd) async { - await sharePlatform.shareXFiles( - [XFile(fd.path)], - subject: 'some subject to share', - text: 'some text to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + await sharePlatform.share( + ShareParams( + files: [XFile(fd.path)], + subject: 'some subject to share', + text: 'some text to share', + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + ), ); verify(mockChannel.invokeMethod( - 'shareFiles', + 'share', { 'paths': [fd.path], 'mimeTypes': ['image/png'], @@ -113,8 +119,8 @@ void main() { test('sharing file sets correct mimeType', () async { await withFile('tempfile-83649b.png', (File fd) async { - await sharePlatform.shareXFiles([XFile(fd.path)]); - verify(mockChannel.invokeMethod('shareFiles', { + await sharePlatform.share(ShareParams(files: [XFile(fd.path)])); + verify(mockChannel.invokeMethod('share', { 'paths': [fd.path], 'mimeTypes': ['image/png'], })); @@ -123,8 +129,12 @@ void main() { test('sharing file sets passed mimeType', () async { await withFile('tempfile-83649c.png', (File fd) async { - await sharePlatform.shareXFiles([XFile(fd.path, mimeType: '*/*')]); - verify(mockChannel.invokeMethod('shareFiles', { + await sharePlatform.share( + ShareParams( + files: [XFile(fd.path, mimeType: '*/*')], + ), + ); + verify(mockChannel.invokeMethod('share', { 'paths': [fd.path], 'mimeTypes': ['*/*'], })); @@ -138,13 +148,19 @@ void main() { ); expect( - sharePlatform.share('some text to share'), + sharePlatform.share( + ShareParams(text: 'some text to share'), + ), completion(equals(resultUnavailable)), ); await withFile('tempfile-83649d.png', (File fd) async { expect( - sharePlatform.shareXFiles([XFile(fd.path)]), + sharePlatform.share( + ShareParams( + files: [XFile(fd.path)], + ), + ), completion(equals(resultUnavailable)), ); }); @@ -152,9 +168,11 @@ void main() { test('withResult methods invoke normal share on non IOS & Android', () async { await sharePlatform.share( - 'some text to share', - subject: 'some subject to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + ShareParams( + text: 'some text to share', + subject: 'some subject to share', + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + ), ); verify(mockChannel.invokeMethod('share', { 'text': 'some text to share', @@ -166,8 +184,12 @@ void main() { })); await withFile('tempfile-83649e.png', (File fd) async { - await sharePlatform.shareXFiles([XFile(fd.path)]); - verify(mockChannel.invokeMethod('shareFiles', { + await sharePlatform.share( + ShareParams( + files: [XFile(fd.path)], + ), + ); + verify(mockChannel.invokeMethod('share', { 'paths': [fd.path], 'mimeTypes': ['image/png'], }));