refactor: Load bytes from sending files later to not let app crash
This commit is contained in:
parent
6866a996a3
commit
5c9880f0b2
7 changed files with 242 additions and 213 deletions
|
|
@ -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,
|
||||
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
Loading…
Add table
Add a link
Reference in a new issue