Skip to content

Commit

Permalink
feat(share_plus)!: Migrate to package:web (#2709)
Browse files Browse the repository at this point in the history
  • Loading branch information
koji-1009 committed Mar 17, 2024
1 parent c7e56de commit 641e790
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 32 deletions.
3 changes: 2 additions & 1 deletion packages/share_plus/share_plus/example/pubspec.yaml
Expand Up @@ -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'
2 changes: 1 addition & 1 deletion packages/share_plus/share_plus/lib/share_plus.dart
Expand Up @@ -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 {
Expand Down
261 changes: 233 additions & 28 deletions 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 {
Expand All @@ -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<void> 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<void> share(
String text, {
String? subject,
Rect? sharePositionOrigin,
}) async {
await shareWithResult(
text,
subject: subject,
sharePositionOrigin: sharePositionOrigin,
);
}

@override
Future<ShareResult> 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,
Expand All @@ -57,18 +127,58 @@ 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<void> shareFiles(
List<String> paths, {
List<String>? mimeTypes,
String? subject,
String? text,
Rect? sharePositionOrigin,
}) {
}) async {
await shareFilesWithResult(
paths,
mimeTypes: mimeTypes,
subject: subject,
text: text,
sharePositionOrigin: sharePositionOrigin,
);
}

@override
Future<ShareResult> shareFilesWithResult(
List<String> paths, {
List<String>? mimeTypes,
String? subject,
String? text,
Rect? sharePositionOrigin,
}) async {
final files = <XFile>[];
for (var i = 0; i < paths.length; i++) {
files.add(XFile(paths[i], mimeType: mimeTypes?[i]));
Expand All @@ -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<ShareResult> shareXFiles(
Expand All @@ -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 = <html.File>[];
final webFiles = <web.File>[];
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<html.File> _fromXFile(XFile file) async {
static Future<web.File> _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),
);
}

Expand All @@ -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<web.File> files,
});

external factory ShareData.filesWithText({
JSArray<web.File> files,
String text,
});

external factory ShareData.filesWithTitle({
JSArray<web.File> files,
String title,
});

external factory ShareData.filesWithTextAndTitle({
JSArray<web.File> files,
String text,
String title,
});

external factory ShareData.url({
String url,
});
}
5 changes: 3 additions & 2 deletions packages/share_plus/share_plus/pubspec.yaml
Expand Up @@ -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"
Expand All @@ -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"

0 comments on commit 641e790

Please sign in to comment.