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

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

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

@ -1,18 +1,25 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cross_file/cross_file.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:mime/mime.dart';
import 'package:fluffychat/config/app_config.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 '../../utils/resize_image.dart';
import '../../utils/resize_video.dart';
class SendFileDialog extends StatefulWidget {
final Room room;
final List<MatrixFile> files;
final List<XFile> files;
const SendFileDialog({
required this.room,
@ -33,158 +40,233 @@ class SendFileDialogState extends State<SendFileDialog> {
Future<void> _send() async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context)!;
for (var file in widget.files) {
MatrixImageFile? thumbnail;
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
await showFutureLoadingDialog(
context: context,
future: () async {
file = origImage ? file : await file.resizeVideo();
thumbnail = await file.getVideoThumbnail();
},
);
}
widget.room
.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600,
)
.catchError(
(e, s) {
if (e is FileTooBigMatrixException) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.fileIsTooBigForServer)),
);
return null;
}
ErrorReporter(context, 'Unable to send file').onErrorCallback(e, s);
return null;
},
);
}
Navigator.of(context, rootNavigator: false).pop();
showFutureLoadingDialog(
context: context,
future: () async {
final clientConfig = await widget.room.client.getConfig();
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
.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600,
)
.catchError(
(e, s) {
if (e is FileTooBigMatrixException) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.fileIsTooBigForServer)),
);
return null;
}
ErrorReporter(context, 'Unable to send file')
.onErrorCallback(e, s);
return null;
},
);
}
},
);
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
Widget build(BuildContext context) {
final theme = Theme.of(context);
var sendStr = L10n.of(context)!.sendFile;
final allFilesAreImages =
widget.files.every((file) => file is MatrixImageFile);
final sizeString = widget.files
.fold<double>(0, (p, file) => p + file.bytes.length)
.sizeString;
final uniqueMimeType = widget.files
.map((file) => file.mimeType ?? lookupMimeType(file.path))
.toSet()
.singleOrNull;
final fileName = widget.files.length == 1
? widget.files.single.name
: L10n.of(context)!.countFiles(widget.files.length.toString());
if (allFilesAreImages) {
if (uniqueMimeType?.startsWith('image') ?? false) {
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;
} else if (widget.files.every((file) => file is MatrixVideoFile)) {
} else if (uniqueMimeType?.startsWith('video') ?? false) {
sendStr = L10n.of(context)!.sendVideo;
}
Widget contentWidget;
if (allFilesAreImages) {
contentWidget = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
elevation: theme.appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: theme.appBarTheme.shadowColor,
clipBehavior: Clip.hardEdge,
child: Image.memory(
widget.files.first.bytes,
fit: BoxFit.contain,
height: 256,
),
),
),
const SizedBox(height: 16),
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CupertinoSwitch(
value: origImage,
onChanged: (v) => setState(() => origImage = v),
return FutureBuilder<String>(
future: _calcCombinedFileSize(),
builder: (context, snapshot) {
final sizeString =
snapshot.data ?? L10n.of(context)!.calculatingFileSize;
Widget contentWidget;
if (uniqueMimeType?.startsWith('image') ?? false) {
contentWidget = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
elevation: theme.appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: theme.appBarTheme.shadowColor,
clipBehavior: Clip.hardEdge,
child: kIsWeb
? Image.network(
widget.files.first.path,
fit: BoxFit.contain,
height: 256,
)
: Image.file(
File(widget.files.first.path),
fit: BoxFit.contain,
height: 256,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L10n.of(context)!.sendOriginal,
style: const TextStyle(fontWeight: FontWeight.bold),
const SizedBox(height: 16),
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CupertinoSwitch(
value: origImage,
onChanged: (v) => setState(() => origImage = v),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L10n.of(context)!.sendOriginal,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(sizeString),
],
),
Text(sizeString),
],
),
),
],
),
],
),
],
);
} else if (widget.files.every((file) => file is MatrixVideoFile)) {
contentWidget = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(fileName),
const SizedBox(height: 16),
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CupertinoSwitch(
value: origImage,
onChanged: (v) => setState(() => origImage = v),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
);
} else {
final fileNameParts = fileName.split('.');
contentWidget = SizedBox(
width: 256,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
L10n.of(context)!.sendOriginal,
style: const TextStyle(fontWeight: FontWeight.bold),
Icon(
uniqueMimeType == null
? Icons.description_outlined
: uniqueMimeType.startsWith('video')
? Icons.video_file_outlined
: uniqueMimeType.startsWith('audio')
? Icons.audio_file_outlined
: Icons.description_outlined,
),
Text(sizeString),
const SizedBox(width: 8),
Expanded(
child: Text(
fileNameParts.first,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (fileNameParts.length > 1)
Text('.${fileNameParts.last}'),
Text(' ($sizeString)'),
],
),
),
],
),
],
);
} else {
contentWidget = Text('$fileName ($sizeString)');
}
return AlertDialog.adaptive(
title: Text(sendStr),
content: contentWidget,
actions: <Widget>[
TextButton(
onPressed: () {
// just close the dialog
Navigator.of(context, rootNavigator: false).pop();
},
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: _send,
child: Text(L10n.of(context)!.send),
),
],
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
if (uniqueMimeType != null &&
uniqueMimeType.startsWith('video') &&
PlatformInfos.isMobile)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CupertinoSwitch(
value: origImage,
onChanged: (v) => setState(() => origImage = v),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L10n.of(context)!.sendOriginal,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
Text(sizeString),
],
),
),
],
),
],
),
);
}
return AlertDialog.adaptive(
title: Text(sendStr),
content: contentWidget,
actions: <Widget>[
TextButton(
onPressed: () {
// just close the dialog
Navigator.of(context, rootNavigator: false).pop();
},
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: _send,
child: Text(L10n.of(context)!.send),
),
],
);
},
);
}
}

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

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

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

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

Loading…
Cancel
Save