Support sharing a reduced quality preview

This commit is contained in:
Ming Ming 2022-09-10 00:29:33 +08:00
parent 3f3f5b9f81
commit c47880be37
10 changed files with 202 additions and 16 deletions

View file

@ -1,6 +1,7 @@
package com.nkming.nc_photos package com.nkming.nc_photos
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -54,14 +55,26 @@ class ShareChannelHandler(activity: Activity) :
val shareIntent = if (uris.size == 1) Intent().apply { val shareIntent = if (uris.size == 1) Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uris[0]) putExtra(Intent.EXTRA_STREAM, uris[0])
// setting clipdata is needed for FLAG_GRANT_READ_URI_PERMISSION to
// work, see: https://developer.android.com/reference/android/content/Intent#ACTION_SEND
clipData =
ClipData.newUri(_context.contentResolver, "Share", uris[0])
type = mimeTypes[0] ?: "*/*" type = mimeTypes[0] ?: "*/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
} else Intent().apply { } else Intent().apply {
action = Intent.ACTION_SEND_MULTIPLE action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris)) putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
type = clipData =
if (mimeTypes.all { it?.startsWith("image/") == true }) "image/*" else "*/*" ClipData.newUri(_context.contentResolver, "Share", uris[0])
.apply {
for (uri in uris.subList(1, uris.size)) {
addItem(ClipData.Item(uri))
}
}
type = if (mimeTypes.all {
it?.startsWith("image/") == true
}) "image/*" else "*/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
} }

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<external-path name="downloads" path="Download"/> <external-path name="downloads" path="Download"/>
<cache-path name="previews" path="largeImageCache"/>
</paths> </paths>

View file

@ -51,6 +51,7 @@ class ThumbnailCacheManager {
/// very large in size. Since large images are only viewed one by one (unlike /// very large in size. Since large images are only viewed one by one (unlike
/// thumbnails), they are less critical to the overall app responsiveness /// thumbnails), they are less critical to the overall app responsiveness
class LargeImageCacheManager { class LargeImageCacheManager {
// used in file_paths.xml, must not change
static const key = "largeImageCache"; static const key = "largeImageCache";
static CacheManager inst = CacheManager( static CacheManager inst = CacheManager(
Config( Config(

View file

@ -906,11 +906,16 @@
"@shareMethodDialogTitle": { "@shareMethodDialogTitle": {
"description": "Let the user pick how they want to share" "description": "Let the user pick how they want to share"
}, },
"shareMethodFileTitle": "File", "shareMethodPreviewTitle": "Preview",
"@shareMethodFileTitle": { "@shareMethodPreviewTitle": {
"description": "Share the actual file" "description": "Share the preview of a file"
}, },
"shareMethodFileDescription": "Download the file and share it to other apps", "shareMethodPreviewDescription": "Share a reduced quality preview to other apps (only support images)",
"shareMethodOriginalFileTitle": "Original file",
"@shareMethodOriginalFileTitle": {
"description": "Share the original file"
},
"shareMethodOriginalFileDescription": "Download the original file and share it to other apps",
"shareMethodPublicLinkTitle": "Public link", "shareMethodPublicLinkTitle": "Public link",
"@shareMethodPublicLinkTitle": { "@shareMethodPublicLinkTitle": {
"description": "Create a share link on server and share it" "description": "Create a share link on server and share it"

View file

@ -41,6 +41,10 @@
"sortOptionFilenameDescendingLabel", "sortOptionFilenameDescendingLabel",
"sortOptionManualLabel", "sortOptionManualLabel",
"helpButtonLabel", "helpButtonLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"collectionSharingLabel", "collectionSharingLabel",
"fileLastSharedDescription", "fileLastSharedDescription",
"fileLastSharedByOthersDescription", "fileLastSharedByOthersDescription",
@ -210,8 +214,10 @@
"slideshowSetupDialogRepeatTitle", "slideshowSetupDialogRepeatTitle",
"linkCopiedNotification", "linkCopiedNotification",
"shareMethodDialogTitle", "shareMethodDialogTitle",
"shareMethodFileTitle", "shareMethodPreviewTitle",
"shareMethodFileDescription", "shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"shareMethodPublicLinkTitle", "shareMethodPublicLinkTitle",
"shareMethodPublicLinkDescription", "shareMethodPublicLinkDescription",
"shareMethodPasswordLinkTitle", "shareMethodPasswordLinkTitle",
@ -346,6 +352,10 @@
"settingsMemoriesRangeTitle", "settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText", "settingsMemoriesRangeValueText",
"settingsDoubleTapExitTitle", "settingsDoubleTapExitTitle",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceStyleTransferStyleDialogTitle", "enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification", "doubleTapExitNotification",
"imageEditDiscardDialogTitle", "imageEditDiscardDialogTitle",
@ -397,6 +407,10 @@
"settingsMemoriesRangeTitle", "settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText", "settingsMemoriesRangeValueText",
"rootPickerSkipConfirmationDialogContent2", "rootPickerSkipConfirmationDialogContent2",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"imageEditToolbarColorLabel", "imageEditToolbarColorLabel",
"imageEditToolbarTransformLabel", "imageEditToolbarTransformLabel",
"imageEditTransformOrientation", "imageEditTransformOrientation",
@ -415,6 +429,10 @@
"settingsPhotosPageTitle", "settingsPhotosPageTitle",
"settingsMemoriesRangeTitle", "settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText", "settingsMemoriesRangeValueText",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"imageEditToolbarColorLabel", "imageEditToolbarColorLabel",
"imageEditToolbarTransformLabel", "imageEditToolbarTransformLabel",
"imageEditTransformOrientation", "imageEditTransformOrientation",
@ -451,6 +469,10 @@
"helpTooltip", "helpTooltip",
"helpButtonLabel", "helpButtonLabel",
"removeFromAlbumTooltip", "removeFromAlbumTooltip",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip", "enhanceTooltip",
"enhanceButtonLabel", "enhanceButtonLabel",
"enhanceIntroDialogTitle", "enhanceIntroDialogTitle",
@ -527,6 +549,10 @@
"settingsPhotosTabSortByNameTitle", "settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel", "sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel", "sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"createCollectionTooltip", "createCollectionTooltip",
"createCollectionDialogAlbumLabel", "createCollectionDialogAlbumLabel",
"createCollectionDialogAlbumDescription", "createCollectionDialogAlbumDescription",
@ -624,6 +650,10 @@
"settingsPhotosTabSortByNameTitle", "settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel", "sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel", "sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip", "enhanceTooltip",
"enhanceButtonLabel", "enhanceButtonLabel",
"enhanceIntroDialogTitle", "enhanceIntroDialogTitle",
@ -700,6 +730,10 @@
"settingsPhotosTabSortByNameTitle", "settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel", "sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel", "sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip", "enhanceTooltip",
"enhanceButtonLabel", "enhanceButtonLabel",
"enhanceIntroDialogTitle", "enhanceIntroDialogTitle",
@ -776,6 +810,10 @@
"settingsPhotosTabSortByNameTitle", "settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel", "sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel", "sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip", "enhanceTooltip",
"enhanceButtonLabel", "enhanceButtonLabel",
"enhanceIntroDialogTitle", "enhanceIntroDialogTitle",
@ -852,6 +890,10 @@
"settingsPhotosTabSortByNameTitle", "settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel", "sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel", "sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip", "enhanceTooltip",
"enhanceButtonLabel", "enhanceButtonLabel",
"enhanceIntroDialogTitle", "enhanceIntroDialogTitle",

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/entity/share/data_source.dart';
@ -24,6 +25,7 @@ import 'package:nc_photos/use_case/copy.dart';
import 'package:nc_photos/use_case/create_dir.dart'; import 'package:nc_photos/use_case/create_dir.dart';
import 'package:nc_photos/use_case/create_share.dart'; import 'package:nc_photos/use_case/create_share.dart';
import 'package:nc_photos/use_case/download_file.dart'; import 'package:nc_photos/use_case/download_file.dart';
import 'package:nc_photos/use_case/download_preview.dart';
import 'package:nc_photos/use_case/share_local.dart'; import 'package:nc_photos/use_case/share_local.dart';
import 'package:nc_photos/widget/processing_dialog.dart'; import 'package:nc_photos/widget/processing_dialog.dart';
import 'package:nc_photos/widget/share_link_multiple_files_dialog.dart'; import 'package:nc_photos/widget/share_link_multiple_files_dialog.dart';
@ -67,7 +69,7 @@ class ShareHandler {
Future<void> shareFiles(Account account, List<File> files) async { Future<void> shareFiles(Account account, List<File> files) async {
try { try {
final method = await _askShareMethod(); final method = await _askShareMethod(files);
if (method == null) { if (method == null) {
// user canceled // user canceled
return; return;
@ -75,6 +77,8 @@ class ShareHandler {
return await _shareAsLink(account, files, false); return await _shareAsLink(account, files, false);
} else if (method == ShareMethod.passwordLink) { } else if (method == ShareMethod.passwordLink) {
return await _shareAsLink(account, files, true); return await _shareAsLink(account, files, true);
} else if (method == ShareMethod.preview) {
return await _shareAsPreview(account, files);
} else { } else {
return await _shareAsFile(account, files); return await _shareAsFile(account, files);
} }
@ -91,9 +95,59 @@ class ShareHandler {
} }
} }
Future<ShareMethod?> _askShareMethod() { Future<ShareMethod?> _askShareMethod(List<File> files) {
return showDialog<ShareMethod>( return showDialog<ShareMethod>(
context: context, builder: (context) => const ShareMethodDialog()); context: context,
builder: (context) => ShareMethodDialog(
isSupportPerview: files.any((f) => file_util.isSupportedImageFormat(f)),
),
);
}
Future<void> _shareAsPreview(Account account, List<File> files) async {
assert(platform_k.isAndroid);
final controller = StreamController<String>();
unawaited(
showDialog(
context: context,
builder: (context) => StreamBuilder(
stream: controller.stream,
builder: (context, snapshot) => ProcessingDialog(
text: L10n.global().shareDownloadingDialogContent +
(snapshot.hasData ? " ${snapshot.data}" : ""),
),
),
),
);
final results = <Tuple2<File, dynamic>>[];
for (final pair in files.withIndex()) {
final i = pair.item1, f = pair.item2;
controller.add("($i/${files.length})");
try {
final dynamic uri;
if (file_util.isSupportedImageFormat(f) &&
f.contentType != "image/gif") {
uri = await DownloadPreview()(account, f);
} else {
uri = await DownloadFile()(account, f);
}
results.add(Tuple2(f, uri));
} catch (e, stacktrace) {
_log.shout(
"[_shareAsPreview] Failed while DownloadPreview", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
}
}
// dismiss the dialog
Navigator.of(context).pop();
final share = AndroidFileShare(
results.map((e) => e.item2 as String).toList(),
results.map((e) => e.item1.contentType).toList());
return share.share();
} }
Future<void> _shareAsFile(Account account, List<File> files) async { Future<void> _shareAsFile(Account account, List<File> files) async {

View file

@ -0,0 +1,26 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
class DownloadPreview {
Future<dynamic> call(Account account, File file) async {
assert(platform_k.isAndroid);
final previewUrl = api_util.getFilePreviewUrl(
account,
file,
width: k.photoLargeSize,
height: k.photoLargeSize,
a: true,
);
final fileInfo =
await LargeImageCacheManager.inst.getSingleFile(previewUrl, headers: {
"authorization": Api.getAuthorizationHeaderValue(account),
});
return ContentUri.getUriForFile(fileInfo.absolute.path);
}
}

View file

@ -4,30 +4,43 @@ import 'package:nc_photos/platform/k.dart' as platform_k;
enum ShareMethod { enum ShareMethod {
file, file,
preview,
publicLink, publicLink,
passwordLink, passwordLink,
} }
class ShareMethodDialog extends StatelessWidget { class ShareMethodDialog extends StatelessWidget {
const ShareMethodDialog({ const ShareMethodDialog({
Key? key, super.key,
}) : super(key: key); required this.isSupportPerview,
});
@override @override
build(BuildContext context) { build(BuildContext context) {
return SimpleDialog( return SimpleDialog(
title: Text(L10n.global().shareMethodDialogTitle), title: Text(L10n.global().shareMethodDialogTitle),
children: [ children: [
if (platform_k.isAndroid) if (platform_k.isAndroid) ...[
if (isSupportPerview)
SimpleDialogOption(
child: ListTile(
title: Text(L10n.global().shareMethodPreviewTitle),
subtitle: Text(L10n.global().shareMethodPreviewDescription),
),
onPressed: () {
Navigator.of(context).pop(ShareMethod.preview);
},
),
SimpleDialogOption( SimpleDialogOption(
child: ListTile( child: ListTile(
title: Text(L10n.global().shareMethodFileTitle), title: Text(L10n.global().shareMethodOriginalFileTitle),
subtitle: Text(L10n.global().shareMethodFileDescription), subtitle: Text(L10n.global().shareMethodOriginalFileDescription),
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(ShareMethod.file); Navigator.of(context).pop(ShareMethod.file);
}, },
), ),
],
SimpleDialogOption( SimpleDialogOption(
child: ListTile( child: ListTile(
title: Text(L10n.global().shareMethodPublicLinkTitle), title: Text(L10n.global().shareMethodPublicLinkTitle),
@ -49,4 +62,6 @@ class ShareMethodDialog extends StatelessWidget {
], ],
); );
} }
final bool isSupportPerview;
} }

View file

@ -2,8 +2,10 @@ package com.nkming.nc_photos.plugin
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
class ContentUriChannelHandler(context: Context) : class ContentUriChannelHandler(context: Context) :
@ -24,6 +26,14 @@ class ContentUriChannelHandler(context: Context) :
} }
} }
"getUriForFile" -> {
try {
getUriForFile(call.argument("filePath")!!, result)
} catch (e: Throwable) {
result.error("systemException", e.toString(), null)
}
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -46,5 +56,18 @@ class ContentUriChannelHandler(context: Context) :
} }
} }
private fun getUriForFile(filePath: String, result: MethodChannel.Result) {
try {
val file = File(filePath)
val contentUri = FileProvider.getUriForFile(
context, "${context.packageName}.fileprovider", file
)
result.success(contentUri.toString())
} catch (e: IllegalArgumentException) {
logE(TAG, "[getUriForFile] Unsupported file path: $filePath")
throw e
}
}
private val context = context private val context = context
} }

View file

@ -20,6 +20,12 @@ class ContentUri {
} }
} }
static Future<String> getUriForFile(String filePath) async {
return await _methodChannel.invokeMethod("getUriForFile", <String, dynamic>{
"filePath": filePath,
});
}
static const _methodChannel = MethodChannel("${k.libId}/content_uri_method"); static const _methodChannel = MethodChannel("${k.libId}/content_uri_method");
static const _exceptionFileNotFound = "fileNotFoundException"; static const _exceptionFileNotFound = "fileNotFoundException";