Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(share_plus): support rich preview on the share sheet #3372

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions packages/share_plus/share_plus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ sharing to email.
Share.share('check out my website https://example.com', subject: 'Look what I made!');
```

The optional `title` and `thumbnail` parameters enable
[rich content preview](https://developer.android.com/training/sharing/send#adding-rich-content-previews)
on Android when sharing text.

On the web the `title` or the `subject` (when the `title` is omitted) is passed to the
[Web Share API](https://web.dev/web-share/)'s title parameter.

```dart
Share.share('Content which will be shared', title: 'Preview title', thumbnail: XFile('path/to/thumbnail.png'));
```

> [!CAUTION]
> For the `thumbnail` parameter the
> [Sharing data created with XFile.fromData](#sharing-data-created-with-xfilefromdata)
> limitation has to be considered.

`share()` returns `status` object that allows to check the result of user action in the share sheet.

```dart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ internal class MethodCallHandler(
call.argument<Any>("text") as String,
call.argument<Any>("subject") as String?,
isWithResult,
title = call.argument<String?>("title"),
thumbnailPath = call.argument<String?>("thumbnailPath"),
)
success(isWithResult, result)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package dev.fluttercommunity.plus.share

import android.app.Activity
import android.app.PendingIntent
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.content.FileProvider
import java.io.File
import java.io.IOException
Expand All @@ -21,6 +23,10 @@ internal class Share(
private var activity: Activity?,
private val manager: ShareSuccessManager
) {
companion object {
const val TAG = "FlutterSharePlus"
}

private val providerAuthority: String by lazy {
getContext().packageName + ".flutter.share_provider"
}
Expand Down Expand Up @@ -55,14 +61,26 @@ internal class Share(
this.activity = activity
}

fun share(text: String, subject: String?, withResult: Boolean) {
fun share(
text: String,
subject: String?,
withResult: Boolean,
title: String? = null,
thumbnailPath: String? = null,
) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
if (subject != null) {
putExtra(Intent.EXTRA_SUBJECT, subject)
}
if (title != null) {
putExtra(Intent.EXTRA_TITLE, title)
}
if (thumbnailPath != null) {
addThumbnail(this, thumbnailPath)
}
}
// If we dont want the result we use the old 'createChooser'
val chooserIntent =
Expand Down Expand Up @@ -251,4 +269,18 @@ internal class Share(
file.copyTo(newFile, true)
return newFile
}

private fun addThumbnail(intent: Intent, thumbnailPath: String) {
try {
clearShareCacheFolder()
val uri = getUrisForPaths(listOf(thumbnailPath)).first()
intent.apply {
clipData = ClipData.newUri(getContext().contentResolver, null, uri)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
} catch (e: IOException) {
// do not prevent sharing if the thumbnail cannot be added
Log.e(TAG, "Failed to add thumbnail", e)
}
}
}
95 changes: 65 additions & 30 deletions packages/share_plus/share_plus/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ class DemoApp extends StatefulWidget {
class DemoAppState extends State<DemoApp> {
String text = '';
String subject = '';
String title = '';
String uri = '';
String fileName = '';
List<String> imageNames = [];
List<String> imagePaths = [];
XFile? thumbnail;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -80,6 +82,18 @@ class DemoAppState extends State<DemoApp> {
}),
),
const SizedBox(height: 16),
TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Share title',
hintText: 'Enter title to share (optional)',
),
maxLines: null,
onChanged: (String value) => setState(() {
title = value;
}),
),
const SizedBox(height: 16),
TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
Expand Down Expand Up @@ -108,39 +122,21 @@ class DemoAppState extends State<DemoApp> {
ElevatedButton.icon(
label: const Text('Add image'),
onPressed: () async {
// Using `package:image_picker` to get image from gallery.
if (!kIsWeb &&
(Platform.isMacOS ||
Platform.isLinux ||
Platform.isWindows)) {
// Using `package:file_selector` on windows, macos & Linux, since `package:image_picker` is not supported.
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'jpeg', 'png', 'gif'],
);
final file = await openFile(
acceptedTypeGroups: <XTypeGroup>[typeGroup]);
if (file != null) {
setState(() {
imagePaths.add(file.path);
imageNames.add(file.name);
});
}
} else {
final imagePicker = ImagePicker();
final pickedFile = await imagePicker.pickImage(
source: ImageSource.gallery,
);
if (pickedFile != null) {
setState(() {
imagePaths.add(pickedFile.path);
imageNames.add(pickedFile.name);
});
}
}
await _pickImage();
},
icon: const Icon(Icons.add),
),
const SizedBox(height: 16),
if (thumbnail != null)
ImagePreviews([thumbnail!.path], onDelete: _onDeleteThumbnail)
else
ElevatedButton.icon(
label: const Text('Add thumbnail'),
icon: const Icon(Icons.image),
onPressed: () async {
await _pickImage(pickThumbnail: true);
},
),
const SizedBox(height: 32),
Builder(
builder: (BuildContext context) {
Expand Down Expand Up @@ -200,6 +196,12 @@ class DemoAppState extends State<DemoApp> {
});
}

void _onDeleteThumbnail(int position) {
setState(() {
thumbnail = null;
});
}

void _onShareWithResult(BuildContext context) async {
// A builder is used to retrieve the context immediately
// surrounding the ElevatedButton.
Expand Down Expand Up @@ -232,6 +234,8 @@ class DemoAppState extends State<DemoApp> {
shareResult = await Share.share(
text,
subject: subject,
title: title,
thumbnail: thumbnail,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
}
Expand Down Expand Up @@ -276,6 +280,37 @@ class DemoAppState extends State<DemoApp> {
scaffoldMessenger.showSnackBar(getResultSnackBar(shareResult));
}

Future<void> _pickImage({bool pickThumbnail = false}) async {
// Using `package:image_picker` to get image from gallery.
late final XFile? pickedFile;
if (!kIsWeb &&
(Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
// Using `package:file_selector` on windows, macos & Linux, since `package:image_picker` is not supported.
const XTypeGroup typeGroup = XTypeGroup(
label: 'images',
extensions: <String>['jpg', 'jpeg', 'png', 'gif'],
);
pickedFile = await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
} else {
final imagePicker = ImagePicker();
pickedFile = await imagePicker.pickImage(
source: ImageSource.gallery,
);
}

setState(() {
if (pickedFile == null) {
return;
}
if (pickThumbnail) {
thumbnail = pickedFile;
} else {
imagePaths.add(pickedFile.path);
imageNames.add(pickedFile.name);
}
});
}

SnackBar getResultSnackBar(ShareResult result) {
return SnackBar(
content: Column(
Expand Down
12 changes: 12 additions & 0 deletions packages/share_plus/share_plus/lib/share_plus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ class Share {
/// origin rect for the share sheet to popover from on iPads and Macs. It has no effect
/// on other devices.
///
/// The optional [title] parameter can be used to specify a title for the shared text.
/// This works only on Android and on the Web for text only sharing as additional context.
/// It is not part of the shared data.
///
/// The optional [thumbnail] parameter can be used to specify a thumbnail for
/// the shared text on Android. This is only displayed on the share sheet
/// for additional context, it is not part of the shared data.
///
/// May throw [PlatformException] or [FormatException]
/// from [MethodChannel].
///
Expand All @@ -82,13 +90,17 @@ class Share {
static Future<ShareResult> share(
String text, {
String? subject,
String? title,
Rect? sharePositionOrigin,
XFile? thumbnail,
}) async {
assert(text.isNotEmpty);
return _platform.share(
text,
subject: subject,
sharePositionOrigin: sharePositionOrigin,
title: title,
thumbnail: thumbnail,
);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/share_plus/share_plus/lib/src/share_plus_linux.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class SharePlusLinuxPlugin extends SharePlatform {
Future<ShareResult> share(
String text, {
String? subject,
String? title,
Rect? sharePositionOrigin,
XFile? thumbnail,
}) async {
final queryParameters = {
if (subject != null) 'subject': subject,
Expand Down
8 changes: 6 additions & 2 deletions packages/share_plus/share_plus/lib/src/share_plus_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,16 @@ class SharePlusWebPlugin extends SharePlatform {
Future<ShareResult> share(
String text, {
String? subject,
String? title,
Rect? sharePositionOrigin,
XFile? thumbnail,
}) async {
final ShareData data;
if (subject != null && subject.isNotEmpty) {
final hasSubject = subject != null && subject.isNotEmpty;
final hasTitle = title != null && title.isNotEmpty;
if (hasTitle || hasSubject) {
data = ShareData(
title: subject,
title: hasTitle ? title : subject!,
text: text,
);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class SharePlusWindowsPlugin extends SharePlatform {
Future<ShareResult> share(
String text, {
String? subject,
String? title,
Rect? sharePositionOrigin,
XFile? thumbnail,
}) async {
final queryParameters = {
if (subject != null) 'subject': subject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@

import 'dart:async';
import 'dart:io';

// Keep dart:ui for retrocompatiblity with Flutter <3.3.0
// ignore: unnecessary_import
import 'dart:ui';

import 'package:flutter/services.dart';
import 'package:meta/meta.dart' show visibleForTesting;
import 'package:mime/mime.dart' show extensionFromMime, lookupMimeType;
import 'package:share_plus_platform_interface/share_plus_platform_interface.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus_platform_interface/share_plus_platform_interface.dart';
import 'package:uuid/uuid.dart';

/// Plugin for summoning a platform share sheet.
Expand Down Expand Up @@ -48,12 +47,15 @@ class MethodChannelShare extends SharePlatform {
Future<ShareResult> share(
String text, {
String? subject,
String? title,
Rect? sharePositionOrigin,
XFile? thumbnail,
}) async {
assert(text.isNotEmpty);
final params = <String, dynamic>{
'text': text,
'subject': subject,
'title': title,
};

if (sharePositionOrigin != null) {
Expand All @@ -63,6 +65,11 @@ class MethodChannelShare extends SharePlatform {
params['originHeight'] = sharePositionOrigin.height;
}

if (thumbnail != null) {
final thumbnailFile = await _getFile(thumbnail);
params['thumbnailPath'] = thumbnailFile.path;
}

final result = await channel.invokeMethod<String>('share', params) ??
'dev.fluttercommunity.plus/share/unavailable';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,16 @@ class SharePlatform extends PlatformInterface {
Future<ShareResult> share(
String text, {
String? subject,
String? title,
Rect? sharePositionOrigin,
XFile? thumbnail,
}) async {
return await _instance.share(
text,
subject: subject,
sharePositionOrigin: sharePositionOrigin,
title: title,
thumbnail: thumbnail,
);
}

Expand Down
Loading
Loading