diff --git a/CHANGELOG.md b/CHANGELOG.md index a7aecbd..1124e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features - Add ability to enable / disable emotes globally - Add ability to manage emote packs with different state keys +### Changes +- Re-scale images in a separate isolate to prevent the UI from freezing ### Fixes - Fix amoled / theme settings not always saving properly - Show device name in account information correctly diff --git a/lib/components/dialogs/send_file_dialog.dart b/lib/components/dialogs/send_file_dialog.dart index 74aa87c..1ad33ef 100644 --- a/lib/components/dialogs/send_file_dialog.dart +++ b/lib/components/dialogs/send_file_dialog.dart @@ -1,14 +1,11 @@ -import 'dart:typed_data'; -import 'dart:ui'; - import 'package:famedlysdk/famedlysdk.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:native_imaging/native_imaging.dart' as native; import '../../components/dialogs/simple_dialogs.dart'; import '../../utils/matrix_file_extension.dart'; import '../../utils/room_send_file_extension.dart'; +import '../../utils/resize_image.dart'; class SendFileDialog extends StatefulWidget { final Room room; @@ -26,46 +23,8 @@ class _SendFileDialogState extends State { 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(); + file = await resizeImage(file, max: 1600); } catch (e) { // couldn't resize } diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 0a2bfff..93dc0da 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1717,5 +1717,10 @@ "@emotePacks": { "type": "text", "placeholders": {} + }, + "changeDeviceName": "Gerätename ändern", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_et.arb b/lib/l10n/intl_et.arb index 84d9556..53f0b4c 100644 --- a/lib/l10n/intl_et.arb +++ b/lib/l10n/intl_et.arb @@ -1707,5 +1707,20 @@ "@privacy": { "type": "text", "placeholders": {} + }, + "enableEmotesGlobally": "Võta emotsioonitegevuste pakid läbivalt kasutusele", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "Emotsioonitegevuste pakid jututoa jaoks", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "changeDeviceName": "Muuda seadme nime", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index ce3b836..caf5142 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1717,5 +1717,10 @@ "@emotePacks": { "type": "text", "placeholders": {} + }, + "changeDeviceName": "Modifier le nom de l'appareil", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_hr.arb b/lib/l10n/intl_hr.arb index 7620a59..d8d098c 100644 --- a/lib/l10n/intl_hr.arb +++ b/lib/l10n/intl_hr.arb @@ -1717,5 +1717,10 @@ "@privacy": { "type": "text", "placeholders": {} + }, + "changeDeviceName": "Promijeni ime uređaja", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 0967ef4..bf5cc93 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1 +1,198 @@ -{} +{ + "changedTheGuestAccessRules": "{username} ha cambiato le regole di accesso per ospiti", + "@changedTheGuestAccessRules": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changeTheHomeserver": "Cambia l'homeserver", + "@changeTheHomeserver": { + "type": "text", + "placeholders": {} + }, + "changedTheDisplaynameTo": "{username} ha cambiato nome in: {displayname}", + "@changedTheDisplaynameTo": { + "type": "text", + "placeholders": { + "username": {}, + "displayname": {} + } + }, + "changedTheChatPermissions": "{username} ha cambiato i permessi della chat", + "@changedTheChatPermissions": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheChatDescriptionTo": "{username} ha cambiato la descrizione della chat in: '{description}'", + "@changedTheChatDescriptionTo": { + "type": "text", + "placeholders": { + "username": {}, + "description": {} + } + }, + "changedTheChatNameTo": "{username} ha cambiato il nome della chat in: '{chatname}'", + "@changedTheChatNameTo": { + "type": "text", + "placeholders": { + "username": {}, + "chatname": {} + } + }, + "changeDeviceName": "Cambia nome dispositivo", + "@changeDeviceName": { + "type": "text", + "placeholders": {} + }, + "cancel": "Cancella", + "@cancel": { + "type": "text", + "placeholders": {} + }, + "cachedKeys": "Chiavi salvate in cache con successo!", + "@cachedKeys": { + "type": "text", + "placeholders": {} + }, + "byDefaultYouWillBeConnectedTo": "Di default sarai connesso a {homeserver}", + "@byDefaultYouWillBeConnectedTo": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "blockDevice": "Blocca dispositivo", + "@blockDevice": { + "type": "text", + "placeholders": {} + }, + "bannedUser": "{username} ha bannato {targetName}", + "@bannedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "banned": "Bannato", + "@banned": { + "type": "text", + "placeholders": {} + }, + "banFromChat": "Bannato dalla chat", + "@banFromChat": { + "type": "text", + "placeholders": {} + }, + "avatarHasBeenChanged": "L'avatar è stato cambiato", + "@avatarHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "authentication": "Autenticazione", + "@authentication": { + "type": "text", + "placeholders": {} + }, + "askVerificationRequest": "Accettare questa richiesta di verifica da {username}?", + "@askVerificationRequest": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "askSSSSVerify": "Per favore inserisci la tua passphrase o recovery key per verificare la sessione.", + "@askSSSSVerify": { + "type": "text", + "placeholders": {} + }, + "areYouSure": "Sicuro?", + "@areYouSure": { + "type": "text", + "placeholders": {} + }, + "areGuestsAllowedToJoin": "Gli utenti guest possono partecipare", + "@areGuestsAllowedToJoin": { + "type": "text", + "placeholders": {} + }, + "archivedRoom": "Stanza Archiviata", + "@archivedRoom": { + "type": "text", + "placeholders": {} + }, + "archive": "Archivia", + "@archive": { + "type": "text", + "placeholders": {} + }, + "anyoneCanJoin": "Tutti possono partecipare", + "@anyoneCanJoin": { + "type": "text", + "placeholders": {} + }, + "answeredTheCall": "{senderName} ha risposto alla chiamata", + "@answeredTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "alreadyHaveAnAccount": "Hai già un account?", + "@alreadyHaveAnAccount": { + "type": "text", + "placeholders": {} + }, + "alias": "alias", + "@alias": { + "type": "text", + "placeholders": {} + }, + "admin": "Admin", + "@admin": { + "type": "text", + "placeholders": {} + }, + "addGroupDescription": "Aggiungi descrizione al gruppo", + "@addGroupDescription": { + "type": "text", + "placeholders": {} + }, + "activatedEndToEndEncryption": "{username} Ha abilitato la crittografia end to end", + "@activatedEndToEndEncryption": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "accountInformation": "Informazioni account", + "@accountInformation": { + "type": "text", + "placeholders": {} + }, + "account": "Account", + "@account": { + "type": "text", + "placeholders": {} + }, + "acceptedTheInvitation": "{username} ha accettato l'invito", + "@acceptedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "accept": "Accetta", + "@accept": { + "type": "text", + "placeholders": {} + }, + "about": "Informazioni", + "@about": { + "type": "text", + "placeholders": {} + } +} diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 6d7fbdb..d8960c1 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1702,5 +1702,25 @@ "@deactivateAccountWarning": { "type": "text", "placeholders": {} + }, + "privacy": "Конфиденциальность", + "@privacy": { + "type": "text", + "placeholders": {} + }, + "enableEmotesGlobally": "Включить набор эмоджи глобально", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "Наборы эмоджи для комнаты", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "changeDeviceName": "Изменить имя устройства", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 1895c47..c767ae9 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1717,5 +1717,10 @@ "@deactivateAccountWarning": { "type": "text", "placeholders": {} + }, + "changeDeviceName": "Cihaz adını değiştir", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } diff --git a/lib/utils/resize_image.dart b/lib/utils/resize_image.dart new file mode 100644 index 0000000..93b328f --- /dev/null +++ b/lib/utils/resize_image.dart @@ -0,0 +1,105 @@ +import 'dart:ui'; +import 'dart:typed_data'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:native_imaging/native_imaging.dart' as native; + +import 'run_in_background.dart'; + +Future resizeImage(MatrixImageFile file, + {int max = 800}) async { + // we want to resize the image in a separate isolate, because otherwise that can + // freeze up the UI a bit + + // we can't do width / height fetching in a separate isolate, as that may use the UI stuff + await native.init(); + + _IsolateArgs args; + try { + final nativeImg = native.Image(); + await nativeImg.loadEncoded(file.bytes); + file.width = nativeImg.width(); + file.height = nativeImg.height(); + args = _IsolateArgs( + width: file.width, height: file.height, bytes: file.bytes, max: max); + nativeImg.free(); + } on UnsupportedError { + final dartCodec = await instantiateImageCodec(file.bytes); + final dartFrame = await dartCodec.getNextFrame(); + file.width = dartFrame.image.width; + file.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(); + args = _IsolateArgs( + width: file.width, height: file.height, bytes: rgba, max: max); + } + + final res = await runInBackground(_isolateFunction, args); + file.blurhash = res.blurhash; + final thumbnail = MatrixImageFile( + bytes: res.jpegBytes, + name: file.name != null + ? 'scaled_' + file.name.split('.').first + '.jpg' + : 'thumbnail.jpg', + mimeType: 'image/jpeg', + width: res.width, + height: res.height, + ); + // only return the thumbnail if the size actually decreased + return thumbnail.size >= file.size ? file : thumbnail; +} + +class _IsolateArgs { + final int width; + final int height; + final Uint8List bytes; + final int max; + final String name; + _IsolateArgs({this.width, this.height, this.bytes, this.max, this.name}); +} + +class _IsolateResponse { + final String blurhash; + final Uint8List jpegBytes; + final int width; + final int height; + _IsolateResponse({this.blurhash, this.jpegBytes, this.width, this.height}); +} + +Future<_IsolateResponse> _isolateFunction(_IsolateArgs args) async { + await native.init(); + var nativeImg = native.Image(); + + try { + await nativeImg.loadEncoded(args.bytes); + } on UnsupportedError { + nativeImg.loadRGBA(args.width, args.height, args.bytes); + } + if (args.width > args.max || args.height > args.max) { + var w = args.max, h = args.max; + if (args.width > args.height) { + h = args.max * args.height ~/ args.width; + } else { + w = args.max * args.width ~/ args.height; + } + + final scaledImg = nativeImg.resample(w, h, native.Transform.lanczos); + nativeImg.free(); + nativeImg = scaledImg; + } + final jpegBytes = await nativeImg.toJpeg(75); + final blurhash = nativeImg.toBlurhash(3, 3); + + final ret = _IsolateResponse( + blurhash: blurhash, + jpegBytes: jpegBytes, + width: nativeImg.width(), + height: nativeImg.height()); + + nativeImg.free(); + + return ret; +} diff --git a/lib/utils/room_send_file_extension.dart b/lib/utils/room_send_file_extension.dart index 04ab707..55b2756 100644 --- a/lib/utils/room_send_file_extension.dart +++ b/lib/utils/room_send_file_extension.dart @@ -16,11 +16,9 @@ * along with this program. If not, see . */ -import 'dart:typed_data'; -import 'dart:ui'; - import 'package:famedlysdk/famedlysdk.dart'; -import 'package:native_imaging/native_imaging.dart' as native; + +import 'resize_image.dart'; extension RoomSendFileExtension on Room { Future sendFileEventWithThumbnail( @@ -33,50 +31,7 @@ extension RoomSendFileExtension on Room { MatrixFile thumbnail; try { if (file is MatrixImageFile) { - await native.init(); - var nativeImg = native.Image(); - try { - await nativeImg.loadEncoded(file.bytes); - file.width = nativeImg.width(); - file.height = nativeImg.height(); - } on UnsupportedError { - final dartCodec = await instantiateImageCodec(file.bytes); - final dartFrame = await dartCodec.getNextFrame(); - file.width = dartFrame.image.width; - file.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(file.width, file.height, rgba); - } - - const max = 800; - if (file.width > max || file.height > max) { - var w = max, h = max; - if (file.width > file.height) { - h = max * file.height ~/ file.width; - } else { - w = max * file.width ~/ file.height; - } - - final scaledImg = nativeImg.resample(w, h, native.Transform.lanczos); - nativeImg.free(); - nativeImg = scaledImg; - } - final jpegBytes = await nativeImg.toJpeg(75); - file.blurhash = nativeImg.toBlurhash(3, 3); - - thumbnail = MatrixImageFile( - bytes: jpegBytes, - name: 'thumbnail.jpg', - mimeType: 'image/jpeg', - width: nativeImg.width(), - height: nativeImg.height(), - ); - - nativeImg.free(); + thumbnail = await resizeImage(file); if (thumbnail.size > file.size ~/ 2) { thumbnail = null; diff --git a/lib/utils/run_in_background.dart b/lib/utils/run_in_background.dart new file mode 100644 index 0000000..8a99337 --- /dev/null +++ b/lib/utils/run_in_background.dart @@ -0,0 +1,12 @@ +import 'package:isolate/isolate.dart'; +import 'dart:async'; + +Future runInBackground( + FutureOr Function(U arg) function, U arg) async { + final isolate = await IsolateRunner.spawn(); + try { + return await isolate.run(function, arg); + } finally { + await isolate.close(); + } +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 7566b63..5bb4cbf 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -227,31 +227,16 @@ class _ChatState extends State<_Chat> { } void sendImageAction(BuildContext context) async { - MatrixImageFile file; - if (PlatformInfos.isMobile) { - final result = await ImagePicker().getImage( - source: ImageSource.gallery, - imageQuality: 50, - maxWidth: 1600, - maxHeight: 1600); - if (result == null) return; - file = MatrixImageFile( - bytes: await result.readAsBytes(), - name: result.path, - ); - } else { - final result = - await FilePickerCross.importFromStorage(type: FileTypeCross.image); - if (result == null) return; - file = MatrixImageFile( - bytes: result.toUint8List(), - name: result.fileName, - ); - } + final result = + await FilePickerCross.importFromStorage(type: FileTypeCross.image); + if (result == null) return; await showDialog( context: context, builder: (context) => SendFileDialog( - file: file, + file: MatrixImageFile( + bytes: result.toUint8List(), + name: result.fileName, + ), room: room, ), );