diff --git a/CHANGELOG.md b/CHANGELOG.md index f022e05..074850b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,14 @@ ### Features - Added translations: Armenian, Turkish, Chinese (Simplified) - Url-ify matrix identifiers +- Use server-side generated thumbnails in cleartext rooms +- Add option to send images in their original resolution +- Add additional confirmation for sending files & share intents ### Changes - Tapping links, pills, etc. now does stuff ### Fixes: - Various html rendering and url-ifying fixes - Added support for blurhashes -- Use server-side generated thumbnails in cleartext rooms - Image viewer now eventually displays the original image, not only the thumbnail # Version 0.17.0 - 2020-08-31 diff --git a/lib/components/dialogs/send_file_dialog.dart b/lib/components/dialogs/send_file_dialog.dart new file mode 100644 index 0000000..6f5b7da --- /dev/null +++ b/lib/components/dialogs/send_file_dialog.dart @@ -0,0 +1,134 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:native_imaging/native_imaging.dart' as native; + +import '../../utils/matrix_file_extension.dart'; +import '../../utils/room_send_file_extension.dart'; +import '../../components/dialogs/simple_dialogs.dart'; +import '../../l10n/l10n.dart'; + +class SendFileDialog extends StatefulWidget { + final Room room; + final MatrixFile file; + + const SendFileDialog({this.room, this.file, Key key}) : super(key: key); + + @override + _SendFileDialogState createState() => _SendFileDialogState(); +} + +class _SendFileDialogState extends State { + bool origImage = false; + + Future _send() async { + var file = widget.file; + if (file is MatrixImageFile && !origImage) { + final imgFile = file as MatrixImageFile; + // resize to max 1600 x 1600 + try { + await native.init(); + var nativeImg = native.Image(); + try { + await nativeImg.loadEncoded(imgFile.bytes); + imgFile.width = nativeImg.width(); + imgFile.height = nativeImg.height(); + } on UnsupportedError { + final dartCodec = await instantiateImageCodec(imgFile.bytes); + final dartFrame = await dartCodec.getNextFrame(); + imgFile.width = dartFrame.image.width; + imgFile.height = dartFrame.image.height; + final rgbaData = await dartFrame.image.toByteData(); + final rgba = Uint8List.view( + rgbaData.buffer, rgbaData.offsetInBytes, rgbaData.lengthInBytes); + dartFrame.image.dispose(); + dartCodec.dispose(); + nativeImg.loadRGBA(imgFile.width, imgFile.height, rgba); + } + + const max = 1600; + if (imgFile.width > max || imgFile.height > max) { + var w = max, h = max; + if (imgFile.width > imgFile.height) { + h = max * imgFile.height ~/ imgFile.width; + } else { + w = max * imgFile.width ~/ imgFile.height; + } + + final scaledImg = nativeImg.resample(w, h, native.Transform.lanczos); + nativeImg.free(); + nativeImg = scaledImg; + } + final jpegBytes = await nativeImg.toJpeg(75); + file = MatrixImageFile( + bytes: jpegBytes, + name: 'scaled_' + imgFile.name.split('.').first + '.jpg'); + nativeImg.free(); + } catch (e) { + // couldn't resize + } + } + await widget.room.sendFileEventWithThumbnail(file); + } + + @override + Widget build(BuildContext context) { + var sendStr = L10n.of(context).sendFile; + if (widget.file is MatrixImageFile) { + sendStr = L10n.of(context).sendImage; + } else if (widget.file is MatrixAudioFile) { + sendStr = L10n.of(context).sendAudio; + } else if (widget.file is MatrixVideoFile) { + sendStr = L10n.of(context).sendVideo; + } + Widget contentWidget; + if (widget.file is MatrixImageFile) { + contentWidget = Column(mainAxisSize: MainAxisSize.min, children: [ + Flexible( + child: Image.memory( + widget.file.bytes, + fit: BoxFit.contain, + ), + ), + Text(widget.file.name), + Row( + children: [ + Checkbox( + value: origImage, + onChanged: (v) => setState(() => origImage = v), + ), + InkWell( + onTap: () => setState(() => origImage = !origImage), + child: Text(L10n.of(context).sendOriginal + + ' (${widget.file.sizeString})'), + ), + ], + ) + ]); + } else { + contentWidget = Text('${widget.file.name} (${widget.file.sizeString})'); + } + return AlertDialog( + title: Text(sendStr), + content: contentWidget, + actions: [ + FlatButton( + child: Text(L10n.of(context).cancel), + onPressed: () { + // just close the dialog + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text(L10n.of(context).send), + onPressed: () async { + await SimpleDialogs(context).tryRequestWithLoadingDialog(_send()); + await Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index 6680e01..a6d314b 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -13,6 +13,7 @@ import '../theme_switcher.dart'; import '../avatar.dart'; import '../dialogs/simple_dialogs.dart'; import '../matrix.dart'; +import '../dialogs/send_file_dialog.dart'; class ChatListItem extends StatelessWidget { final Room room; @@ -73,11 +74,12 @@ class ChatListItem extends StatelessWidget { if (Matrix.of(context).shareContent != null) { if (Matrix.of(context).shareContent['msgtype'] == 'chat.fluffy.shared_file') { - await SimpleDialogs(context).tryRequestWithErrorToast( - room.sendFileEvent( - Matrix.of(context).shareContent['file'], - ), - ); + await showDialog( + context: context, + builder: (context) => SendFileDialog( + file: Matrix.of(context).shareContent['file'], + room: room, + )); } else { unawaited(room.sendEvent(Matrix.of(context).shareContent)); } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index f1ff9c4..cf43319 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2020-08-16T12:43:17.825046", + "@@last_modified": "2020-09-04T14:58:35.809079", "About": "About", "@About": { "type": "text", @@ -1184,6 +1184,11 @@ "type": "text", "placeholders": {} }, + "Send audio": "Send audio", + "@Send audio": { + "type": "text", + "placeholders": {} + }, "Send file": "Send file", "@Send file": { "type": "text", @@ -1194,6 +1199,16 @@ "type": "text", "placeholders": {} }, + "Send original": "Send original", + "@Send original": { + "type": "text", + "placeholders": {} + }, + "Send video": "Send video", + "@Send video": { + "type": "text", + "placeholders": {} + }, "sentAFile": "{username} sent a file", "@sentAFile": { "type": "text", diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 4a302cd..6a785cf 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -735,10 +735,16 @@ class L10n extends MatrixLocalizations { String get sendAMessage => Intl.message("Send a message"); + String get sendAudio => Intl.message('Send audio'); + String get sendFile => Intl.message('Send file'); String get sendImage => Intl.message('Send image'); + String get sendOriginal => Intl.message('Send original'); + + String get sendVideo => Intl.message('Send video'); + String sentAFile(String username) => Intl.message( "$username sent a file", name: "sentAFile", diff --git a/lib/l10n/messages_messages.dart b/lib/l10n/messages_messages.dart index 1c69e79..5532342 100644 --- a/lib/l10n/messages_messages.dart +++ b/lib/l10n/messages_messages.dart @@ -394,8 +394,11 @@ class MessageLookup extends MessageLookupByLibrary { "Send": MessageLookupByLibrary.simpleMessage("Send"), "Send a message": MessageLookupByLibrary.simpleMessage("Send a message"), + "Send audio": MessageLookupByLibrary.simpleMessage("Send audio"), "Send file": MessageLookupByLibrary.simpleMessage("Send file"), "Send image": MessageLookupByLibrary.simpleMessage("Send image"), + "Send original": MessageLookupByLibrary.simpleMessage("Send original"), + "Send video": MessageLookupByLibrary.simpleMessage("Send video"), "Set a profile picture": MessageLookupByLibrary.simpleMessage("Set a profile picture"), "Set group description": diff --git a/lib/utils/matrix_file_extension.dart b/lib/utils/matrix_file_extension.dart index f2c92b9..d6bf725 100644 --- a/lib/utils/matrix_file_extension.dart +++ b/lib/utils/matrix_file_extension.dart @@ -31,4 +31,34 @@ extension MatrixFileExtension on MatrixFile { } return; } + + MatrixFile get detectFileType { + if (msgType == MessageTypes.Image) { + return MatrixImageFile(bytes: bytes, name: name); + } + if (msgType == MessageTypes.Video) { + return MatrixVideoFile(bytes: bytes, name: name); + } + if (msgType == MessageTypes.Audio) { + return MatrixAudioFile(bytes: bytes, name: name); + } + return this; + } + + String get sizeString { + var size = this.size.toDouble(); + if (size < 1000000) { + size = size / 1000; + size = (size * 10).round() / 10; + return '${size.toString()} KB'; + } else if (size < 1000000000) { + size = size / 1000000; + size = (size * 10).round() / 10; + return '${size.toString()} MB'; + } else { + size = size / 1000000000; + size = (size * 10).round() / 10; + return '${size.toString()} GB'; + } + } } diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 6d490af..ec65569 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -23,11 +23,13 @@ import 'package:flutter/services.dart'; import 'package:memoryfilepicker/memoryfilepicker.dart'; import 'package:pedantic/pedantic.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'chat_details.dart'; import 'chat_list.dart'; import '../components/input_bar.dart'; -import '../utils/room_send_file_extension.dart'; +import '../components/dialogs/send_file_dialog.dart'; +import '../utils/matrix_file_extension.dart'; class ChatView extends StatelessWidget { final String id; @@ -191,39 +193,36 @@ class _ChatState extends State<_Chat> { void sendFileAction(BuildContext context) async { var file = await MemoryFilePicker.getFile(); if (file == null) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - room.sendFileEventWithThumbnail( - MatrixFile(bytes: file.bytes, name: file.path), - ), - ); + await showDialog( + context: context, + builder: (context) => SendFileDialog( + file: + MatrixFile(bytes: file.bytes, name: file.path).detectFileType, + room: room, + )); } void sendImageAction(BuildContext context) async { - var file = await MemoryFilePicker.getImage( - source: ImageSource.gallery, - imageQuality: 50, - maxWidth: 1600, - maxHeight: 1600); + var file = await MemoryFilePicker.getFile(type: FileType.image); if (file == null) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - room.sendFileEventWithThumbnail( - MatrixImageFile(bytes: await file.bytes, name: file.path), - ), - ); + final bytes = await file.bytes; + await showDialog( + context: context, + builder: (context) => SendFileDialog( + file: MatrixImageFile(bytes: bytes, name: file.path), + room: room, + )); } void openCameraAction(BuildContext context) async { - var file = await MemoryFilePicker.getImage( - source: ImageSource.camera, - imageQuality: 50, - maxWidth: 1600, - maxHeight: 1600); + var file = await MemoryFilePicker.getImage(source: ImageSource.camera); if (file == null) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - room.sendFileEventWithThumbnail( - MatrixImageFile(bytes: file.bytes, name: file.path), - ), - ); + await showDialog( + context: context, + builder: (context) => SendFileDialog( + file: MatrixImageFile(bytes: file.bytes, name: file.path), + room: room, + )); } void voiceMessageAction(BuildContext context) async { @@ -235,12 +234,13 @@ class _ChatState extends State<_Chat> { )); if (result == null) return; final audioFile = File(result); - await SimpleDialogs(context).tryRequestWithLoadingDialog( - room.sendFileEvent( - MatrixAudioFile( - bytes: audioFile.readAsBytesSync(), name: audioFile.path), - ), - ); + await showDialog( + context: context, + builder: (context) => SendFileDialog( + file: MatrixAudioFile( + bytes: audioFile.readAsBytesSync(), name: audioFile.path), + room: room, + )); } String _getSelectedEventString(BuildContext context) { diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index 404a33f..0028428 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -17,6 +17,7 @@ import '../components/matrix.dart'; import '../l10n/l10n.dart'; import '../utils/app_route.dart'; import '../utils/url_launcher.dart'; +import '../utils/matrix_file_extension.dart'; import 'archive.dart'; import 'homeserver_picker.dart'; import 'new_group.dart'; @@ -119,7 +120,7 @@ class _ChatListState extends State { }); setState(() => null); }); - _initReceiveSharingINtent(); + _initReceiveSharingIntent(); super.initState(); } @@ -139,7 +140,7 @@ class _ChatListState extends State { 'file': MatrixFile( bytes: file.readAsBytesSync(), name: file.path, - ), + ).detectFileType, }; } @@ -158,7 +159,7 @@ class _ChatListState extends State { }; } - void _initReceiveSharingINtent() { + void _initReceiveSharingIntent() { if (kIsWeb) return; // For sharing images coming from outside the app while the app is in the memory diff --git a/pubspec.lock b/pubspec.lock index 213b400..0d461ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -248,6 +248,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.5.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" flutter_keyboard_visibility: dependency: transitive description: @@ -486,7 +493,7 @@ packages: name: memoryfilepicker url: "https://pub.dartlang.org" source: hosted - version: "0.1.1" + version: "0.1.3" meta: dependency: transitive description: @@ -1000,4 +1007,4 @@ packages: version: "0.1.2" sdks: dart: ">=2.10.0-0.0.dev <2.10.0" - flutter: ">=1.18.0-6.0.pre <2.0.0" + flutter: ">=1.20.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2e9d463..ad16b7e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: localstorage: ^3.0.1+4 bubble: ^1.1.9+1 - memoryfilepicker: ^0.1.1 + memoryfilepicker: ^0.1.3 url_launcher: ^5.4.1 url_launcher_web: ^0.1.0 flutter_advanced_networkimage: