diff --git a/packages/share_plus/share_plus/README.md b/packages/share_plus/share_plus/README.md index 0efc164d4f..238b62a6e6 100644 --- a/packages/share_plus/share_plus/README.md +++ b/packages/share_plus/share_plus/README.md @@ -90,7 +90,7 @@ if (result.status == ShareResultStatus.dismissed) { } ``` -On web, you can use `SharePlus.shareXFiles()`. This uses the [Web Share API](https://web.dev/web-share/) +On web, this uses the [Web Share API](https://web.dev/web-share/) if it's available. Otherwise it falls back to downloading the shared files. 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) @@ -101,6 +101,19 @@ package. Share.shareXFiles([XFile('assets/hello.txt')], text: 'Great picture'); ``` +#### Share Data + +You can also share files that you dynamically generate from its data using [`XFile.fromData`](https://pub.dev/documentation/share_plus/latest/share_plus/XFile/XFile.fromData.html). + +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']); +``` + +> [!CAUTION] +> The `name` parameter in the `XFile.fromData` method is ignored in most platforms. Use `fileNameOverrides` instead. + ### Share URI iOS supports fetching metadata from a URI when shared using `UIActivityViewController`. diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 220d626c59..704e2cf955 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -4,6 +4,7 @@ // ignore_for_file: public_member_api_docs +import 'dart:convert'; import 'dart:io'; import 'package:file_selector/file_selector.dart' @@ -32,6 +33,7 @@ class DemoAppState extends State { String text = ''; String subject = ''; String uri = ''; + String fileName = ''; List imageNames = []; List imagePaths = []; @@ -90,6 +92,18 @@ class DemoAppState extends State { }, ), const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Share Text as File', + hintText: 'Enter the filename you want to share your text as', + ), + maxLines: null, + onChanged: (String value) { + setState(() => fileName = value); + }, + ), + const SizedBox(height: 16), ImagePreviews(imagePaths, onDelete: _onDeleteImage), ElevatedButton.icon( label: const Text('Add image'), @@ -157,6 +171,21 @@ class DemoAppState extends State { ); }, ), + const SizedBox(height: 16), + Builder( + builder: (BuildContext context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + ), + onPressed: fileName.isEmpty || text.isEmpty + ? null + : () => _onShareTextAsXFile(context), + child: const Text('Share text as XFile'), + ); + }, + ), ], ), ), @@ -228,6 +257,25 @@ class DemoAppState extends State { scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); } + void _onShareTextAsXFile(BuildContext context) async { + final box = context.findRenderObject() as RenderBox?; + final scaffoldMessenger = ScaffoldMessenger.of(context); + final data = utf8.encode(text); + final shareResult = await Share.shareXFiles( + [ + XFile.fromData( + data, + // name: fileName, // Notice, how setting the name here does not work. + mimeType: 'text/plain', + ), + ], + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + fileNameOverrides: [fileName], + ); + + scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); + } + SnackBar getResultSnackBar(ShareResult result) { return SnackBar( content: Column( diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 4430151a78..52e50d624d 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -111,6 +111,11 @@ class Share { /// origin rect for the share sheet to popover from on iPads and Macs. It has no effect /// on other devices. /// + /// The optional parameter [fileNameOverrides] can be used to 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. + /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. /// @@ -120,6 +125,7 @@ class Share { String? subject, String? text, Rect? sharePositionOrigin, + List? fileNameOverrides, }) async { assert(files.isNotEmpty); return _platform.shareXFiles( @@ -127,6 +133,7 @@ class Share { subject: subject, text: text, sharePositionOrigin: sharePositionOrigin, + fileNameOverrides: fileNameOverrides, ); } } 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 27f74d3f1d..03132bddc0 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 @@ -68,6 +68,7 @@ class SharePlusLinuxPlugin extends SharePlatform { 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 567e7c802a..a2fb00dbf8 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 @@ -156,10 +156,15 @@ class SharePlusWebPlugin extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, + List? fileNameOverrides, }) async { + assert( + fileNameOverrides == null || files.length == fileNameOverrides.length); final webFiles = []; - for (final xFile in files) { - webFiles.add(await _fromXFile(xFile)); + 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; @@ -222,11 +227,11 @@ class SharePlusWebPlugin extends SharePlatform { } } - static Future _fromXFile(XFile file) async { + static Future _fromXFile(XFile file, {String? nameOverride}) async { final bytes = await file.readAsBytes(); return web.File( [bytes.buffer.toJS].toJS, - file.name, + nameOverride ?? file.name, web.FilePropertyBag() ..type = file.mimeType ?? _mimeTypeForPath(file, bytes), ); 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 cb2cba9ad6..5a660e4763 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 @@ -73,6 +73,7 @@ class SharePlusWindowsPlugin extends SharePlatform { 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 b512f4b2a9..20edd2108b 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 @@ -76,10 +76,14 @@ class MethodChannelShare extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, + List? fileNameOverrides, }) async { assert(files.isNotEmpty); - - final filesWithPath = await _getFiles(files); + 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 @@ -117,7 +121,11 @@ class MethodChannelShare extends SharePlatform { /// then make new file in TemporaryDirectory and return with path /// the system will automatically delete files in this /// TemporaryDirectory as disk space is needed elsewhere on the device - Future _getFile(XFile file, {String? tempRoot}) async { + Future _getFile( + XFile file, { + String? tempRoot, + String? nameOverride, + }) async { if (file.path.isNotEmpty) { return file; } else { @@ -137,13 +145,16 @@ class MethodChannelShare extends SharePlatform { final tempSubfolderPath = "$tempRoot/${const Uuid().v4()}"; await Directory(tempSubfolderPath).create(recursive: true); + // True if filename exists or the filename has a valid extension + final filenameNotEmptyOrHasValidExt = + file.name.isNotEmpty || lookupMimeType(file.name) != null; + + //Per Issue [#3032](https://github.com/fluttercommunity/plus_plugins/issues/3032): use overridden name when available. //Per Issue [#1548](https://github.com/fluttercommunity/plus_plugins/issues/1548): attempt to use XFile.name when available - final filename = file.name.isNotEmpty // If filename exists - || - lookupMimeType(file.name) != - null //If the filename has a valid extension - ? file.name - : "${const Uuid().v1().substring(10)}.$extension"; + final filename = nameOverride ?? + (filenameNotEmptyOrHasValidExt + ? file.name + : "${const Uuid().v1().substring(10)}.$extension"); final path = "$tempSubfolderPath/$filename"; @@ -155,8 +166,18 @@ class MethodChannelShare extends SharePlatform { } /// A wrapper of [MethodChannelShare._getFile] for multiple files. - Future> _getFiles(List files) async => - await Future.wait(files.map((entry) => _getFile(entry))); + Future> _getFiles( + List files, + List? fileNameOverrides, + ) async { + return Future.wait([ + for (var index = 0; index < files.length; index++) + _getFile( + files[index], + nameOverride: fileNameOverrides?.elementAt(index), + ) + ]); + } static String _mimeTypeForPath(String path) { return lookupMimeType(path) ?? 'application/octet-stream'; 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 08df3cc8fe..f1840624ef 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 @@ -61,12 +61,14 @@ class SharePlatform extends PlatformInterface { String? subject, String? text, Rect? sharePositionOrigin, + List? fileNameOverrides, }) async { return _instance.shareXFiles( files, subject: subject, text: text, sharePositionOrigin: sharePositionOrigin, + fileNameOverrides: fileNameOverrides, ); } }