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

This commit is contained in:
krille-chan 2024-09-22 15:45:41 +02:00
commit 5c9880f0b2
No known key found for this signature in database
7 changed files with 242 additions and 213 deletions

View file

@ -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,
),
);

View file

@ -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,
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(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),
],
),
),
],
),
],
);
} else {
final fileNameParts = fileName.split('.');
contentWidget = SizedBox(
width: 256,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
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
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),
],
),
),
],
),
],
),
),
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),
],
),
),
],
),
],
);
} 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,
children: [
Text(
L10n.of(context)!.sendOriginal,
style: const TextStyle(fontWeight: FontWeight.bold),
),
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),
),
],
);
}
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),
),
],
);
},
);
}
}

View file

@ -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,
),
);

View file

@ -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,