Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/share_plus/share_plus/example/pubspec.yaml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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"