From 0c39dc7578dd477539d1073ce16592a400bf4cba Mon Sep 17 00:00:00 2001 From: Koji Wakamiya Date: Sun, 17 Mar 2024 17:38:05 +0900 Subject: [PATCH] feat(share_plus)!: Migrate to package:web --- .../share_plus/example/pubspec.yaml | 3 +- .../share_plus/share_plus/lib/share_plus.dart | 2 +- .../share_plus/lib/src/share_plus_web.dart | 261 ++++++++++++++++-- packages/share_plus/share_plus/pubspec.yaml | 5 +- 4 files changed, 239 insertions(+), 32 deletions(-) diff --git a/packages/share_plus/share_plus/example/pubspec.yaml b/packages/share_plus/share_plus/example/pubspec.yaml index 3d60efd8b8..b7b9c81ecb 100644 --- a/packages/share_plus/share_plus/example/pubspec.yaml +++ b/packages/share_plus/share_plus/example/pubspec.yaml @@ -24,4 +24,5 @@ flutter: - assets/flutter_logo.png environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.19.0' diff --git a/packages/share_plus/share_plus/lib/share_plus.dart b/packages/share_plus/share_plus/lib/share_plus.dart index 2aeb88f0b5..d36871b794 100644 --- a/packages/share_plus/share_plus/lib/share_plus.dart +++ b/packages/share_plus/share_plus/lib/share_plus.dart @@ -12,7 +12,7 @@ export 'package:share_plus_platform_interface/share_plus_platform_interface.dart export 'src/share_plus_linux.dart'; export 'src/share_plus_windows.dart' - if (dart.library.html) 'src/share_plus_web.dart'; + if (dart.library.js_interop) 'src/share_plus_web.dart'; /// Plugin for summoning a platform share sheet. class Share { 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 562f318831..a24ed4e3da 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,16 @@ -import 'dart:html' as html; +import 'dart:developer' as developer; +import 'dart:js_interop'; import 'dart:typed_data'; +import 'dart:ui'; -import 'package:flutter/widgets.dart'; 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_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'; +import 'package:web/web.dart' as web + show DOMException, File, FilePropertyBag, Navigator, window; /// The web implementation of [SharePlatform]. class SharePlusWebPlugin extends SharePlatform { @@ -17,25 +21,91 @@ class SharePlusWebPlugin extends SharePlatform { SharePlatform.instance = SharePlusWebPlugin(UrlLauncherPlugin()); } - final html.Navigator _navigator; + final web.Navigator _navigator; /// A constructor that allows tests to override the window object used by the plugin. SharePlusWebPlugin( this.urlLauncher, { - @visibleForTesting html.Navigator? debugNavigator, - }) : _navigator = debugNavigator ?? html.window.navigator; + @visibleForTesting web.Navigator? debugNavigator, + }) : _navigator = debugNavigator ?? web.window.navigator; + + @override + Future shareUri( + Uri uri, { + Rect? sharePositionOrigin, + }) async { + final data = ShareData.url( + 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, + ); + + return; + } + + if (!canShare) { + return; + } + + try { + await _navigator.share(data).toDart; + } on web.DOMException catch (e) { + // Ignore DOMException + developer.log( + 'Failed to share uri', + error: '${e.name}: ${e.message}', + ); + } + } - /// Share text @override Future share( String text, { String? subject, Rect? sharePositionOrigin, }) async { + await shareWithResult( + text, + subject: subject, + sharePositionOrigin: sharePositionOrigin, + ); + } + + @override + Future shareWithResult( + String text, { + String? subject, + Rect? sharePositionOrigin, + }) async { + final ShareData data; + if (subject != null && subject.isNotEmpty) { + data = ShareData.textWithTitle( + title: subject, + text: text, + ); + } else { + data = ShareData.text( + text: text, + ); + } + + final bool canShare; try { - await _navigator.share({'title': subject, 'text': text}); - } on NoSuchMethodError catch (_) { - //Navigator is not available or the webPage is not served on https + 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, @@ -57,10 +127,33 @@ class SharePlusWebPlugin extends SharePlatform { if (!launchResult) { throw Exception('Failed to launch $uri'); } + + return _resultUnavailable; } + + if (!canShare) { + return _resultUnavailable; + } + + try { + await _navigator.share(data).toDart; + + // actions is success, but can't get the action name + return _resultUnavailable; + } on web.DOMException catch (e) { + if (e.name case 'AbortError') { + return _resultDismissed; + } + + developer.log( + 'Failed to share text', + error: '${e.name}: ${e.message}', + ); + } + + return _resultUnavailable; } - /// Share files @override Future shareFiles( List paths, { @@ -68,7 +161,24 @@ class SharePlusWebPlugin extends SharePlatform { String? subject, String? text, Rect? sharePositionOrigin, - }) { + }) async { + await shareFilesWithResult( + paths, + mimeTypes: mimeTypes, + subject: subject, + text: text, + sharePositionOrigin: sharePositionOrigin, + ); + } + + @override + Future shareFilesWithResult( + List paths, { + List? mimeTypes, + String? subject, + String? text, + Rect? sharePositionOrigin, + }) async { final files = []; for (var i = 0; i < paths.length; i++) { files.add(XFile(paths[i], mimeType: mimeTypes?[i])); @@ -85,9 +195,7 @@ class SharePlusWebPlugin extends SharePlatform { /// /// Remarks for the web implementation: /// This uses the [Web Share API](https://web.dev/web-share/) if it's - /// available. Otherwise, uncaught Errors will be thrown. - /// See [Can I Use - Web Share API](https://caniuse.com/web-share) to - /// understand which browsers are supported. This builds on the + /// available. This builds on the /// [`cross_file`](https://pub.dev/packages/cross_file) package. @override Future shareXFiles( @@ -96,29 +204,78 @@ class SharePlusWebPlugin extends SharePlatform { String? text, Rect? sharePositionOrigin, }) async { - // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share - - final webFiles = []; + final webFiles = []; for (final xFile in files) { webFiles.add(await _fromXFile(xFile)); } - await _navigator.share({ - if (subject?.isNotEmpty ?? false) 'title': subject, - if (text?.isNotEmpty ?? false) 'text': text, - if (webFiles.isNotEmpty) 'files': webFiles, - }); + + final ShareData data; + if (text != null && text.isNotEmpty) { + if (subject != null && subject.isNotEmpty) { + data = ShareData.filesWithTextAndTitle( + files: webFiles.toJS, + text: text, + title: subject, + ); + } else { + data = ShareData.filesWithText( + files: webFiles.toJS, + text: text, + ); + } + } else if (subject != null && subject.isNotEmpty) { + data = ShareData.filesWithTitle( + files: webFiles.toJS, + title: subject, + ); + } else { + data = ShareData.files( + 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 _resultUnavailable; + } + + if (!canShare) { + return _resultUnavailable; + } + + try { + await _navigator.share(data).toDart; + + // actions is success, but can't get the action name + return _resultUnavailable; + } on web.DOMException catch (e) { + if (e.name case 'AbortError') { + return _resultDismissed; + } + + developer.log( + 'Failed to share files', + error: '${e.name}: ${e.message}', + ); + } return _resultUnavailable; } - static Future _fromXFile(XFile file) async { + static Future _fromXFile(XFile file) async { final bytes = await file.readAsBytes(); - return html.File( - [ByteData.sublistView(bytes)], + return web.File( + [bytes.buffer.toJS].toJS, file.name, - { - 'type': file.mimeType ?? _mimeTypeForPath(file, bytes), - }, + web.FilePropertyBag() + ..type = file.mimeType ?? _mimeTypeForPath(file, bytes), ); } @@ -128,7 +285,55 @@ class SharePlusWebPlugin extends SharePlatform { } } +const _resultDismissed = ShareResult( + '', + ShareResultStatus.dismissed, +); + const _resultUnavailable = ShareResult( 'dev.fluttercommunity.plus/share/unavailable', ShareResultStatus.unavailable, ); + +extension on web.Navigator { + /// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/canShare + external bool canShare(ShareData data); + + /// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share + external JSPromise share(ShareData data); +} + +extension type ShareData._(JSObject _) implements JSObject { + external factory ShareData.text({ + String text, + }); + + external factory ShareData.textWithTitle({ + String text, + String title, + }); + + external factory ShareData.files({ + JSArray files, + }); + + external factory ShareData.filesWithText({ + JSArray files, + String text, + }); + + external factory ShareData.filesWithTitle({ + JSArray files, + String title, + }); + + external factory ShareData.filesWithTextAndTitle({ + JSArray files, + String text, + String title, + }); + + external factory ShareData.url({ + String url, + }); +} diff --git a/packages/share_plus/share_plus/pubspec.yaml b/packages/share_plus/share_plus/pubspec.yaml index 7d52139497..de63c71b67 100644 --- a/packages/share_plus/share_plus/pubspec.yaml +++ b/packages/share_plus/share_plus/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: url_launcher_linux: ^3.0.5 url_launcher_platform_interface: ^2.1.2 ffi: ^2.0.1 + web: ^0.5.0 # win32 is compatible across v4 and v5 for Win32 only (not COM) win32: ">=4.0.0 <6.0.0" @@ -49,5 +50,5 @@ dev_dependencies: flutter_lints: ">=2.0.1 <4.0.0" environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0"