Skip to content

Commit

Permalink
[share_plus] add support for thumbnail and title
Browse files Browse the repository at this point in the history
  • Loading branch information
slaci committed Nov 27, 2024
1 parent 2d69758 commit 293c3d8
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 42 deletions.
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

0 comments on commit 293c3d8

Please sign in to comment.