Skip to content

Commit

Permalink
TW-731: add image copy/paste in android [temp]
Browse files Browse the repository at this point in the history
  • Loading branch information
sherlockvn committed Oct 8, 2023
1 parent d629b94 commit a8de718
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 53 deletions.
37 changes: 35 additions & 2 deletions lib/pages/chat/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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';
Expand All @@ -45,7 +46,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:fluttertoast/fluttertoast.dart';
Expand Down Expand Up @@ -756,8 +757,40 @@ class ChatController extends State<Chat>
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();
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/chat/chat_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
62 changes: 62 additions & 0 deletions lib/pages/chat/input_bar.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
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/extension/raw_key_event_extension.dart';
import 'package:fluffychat/widgets/avatar/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
Expand Down Expand Up @@ -339,6 +344,63 @@ class InputBar extends StatelessWidget {
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
) {
return AdaptiveTextSelectionToolbar.editable(
anchors: editableTextState.contextMenuAnchors,
clipboardStatus: ClipboardStatus.pasteable,
// to apply the normal behavior when click on copy (copy in clipboard close toolbar)
// use an empty function `() {}` to hide this option from the toolbar
onCopy: () => editableTextState
.copySelection(SelectionChangedCause.toolbar),
// to apply the normal behavior when click on cut
onCut: () =>
editableTextState.cutSelection(SelectionChangedCause.toolbar),
onPaste: () async {
if (room == null) {
// FIXME: need to handle the case when in draft chat
return;
}
// HERE will be called when the paste button is clicked in the toolbar
// apply your own logic here

if (await Clipboard.instance.isReadableImageFormat()) {
await Clipboard.instance
.readImage((ClipboardImageInfo imageClipboard) async {
// FIXME: temp demo for demo in upstream
final data = await imageClipboard.stream.toList();
final 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,
)
],
);
},
);
});
} else if (Clipboard.instance.isReadableTextFormat()) {
editableTextState.pasteText(SelectionChangedCause.toolbar);
}

// to apply the normal behavior when click on paste (add in input and close toolbar)
// editableTextState.pasteText(SelectionChangedCause.toolbar);
},
// to apply the normal behavior when click on select all
onSelectAll: () =>
editableTextState.selectAll(SelectionChangedCause.toolbar),
);
},
),
suggestionsCallback: getSuggestions,
itemBuilder: (context, suggestion) => SuggestionTile(
Expand Down
82 changes: 32 additions & 50 deletions lib/pages/chat/send_file_dialog.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,8 +22,6 @@ class SendFileDialog extends StatefulWidget {
}

class SendFileDialogState extends State<SendFileDialog> {
bool origImage = false;

/// Images smaller than 20kb don't need compression.
static const int minSizeToCompress = 20 * 1024;

Expand All @@ -43,18 +41,13 @@ class SendFileDialogState extends State<SendFileDialog> {
}
// 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();

Expand All @@ -63,25 +56,25 @@ class SendFileDialogState extends State<SendFileDialog> {

@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<double>(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<double>(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: <Widget>[
Expand All @@ -91,37 +84,26 @@ class SendFileDialogState extends State<SendFileDialog> {
fit: BoxFit.contain,
),
),
Row(
children: <Widget>[
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: <Widget>[
TextButton(
onPressed: () {
// 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"),
),
],
);
Expand Down
19 changes: 19 additions & 0 deletions lib/presentation/model/clipboard/clipboard_image_info.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/services.dart';

class ClipboardImageInfo with EquatableMixin {
final Stream<Uint8List> stream;

final String? fileName;

final int? fileSize;

ClipboardImageInfo({
required this.stream,
this.fileName,
this.fileSize,
});

@override
List<Object?> get props => [stream, fileName, fileSize];
}
Loading

0 comments on commit a8de718

Please sign in to comment.