diff --git a/app/android/app/src/main/kotlin/com/nkming/nc_photos/ShareChannelHandler.kt b/app/android/app/src/main/kotlin/com/nkming/nc_photos/ShareChannelHandler.kt
index d2be2e97..6ed0a1c2 100644
--- a/app/android/app/src/main/kotlin/com/nkming/nc_photos/ShareChannelHandler.kt
+++ b/app/android/app/src/main/kotlin/com/nkming/nc_photos/ShareChannelHandler.kt
@@ -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)
}
diff --git a/app/android/app/src/main/res/xml/file_paths.xml b/app/android/app/src/main/res/xml/file_paths.xml
index dfb9258a..567fa808 100644
--- a/app/android/app/src/main/res/xml/file_paths.xml
+++ b/app/android/app/src/main/res/xml/file_paths.xml
@@ -1,4 +1,5 @@
+
diff --git a/app/lib/cache_manager_util.dart b/app/lib/cache_manager_util.dart
index 2c911dcb..d98bbc1d 100644
--- a/app/lib/cache_manager_util.dart
+++ b/app/lib/cache_manager_util.dart
@@ -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(
diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb
index 31aaa44a..ebabd151 100644
--- a/app/lib/l10n/app_en.arb
+++ b/app/lib/l10n/app_en.arb
@@ -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"
diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt
index ea179029..6fd6c4e3 100644
--- a/app/lib/l10n/untranslated-messages.txt
+++ b/app/lib/l10n/untranslated-messages.txt
@@ -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",
diff --git a/app/lib/share_handler.dart b/app/lib/share_handler.dart
index 84373dcf..5848a9b5 100644
--- a/app/lib/share_handler.dart
+++ b/app/lib/share_handler.dart
@@ -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 shareFiles(Account account, List 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 _askShareMethod() {
+ Future _askShareMethod(List files) {
return showDialog(
- context: context, builder: (context) => const ShareMethodDialog());
+ context: context,
+ builder: (context) => ShareMethodDialog(
+ isSupportPerview: files.any((f) => file_util.isSupportedImageFormat(f)),
+ ),
+ );
+ }
+
+ Future _shareAsPreview(Account account, List files) async {
+ assert(platform_k.isAndroid);
+ final controller = StreamController();
+ unawaited(
+ showDialog(
+ context: context,
+ builder: (context) => StreamBuilder(
+ stream: controller.stream,
+ builder: (context, snapshot) => ProcessingDialog(
+ text: L10n.global().shareDownloadingDialogContent +
+ (snapshot.hasData ? " ${snapshot.data}" : ""),
+ ),
+ ),
+ ),
+ );
+ final results = >[];
+ 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 _shareAsFile(Account account, List files) async {
diff --git a/app/lib/use_case/download_preview.dart b/app/lib/use_case/download_preview.dart
new file mode 100644
index 00000000..9cba41f1
--- /dev/null
+++ b/app/lib/use_case/download_preview.dart
@@ -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 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);
+ }
+}
diff --git a/app/lib/widget/share_method_dialog.dart b/app/lib/widget/share_method_dialog.dart
index 660a982f..99f260be 100644
--- a/app/lib/widget/share_method_dialog.dart
+++ b/app/lib/widget/share_method_dialog.dart
@@ -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;
}
diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ContentUriChannelHandler.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ContentUriChannelHandler.kt
index f04c0107..3bea17cb 100644
--- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ContentUriChannelHandler.kt
+++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ContentUriChannelHandler.kt
@@ -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
}
diff --git a/plugin/lib/src/content_uri.dart b/plugin/lib/src/content_uri.dart
index 74c953ea..78111f62 100644
--- a/plugin/lib/src/content_uri.dart
+++ b/plugin/lib/src/content_uri.dart
@@ -20,6 +20,12 @@ class ContentUri {
}
}
+ static Future getUriForFile(String filePath) async {
+ return await _methodChannel.invokeMethod("getUriForFile", {
+ "filePath": filePath,
+ });
+ }
+
static const _methodChannel = MethodChannel("${k.libId}/content_uri_method");
static const _exceptionFileNotFound = "fileNotFoundException";