From 95a12ee3982dd61de5d07005de62f81c2e99eb08 Mon Sep 17 00:00:00 2001 From: Eugene Kuleshov Date: Thu, 12 Dec 2024 14:31:36 -0500 Subject: [PATCH] fix(share_plus): fallback for shareXFiles() to use download on web (#3388) Co-authored-by: Miguel Beltran --- packages/share_plus/share_plus/README.md | 6 ++ .../share_plus/example/lib/main.dart | 69 +++++++++++-------- .../share_plus/share_plus/lib/share_plus.dart | 3 + .../share_plus/lib/src/share_plus_web.dart | 63 +++++++++++++++-- 4 files changed, 107 insertions(+), 34 deletions(-) diff --git a/packages/share_plus/share_plus/README.md b/packages/share_plus/share_plus/README.md index 1147ca20ff..282bea03a7 100644 --- a/packages/share_plus/share_plus/README.md +++ b/packages/share_plus/share_plus/README.md @@ -101,6 +101,12 @@ package. Share.shareXFiles([XFile('assets/hello.txt')], text: 'Great picture'); ``` +File downloading fallback mechanism for web can be disabled by setting: + +```dart +Share.downloadFallbackEnabled = false; +``` + #### 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). diff --git a/packages/share_plus/share_plus/example/lib/main.dart b/packages/share_plus/share_plus/example/lib/main.dart index 704e2cf955..913a6e33ce 100644 --- a/packages/share_plus/share_plus/example/lib/main.dart +++ b/packages/share_plus/share_plus/example/lib/main.dart @@ -19,6 +19,10 @@ 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()); } @@ -241,39 +245,50 @@ class DemoAppState extends State { void _onShareXFileFromAssets(BuildContext context) async { final box = context.findRenderObject() as RenderBox?; final scaffoldMessenger = ScaffoldMessenger.of(context); - 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, - ); - - scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); + 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, + ); + scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); + } catch (e) { + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } } 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)); + 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], + ); + + scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult)); + } catch (e) { + scaffoldMessenger.showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } } SnackBar getResultSnackBar(ShareResult result) { diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 52e50d624d..e6e2f64a8b 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -18,6 +18,9 @@ export 'src/share_plus_windows.dart' class Share { static SharePlatform get _platform => SharePlatform.instance; + /// Whether to fall back to downloading files if [shareXFiles] fails on web. + static bool downloadFallbackEnabled = true; + /// Summons the platform's share sheet to share uri. /// /// Wraps the platform's native share dialog. Can share a URL. 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 45d7968aee..d0456d327c 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 @@ -6,6 +6,7 @@ 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'; @@ -204,11 +205,19 @@ class SharePlusWebPlugin extends SharePlatform { error: e, ); - throw Exception('Navigator.canShare() is unavailable'); + return _downloadIfFallbackEnabled( + files, + fileNameOverrides, + 'Navigator.canShare() is unavailable', + ); } if (!canShare) { - throw Exception('Navigator.canShare() is false'); + return _downloadIfFallbackEnabled( + files, + fileNameOverrides, + 'Navigator.canShare() is false', + ); } try { @@ -217,16 +226,56 @@ class SharePlusWebPlugin extends SharePlatform { // actions is success, but can't get the action name return ShareResult.unavailable; } on DOMException catch (e) { - if (e.name case 'AbortError') { + final name = e.name; + final message = e.message; + + if (name case 'AbortError') { return _resultDismissed; } - developer.log( - 'Failed to share files', - error: '${e.name}: ${e.message}', + return _downloadIfFallbackEnabled( + files, + fileNameOverrides, + 'Navigator.share() failed: $message', ); + } + } - throw Exception('Navigator.share() failed: ${e.message}'); + Future _downloadIfFallbackEnabled( + List files, + List? fileNameOverrides, + String message, + ) { + developer.log(message); + if (Share.downloadFallbackEnabled) { + return _download(files, fileNameOverrides); + } else { + throw Exception(message); + } + } + + Future _download( + List files, + List? fileNameOverrides, + ) async { + developer.log('Download files as fallback'); + try { + for (final (index, file) in files.indexed) { + final bytes = await file.readAsBytes(); + + final anchor = document.createElement('a') as HTMLAnchorElement + ..href = Uri.dataFromBytes(bytes).toString() + ..style.display = 'none' + ..download = fileNameOverrides?.elementAt(index) ?? file.name; + document.body!.children.add(anchor); + anchor.click(); + anchor.remove(); + } + + return ShareResult.unavailable; + } catch (error) { + developer.log('Failed to download files', error: error); + throw Exception('Failed to to download files: $error'); } }