From 63a1c064617adbfa33d1d9a62ee8893983fae8a1 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 08:42:29 +0100 Subject: [PATCH 01/19] created new SharePlus public API --- .../share_plus/example/lib/main.dart | 1 + .../share_plus/share_plus/lib/share_plus.dart | 77 +++++++++++++------ .../test/share_plus_linux_test.dart | 2 +- .../share_plus_platform.dart | 46 +++++++++++ 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 913a6e33ce..745abded82 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -21,6 +21,7 @@ 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()); diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index e6e2f64a8b..e7146f5821 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -1,8 +1,3 @@ -// Copyright 2019 The Flutter Authors. 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:async'; import 'dart:ui'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; @@ -10,15 +5,39 @@ 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; -export 'src/share_plus_linux.dart'; -export 'src/share_plus_windows.dart' - if (dart.library.js_interop) 'src/share_plus_web.dart'; +// 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 { +class SharePlus { static SharePlatform get _platform => SharePlatform.instance; + 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.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'); + } + + return _platform.shareNew(params); + } +} + +@Deprecated('Use SharePlus instead') +class Share { + static final SharePlus _sharePlus = SharePlus(); + /// 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 +56,17 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. + @Deprecated('Use SharePlus.share() instead') static Future shareUri( Uri uri, { Rect? sharePositionOrigin, }) async { - return _platform.shareUri( - uri, - sharePositionOrigin: sharePositionOrigin, + return _sharePlus.share( + ShareParams( + uri: uri, + sharePositionOrigin: sharePositionOrigin, + downloadFallbackEnabled: downloadFallbackEnabled, + ), ); } @@ -82,16 +105,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.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.share( + ShareParams( + text: text, + subject: subject, + sharePositionOrigin: sharePositionOrigin, + downloadFallbackEnabled: downloadFallbackEnabled, + ), ); } @@ -123,6 +150,7 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. + @Deprecated('Use SharePlus.share() instead') static Future shareXFiles( List files, { String? subject, @@ -131,12 +159,15 @@ class Share { List? fileNameOverrides, }) async { assert(files.isNotEmpty); - return _platform.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: sharePositionOrigin, - fileNameOverrides: fileNameOverrides, + return _sharePlus.share( + ShareParams( + files: files, + subject: subject, + text: text, + sharePositionOrigin: sharePositionOrigin, + fileNameOverrides: fileNameOverrides, + downloadFallbackEnabled: downloadFallbackEnabled, + ), ); } } 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..a542e91923 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 @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:share_plus/share_plus.dart'; +import 'package:share_plus/share_plus_old.dart'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; import 'url_launcher_mock.dart'; 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..56a6e67d5d 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 @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:cross_file/cross_file.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:share_plus/share_plus.dart'; import '../method_channel/method_channel_share.dart'; @@ -31,6 +32,7 @@ class SharePlatform extends PlatformInterface { _instance = instance; } + // TODO: Delete /// Share uri. Future shareUri( Uri uri, { @@ -42,6 +44,7 @@ class SharePlatform extends PlatformInterface { ); } + // TODO: Delete /// Share text with Result. Future share( String text, { @@ -55,6 +58,7 @@ class SharePlatform extends PlatformInterface { ); } + // TODO: Delete /// Share [XFile] objects with Result. Future shareXFiles( List files, { @@ -71,6 +75,48 @@ class SharePlatform extends PlatformInterface { fileNameOverrides: fileNameOverrides, ); } + + Future shareNew(ShareParams params) async { + return _instance.shareNew(params); + } +} + +class ShareParams { + /// + final String? text; + + /// Used as share sheet title where supported (e.g. EXTRA_TITLE on Android) + final String? title; + + /// Only used as email subject where supported (e.g. EXTRA_SUBJECT on Android) + final String? subject; + + /// Only used in Android as preview thumbnail + final XFile? previewThumbnail; + + /// Only used in iPad and Mac as share sheet origin + final Rect? sharePositionOrigin; + + /// Share a URI, alternative to sharing [text] + final Uri? uri; + + /// Share multiple files, can be used in combination with [text] + final List? files; + final List? fileNameOverrides; + + final bool downloadFallbackEnabled; + + ShareParams({ + this.text, + this.subject, + this.title, + this.previewThumbnail, + this.sharePositionOrigin, + this.uri, + this.files, + this.fileNameOverrides, + this.downloadFallbackEnabled = true, + }); } /// The result of a share to determine what action the From cb779d1fc2b6481b7ce90e8f56dc5ad8cd1f2c84 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 08:45:08 +0100 Subject: [PATCH 02/19] file cleanup --- packages/share_plus/share_plus/lib/share_plus.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index e7146f5821..7f7ec67041 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Flutter Authors. 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:ui'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; @@ -5,10 +9,6 @@ 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; -// export 'src/share_plus_linux.dart'; -// export 'src/share_plus_windows.dart' -// if (dart.library.js_interop) 'src/share_plus_web.dart'; - class SharePlus { static SharePlatform get _platform => SharePlatform.instance; From a9fd2fcf67d16e12c3eb7577ffc8b2c9ba9173d4 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 08:59:08 +0100 Subject: [PATCH 03/19] implemented Linux --- .../share_plus/share_plus/lib/share_plus.dart | 10 +- .../share_plus/lib/src/share_plus_linux.dart | 43 +- .../share_plus/lib/src/share_plus_web.dart | 422 +++++++++--------- .../lib/src/share_plus_windows.dart | 102 ++--- .../method_channel/method_channel_share.dart | 182 ++++---- .../share_plus_platform.dart | 51 +-- 6 files changed, 373 insertions(+), 437 deletions(-) diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 7f7ec67041..5ff17603a1 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -9,6 +9,10 @@ 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; +export 'src/share_plus_linux.dart'; +export 'src/share_plus_windows.dart' + if (dart.library.js_interop) 'src/share_plus_web.dart'; + class SharePlus { static SharePlatform get _platform => SharePlatform.instance; @@ -20,6 +24,10 @@ class SharePlus { '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'); } @@ -28,7 +36,7 @@ class SharePlus { throw ArgumentError('files provided, but cannot be empty'); } - return _platform.shareNew(params); + return _platform.share(params); } } 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 03132bddc0..808e0baeb1 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 share_plus_linux; -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..69c13dbadd 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 @@ -29,217 +29,217 @@ class SharePlusWebPlugin extends SharePlatform { @visibleForTesting Navigator? debugNavigator, }) : _navigator = debugNavigator ?? window.navigator; - @override - Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) async { - final data = ShareData( - url: uri.toString(), - ); - - 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, - ); - - throw Exception('Navigator.canShare() is unavailable'); - } - - if (!canShare) { - throw Exception('Navigator.canShare() is false'); - } - - try { - await _navigator.share(data).toDart; - } on DOMException catch (e) { - if (e.name case 'AbortError') { - return _resultDismissed; - } - - developer.log( - 'Failed to share uri', - error: '${e.name}: ${e.message}', - ); - - throw Exception('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('&'), - ); - - final launchResult = await urlLauncher.launchUrl( - uri.toString(), - const LaunchOptions(), - ); - if (!launchResult) { - throw Exception('Failed to launch $uri'); - } - - 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}', - ); - - 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)); - } - - 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) { - data = ShareData( - files: webFiles.toJS, - title: subject, - ); - } else { - data = ShareData( - 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, - ); - - return _downloadIfFallbackEnabled( - files, - fileNameOverrides, - 'Navigator.canShare() is unavailable', - ); - } - - if (!canShare) { - return _downloadIfFallbackEnabled( - files, - fileNameOverrides, - '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) { - final name = e.name; - final message = e.message; - - if (name case 'AbortError') { - return _resultDismissed; - } - - return _downloadIfFallbackEnabled( - files, - fileNameOverrides, - 'Navigator.share() failed: $message', - ); - } - } + // @override + // Future shareUri( + // Uri uri, { + // Rect? sharePositionOrigin, + // }) async { + // final data = ShareData( + // url: uri.toString(), + // ); + + // 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, + // ); + + // throw Exception('Navigator.canShare() is unavailable'); + // } + + // if (!canShare) { + // throw Exception('Navigator.canShare() is false'); + // } + + // try { + // await _navigator.share(data).toDart; + // } on DOMException catch (e) { + // if (e.name case 'AbortError') { + // return _resultDismissed; + // } + + // developer.log( + // 'Failed to share uri', + // error: '${e.name}: ${e.message}', + // ); + + // throw Exception('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('&'), + // ); + + // final launchResult = await urlLauncher.launchUrl( + // uri.toString(), + // const LaunchOptions(), + // ); + // if (!launchResult) { + // throw Exception('Failed to launch $uri'); + // } + + // 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}', + // ); + + // 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)); + // } + + // 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) { + // data = ShareData( + // files: webFiles.toJS, + // title: subject, + // ); + // } else { + // data = ShareData( + // 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, + // ); + + // return _downloadIfFallbackEnabled( + // files, + // fileNameOverrides, + // 'Navigator.canShare() is unavailable', + // ); + // } + + // if (!canShare) { + // return _downloadIfFallbackEnabled( + // files, + // fileNameOverrides, + // '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) { + // final name = e.name; + // final message = e.message; + + // if (name case 'AbortError') { + // return _resultDismissed; + // } + + // return _downloadIfFallbackEnabled( + // files, + // fileNameOverrides, + // 'Navigator.share() failed: $message', + // ); + // } + // } Future _downloadIfFallbackEnabled( List files, 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 5a660e4763..d927530aa1 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 @@ -23,60 +23,60 @@ 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().'); - } + // @override + // Future shareUri( + // Uri uri, { + // String? subject, + // String? text, + // Rect? sharePositionOrigin, + // }) async { + // throw UnimplementedError( + // 'shareUri() has not been implemented on Windows. Use share().'); + // } - /// Share text. - @override - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { - final queryParameters = { - if (subject != null) 'subject': subject, - 'body': text, - }; + // /// Share text. + // @override + // Future share( + // String text, { + // String? subject, + // Rect? sharePositionOrigin, + // }) async { + // 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('&'), - ); + // // 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('Failed to launch $uri'); - } + // final launchResult = await urlLauncher.launchUrl( + // uri.toString(), + // const LaunchOptions(), + // ); + // if (!launchResult) { + // throw Exception('Failed to launch $uri'); + // } - return ShareResult.unavailable; - } + // 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}.', - ); - } + // /// 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_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..67d290e05b 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 @@ -23,97 +23,97 @@ class MethodChannelShare extends SharePlatform { static const MethodChannel channel = 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) ?? - '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); - assert( - fileNameOverrides == null || files.length == fileNameOverrides.length, - "fileNameOverrides list must have the same length as files list.", - ); - 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 paths = filesWithPath.map((e) => e.path).toList(); - assert(paths.length == mimeTypes.length); - assert(mimeTypes.every((element) => element.isNotEmpty)); - - final params = { - 'paths': paths, - 'mimeTypes': mimeTypes, - }; - - if (subject != null) params['subject'] = subject; - if (text != null) params['text'] = text; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - final result = await channel.invokeMethod('shareFiles', params) ?? - 'dev.fluttercommunity.plus/share/unavailable'; - - return ShareResult(result, _statusFromResult(result)); - } + // @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) ?? + // '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); + // assert( + // fileNameOverrides == null || files.length == fileNameOverrides.length, + // "fileNameOverrides list must have the same length as files list.", + // ); + // 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 paths = filesWithPath.map((e) => e.path).toList(); + // assert(paths.length == mimeTypes.length); + // assert(mimeTypes.every((element) => element.isNotEmpty)); + + // final params = { + // 'paths': paths, + // 'mimeTypes': mimeTypes, + // }; + + // if (subject != null) params['subject'] = subject; + // if (text != null) params['text'] = text; + + // if (sharePositionOrigin != null) { + // params['originX'] = sharePositionOrigin.left; + // params['originY'] = sharePositionOrigin.top; + // params['originWidth'] = sharePositionOrigin.width; + // params['originHeight'] = sharePositionOrigin.height; + // } + + // final result = await channel.invokeMethod('shareFiles', params) ?? + // 'dev.fluttercommunity.plus/share/unavailable'; + + // return ShareResult(result, _statusFromResult(result)); + // } /// 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 56a6e67d5d..4f2e029b50 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 @@ -7,7 +7,6 @@ import 'dart:ui'; import 'package:cross_file/cross_file.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:share_plus/share_plus.dart'; import '../method_channel/method_channel_share.dart'; @@ -32,57 +31,13 @@ class SharePlatform extends PlatformInterface { _instance = instance; } - // TODO: Delete - /// Share uri. - Future shareUri( - Uri uri, { - Rect? sharePositionOrigin, - }) { - return _instance.shareUri( - uri, - sharePositionOrigin: sharePositionOrigin, - ); - } - - // TODO: Delete - /// Share text with Result. - Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) async { - return await _instance.share( - text, - subject: subject, - sharePositionOrigin: sharePositionOrigin, - ); - } - - // TODO: Delete - /// 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, - ); - } - - Future shareNew(ShareParams params) async { - return _instance.shareNew(params); + Future share(ShareParams params) async { + return _instance.share(params); } } class ShareParams { - /// + /// The text to share, cannot be provided at the same time as [uri] final String? text; /// Used as share sheet title where supported (e.g. EXTRA_TITLE on Android) From 1ebcf0d8c0fa92b3ce313ee2c0b1b3d850dde45f Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 09:30:03 +0100 Subject: [PATCH 04/19] windows fallback implementation and documentation --- .../share_plus/share_plus/lib/share_plus.dart | 44 +++++++++- .../lib/src/share_plus_windows.dart | 85 +++++++------------ .../share_plus_platform.dart | 57 ++++++++++++- 3 files changed, 128 insertions(+), 58 deletions(-) diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 5ff17603a1..92a4912d8d 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -16,12 +16,46 @@ export 'src/share_plus_windows.dart' class SharePlus { static SharePlatform get _platform => SharePlatform.instance; + /// 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. 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'); + 'At least one of uri, files or text must be provided', + ); } if (params.uri != null && params.text != null) { @@ -36,6 +70,14 @@ class SharePlus { 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); } } 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 d927530aa1..44d0dcc8e4 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 @@ -23,60 +23,37 @@ 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().'); - // } - - // /// Share text. - // @override - // Future share( - // String text, { - // String? subject, - // Rect? sharePositionOrigin, - // }) async { - // 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('Failed to launch $uri'); - // } + @override + 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}.', + ); + } - // return ShareResult.unavailable; - // } + final queryParameters = { + 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 + 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('Failed to launch $uri'); + } - // /// 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}.', - // ); - // } + return ShareResult.unavailable; + } } 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 4f2e029b50..f951611348 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 @@ -38,27 +38,78 @@ class SharePlatform extends PlatformInterface { class ShareParams { /// The text to share, cannot be provided at the same time as [uri] + /// + /// * Supported platforms: All final String? text; /// Used as share sheet title where supported (e.g. EXTRA_TITLE on Android) + /// + /// * Supported platforms: ??? final String? title; /// Only used as email subject where supported (e.g. EXTRA_SUBJECT on Android) + /// + /// * Supported platforms: ??? final String? subject; - /// Only used in Android as preview thumbnail + /// Preview thumbnail + /// + /// * Supported platforms: Android + /// Parameter ignored on other platforms. final XFile? previewThumbnail; - /// Only used in iPad and Mac as share sheet origin + /// 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, alternative to sharing [text] + /// Share a URI. + /// + /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` + /// on iOS. [share] 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. + /// + /// 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; ShareParams({ From bc7d7ba2e5b915821e8bc9f859130bf9fd5fc1e4 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 09:37:59 +0100 Subject: [PATCH 05/19] update example --- .../share_plus/example/lib/main.dart | 79 ++++++++++--------- .../share_plus/share_plus/lib/share_plus.dart | 13 +-- .../lib/src/share_plus_windows.dart | 2 - 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 745abded82..818f1f965a 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -19,11 +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()); } @@ -222,22 +217,28 @@ 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, + subject: subject, + 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), + 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, + subject: subject, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); } scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); @@ -249,15 +250,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) { @@ -272,16 +276,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/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 92a4912d8d..2f347292f1 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -7,7 +7,7 @@ import 'dart:ui'; 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' @@ -16,6 +16,9 @@ export 'src/share_plus_windows.dart' class SharePlus { static SharePlatform get _platform => SharePlatform.instance; + /// The default instance of [SharePlus]. + static final SharePlus instance = SharePlus(); + /// Summons the platform's share sheet to share context. /// /// Wraps the platform's native share dialog. Can share a text and/or a URL. @@ -84,8 +87,6 @@ class SharePlus { @Deprecated('Use SharePlus instead') class Share { - static final SharePlus _sharePlus = SharePlus(); - /// Whether to fall back to downloading files if [shareXFiles] fails on web. @Deprecated('Use ShareParams.downloadFallbackEnabled instead') static bool downloadFallbackEnabled = true; @@ -111,7 +112,7 @@ class Share { Uri uri, { Rect? sharePositionOrigin, }) async { - return _sharePlus.share( + return SharePlus.instance.share( ShareParams( uri: uri, sharePositionOrigin: sharePositionOrigin, @@ -162,7 +163,7 @@ class Share { Rect? sharePositionOrigin, }) async { assert(text.isNotEmpty); - return _sharePlus.share( + return SharePlus.instance.share( ShareParams( text: text, subject: subject, @@ -209,7 +210,7 @@ class Share { List? fileNameOverrides, }) async { assert(files.isNotEmpty); - return _sharePlus.share( + return SharePlus.instance.share( ShareParams( files: files, subject: subject, 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 44d0dcc8e4..c2e39db4ac 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 share_plus_windows; -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'; From 5952490291837b714bf2355e19542e2029d0398a Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 09:52:06 +0100 Subject: [PATCH 06/19] create constructor, fix linux and windows tests --- .../share_plus/share_plus/lib/share_plus.dart | 8 +++++-- .../test/share_plus_linux_test.dart | 21 ++++++++++++------- .../test/share_plus_windows_test.dart | 18 ++++++++++------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 2f347292f1..039979a60d 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -14,10 +14,14 @@ export 'src/share_plus_windows.dart' if (dart.library.js_interop) 'src/share_plus_web.dart'; class SharePlus { - static SharePlatform get _platform => SharePlatform.instance; + /// 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(); + static final SharePlus instance = SharePlus._(SharePlatform.instance); /// Summons the platform's share sheet to share context. /// 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 a542e91923..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 @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:share_plus/share_plus_old.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:share_plus_platform_interface/share_plus_platform_interface.dart'; import 'url_launcher_mock.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_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, ); From 92941092d9a5888bc14e0aa0ba2fd9ae2eb6996d Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 10:38:44 +0100 Subject: [PATCH 07/19] implemented web --- .../share_plus/lib/src/share_plus_web.dart | 379 +++++++----------- .../share_plus_platform.dart | 11 +- 2 files changed, 166 insertions(+), 224 deletions(-) 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 69c13dbadd..09a34360d2 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'; @@ -29,229 +27,164 @@ class SharePlusWebPlugin extends SharePlatform { @visibleForTesting Navigator? debugNavigator, }) : _navigator = debugNavigator ?? window.navigator; - // @override - // Future shareUri( - // Uri uri, { - // Rect? sharePositionOrigin, - // }) async { - // final data = ShareData( - // url: uri.toString(), - // ); - - // 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, - // ); - - // throw Exception('Navigator.canShare() is unavailable'); - // } - - // if (!canShare) { - // throw Exception('Navigator.canShare() is false'); - // } - - // try { - // await _navigator.share(data).toDart; - // } on DOMException catch (e) { - // if (e.name case 'AbortError') { - // return _resultDismissed; - // } - - // developer.log( - // 'Failed to share uri', - // error: '${e.name}: ${e.message}', - // ); - - // throw Exception('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('&'), - // ); - - // final launchResult = await urlLauncher.launchUrl( - // uri.toString(), - // const LaunchOptions(), - // ); - // if (!launchResult) { - // throw Exception('Failed to launch $uri'); - // } - - // 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}', - // ); - - // 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)); - // } - - // 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) { - // data = ShareData( - // files: webFiles.toJS, - // title: subject, - // ); - // } else { - // data = ShareData( - // 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, - // ); - - // return _downloadIfFallbackEnabled( - // files, - // fileNameOverrides, - // 'Navigator.canShare() is unavailable', - // ); - // } - - // if (!canShare) { - // return _downloadIfFallbackEnabled( - // files, - // fileNameOverrides, - // '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) { - // final name = e.name; - // final message = e.message; - - // if (name case 'AbortError') { - // return _resultDismissed; - // } - - // return _downloadIfFallbackEnabled( - // files, - // fileNameOverrides, - // 'Navigator.share() failed: $message', - // ); - // } - // } - - Future _downloadIfFallbackEnabled( - List files, - List? fileNameOverrides, - String message, - ) { - developer.log(message); - if (Share.downloadFallbackEnabled) { - return _download(files, fileNameOverrides); + @override + 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); + } on NoSuchMethodError catch (e) { + developer.log( + 'Share API is not supported in this User Agent.', + error: e, + ); + + return _fallback(params, 'Navigator.canShare() is unavailable'); + } + + if (!canShare) { + return _fallback(params, 'Navigator.canShare() is false'); + } + + try { + await _navigator.share(data).toDart; + } on DOMException catch (e) { + if (e.name case 'AbortError') { + return _resultDismissed; + } + + developer.log( + 'Failed to share uri', + error: '${e.name}: ${e.message}', + ); + + return _fallback(params, 'Navigator.share() failed: ${e.message}'); + } + + return ShareResult.unavailable; + } + + 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; + + // 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)); + } + } + } + + if (uri == null && text == null && webFiles.isEmpty) { + throw ArgumentError( + 'At least one of uri, text, or files must be provided', + ); + } + + if (uri != null && text != null) { + throw ArgumentError('Only one of uri or text can be provided'); + } + + if (uri != null) { + data = ShareData( + url: uri, + ); + } else if (webFiles.isNotEmpty && text != null && title != null) { + data = ShareData( + text: text, + title: title, + files: webFiles.toJS, + ); + } else if (webFiles.isNotEmpty && text != null) { + data = ShareData( + text: text, + files: webFiles.toJS, + ); + } else if (webFiles.isNotEmpty && title != null) { + data = ShareData( + title: title, + files: webFiles.toJS, + ); + } else if (webFiles.isNotEmpty) { + data = ShareData( + files: webFiles.toJS, + ); + } else if (text != null && title != null) { + data = ShareData( + text: text, + title: title, + ); } else { - throw Exception(message); + data = ShareData( + text: text!, + ); + } + + return data; + } + + /// 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 title = params.title ?? 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); + } } + + if (!mailToFallbackEnabled) { + throw Exception(error); + } + + final queryParameters = { + if (title != null) 'subject': title, + '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_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 f951611348..a3b0872afd 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 @@ -44,7 +44,9 @@ class ShareParams { /// Used as share sheet title where supported (e.g. EXTRA_TITLE on Android) /// - /// * Supported platforms: ??? + /// Provided to web Navigator Share API as title. + /// + /// * Supported platforms: Android, Web final String? title; /// Only used as email subject where supported (e.g. EXTRA_SUBJECT on Android) @@ -112,6 +114,12 @@ class ShareParams { /// 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, @@ -122,6 +130,7 @@ class ShareParams { this.files, this.fileNameOverrides, this.downloadFallbackEnabled = true, + this.mailToFallbackEnabled = true, }); } From 52ea7ab3843f65743c6130ae6883d0ccdd5147b3 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 12:11:07 +0100 Subject: [PATCH 08/19] implementing the method channel --- .../share_plus/share_plus/lib/share_plus.dart | 3 + .../share_plus/lib/src/share_plus_web.dart | 4 +- .../method_channel/method_channel_share.dart | 141 +++++++----------- .../share_plus_platform.dart | 10 +- .../share_plus_platform_interface_test.dart | 74 +++++---- 5 files changed, 110 insertions(+), 122 deletions(-) diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 039979a60d..dd9a4bd4f3 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -56,6 +56,9 @@ class SharePlus { /// 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) && 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 09a34360d2..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 @@ -142,7 +142,7 @@ class SharePlusWebPlugin extends SharePlatform { Future _fallback(ShareParams params, String error) async { developer.log(error); - final title = params.title ?? params.subject; + final subject = params.subject; final text = params.text ?? params.uri?.toString() ?? ''; final files = params.files; final fileNameOverrides = params.fileNameOverrides; @@ -162,7 +162,7 @@ class SharePlusWebPlugin extends SharePlatform { } final queryParameters = { - if (title != null) 'subject': title, + if (subject != null) 'subject': subject, 'body': text, }; 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 67d290e05b..9ad6589f73 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 @@ -23,97 +23,56 @@ class MethodChannelShare extends SharePlatform { static const MethodChannel channel = 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) ?? - // '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); - // assert( - // fileNameOverrides == null || files.length == fileNameOverrides.length, - // "fileNameOverrides list must have the same length as files list.", - // ); - // 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 paths = filesWithPath.map((e) => e.path).toList(); - // assert(paths.length == mimeTypes.length); - // assert(mimeTypes.every((element) => element.isNotEmpty)); - - // final params = { - // 'paths': paths, - // 'mimeTypes': mimeTypes, - // }; - - // if (subject != null) params['subject'] = subject; - // if (text != null) params['text'] = text; - - // if (sharePositionOrigin != null) { - // params['originX'] = sharePositionOrigin.left; - // params['originY'] = sharePositionOrigin.top; - // params['originWidth'] = sharePositionOrigin.width; - // params['originHeight'] = sharePositionOrigin.height; - // } - - // final result = await channel.invokeMethod('shareFiles', params) ?? - // 'dev.fluttercommunity.plus/share/unavailable'; - - // return ShareResult(result, _statusFromResult(result)); - // } + @override + 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)); + } + + Future> _toPlatformMap(ShareParams params) async { + assert( + params.text != null || + params.uri != null || + (params.files != null && params.files!.isNotEmpty), + 'At least one of text, uri or files must be provided', + ); + + 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(), + }; + + if (params.sharePositionOrigin != null) { + map['originX'] = params.sharePositionOrigin!.left; + map['originY'] = params.sharePositionOrigin!.top; + map['originWidth'] = params.sharePositionOrigin!.width; + map['originHeight'] = params.sharePositionOrigin!.height; + } + + if (params.files != null) { + final filesWithPath = + await _getFiles(params.files!, params.fileNameOverrides); + assert(filesWithPath.every((element) => element.path.isNotEmpty)); + + final mimeTypes = filesWithPath + .map((e) => e.mimeType ?? _mimeTypeForPath(e.path)) + .toList(); + + final paths = filesWithPath.map((e) => e.path).toList(); + assert(paths.length == mimeTypes.length); + assert(mimeTypes.every((element) => element.isNotEmpty)); + + map['paths'] = paths; + map['mimeTypes'] = mimeTypes; + } + + 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 a3b0872afd..a26d86eaf1 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 @@ -46,16 +46,20 @@ class ShareParams { /// /// Provided to web Navigator Share API as title. /// - /// * Supported platforms: Android, Web + /// * Supported platforms: All final String? title; - /// Only used as email subject where supported (e.g. EXTRA_SUBJECT on Android) + /// 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: ??? + /// * 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; 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'], })); From d2d38a3cf24ab54f404a98eaeecb1a344df3fd66 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 12:32:30 +0100 Subject: [PATCH 09/19] remove unnecessary import --- .../lib/method_channel/method_channel_share.dart | 4 ---- 1 file changed, 4 deletions(-) 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 9ad6589f73..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; From 031afbef241cd08052dddb591691fd4c8895576a Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 14:05:09 +0100 Subject: [PATCH 10/19] implemented and tested Android --- .../plus/share/MethodCallHandler.kt | 28 +--- .../dev/fluttercommunity/plus/share/Share.kt | 140 +++++++++--------- .../share_plus/example/lib/main.dart | 25 +++- .../share_plus_platform.dart | 4 +- 4 files changed, 93 insertions(+), 104 deletions(-) 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/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 818f1f965a..e698caa98c 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -32,6 +32,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; + String title = ''; String uri = ''; String fileName = ''; List imageNames = []; @@ -80,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(), @@ -219,8 +232,9 @@ class DemoAppState extends State { } shareResult = await SharePlus.instance.share( ShareParams( - text: text, - subject: subject, + text: text.isEmpty ? null : text, + subject: subject.isEmpty ? null : subject, + title: title.isEmpty ? null : title, files: files, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), @@ -229,14 +243,17 @@ class DemoAppState extends State { 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 SharePlus.instance.share( ShareParams( - text: text, - subject: subject, + text: text.isEmpty ? null : text, + subject: subject.isEmpty ? null : subject, + title: title.isEmpty ? null : title, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); 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 a26d86eaf1..81b12b31a7 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 @@ -42,8 +42,10 @@ class ShareParams { /// * Supported platforms: All final String? text; - /// Used as share sheet title where supported (e.g. EXTRA_TITLE on Android) + /// 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 From c03c6d579f5f944df79dca283c892adf7e269f46 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 18 Dec 2024 14:25:45 +0100 Subject: [PATCH 11/19] fixing comments --- .../platform_interface/share_plus_platform.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 81b12b31a7..ee730e52f5 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 @@ -37,7 +37,15 @@ class SharePlatform extends PlatformInterface { } class ShareParams { - /// The text to share, cannot be provided at the same time as [uri] + /// 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; @@ -46,6 +54,7 @@ class ShareParams { /// /// 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 @@ -76,11 +85,12 @@ class ShareParams { /// Share a URI. /// - /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` - /// on iOS. [share] will trigger the iOS system to fetch the html page + /// 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 From c67216e5034d9ab5d4687795408a6a4091bfdc1a Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 19 Dec 2024 08:48:15 +0100 Subject: [PATCH 12/19] add SharePlus public API tests --- .../share_plus/share_plus/lib/share_plus.dart | 6 ++ .../share_plus/test/share_plus_test.dart | 75 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/share_plus/share_plus/test/share_plus_test.dart diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index dd9a4bd4f3..69083df96b 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -4,6 +4,7 @@ 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' @@ -23,6 +24,11 @@ class SharePlus { /// 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. 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); + } +} From 91efda9391cf35ea5a899efb23f83b63f4cab1ea Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 19 Dec 2024 08:50:28 +0100 Subject: [PATCH 13/19] improve deprecated warning --- packages/share_plus/share_plus/lib/share_plus.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 69083df96b..904a548016 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -120,7 +120,7 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. - @Deprecated('Use SharePlus.share() instead') + @Deprecated('Use SharePlus.instance.share() instead') static Future shareUri( Uri uri, { Rect? sharePositionOrigin, @@ -169,7 +169,7 @@ 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.share() instead') + @Deprecated('Use SharePlus.instance.share() instead') static Future share( String text, { String? subject, @@ -214,7 +214,7 @@ class Share { /// from [MethodChannel]. /// /// See documentation about [ShareResult] on [share] method. - @Deprecated('Use SharePlus.share() instead') + @Deprecated('Use SharePlus.instance.share() instead') static Future shareXFiles( List files, { String? subject, From ed77e6dee67d6849f274a1277e6a07ea8888c262 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 19 Dec 2024 10:33:48 +0100 Subject: [PATCH 14/19] refactor ios code --- .../example/ios/Runner/AppDelegate.swift | 2 +- .../Sources/share_plus/FPPSharePlusPlugin.m | 202 +++++++++--------- 2 files changed, 107 insertions(+), 97 deletions(-) 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/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m index a3afbfa5c3..b19c37e1bd 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,123 @@ + (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"]; + + // Check if text provided is valid + if (shareText && shareText.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty text expected" + details:nil]); + return; + } - if (shareText.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty text 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; + } - 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 uri provided is valid + if (uri && uri.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty uri 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 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" - 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" + message:@"Each path must not be empty" 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 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 From 59389962294ae743d22e171c6da0ec8d50dbe3fd Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 19 Dec 2024 10:37:55 +0100 Subject: [PATCH 15/19] backwards compatible with subject on iOS --- .../ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m | 4 ++++ 1 file changed, 4 insertions(+) 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 b19c37e1bd..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 @@ -271,6 +271,10 @@ + (void)registerWithRegistrar:(NSObject *)registrar { // 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"]; + } // Check if text provided is valid if (shareText && shareText.length == 0) { From c595508b1192766462532d26608861f08c378758 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 19 Dec 2024 11:14:57 +0100 Subject: [PATCH 16/19] implemented macos --- .../share_plus/SharePlusMacosPlugin.swift | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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) } From 69f0bade0d47dffb74f30ec87e3d3e2fd6a59d69 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Fri, 20 Dec 2024 10:13:19 +0100 Subject: [PATCH 17/19] Refactor windows --- .../example/windows/flutter/CMakeLists.txt | 7 +- .../example/windows/runner/Runner.rc | 10 +- .../share_plus/windows/share_plus_plugin.cpp | 136 +++++++----------- .../windows/share_plus_windows_plugin.h | 6 +- 4 files changed, 68 insertions(+), 91 deletions(-) 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/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_ = {}; }; From c2bd175958e21ab386739d6a97ac76ed8fbb479d Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Fri, 20 Dec 2024 10:43:59 +0100 Subject: [PATCH 18/19] update documentation --- packages/share_plus/share_plus/README.md | 121 +++++++++++++++++------ 1 file changed, 92 insertions(+), 29 deletions(-) diff --git a/packages/share_plus/share_plus/README.md b/packages/share_plus/share_plus/README.md index 282bea03a7..cf1f5bad1a 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 supproted). +- `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) From 869e03b768f0ecef57d14663ee4c768e07426931 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Fri, 20 Dec 2024 10:52:18 +0100 Subject: [PATCH 19/19] fix format --- .../lib/platform_interface/share_plus_platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ee730e52f5..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 @@ -54,7 +54,7 @@ class ShareParams { /// /// 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