refactor: Load bytes from sending files later to not let app crash

pull/1354/head
krille-chan 10 months ago
parent 6866a996a3
commit 5c9880f0b2
No known key found for this signature in database

@ -2761,5 +2761,6 @@
"discoverHomeservers": "Discover homeservers", "discoverHomeservers": "Discover homeservers",
"whatIsAHomeserver": "What is a homeserver?", "whatIsAHomeserver": "What is a homeserver?",
"homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.", "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.",
"doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?" "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?",
"calculatingFileSize": "Calculating file size..."
} }

@ -37,7 +37,6 @@ import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/account_bundles.dart'; import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'send_file_dialog.dart'; import 'send_file_dialog.dart';
import 'send_location_dialog.dart'; import 'send_location_dialog.dart';
@ -123,36 +122,11 @@ class ChatController extends State<ChatPageWithRoom>
void onDragDone(DropDoneDetails details) async { void onDragDone(DropDoneDetails details) async {
setState(() => dragging = false); setState(() => dragging = false);
if (details.files.isEmpty) return; if (details.files.isEmpty) return;
final result = await showFutureLoadingDialog(
context: context,
future: () async {
final clientConfig = await room.client.getConfig();
final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024;
final matrixFiles = await Future.wait(
details.files.map(
(xfile) async {
final length = await xfile.length();
if (length > maxUploadSize) {
throw FileTooBigMatrixException(length, maxUploadSize);
}
return MatrixFile(
bytes: await xfile.readAsBytes(),
name: xfile.name,
mimeType: xfile.mimeType,
).detectFileType;
},
),
);
return matrixFiles;
},
);
final matrixFiles = result.result;
if (matrixFiles == null || matrixFiles.isEmpty) return;
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: matrixFiles, files: details.files,
room: room, room: room,
), ),
); );
@ -510,36 +484,24 @@ class ChatController extends State<ChatPageWithRoom>
FilePicker.platform.pickFiles( FilePicker.platform.pickFiles(
compressionQuality: 0, compressionQuality: 0,
allowMultiple: false, allowMultiple: false,
withData: true,
), ),
); );
if (result == null || result.files.isEmpty) return; if (result == null || result.files.isEmpty) return;
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: result.files files: result.xFiles,
.map(
(xfile) => MatrixFile(
bytes: xfile.bytes!,
name: xfile.name,
).detectFileType,
)
.toList(),
room: room, room: room,
), ),
); );
} }
void sendImageFromClipBoard(Uint8List? image) async { void sendImageFromClipBoard(Uint8List? image) async {
if (image == null) return;
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [ files: [XFile.fromData(image)],
MatrixFile(
bytes: image!,
name: "image from Clipboard",
).detectFileType,
],
room: room, room: room,
), ),
); );
@ -550,7 +512,6 @@ class ChatController extends State<ChatPageWithRoom>
FilePicker.platform.pickFiles( FilePicker.platform.pickFiles(
compressionQuality: 0, compressionQuality: 0,
type: FileType.image, type: FileType.image,
withData: true,
allowMultiple: false, allowMultiple: false,
), ),
); );
@ -559,14 +520,7 @@ class ChatController extends State<ChatPageWithRoom>
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: result.files files: result.xFiles,
.map(
(xfile) => MatrixFile(
bytes: xfile.bytes!,
name: xfile.name,
).detectFileType,
)
.toList(),
room: room, room: room,
), ),
); );
@ -577,16 +531,11 @@ class ChatController extends State<ChatPageWithRoom>
FocusScope.of(context).requestFocus(FocusNode()); FocusScope.of(context).requestFocus(FocusNode());
final file = await ImagePicker().pickImage(source: ImageSource.camera); final file = await ImagePicker().pickImage(source: ImageSource.camera);
if (file == null) return; if (file == null) return;
final bytes = await file.readAsBytes();
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [ files: [file],
MatrixImageFile(
bytes: bytes,
name: file.path,
),
],
room: room, room: room,
), ),
); );
@ -600,16 +549,11 @@ class ChatController extends State<ChatPageWithRoom>
maxDuration: const Duration(minutes: 1), maxDuration: const Duration(minutes: 1),
); );
if (file == null) return; if (file == null) return;
final bytes = await file.readAsBytes();
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [ files: [file],
MatrixVideoFile(
bytes: bytes,
name: file.path,
),
],
room: room, room: room,
), ),
); );

@ -1,18 +1,25 @@
import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:mime/mime.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/size_string.dart'; import 'package:fluffychat/utils/size_string.dart';
import '../../utils/resize_image.dart'; import '../../utils/resize_video.dart';
class SendFileDialog extends StatefulWidget { class SendFileDialog extends StatefulWidget {
final Room room; final Room room;
final List<MatrixFile> files; final List<XFile> files;
const SendFileDialog({ const SendFileDialog({
required this.room, required this.room,
@ -33,17 +40,41 @@ class SendFileDialogState extends State<SendFileDialog> {
Future<void> _send() async { Future<void> _send() async {
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context)!; final l10n = L10n.of(context)!;
for (var file in widget.files) {
MatrixImageFile? thumbnail; Navigator.of(context, rootNavigator: false).pop();
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
await showFutureLoadingDialog( showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
file = origImage ? file : await file.resizeVideo(); final clientConfig = await widget.room.client.getConfig();
thumbnail = await file.getVideoThumbnail(); final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024;
},
); for (final xfile in widget.files) {
final MatrixFile file;
MatrixImageFile? thumbnail;
final length = await xfile.length();
final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path);
// If file is a video, shrink it!
if (mimeType != null &&
mimeType.startsWith('video') &&
length > minSizeToCompress &&
!origImage) {
file = await xfile.resizeVideo();
thumbnail = await xfile.getVideoThumbnail();
} else {
// Else we just create a MatrixFile
file = MatrixFile(
bytes: await xfile.readAsBytes(),
name: xfile.name,
mimeType: xfile.mimeType,
).detectFileType;
} }
if (file.bytes.length > maxUploadSize) {
throw FileTooBigMatrixException(length, maxUploadSize);
}
widget.room widget.room
.sendFileEvent( .sendFileEvent(
file, file,
@ -58,39 +89,54 @@ class SendFileDialogState extends State<SendFileDialog> {
); );
return null; return null;
} }
ErrorReporter(context, 'Unable to send file').onErrorCallback(e, s); ErrorReporter(context, 'Unable to send file')
.onErrorCallback(e, s);
return null; return null;
}, },
); );
} }
Navigator.of(context, rootNavigator: false).pop(); },
);
return; return;
} }
Future<String> _calcCombinedFileSize() async {
final lengths =
await Future.wait(widget.files.map((file) => file.length()));
return lengths.fold<double>(0, (p, length) => p + length).sizeString;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
var sendStr = L10n.of(context)!.sendFile; var sendStr = L10n.of(context)!.sendFile;
final allFilesAreImages = final uniqueMimeType = widget.files
widget.files.every((file) => file is MatrixImageFile); .map((file) => file.mimeType ?? lookupMimeType(file.path))
final sizeString = widget.files .toSet()
.fold<double>(0, (p, file) => p + file.bytes.length) .singleOrNull;
.sizeString;
final fileName = widget.files.length == 1 final fileName = widget.files.length == 1
? widget.files.single.name ? widget.files.single.name
: L10n.of(context)!.countFiles(widget.files.length.toString()); : L10n.of(context)!.countFiles(widget.files.length.toString());
if (allFilesAreImages) { if (uniqueMimeType?.startsWith('image') ?? false) {
sendStr = L10n.of(context)!.sendImage; sendStr = L10n.of(context)!.sendImage;
} else if (widget.files.every((file) => file is MatrixAudioFile)) { } else if (uniqueMimeType?.startsWith('audio') ?? false) {
sendStr = L10n.of(context)!.sendAudio; sendStr = L10n.of(context)!.sendAudio;
} else if (widget.files.every((file) => file is MatrixVideoFile)) { } else if (uniqueMimeType?.startsWith('video') ?? false) {
sendStr = L10n.of(context)!.sendVideo; sendStr = L10n.of(context)!.sendVideo;
} }
return FutureBuilder<String>(
future: _calcCombinedFileSize(),
builder: (context, snapshot) {
final sizeString =
snapshot.data ?? L10n.of(context)!.calculatingFileSize;
Widget contentWidget; Widget contentWidget;
if (allFilesAreImages) { if (uniqueMimeType?.startsWith('image') ?? false) {
contentWidget = Column( contentWidget = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -100,8 +146,14 @@ class SendFileDialogState extends State<SendFileDialog> {
elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, elevation: theme.appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: theme.appBarTheme.shadowColor, shadowColor: theme.appBarTheme.shadowColor,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: Image.memory( child: kIsWeb
widget.files.first.bytes, ? Image.network(
widget.files.first.path,
fit: BoxFit.contain,
height: 256,
)
: Image.file(
File(widget.files.first.path),
fit: BoxFit.contain, fit: BoxFit.contain,
height: 256, height: 256,
), ),
@ -134,13 +186,41 @@ class SendFileDialogState extends State<SendFileDialog> {
), ),
], ],
); );
} else if (widget.files.every((file) => file is MatrixVideoFile)) { } else {
contentWidget = Column( final fileNameParts = fileName.split('.');
contentWidget = SizedBox(
width: 256,
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: [
Text(fileName), Row(
const SizedBox(height: 16), children: [
Icon(
uniqueMimeType == null
? Icons.description_outlined
: uniqueMimeType.startsWith('video')
? Icons.video_file_outlined
: uniqueMimeType.startsWith('audio')
? Icons.audio_file_outlined
: Icons.description_outlined,
),
const SizedBox(width: 8),
Expanded(
child: Text(
fileNameParts.first,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (fileNameParts.length > 1)
Text('.${fileNameParts.last}'),
Text(' ($sizeString)'),
],
),
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
if (uniqueMimeType != null &&
uniqueMimeType.startsWith('video') &&
PlatformInfos.isMobile)
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -156,7 +236,8 @@ class SendFileDialogState extends State<SendFileDialog> {
children: [ children: [
Text( Text(
L10n.of(context)!.sendOriginal, L10n.of(context)!.sendOriginal,
style: const TextStyle(fontWeight: FontWeight.bold), style:
const TextStyle(fontWeight: FontWeight.bold),
), ),
Text(sizeString), Text(sizeString),
], ],
@ -165,9 +246,8 @@ class SendFileDialogState extends State<SendFileDialog> {
], ],
), ),
], ],
),
); );
} else {
contentWidget = Text('$fileName ($sizeString)');
} }
return AlertDialog.adaptive( return AlertDialog.adaptive(
title: Text(sendStr), title: Text(sendStr),
@ -186,5 +266,7 @@ class SendFileDialogState extends State<SendFileDialog> {
), ),
], ],
); );
},
);
} }
} }

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_shortcuts/flutter_shortcuts.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
@ -205,7 +206,13 @@ class ChatListController extends State<ChatList>
context: context, context: context,
useRootNavigator: false, useRootNavigator: false,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [shareFile], files: [
XFile.fromData(
shareFile.bytes,
name: shareFile.name,
mimeType: shareFile.mimeType,
),
],
room: room, room: room,
), ),
); );

@ -1,28 +1,25 @@
import 'dart:io'; import 'package:cross_file/cross_file.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
extension ResizeImage on MatrixFile { extension ResizeImage on XFile {
static const int max = 1200; static const int max = 1200;
static const int quality = 40; static const int quality = 40;
Future<MatrixVideoFile> resizeVideo() async { Future<MatrixVideoFile> resizeVideo() async {
final tmpDir = await getTemporaryDirectory();
final tmpFile = File('${tmpDir.path}/$name');
MediaInfo? mediaInfo; MediaInfo? mediaInfo;
await tmpFile.writeAsBytes(bytes);
try { try {
if (PlatformInfos.isMobile) {
// will throw an error e.g. on Android SDK < 18 // will throw an error e.g. on Android SDK < 18
mediaInfo = await VideoCompress.compressVideo(tmpFile.path); mediaInfo = await VideoCompress.compressVideo(path);
}
} catch (e, s) { } catch (e, s) {
Logs().w('Error while compressing video', e, s); Logs().w('Error while compressing video', e, s);
} }
return MatrixVideoFile( return MatrixVideoFile(
bytes: (await mediaInfo?.file?.readAsBytes()) ?? bytes, bytes: (await mediaInfo?.file?.readAsBytes()) ?? await readAsBytes(),
name: name, name: name,
mimeType: mimeType, mimeType: mimeType,
width: mediaInfo?.width, width: mediaInfo?.width,
@ -33,13 +30,9 @@ extension ResizeImage on MatrixFile {
Future<MatrixImageFile?> getVideoThumbnail() async { Future<MatrixImageFile?> getVideoThumbnail() async {
if (!PlatformInfos.isMobile) return null; if (!PlatformInfos.isMobile) return null;
final tmpDir = await getTemporaryDirectory();
final tmpFile = File('${tmpDir.path}/$name');
if (await tmpFile.exists() == false) {
await tmpFile.writeAsBytes(bytes);
}
try { try {
final bytes = await VideoCompress.getByteThumbnail(tmpFile.path); final bytes = await VideoCompress.getByteThumbnail(path);
if (bytes == null) return null; if (bytes == null) return null;
return MatrixImageFile( return MatrixImageFile(
bytes: bytes, bytes: bytes,

@ -231,7 +231,7 @@ packages:
source: hosted source: hosted
version: "1.9.2" version: "1.9.2"
cross_file: cross_file:
dependency: transitive dependency: "direct main"
description: description:
name: cross_file name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
@ -1228,7 +1228,7 @@ packages:
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
mime: mime:
dependency: transitive dependency: "direct main"
description: description:
name: mime name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"

@ -17,6 +17,7 @@ dependencies:
callkeep: ^0.3.2 callkeep: ^0.3.2
chewie: ^1.8.1 chewie: ^1.8.1
collection: ^1.18.0 collection: ^1.18.0
cross_file: ^0.3.4+2
cupertino_icons: any cupertino_icons: any
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
desktop_notifications: ^0.6.3 desktop_notifications: ^0.6.3
@ -65,6 +66,7 @@ dependencies:
latlong2: ^0.9.1 latlong2: ^0.9.1
linkify: ^5.0.0 linkify: ^5.0.0
matrix: ^0.33.0 matrix: ^0.33.0
mime: ^1.0.6
native_imaging: ^0.1.1 native_imaging: ^0.1.1
opus_caf_converter_dart: ^1.0.1 opus_caf_converter_dart: ^1.0.1
package_info_plus: ^6.0.0 package_info_plus: ^6.0.0

Loading…
Cancel
Save