From ecdbdac1914a5f70171c6a0af900bb411d1de379 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 10 Oct 2023 18:17:51 +0700 Subject: [PATCH] TW-731: paste images in web, mobile --- assets/l10n/intl_en.arb | 4 +- lib/pages/chat/chat.dart | 39 ++- lib/pages/chat/chat_view.dart | 2 +- lib/pages/chat/input_bar.dart | 230 +++++++++++++++--- lib/pages/chat/send_file_dialog.dart | 82 +++---- .../enum/chat/popup_menu_item_web_enum.dart | 5 + .../text_editting_controller_extension.dart | 40 +++ .../mixins/paste_image_mixin.dart | 49 ++++ .../model/clipboard/clipboard_image_info.dart | 19 ++ lib/utils/clipboard.dart | 173 +++++++++++++ 10 files changed, 549 insertions(+), 94 deletions(-) create mode 100644 lib/presentation/enum/chat/popup_menu_item_web_enum.dart create mode 100644 lib/presentation/extensions/text_editting_controller_extension.dart create mode 100644 lib/presentation/mixins/paste_image_mixin.dart create mode 100644 lib/presentation/model/clipboard/clipboard_image_info.dart create mode 100644 lib/utils/clipboard.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 424e327543..85e3592f49 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2758,5 +2758,7 @@ "thisMessageHasBeenEncrypted": "This message has been encrypted", "roomCreationFailed": "Room creation failed", "errorGettingPdf": "Error getting PDF", - "errorPreviewingFile": "Error previewing file" + "errorPreviewingFile": "Error previewing file", + "paste": "Paste", + "cut": "Cut" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index b12c3f1057..c8be6dace7 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -31,6 +31,7 @@ import 'package:fluffychat/presentation/mixins/media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/send_files_mixin.dart'; import 'package:fluffychat/presentation/model/forward/forward_argument.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -47,7 +48,7 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/services.dart' as flutter; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -777,8 +778,40 @@ class ChatController extends State return copyString; } + void copySingleEventAction() async { + if (selectedEvents.length == 1) { + final event = selectedEvents.first; + final matrixFile = event.getMatrixFile() ?? + await event.downloadAndDecryptAttachment( + getThumbnail: true, + ); + if (event.messageType == MessageTypes.Image) { + if (matrixFile.filePath != null) { + Clipboard.instance.copyImageAsStream( + File(matrixFile.filePath!), + mimeType: event.mimeType, + ); + } else if (matrixFile.bytes != null) { + Clipboard.instance.copyImageAsBytes( + matrixFile.bytes!, + mimeType: event.mimeType, + ); + } else { + Logs().e( + 'copySingleEventAction(): failed to copy file ${matrixFile.name}', + ); + } + } + } + } + void copyEventsAction() { - Clipboard.setData(ClipboardData(text: _getSelectedEventString())); + flutter.Clipboard.setData( + flutter.ClipboardData( + text: _getSelectedEventString(), + ), + ); + showEmojiPickerNotifier.value = false; setState(() { selectedEvents.clear(); @@ -1469,7 +1502,7 @@ class ChatController extends State break; case ChatContextMenuActions.copyMessage: onSelectMessage(event); - copyEventsAction(); + copySingleEventAction(); break; case ChatContextMenuActions.pinMessage: onSelectMessage(event); diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index e84b126f2e..9c01c495f0 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -37,7 +37,7 @@ class ChatView extends StatelessWidget { TwakeIconButton( icon: Icons.copy_outlined, tooltip: L10n.of(context)!.copy, - onTap: controller.copyEventsAction, + onTap: controller.copySingleEventAction, ), if (controller.canRedactSelectedEvents) TwakeIconButton( diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index bce81fe17c..dc1d620c2c 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -1,7 +1,14 @@ +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/presentation/enum/chat/popup_menu_item_web_enum.dart'; +import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; +import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; +import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/extension/raw_key_event_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as flutter; import 'package:emojis/emoji.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -15,7 +22,7 @@ import 'package:fluffychat/widgets/mxc_image.dart'; import 'command_hints.dart'; -class InputBar extends StatelessWidget { +class InputBar extends StatelessWidget with PasteImageMixin { final Room? room; final int? minLines; final int? maxLines; @@ -311,52 +318,197 @@ class InputBar extends StatelessWidget { onSubmitted?.call(controller?.text ?? ''); } }, - child: TypeAheadField>( - direction: AxisDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - keepSuggestionsOnSuggestionSelected: true, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - textFieldConfiguration: TextFieldConfiguration( - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - style: InputBarStyle.getTypeAheadTextStyle(context), - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); + child: CallbackShortcuts( + bindings: { + const SingleActivator( + flutter.LogicalKeyboardKey.keyV, + meta: true, + ): () async { + if (await Clipboard.instance.isReadableImageFormat()) { + await pasteImage(context, room!); + } else if (controller != null) { + await controller!.pasteText(); + } }, - controller: controller, - decoration: decoration!, - focusNode: focusNode, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); + const SingleActivator( + flutter.LogicalKeyboardKey.keyC, + meta: true, + ): () { + if (controller != null) { + controller!.copyText(); + } }, - textCapitalization: TextCapitalization.sentences, - ), - suggestionsCallback: getSuggestions, - itemBuilder: (context, suggestion) => SuggestionTile( - suggestion: suggestion, - client: Matrix.of(context).client, + }, + child: Listener( + onPointerDown: (PointerDownEvent event) async { + if (event.kind == PointerDeviceKind.mouse && + event.buttons == kSecondaryMouseButton) { + final screenSize = MediaQuery.of(context).size; + final offset = event.position; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + final menuItem = await showMenu( + useRootNavigator: PlatformInfos.isWeb, + context: context, + items: [ + PopupMenuItem( + value: PopupMenuItemWebEnum.copy, + child: Text(L10n.of(context)!.copy), + ), + PopupMenuItem( + value: PopupMenuItemWebEnum.cut, + child: Text(L10n.of(context)!.cut), + ), + PopupMenuItem( + value: PopupMenuItemWebEnum.paste, + child: Text(L10n.of(context)!.paste), + ), + ], + position: position, + ); + + if (menuItem == null) { + return; + } + + if (controller == null) { + return; + } + + switch (menuItem) { + case PopupMenuItemWebEnum.copy: + controller!.copyText(); + break; + case PopupMenuItemWebEnum.cut: + controller!.cutText(); + break; + case PopupMenuItemWebEnum.paste: + if (await Clipboard.instance.isReadableImageFormat()) { + await pasteImage(context, room!); + } else { + await controller!.pasteText(); + } + break; + } + } + }, + child: TypeAheadField>( + direction: AxisDirection.up, + hideOnEmpty: true, + hideOnLoading: true, + keepSuggestionsOnSuggestionSelected: true, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + textFieldConfiguration: TextFieldConfiguration( + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + style: InputBarStyle.getTypeAheadTextStyle(context), + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, + controller: controller, + decoration: decoration!, + focusNode: focusNode, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + textCapitalization: TextCapitalization.sentences, + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (keyboardInsertContent) async { + if (room == null || !keyboardInsertContent.hasData) { + return; + } + await showDialog( + context: context, + builder: (context) { + return SendFileDialog( + room: room!, + files: [ + MatrixFile( + name: keyboardInsertContent.uri, + bytes: keyboardInsertContent.data, + ) + ], + ); + }, + ); + }, + ), + contextMenuBuilder: ( + BuildContext context, + EditableTextState editableTextState, + ) { + return AdaptiveTextSelectionToolbar.editable( + anchors: editableTextState.contextMenuAnchors, + clipboardStatus: ClipboardStatus.pasteable, + onPaste: !PlatformInfos.isWeb + ? () async { + if (room == null) { + // FIXME: need to handle the case when in draft chat + return; + } + + if (await Clipboard.instance + .isReadableImageFormat()) { + await pasteImage(context, room!); + } else { + editableTextState + .pasteText(SelectionChangedCause.toolbar); + } + } + : null, + onCopy: () { + editableTextState + .copySelection(SelectionChangedCause.toolbar); + }, + onCut: () { + editableTextState + .cutSelection(SelectionChangedCause.toolbar); + }, + onSelectAll: () { + editableTextState.selectAll(SelectionChangedCause.toolbar); + }, + ); + }, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, suggestion) => SuggestionTile( + suggestion: suggestion, + client: Matrix.of(context).client, + ), + onSuggestionSelected: (Map suggestion) => + insertSuggestion(context, suggestion), + errorBuilder: (BuildContext context, Object? error) => Container(), + loadingBuilder: (BuildContext context) => Container(), + // fix loading briefly flickering a dark box + noItemsFoundBuilder: (BuildContext context) => + Container(), // fix loading briefly showing no suggestions + ), ), - onSuggestionSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => Container(), - loadingBuilder: (BuildContext context) => Container(), - // fix loading briefly flickering a dark box - noItemsFoundBuilder: (BuildContext context) => - Container(), // fix loading briefly showing no suggestions ), ); } } +class PasteIntent extends Intent { + const PasteIntent(); +} + +class CopyIntent extends Intent { + const CopyIntent(); +} + class NewLineIntent extends Intent {} class SubmitLineIntent extends Intent {} diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 42bf1949e9..7ed2e9e29b 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,10 +1,10 @@ +import 'package:fluffychat/presentation/extensions/send_file_extension.dart'; +import 'package:fluffychat/presentation/extensions/send_file_web_extension.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/size_string.dart'; import '../../utils/resize_image.dart'; class SendFileDialog extends StatefulWidget { @@ -22,8 +22,6 @@ class SendFileDialog extends StatefulWidget { } class SendFileDialogState extends State { - bool origImage = false; - /// Images smaller than 20kb don't need compression. static const int minSizeToCompress = 20 * 1024; @@ -43,18 +41,13 @@ class SendFileDialogState extends State { } // ignore: unused_local_variable final scaffoldMessenger = ScaffoldMessenger.of(context); - // widget.room - // .sendFileEvent( - // file, - // thumbnail: thumbnail, - // shrinkImageMaxDimension: origImage ? null : 1600, - // ) - // .catchError((e) { - // scaffoldMessenger.showSnackBar( - // SnackBar(content: Text((e as Object).toLocalizedString(context))), - // ); - // return null; - // }); + if (file.filePath != null) { + widget.room.sendFileEvent( + FileInfo(file.name, file.filePath!, file.size), + ); + } else { + widget.room.sendFileOnWebEvent(file); + } } Navigator.of(context, rootNavigator: false).pop(); @@ -63,25 +56,25 @@ class SendFileDialogState extends State { @override Widget build(BuildContext context) { - var sendStr = L10n.of(context)!.sendFile; - final bool allFilesAreImages = - widget.files.every((file) => file is MatrixImageFile); - final sizeString = widget.files - .fold(0, (p, file) => p + (file.bytes?.length ?? 0)) - .sizeString; - final fileName = widget.files.length == 1 - ? widget.files.single.name - : L10n.of(context)!.countFiles(widget.files.length.toString()); + // var sendStr = L10n.of(context)!.sendFile; + // final bool allFilesAreImages = + // widget.files.every((file) => file is MatrixImageFile); + // final sizeString = widget.files + // .fold(0, (p, file) => p + (file.bytes?.length ?? 0)) + // .sizeString; + // final fileName = widget.files.length == 1 + // ? widget.files.single.name + // : L10n.of(context)!.countFiles(widget.files.length.toString()); - if (allFilesAreImages) { - sendStr = L10n.of(context)!.sendImage; - } else if (widget.files.every((file) => file is MatrixAudioFile)) { - sendStr = L10n.of(context)!.sendAudio; - } else if (widget.files.every((file) => file is MatrixVideoFile)) { - sendStr = L10n.of(context)!.sendVideo; - } + // if (allFilesAreImages) { + // sendStr = L10n.of(context)!.sendImage; + // } else if (widget.files.every((file) => file is MatrixAudioFile)) { + // sendStr = L10n.of(context)!.sendAudio; + // } else if (widget.files.every((file) => file is MatrixVideoFile)) { + // sendStr = L10n.of(context)!.sendVideo; + // } Widget contentWidget; - if (allFilesAreImages) { + if (true) { contentWidget = Column( mainAxisSize: MainAxisSize.min, children: [ @@ -91,25 +84,14 @@ class SendFileDialogState extends State { fit: BoxFit.contain, ), ), - Row( - children: [ - Checkbox( - value: origImage, - onChanged: (v) => setState(() => origImage = v ?? false), - ), - InkWell( - onTap: () => setState(() => origImage = !origImage), - child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'), - ), - ], - ) ], ); - } else { - contentWidget = Text('$fileName ($sizeString)'); } + // } else { + // contentWidget = Text('$fileName ($sizeString)'); + // } return AlertDialog( - title: Text(sendStr), + title: const Text('send Image'), content: contentWidget, actions: [ TextButton( @@ -117,11 +99,11 @@ class SendFileDialogState extends State { // just close the dialog Navigator.of(context, rootNavigator: false).pop(); }, - child: Text(L10n.of(context)!.cancel), + child: const Text("Cancel"), ), TextButton( onPressed: _send, - child: Text(L10n.of(context)!.send), + child: const Text("Send"), ), ], ); diff --git a/lib/presentation/enum/chat/popup_menu_item_web_enum.dart b/lib/presentation/enum/chat/popup_menu_item_web_enum.dart new file mode 100644 index 0000000000..983daae10f --- /dev/null +++ b/lib/presentation/enum/chat/popup_menu_item_web_enum.dart @@ -0,0 +1,5 @@ +enum PopupMenuItemWebEnum { + copy, + cut, + paste, +} diff --git a/lib/presentation/extensions/text_editting_controller_extension.dart b/lib/presentation/extensions/text_editting_controller_extension.dart new file mode 100644 index 0000000000..2d0c47aa54 --- /dev/null +++ b/lib/presentation/extensions/text_editting_controller_extension.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as flutter; + +extension TextEdittingControllerExtension on TextEditingController { + Future pasteText() async { + final start = selection.start; + final end = selection.end; + final pastedText = await Clipboard.instance.pasteText(); + if (pastedText != null) { + if (start == -1 || end == -1) { + text = pastedText + text; + return; + } + if (start == end) { + final startText = text.substring(0, start); + final trailingText = text.substring(end, text.length); + text = startText + pastedText + trailingText; + } else { + text = text.replaceRange(start, end, pastedText); + } + } + } + + Future copyText() async { + final start = selection.start; + final end = selection.end; + if (start < end) { + await flutter.Clipboard.setData( + flutter.ClipboardData( + text: text.substring(start, end), + ), + ); + } + } + + Future cutText() async { + //TO-DO: + } +} diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart new file mode 100644 index 0000000000..c66eee955f --- /dev/null +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; + +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/presentation/model/clipboard/clipboard_image_info.dart'; +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +mixin PasteImageMixin { + Future pasteImage(BuildContext context, Room room) async { + await Clipboard.instance.initReader(); + if (!(await Clipboard.instance.isReadableImageFormat())) { + Logs().e('PasteImageMixin::pasteImage(): not readable image format'); + return; + } + Uint8List? imageData; + ClipboardImageInfo? imageClipboard; + if (PlatformInfos.isWeb) { + imageData = await Clipboard.instance.pasteImageUsingBytes(); + } else { + imageClipboard = await Clipboard.instance.pasteImageUsingStream(); + if (imageClipboard == null) { + return; + } + // FIXME: need to update the SendFileDialog to have FileInfo inside + // after update we can use stream to read files instead of convert into raw image data + final data = await imageClipboard.stream.toList(); + imageData = Uint8List.fromList( + data.expand((Uint8List uint8List) => uint8List).toList(), + ); + } + + await showDialog( + context: context, + builder: (context) { + return SendFileDialog( + room: room, + files: [ + MatrixFile( + name: imageClipboard?.fileName ?? 'copied', + bytes: imageData, + ) + ], + ); + }, + ); + } +} diff --git a/lib/presentation/model/clipboard/clipboard_image_info.dart b/lib/presentation/model/clipboard/clipboard_image_info.dart new file mode 100644 index 0000000000..a172e3183f --- /dev/null +++ b/lib/presentation/model/clipboard/clipboard_image_info.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/services.dart'; + +class ClipboardImageInfo with EquatableMixin { + final Stream stream; + + final String? fileName; + + final int? fileSize; + + ClipboardImageInfo({ + required this.stream, + this.fileName, + this.fileSize, + }); + + @override + List get props => [stream, fileName, fileSize]; +} diff --git a/lib/utils/clipboard.dart b/lib/utils/clipboard.dart new file mode 100644 index 0000000000..1a96c4d7fd --- /dev/null +++ b/lib/utils/clipboard.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:fluffychat/presentation/model/clipboard/clipboard_image_info.dart'; +import 'package:flutter/services.dart'; +import 'package:matrix/matrix.dart'; +import 'package:mime/mime.dart'; +import 'package:super_clipboard/super_clipboard.dart'; + +class Clipboard { + static final _clipboard = Clipboard._(); + + Clipboard._(); + + ClipboardReader? _reader; + + static Clipboard get instance => _clipboard; + + static const allImageFormatsSupported = [ + Formats.png, + Formats.jpeg, + Formats.heic, + Formats.heif, + Formats.svg, + ]; + + Future copyImageAsStream(File image, {String? mimeType}) async { + final item = DataWriterItem(suggestedName: image.path); + final imageStream = image.openRead(); + final mime = mimeType ?? lookupMimeType(image.path); + await imageStream.forEach((data) { + item.add(getFormatFrom(mime)(Uint8List.fromList(data))); + }); + try { + await ClipboardWriter.instance.write([item]); + } catch (e) { + Logs().e('Clipboard::copyImageAsStream(): $e'); + } + } + + Future copyImageAsBytes(Uint8List data, {String? mimeType}) async { + try { + final item = DataWriterItem(); + item.add(getFormatFrom(mimeType)(data)); + await ClipboardWriter.instance.write([item]); + } catch (e) { + Logs().e('Clipboard::copyImageAsBytes(): $e'); + } + } + + Future initReader() async { + _reader = await ClipboardReader.readClipboard(); + } + + Future pasteImageUsingStream() async { + _reader ??= await ClipboardReader.readClipboard(); + ClipboardImageInfo? imageInfo; + + final readableFormats = _reader!.getFormats(allImageFormatsSupported); + if (readableFormats.isEmpty != false && + readableFormats[0] is! SimpleFileFormat) { + return imageInfo; + } + + final c = Completer(); + final progress = _reader!.getFile( + readableFormats[0] as SimpleFileFormat, + (file) async { + try { + imageInfo = ClipboardImageInfo( + stream: file.getStream(), + fileName: file.fileName, + fileSize: file.fileSize, + ); + c.complete(imageInfo); + } catch (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + } + }, + onError: (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + }, + ); + if (progress == null) { + c.complete(null); + } + return c.future; + } + + Future? pasteImageUsingBytes() async { + _reader ??= await ClipboardReader.readClipboard(); + final readableFormats = _reader!.getFormats(allImageFormatsSupported); + if (readableFormats.isEmpty != false && + readableFormats[0] is! SimpleFileFormat) { + return null; + } + + final c = Completer(); + final progress = _reader!.getFile( + readableFormats[0] as SimpleFileFormat, + (file) async { + try { + final all = await file.readAll(); + c.complete(all); + } catch (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + } + }, + onError: (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + }, + ); + if (progress == null) { + c.complete(null); + } + return c.future; + } + + Future isReadableImageFormat() async { + _reader ??= await ClipboardReader.readClipboard(); + return _reader!.canProvide(Formats.png) || + _reader!.canProvide(Formats.jpeg) || + _reader!.canProvide(Formats.heic) || + _reader!.canProvide(Formats.heif) || + _reader!.canProvide(Formats.svg); + } + + Future pasteText() async { + _reader ??= await ClipboardReader.readClipboard(); + String? copied; + + final readersFormat = _reader!.getFormats(Formats.standardFormats); + if (readersFormat.isEmpty) { + return copied; + } + + final c = Completer(); + final progress = _reader!.getValue( + Formats.plainText, + (value) { + copied = value; + c.complete(copied); + }, + onError: (error) { + Logs().e('Clipboard::readText(): $error'); + c.completeError(error); + }, + ); + if (progress == null) { + c.completeError('Clipboard::readText(): error'); + } + return c.future; + } + + SimpleFileFormat getFormatFrom(String? mimeType) { + switch (mimeType) { + case 'image/png': + return Formats.png; + case 'image/jpeg': + return Formats.jpeg; + case 'image/heic': + return Formats.heic; + case 'image/heif': + return Formats.heif; + default: + return Formats.plainTextFile; + } + } +}