mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
A simpler way to share photos to other apps
This commit is contained in:
parent
a2ff2a3788
commit
61c950ecec
11 changed files with 313 additions and 10 deletions
|
@ -17,5 +17,8 @@ class MainActivity : FlutterActivity() {
|
|||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
SelfSignedCertChannelHandler.CHANNEL).setMethodCallHandler(
|
||||
SelfSignedCertChannelHandler(this))
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
ShareChannelHandler.CHANNEL).setMethodCallHandler(
|
||||
ShareChannelHandler(this))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package com.nkming.nc_photos
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class ShareChannelHandler(activity: Activity)
|
||||
: MethodChannel.MethodCallHandler {
|
||||
companion object {
|
||||
const val CHANNEL = "com.nkming.nc_photos/share"
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
if (call.method == "shareItems") {
|
||||
try {
|
||||
shareItems(call.argument<List<String>>("fileUris")!!,
|
||||
call.argument<List<String?>>("mimeTypes")!!,
|
||||
result)
|
||||
} catch (e: Throwable) {
|
||||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareItems(fileUris: List<String>,
|
||||
mimeTypes: List<String?>, result: MethodChannel.Result) {
|
||||
assert(fileUris.isNotEmpty())
|
||||
assert(fileUris.size == mimeTypes.size)
|
||||
val uris = fileUris.map { Uri.parse(it) }
|
||||
|
||||
val shareIntent = if (uris.size == 1) Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, uris[0])
|
||||
type = mimeTypes[0] ?: "*/*"
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
} else Intent().apply {
|
||||
action = Intent.ACTION_SEND_MULTIPLE
|
||||
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
|
||||
type = if (mimeTypes.all { it?.startsWith("image/") == true })
|
||||
"image/*" else "*/*"
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
val shareChooser = Intent.createChooser(shareIntent, _context.getString(
|
||||
R.string.download_successful_notification_action_share_chooser))
|
||||
_context.startActivity(shareChooser)
|
||||
}
|
||||
|
||||
private val _activity = activity
|
||||
private val _context get() = _activity
|
||||
}
|
|
@ -555,6 +555,22 @@
|
|||
"@albumAddTextTooltip": {
|
||||
"description": "Add some text that display between photos to an album"
|
||||
},
|
||||
"shareTooltip": "Share",
|
||||
"@shareTooltip": {
|
||||
"description": "Share the current item to other apps"
|
||||
},
|
||||
"shareSelectedTooltip": "Share selected",
|
||||
"@shareSelectedTooltip": {
|
||||
"description": "Share selected items to other apps"
|
||||
},
|
||||
"shareSelectedEmptyNotification": "Select some photos to share",
|
||||
"@shareSelectedEmptyNotification": {
|
||||
"description": "Shown when user pressed the share button with only non-sharable items (e.g., text labels) selected"
|
||||
},
|
||||
"shareDownloadingDialogContent": "Downloading",
|
||||
"@shareDownloadingDialogContent": {
|
||||
"description": "Downloading photos to be shared"
|
||||
},
|
||||
|
||||
"changelogTitle": "Changelog",
|
||||
"@changelogTitle": {
|
||||
|
|
12
lib/mobile/android/share.dart
Normal file
12
lib/mobile/android/share.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
class Share {
|
||||
static Future<void> shareItems(
|
||||
List<String> fileUris, List<String> mimeTypes) =>
|
||||
_channel.invokeMethod("shareItems", <String, dynamic>{
|
||||
"fileUris": fileUris,
|
||||
"mimeTypes": mimeTypes,
|
||||
});
|
||||
|
||||
static const _channel = const MethodChannel("com.nkming.nc_photos/share");
|
||||
}
|
14
lib/mobile/share.dart
Normal file
14
lib/mobile/share.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:nc_photos/mobile/android/share.dart';
|
||||
import 'package:nc_photos/platform/share.dart' as itf;
|
||||
|
||||
class AndroidShare extends itf.Share {
|
||||
AndroidShare(this.fileUris, this.mimeTypes);
|
||||
|
||||
@override
|
||||
Future<void> share() {
|
||||
return Share.shareItems(fileUris, mimeTypes);
|
||||
}
|
||||
|
||||
final List<String> fileUris;
|
||||
final List<String> mimeTypes;
|
||||
}
|
3
lib/platform/share.dart
Normal file
3
lib/platform/share.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
abstract class Share {
|
||||
Future<void> share();
|
||||
}
|
60
lib/share_handler.dart
Normal file
60
lib/share_handler.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/mobile/share.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/widget/processing_dialog.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
/// Handle sharing to other apps
|
||||
class ShareHandler {
|
||||
Future<void> shareFiles(
|
||||
BuildContext context, Account account, List<File> files) async {
|
||||
assert(platform_k.isAndroid);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ProcessingDialog(
|
||||
text: AppLocalizations.of(context).shareDownloadingDialogContent),
|
||||
);
|
||||
final results = <Tuple2<File, dynamic>>[];
|
||||
for (final f in files) {
|
||||
try {
|
||||
results.add(
|
||||
Tuple2(f, await platform.Downloader().downloadFile(account, f)));
|
||||
} on PermissionException catch (_) {
|
||||
_log.warning("[shareFiles] Permission not granted");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.downloadFailureNoPermissionNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
// dismiss the dialog
|
||||
Navigator.of(context).pop();
|
||||
rethrow;
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[shareFiles] Failed while downloadFile", e, stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(exception_util.toUserString(e, context)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
}
|
||||
// dismiss the dialog
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final share = AndroidShare(results.map((e) => e.item2 as String).toList(),
|
||||
results.map((e) => e.item1.contentType).toList());
|
||||
share.share();
|
||||
}
|
||||
|
||||
static final _log = Logger("share_handler.ShareHandler");
|
||||
}
|
|
@ -15,7 +15,9 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
|
|||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/session_storage.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/resync_album.dart';
|
||||
|
@ -222,6 +224,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
|
||||
Widget _buildSelectionAppBar(BuildContext context) {
|
||||
return buildSelectionAppBar(context, [
|
||||
if (platform_k.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarSharePressed(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: AppLocalizations.of(context).removeSelectedFromAlbumTooltip,
|
||||
|
@ -261,6 +271,27 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
arguments: ViewerArguments(widget.account, _backingFiles, fileIndex));
|
||||
}
|
||||
|
||||
void _onSelectionAppBarSharePressed(BuildContext context) {
|
||||
assert(platform_k.isAndroid);
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
if (selected.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).shareSelectedEmptyNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
ShareHandler().shareFiles(context, widget.account, selected).then((_) {
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onSelectionAppBarRemovePressed() {
|
||||
final selectedIndexes =
|
||||
selectedListItems.map((e) => (e as _ListItem).index).toList();
|
||||
|
@ -526,6 +557,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
} else if (file_util.isSupportedVideoFormat(item.file)) {
|
||||
yield _VideoListItem(
|
||||
index: i,
|
||||
file: item.file,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
|
@ -677,10 +709,31 @@ abstract class _ListItem implements SelectableItem, DraggableItem {
|
|||
final VoidCallback _onDragEndedAny;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _ListItem {
|
||||
_ImageListItem({
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
@required int index,
|
||||
@required this.file,
|
||||
VoidCallback onTap,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
onTap: onTap,
|
||||
onDropBefore: onDropBefore,
|
||||
onDropAfter: onDropAfter,
|
||||
onDragStarted: onDragStarted,
|
||||
onDragEndedAny: onDragEndedAny,
|
||||
);
|
||||
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
@required int index,
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
|
@ -690,6 +743,7 @@ class _ImageListItem extends _ListItem {
|
|||
VoidCallback onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
file: file,
|
||||
onTap: onTap,
|
||||
onDropBefore: onDropBefore,
|
||||
onDropAfter: onDropAfter,
|
||||
|
@ -706,14 +760,14 @@ class _ImageListItem extends _ListItem {
|
|||
);
|
||||
}
|
||||
|
||||
final File file;
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _VideoListItem extends _ListItem {
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
@required int index,
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
|
@ -723,6 +777,7 @@ class _VideoListItem extends _ListItem {
|
|||
VoidCallback onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
file: file,
|
||||
onTap: onTap,
|
||||
onDropBefore: onDropBefore,
|
||||
onDropAfter: onDropAfter,
|
||||
|
|
|
@ -17,6 +17,8 @@ import 'package:nc_photos/exception_util.dart' as exception_util;
|
|||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/populate_album.dart';
|
||||
|
@ -229,6 +231,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
|
||||
Widget _buildSelectionAppBar(BuildContext context) {
|
||||
return buildSelectionAppBar(context, [
|
||||
if (platform_k.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarSharePressed(context);
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
itemBuilder: (context) => [
|
||||
|
@ -313,6 +323,19 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
});
|
||||
}
|
||||
|
||||
void _onSelectionAppBarSharePressed(BuildContext context) {
|
||||
assert(platform_k.isAndroid);
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler().shareFiles(context, widget.account, selected).then((_) {
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onSelectionAppBarDeletePressed() async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
|
@ -392,6 +415,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
);
|
||||
} else if (file_util.isSupportedVideoFormat(f)) {
|
||||
yield _VideoListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
|
@ -433,13 +457,27 @@ abstract class _ListItem implements SelectableItem {
|
|||
final VoidCallback _onTap;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _ListItem {
|
||||
_ImageListItem({
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
@required this.file,
|
||||
VoidCallback onTap,
|
||||
}) : super(
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
}) : super(onTap: onTap);
|
||||
}) : super(
|
||||
file: file,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
|
@ -450,17 +488,20 @@ class _ImageListItem extends _ListItem {
|
|||
);
|
||||
}
|
||||
|
||||
final File file;
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _VideoListItem extends _ListItem {
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
}) : super(onTap: onTap);
|
||||
}) : super(
|
||||
file: file,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
|
|
|
@ -21,8 +21,10 @@ import 'package:nc_photos/exception_util.dart' as exception_util;
|
|||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/metadata_task_manager.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/primitive.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/remove.dart';
|
||||
|
@ -159,6 +161,14 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
title: Text(AppLocalizations.of(context)
|
||||
.selectionAppBarTitle(selectedListItems.length)),
|
||||
actions: [
|
||||
if (platform_k.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarSharePressed(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.playlist_add),
|
||||
tooltip: AppLocalizations.of(context).addSelectedToAlbumTooltip,
|
||||
|
@ -260,6 +270,19 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
arguments: ViewerArguments(widget.account, _backingFiles, index));
|
||||
}
|
||||
|
||||
void _onSelectionAppBarSharePressed(BuildContext context) {
|
||||
assert(platform_k.isAndroid);
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler().shareFiles(context, widget.account, selected).then((_) {
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onSelectionAppBarAddToAlbumPressed(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -699,4 +722,5 @@ class _VideoListItem extends _FileListItem {
|
|||
|
||||
enum _SelectionAppBarMenuOption {
|
||||
archive,
|
||||
delete,
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:nc_photos/mobile/notification.dart';
|
|||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/remove.dart';
|
||||
|
@ -298,6 +299,18 @@ class _ViewerState extends State<Viewer> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
if (platform_k.isAndroid)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.share_outlined,
|
||||
color: Colors.white.withOpacity(.87),
|
||||
),
|
||||
tooltip: AppLocalizations.of(context).shareTooltip,
|
||||
onPressed: () => _onSharePressed(context),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: IconButton(
|
||||
|
@ -543,6 +556,12 @@ class _ViewerState extends State<Viewer> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onSharePressed(BuildContext context) {
|
||||
assert(platform_k.isAndroid);
|
||||
final file = widget.streamFiles[_pageController.page.round()];
|
||||
ShareHandler().shareFiles(context, widget.account, [file]);
|
||||
}
|
||||
|
||||
void _onDownloadPressed(BuildContext context) async {
|
||||
final file = widget.streamFiles[_pageController.page.round()];
|
||||
_log.info("[_onDownloadPressed] Downloading file: ${file.path}");
|
||||
|
|
Loading…
Reference in a new issue