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 6ed0a1c2..b2669dfa 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 @@ -19,24 +19,35 @@ class ShareChannelHandler(activity: Activity) : try { shareItems( call.argument("fileUris")!!, - call.argument("mimeTypes")!!, - result + call.argument("mimeTypes")!!, result ) } catch (e: Throwable) { result.error("systemException", e.toString(), null) } } + "shareText" -> { try { shareText( - call.argument("text")!!, - call.argument("mimeType"), + call.argument("text")!!, call.argument("mimeType"), result ) } catch (e: Throwable) { result.error("systemException", e.toString(), null) } } + + "shareAsAttachData" -> { + try { + shareAsAttachData( + call.argument("fileUri")!!, call.argument("mimeType"), + result + ) + } catch (e: Throwable) { + result.error("systemException", e.toString(), null) + } + } + else -> { result.notImplemented() } @@ -44,8 +55,7 @@ class ShareChannelHandler(activity: Activity) : } private fun shareItems( - fileUris: List, - mimeTypes: List, + fileUris: List, mimeTypes: List, result: MethodChannel.Result ) { assert(fileUris.isNotEmpty()) @@ -104,6 +114,26 @@ class ShareChannelHandler(activity: Activity) : result.success(null) } + private fun shareAsAttachData( + fileUri: String, mimeType: String?, result: MethodChannel.Result + ) { + val intent = Intent().apply { + action = Intent.ACTION_ATTACH_DATA + if (mimeType == null) { + data = Uri.parse(fileUri) + } else { + setDataAndType(Uri.parse(fileUri), mimeType) + } + addCategory(Intent.CATEGORY_DEFAULT) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val chooser = Intent.createChooser( + intent, _context.getString(R.string.attach_data_chooser_title) + ) + _context.startActivity(chooser) + result.success(null) + } + private val _activity = activity private val _context get() = _activity } diff --git a/app/lib/entity/local_file/data_source.dart b/app/lib/entity/local_file/data_source.dart index 88d17643..63dae0aa 100644 --- a/app/lib/entity/local_file/data_source.dart +++ b/app/lib/entity/local_file/data_source.dart @@ -53,8 +53,8 @@ class LocalFileMediaStoreDataSource implements LocalFileDataSource { onFailure?.call(f, ArgumentError("File not supported"), null); }); - final share = AndroidFileShare(uriFiles.map((e) => e.uri).toList(), - uriFiles.map((e) => e.mime).toList()); + final share = AndroidFileShare( + uriFiles.map((e) => AndroidFileShareFile(e.uri, e.mime)).toList()); try { await share.share(); } catch (e, stackTrace) { diff --git a/app/lib/internal_download_handler.dart b/app/lib/internal_download_handler.dart new file mode 100644 index 00000000..27c0a52a --- /dev/null +++ b/app/lib/internal_download_handler.dart @@ -0,0 +1,124 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/entity/file.dart'; +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/map_extension.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/use_case/download_file.dart'; +import 'package:nc_photos/use_case/download_preview.dart'; +import 'package:nc_photos/widget/processing_dialog.dart'; +import 'package:nc_photos_plugin/nc_photos_plugin.dart'; +import 'package:np_codegen/np_codegen.dart'; + +part 'internal_download_handler.g.dart'; + +/// Download file to internal dir +@npLog +class InternalDownloadHandler { + const InternalDownloadHandler(this.account); + + Future> downloadPreviews( + BuildContext context, List files) async { + 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}" : ""), + ), + ), + ), + ); + try { + 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(MapEntry(f, uri)); + } catch (e, stacktrace) { + _log.shout( + "[downloadPreviews] Failed while DownloadPreview", e, stacktrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + } + } + return results.toMap(); + } finally { + // dismiss the dialog + Navigator.of(context).pop(); + } + } + + Future> downloadFiles( + BuildContext context, List files) async { + 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}" : ""), + ), + ), + ), + ); + try { + final results = >[]; + for (final pair in files.withIndex()) { + final i = pair.item1, f = pair.item2; + controller.add("($i/${files.length})"); + try { + results.add(MapEntry( + f, + await DownloadFile()( + account, + f, + shouldNotify: false, + ))); + } on PermissionException catch (_) { + _log.warning("[downloadFiles] Permission not granted"); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().errorNoStoragePermission), + duration: k.snackBarDurationNormal, + )); + rethrow; + } catch (e, stacktrace) { + _log.shout( + "[downloadFiles] Failed while downloadFile", e, stacktrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + } + } + return results.toMap(); + } finally { + // dismiss the dialog + Navigator.of(context).pop(); + } + } + + final Account account; +} diff --git a/app/lib/internal_download_handler.g.dart b/app/lib/internal_download_handler.g.dart new file mode 100644 index 00000000..904439e9 --- /dev/null +++ b/app/lib/internal_download_handler.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'internal_download_handler.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$InternalDownloadHandlerNpLog on InternalDownloadHandler { + // ignore: unused_element + Logger get _log => log; + + static final log = + Logger("internal_download_handler.InternalDownloadHandler"); +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 600213f5..5bec09f3 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1419,6 +1419,10 @@ "removeCollectionsFailedNotification": "Failed to remove some collections", "accountSettingsTooltip": "Account settings", "contributorsTooltip": "Contributors", + "setAsTooltip": "Set as", + "@setAsTooltip": { + "description": "e.g., set as wallpaper" + }, "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 54f39909..917cb505 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -17,7 +17,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "de": [ @@ -106,6 +107,7 @@ "removeCollectionsFailedNotification", "accountSettingsTooltip", "contributorsTooltip", + "setAsTooltip", "errorAlbumDowngrade" ], @@ -216,7 +218,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "es": [ @@ -226,7 +229,8 @@ "settingsSeedColorPickerSystemColorButtonLabel", "searchLandingPeopleListEmptyText2", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "fi": [ @@ -236,7 +240,8 @@ "settingsSeedColorPickerSystemColorButtonLabel", "searchLandingPeopleListEmptyText2", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "fr": [ @@ -366,7 +371,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "it": [ @@ -671,6 +677,7 @@ "removeCollectionsFailedNotification", "accountSettingsTooltip", "contributorsTooltip", + "setAsTooltip", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -1021,6 +1028,7 @@ "removeCollectionsFailedNotification", "accountSettingsTooltip", "contributorsTooltip", + "setAsTooltip", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -1172,7 +1180,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "pt": [ @@ -1193,7 +1202,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "ru": [ @@ -1319,7 +1329,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "zh": [ @@ -1445,7 +1456,8 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ], "zh_Hant": [ @@ -1571,6 +1583,7 @@ "createCollectionDialogNextcloudAlbumDescription", "removeCollectionsFailedNotification", "accountSettingsTooltip", - "contributorsTooltip" + "contributorsTooltip", + "setAsTooltip" ] } diff --git a/app/lib/mobile/android/share.dart b/app/lib/mobile/android/share.dart index f02019c2..9f989879 100644 --- a/app/lib/mobile/android/share.dart +++ b/app/lib/mobile/android/share.dart @@ -8,12 +8,17 @@ class Share { "mimeTypes": mimeTypes, }); - static Future shareText( - String text, String? mimeType) => + static Future shareText(String text, String? mimeType) => _channel.invokeMethod("shareText", { "text": text, "mimeType": mimeType, }); + static Future shareAsAttachData(String fileUri, String? mimeType) => + _channel.invokeMethod("shareAsAttachData", { + "fileUri": fileUri, + "mimeType": mimeType, + }); + static const _channel = MethodChannel("com.nkming.nc_photos/share"); } diff --git a/app/lib/mobile/share.dart b/app/lib/mobile/share.dart index 99da71cb..4acea5fc 100644 --- a/app/lib/mobile/share.dart +++ b/app/lib/mobile/share.dart @@ -1,16 +1,29 @@ import 'package:nc_photos/mobile/android/share.dart'; import 'package:nc_photos/platform/share.dart' as itf; -class AndroidFileShare extends itf.FileShare { - AndroidFileShare(this.fileUris, this.mimeTypes); +class AndroidFileShareFile { + const AndroidFileShareFile(this.fileUri, this.mimeType); + + final String fileUri; + final String? mimeType; +} + +class AndroidFileShare implements itf.FileShare { + const AndroidFileShare(this.files); @override - share() { - return Share.shareItems(fileUris, mimeTypes); + Future share() { + final uris = files.map((e) => e.fileUri).toList(); + final mimes = files.map((e) => e.mimeType).toList(); + return Share.shareItems(uris, mimes); } - final List fileUris; - final List mimeTypes; + Future setAs() { + assert(files.length == 1); + return Share.shareAsAttachData(files.first.fileUri, files.first.mimeType); + } + + final List files; } class AndroidTextShare extends itf.TextShare { diff --git a/app/lib/set_as_handler.dart b/app/lib/set_as_handler.dart new file mode 100644 index 00000000..25656ddf --- /dev/null +++ b/app/lib/set_as_handler.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/internal_download_handler.dart'; +import 'package:nc_photos/k.dart' as k; +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/use_case/inflate_file_descriptor.dart'; +import 'package:nc_photos/widget/set_as_method_dialog.dart'; +import 'package:np_codegen/np_codegen.dart'; + +part 'set_as_handler.g.dart'; + +/// A special way to share image to other apps +@npLog +class SetAsHandler { + SetAsHandler( + this._c, { + required this.context, + this.clearSelection, + }); + + Future setAsFile(Account account, FileDescriptor fd) async { + try { + final file = (await InflateFileDescriptor(_c)(account, [fd])).first; + final method = await _askSetAsMethod(file); + switch (method) { + case SetAsMethod.preview: + return await _setAsAsPreview(account, file); + case SetAsMethod.file: + return await _setAsAsFile(account, file); + case null: + return; + } + } catch (e, stackTrace) { + _log.shout("[setAsFile] Failed while sharing files", e, stackTrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + } finally { + if (!isSelectionCleared) { + clearSelection?.call(); + } + } + } + + Future _askSetAsMethod(File file) { + return showDialog( + context: context, + builder: (context) => const SetAsMethodDialog(), + ); + } + + Future _setAsAsPreview(Account account, File file) async { + assert(platform_k.isAndroid); + final results = await InternalDownloadHandler(account) + .downloadPreviews(context, [file]); + final share = AndroidFileShare(results.entries + .map((e) => AndroidFileShareFile(e.value as String, e.key.contentType)) + .toList()); + return share.setAs(); + } + + Future _setAsAsFile(Account account, File file) async { + assert(platform_k.isAndroid); + final results = + await InternalDownloadHandler(account).downloadFiles(context, [file]); + final share = AndroidFileShare(results.entries + .map((e) => AndroidFileShareFile(e.value as String, e.key.contentType)) + .toList()); + return share.setAs(); + } + + final DiContainer _c; + final BuildContext context; + final VoidCallback? clearSelection; + var isSelectionCleared = false; +} diff --git a/app/lib/set_as_handler.g.dart b/app/lib/set_as_handler.g.dart new file mode 100644 index 00000000..8e3ace0c --- /dev/null +++ b/app/lib/set_as_handler.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'set_as_handler.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$SetAsHandlerNpLog on SetAsHandler { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("set_as_handler.SetAsHandler"); +} diff --git a/app/lib/share_handler.dart b/app/lib/share_handler.dart index 4a7b82a6..fe736cf0 100644 --- a/app/lib/share_handler.dart +++ b/app/lib/share_handler.dart @@ -17,7 +17,7 @@ import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; -import 'package:nc_photos/iterable_extension.dart'; +import 'package:nc_photos/internal_download_handler.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/share.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; @@ -26,17 +26,12 @@ import 'package:nc_photos/snack_bar_manager.dart'; 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/inflate_file_descriptor.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'; import 'package:nc_photos/widget/share_method_dialog.dart'; import 'package:nc_photos/widget/simple_input_dialog.dart'; -import 'package:nc_photos_plugin/nc_photos_plugin.dart'; import 'package:np_codegen/np_codegen.dart'; -import 'package:tuple/tuple.dart'; part 'share_handler.g.dart'; @@ -118,100 +113,21 @@ class ShareHandler { 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()); + final results = + await InternalDownloadHandler(account).downloadPreviews(context, files); + final share = AndroidFileShare(results.entries + .map((e) => AndroidFileShareFile(e.value as String, e.key.contentType)) + .toList()); return share.share(); } Future _shareAsFile(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 { - results.add(Tuple2( - f, - await DownloadFile()( - account, - f, - shouldNotify: false, - ))); - } on PermissionException catch (_) { - _log.warning("[_shareAsFile] Permission not granted"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().errorNoStoragePermission), - duration: k.snackBarDurationNormal, - )); - // dismiss the dialog - Navigator.of(context).pop(); - rethrow; - } catch (e, stacktrace) { - _log.shout("[_shareAsFile] Failed while downloadFile", 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()); + final results = + await InternalDownloadHandler(account).downloadFiles(context, files); + final share = AndroidFileShare(results.entries + .map((e) => AndroidFileShareFile(e.value as String, e.key.contentType)) + .toList()); return share.share(); } diff --git a/app/lib/widget/file_sharer_dialog/bloc.dart b/app/lib/widget/file_sharer_dialog/bloc.dart index 0415d826..dae02e2b 100644 --- a/app/lib/widget/file_sharer_dialog/bloc.dart +++ b/app/lib/widget/file_sharer_dialog/bloc.dart @@ -94,10 +94,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } } if (results.isNotEmpty) { - final share = AndroidFileShare( - results.map((e) => e.item2 as String).toList(), - results.map((e) => e.item1.fdMime).toList(), - ); + final share = AndroidFileShare(results + .map((e) => AndroidFileShareFile(e.item2 as String, e.item1.fdMime)) + .toList()); unawaited(share.share()); } emit(state.copyWith(result: true)); @@ -129,10 +128,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } } if (results.isNotEmpty) { - final share = AndroidFileShare( - results.map((e) => e.item2 as String).toList(), - results.map((e) => e.item1.fdMime).toList(), - ); + final share = AndroidFileShare(results + .map((e) => AndroidFileShareFile(e.item2 as String, e.item1.fdMime)) + .toList()); unawaited(share.share()); } emit(state.copyWith(result: true)); diff --git a/app/lib/widget/set_as_method_dialog.dart b/app/lib/widget/set_as_method_dialog.dart new file mode 100644 index 00000000..45ffd266 --- /dev/null +++ b/app/lib/widget/set_as_method_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:nc_photos/app_localizations.dart'; + +enum SetAsMethod { + file, + preview, +} + +class SetAsMethodDialog extends StatelessWidget { + const SetAsMethodDialog({ + super.key, + this.isSupportPerview = true, + }); + + @override + Widget build(BuildContext context) { + return SimpleDialog( + title: Text(L10n.global().setAsTooltip), + children: [ + if (isSupportPerview) + SimpleDialogOption( + child: ListTile( + title: Text(L10n.global().shareMethodPreviewTitle), + subtitle: Text(L10n.global().shareMethodPreviewDescription), + ), + onPressed: () { + Navigator.of(context).pop(SetAsMethod.preview); + }, + ), + SimpleDialogOption( + child: ListTile( + title: Text(L10n.global().shareMethodOriginalFileTitle), + subtitle: Text(L10n.global().shareMethodOriginalFileDescription), + ), + onPressed: () { + Navigator.of(context).pop(SetAsMethod.file); + }, + ), + ], + ); + } + + final bool isSupportPerview; +} diff --git a/app/lib/widget/viewer_detail_pane.dart b/app/lib/widget/viewer_detail_pane.dart index 3b121ef9..b142a84a 100644 --- a/app/lib/widget/viewer_detail_pane.dart +++ b/app/lib/widget/viewer_detail_pane.dart @@ -18,12 +18,14 @@ import 'package:nc_photos/entity/collection_item.dart'; import 'package:nc_photos/entity/exif_extension.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/location_util.dart' as location_util; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/platform/k.dart' as platform_k; +import 'package:nc_photos/set_as_handler.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; @@ -169,6 +171,13 @@ class _ViewerDetailPaneState extends State { label: L10n.global().addItemToCollectionTooltip, onPressed: () => _onAddToAlbumPressed(context), ), + if (platform_k.isAndroid && + file_util.isSupportedImageFormat(_file!)) + _DetailPaneButton( + icon: Icons.launch, + label: L10n.global().setAsTooltip, + onPressed: () => _onSetAsPressed(context), + ), if (widget.fd.fdIsArchived == true) _DetailPaneButton( icon: Icons.unarchive_outlined, @@ -417,6 +426,12 @@ class _ViewerDetailPaneState extends State { ); } + void _onSetAsPressed(BuildContext context) { + assert(_file != null); + final c = KiwiContainer().resolve(); + SetAsHandler(c, context: context).setAsFile(widget.account, _file!); + } + void _onMapTap() { if (platform_k.isAndroid) { final intent = AndroidIntent( diff --git a/plugin/android/src/main/res/values/strings.xml b/plugin/android/src/main/res/values/strings.xml index 7a9e31fa..7f21ce76 100644 --- a/plugin/android/src/main/res/values/strings.xml +++ b/plugin/android/src/main/res/values/strings.xml @@ -11,4 +11,5 @@ Downloading Logs saved successfully Tap to view your saved logs + Set as