From 2f532176ed244f5ddd1cae12ef955934ba60f387 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 22 Apr 2020 13:19:32 +0900 Subject: [PATCH] selection: share --- .../channelhandlers/AppAdapterHandler.java | 43 +++++++++++++++++-- lib/services/android_app_service.dart | 5 +-- lib/widgets/album/app_bar.dart | 24 ++++++++++- lib/widgets/common/icons.dart | 10 ++++- .../fullscreen_action_delegate.dart | 4 +- .../fullscreen/fullscreen_actions.dart | 11 ++--- 6 files changed, 81 insertions(+), 16 deletions(-) diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java index 7976027df..22fe63627 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/AppAdapterHandler.java @@ -10,6 +10,7 @@ import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import com.bumptech.glide.Glide; @@ -20,9 +21,13 @@ import com.bumptech.glide.signature.ObjectKey; import java.io.ByteArrayOutputStream; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -81,9 +86,8 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler { } case "share": { String title = call.argument("title"); - Uri uri = Uri.parse(call.argument("uri")); - String mimeType = call.argument("mimeType"); - share(title, uri, mimeType); + Map> urisByMimeType = call.argument("urisByMimeType"); + shareMultiple(title, urisByMimeType); result.success(null); break; } @@ -190,7 +194,7 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler { context.startActivity(Intent.createChooser(intent, title)); } - private void share(String title, Uri uri, String mimeType) { + private void shareSingle(String title, Uri uri, String mimeType) { Intent intent = new Intent(Intent.ACTION_SEND); if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) { String path = uri.getPath(); @@ -205,4 +209,35 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler { intent.setType(mimeType); context.startActivity(Intent.createChooser(intent, title)); } + + private void shareMultiple(String title, @Nullable Map> urisByMimeType) { + if (urisByMimeType == null) return; + + ArrayList uriList = urisByMimeType.values().stream().flatMap(Collection::stream).map(Uri::parse).collect(Collectors.toCollection(ArrayList::new)); + String[] mimeTypes = urisByMimeType.keySet().toArray(new String[0]); + + // simplify share intent for a single item, as some apps can handle one item but not more + if (uriList.size() == 1) { + shareSingle(title, uriList.get(0), mimeTypes[0]); + return; + } + + String mimeType = "*/*"; + if (mimeTypes.length == 1) { + // items have the same mime type & subtype + mimeType = mimeTypes[0]; + } else { + // items have different subtypes + String[] mimeTypeTypes = Arrays.stream(mimeTypes).map(mt -> mt.split("/")[0]).distinct().toArray(String[]::new); + if (mimeTypeTypes.length == 1) { + // items have the same mime type + mimeType = mimeTypeTypes[0] + "/*"; + } + } + + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList); + intent.setType(mimeType); + context.startActivity(Intent.createChooser(intent, title)); + } } diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 4824886b7..19857a192 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -76,12 +76,11 @@ class AndroidAppService { } } - static Future share(String uri, String mimeType) async { + static Future share(Map> urisByMimeType) async { try { await platform.invokeMethod('share', { 'title': 'Share via:', - 'uri': uri, - 'mimeType': mimeType, + 'urisByMimeType': urisByMimeType, }); } on PlatformException catch (e) { debugPrint('share failed with code=${e.code}, exception=${e.message}, details=${e.details}'); diff --git a/lib/widgets/album/app_bar.dart b/lib/widgets/album/app_bar.dart index f6ee63f16..abc8c8730 100644 --- a/lib/widgets/album/app_bar.dart +++ b/lib/widgets/album/app_bar.dart @@ -1,10 +1,14 @@ import 'package:aves/model/collection_lens.dart'; +import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/services/android_app_service.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/common/menu_row.dart'; +import 'package:aves/widgets/fullscreen/fullscreen_actions.dart'; import 'package:aves/widgets/stats/stats.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -128,7 +132,8 @@ class _CollectionAppBarState extends State with SingleTickerPr animation: collection.selectionChangeNotifier, builder: (context, child) { final selection = collection.selection; - return Text(selection.isEmpty ? 'Select items' : '${selection.length} ${Intl.plural(selection.length, one: 'item', other: 'items')}'); + final count = selection.length; + return Text(selection.isEmpty ? 'Select items' : '${count} ${Intl.plural(count, one: 'item', other: 'items')}'); }, ); } @@ -142,6 +147,18 @@ class _CollectionAppBarState extends State with SingleTickerPr icon: const Icon(OMIcons.search), onPressed: _goToSearch, ), + if (collection.isSelecting) + AnimatedBuilder( + animation: collection.selectionChangeNotifier, + builder: (context, child) { + const action = FullscreenAction.share; + return IconButton( + icon: Icon(action.getIcon()), + onPressed: collection.selection.isEmpty ? null : _shareSelection, + tooltip: action.getText(), + ); + }, + ), Builder( builder: (context) => PopupMenuButton( itemBuilder: (context) => [ @@ -258,6 +275,11 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } + void _shareSelection() { + final urisByMimeType = groupBy(collection.selection, (e) => e.mimeType).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); + AndroidAppService.share(urisByMimeType); + } + void _onActivityChange() { if (collection.isSelecting) { _browseToSelectAnimation.forward(); diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index cf82e7108..0c071cc45 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -8,12 +8,18 @@ import 'package:outline_material_icons/outline_material_icons.dart'; class AIcons { static const IconData date = OMIcons.calendarToday; - static const IconData favourite = OMIcons.favoriteBorder; - static const IconData favouriteActive = OMIcons.favorite; static const IconData location = OMIcons.place; static const IconData tag = OMIcons.localOffer; static const IconData video = OMIcons.movie; + static const IconData delete = OMIcons.delete; + static const IconData favourite = OMIcons.favoriteBorder; + static const IconData favouriteActive = OMIcons.favorite; + static const IconData print = OMIcons.print; + static const IconData rotateLeft = OMIcons.rotateLeft; + static const IconData rotateRight = OMIcons.rotateRight; + static const IconData share = OMIcons.share; + static const IconData animated = Icons.slideshow; static const IconData play = Icons.play_circle_outline; static const IconData selected = Icons.check_circle_outline; diff --git a/lib/widgets/fullscreen/fullscreen_action_delegate.dart b/lib/widgets/fullscreen/fullscreen_action_delegate.dart index bd8c3448b..aad2b0ff7 100644 --- a/lib/widgets/fullscreen/fullscreen_action_delegate.dart +++ b/lib/widgets/fullscreen/fullscreen_action_delegate.dart @@ -62,7 +62,9 @@ class FullscreenActionDelegate { AndroidAppService.setAs(entry.uri, entry.mimeType); break; case FullscreenAction.share: - AndroidAppService.share(entry.uri, entry.mimeType); + AndroidAppService.share({ + entry.mimeType: [entry.uri] + }); break; case FullscreenAction.debug: _goToDebug(context, entry); diff --git a/lib/widgets/fullscreen/fullscreen_actions.dart b/lib/widgets/fullscreen/fullscreen_actions.dart index 781011bcc..840c024af 100644 --- a/lib/widgets/fullscreen/fullscreen_actions.dart +++ b/lib/widgets/fullscreen/fullscreen_actions.dart @@ -1,3 +1,4 @@ +import 'package:aves/widgets/common/icons.dart'; import 'package:flutter/material.dart'; import 'package:outline_material_icons/outline_material_icons.dart'; @@ -66,19 +67,19 @@ extension ExtraFullscreenAction on FullscreenAction { // different data depending on toggle state return null; case FullscreenAction.delete: - return OMIcons.delete; + return AIcons.delete; case FullscreenAction.info: return OMIcons.info; case FullscreenAction.rename: return OMIcons.title; case FullscreenAction.rotateCCW: - return OMIcons.rotateLeft; + return AIcons.rotateLeft; case FullscreenAction.rotateCW: - return OMIcons.rotateRight; + return AIcons.rotateRight; case FullscreenAction.print: - return OMIcons.print; + return AIcons.print; case FullscreenAction.share: - return OMIcons.share; + return AIcons.share; // external app actions case FullscreenAction.edit: case FullscreenAction.open: