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
import android.app.Activity
import android.content.ClipData
import android.content.Intent
import android.net.Uri
import io.flutter.plugin.common.MethodCall
@ -54,14 +55,26 @@ class ShareChannelHandler(activity: Activity) :
val shareIntent = if (uris.size == 1) Intent().apply {
action = Intent.ACTION_SEND
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] ?: "*/*"
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 "*/*"
clipData =
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_WRITE_URI_PERMISSION)
}

View file

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

View file

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

View file

@ -906,11 +906,16 @@
"@shareMethodDialogTitle": {
"description": "Let the user pick how they want to share"
},
"shareMethodFileTitle": "File",
"@shareMethodFileTitle": {
"description": "Share the actual file"
"shareMethodPreviewTitle": "Preview",
"@shareMethodPreviewTitle": {
"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": {
"description": "Create a share link on server and share it"

View file

@ -41,6 +41,10 @@
"sortOptionFilenameDescendingLabel",
"sortOptionManualLabel",
"helpButtonLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"collectionSharingLabel",
"fileLastSharedDescription",
"fileLastSharedByOthersDescription",
@ -210,8 +214,10 @@
"slideshowSetupDialogRepeatTitle",
"linkCopiedNotification",
"shareMethodDialogTitle",
"shareMethodFileTitle",
"shareMethodFileDescription",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"shareMethodPublicLinkTitle",
"shareMethodPublicLinkDescription",
"shareMethodPasswordLinkTitle",
@ -346,6 +352,10 @@
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"settingsDoubleTapExitTitle",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceStyleTransferStyleDialogTitle",
"doubleTapExitNotification",
"imageEditDiscardDialogTitle",
@ -397,6 +407,10 @@
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"rootPickerSkipConfirmationDialogContent2",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"imageEditToolbarColorLabel",
"imageEditToolbarTransformLabel",
"imageEditTransformOrientation",
@ -415,6 +429,10 @@
"settingsPhotosPageTitle",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"imageEditToolbarColorLabel",
"imageEditToolbarTransformLabel",
"imageEditTransformOrientation",
@ -451,6 +469,10 @@
"helpTooltip",
"helpButtonLabel",
"removeFromAlbumTooltip",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -527,6 +549,10 @@
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"createCollectionTooltip",
"createCollectionDialogAlbumLabel",
"createCollectionDialogAlbumDescription",
@ -624,6 +650,10 @@
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -700,6 +730,10 @@
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -776,6 +810,10 @@
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip",
"enhanceButtonLabel",
"enhanceIntroDialogTitle",
@ -852,6 +890,10 @@
"settingsPhotosTabSortByNameTitle",
"sortOptionFilenameAscendingLabel",
"sortOptionFilenameDescendingLabel",
"shareMethodPreviewTitle",
"shareMethodPreviewDescription",
"shareMethodOriginalFileTitle",
"shareMethodOriginalFileDescription",
"enhanceTooltip",
"enhanceButtonLabel",
"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/di_container.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/share.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_share.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/widget/processing_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 {
try {
final method = await _askShareMethod();
final method = await _askShareMethod(files);
if (method == null) {
// user canceled
return;
@ -75,6 +77,8 @@ class ShareHandler {
return await _shareAsLink(account, files, false);
} else if (method == ShareMethod.passwordLink) {
return await _shareAsLink(account, files, true);
} else if (method == ShareMethod.preview) {
return await _shareAsPreview(account, files);
} else {
return await _shareAsFile(account, files);
}
@ -91,9 +95,59 @@ class ShareHandler {
}
}
Future<ShareMethod?> _askShareMethod() {
Future<ShareMethod?> _askShareMethod(List<File> files) {
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 {

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 {
file,
preview,
publicLink,
passwordLink,
}
class ShareMethodDialog extends StatelessWidget {
const ShareMethodDialog({
Key? key,
}) : super(key: key);
super.key,
required this.isSupportPerview,
});
@override
build(BuildContext context) {
return SimpleDialog(
title: Text(L10n.global().shareMethodDialogTitle),
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(
child: ListTile(
title: Text(L10n.global().shareMethodFileTitle),
subtitle: Text(L10n.global().shareMethodFileDescription),
title: Text(L10n.global().shareMethodOriginalFileTitle),
subtitle: Text(L10n.global().shareMethodOriginalFileDescription),
),
onPressed: () {
Navigator.of(context).pop(ShareMethod.file);
},
),
],
SimpleDialogOption(
child: ListTile(
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.net.Uri
import androidx.core.content.FileProvider
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileNotFoundException
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()
}
}
@ -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
}

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 _exceptionFileNotFound = "fileNotFoundException";