Merge pull request #1354 from krille-chan/krille/load-bytes-later-when-send-files

Krille/load bytes later when send files
This commit is contained in:
Krille-chan 2024-09-22 15:57:11 +02:00 committed by GitHub
commit b328e95980
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 257 additions and 223 deletions

View file

@ -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..."
} }

View file

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

View file

@ -60,7 +60,10 @@ class ImageBubble extends StatelessWidget {
if (!tapToView) return; if (!tapToView) return;
showDialog( showDialog(
context: context, context: context,
builder: (_) => ImageViewer(event), builder: (_) => ImageViewer(
event,
outerContext: context,
),
); );
} }

View file

@ -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,158 +40,233 @@ 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;
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(); 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; 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;
} }
Widget contentWidget;
if (allFilesAreImages) { return FutureBuilder<String>(
contentWidget = Column( future: _calcCombinedFileSize(),
mainAxisSize: MainAxisSize.min, builder: (context, snapshot) {
children: <Widget>[ final sizeString =
Flexible( snapshot.data ?? L10n.of(context)!.calculatingFileSize;
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius), Widget contentWidget;
elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, if (uniqueMimeType?.startsWith('image') ?? false) {
shadowColor: theme.appBarTheme.shadowColor, contentWidget = Column(
clipBehavior: Clip.hardEdge, mainAxisSize: MainAxisSize.min,
child: Image.memory( children: <Widget>[
widget.files.first.bytes, Flexible(
fit: BoxFit.contain, child: Material(
height: 256, 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 return AlertDialog.adaptive(
Row( title: Text(sendStr),
crossAxisAlignment: CrossAxisAlignment.center, content: contentWidget,
children: [ actions: <Widget>[
CupertinoSwitch( TextButton(
value: origImage, onPressed: () {
onChanged: (v) => setState(() => origImage = v), // just close the dialog
), Navigator.of(context, rootNavigator: false).pop();
const SizedBox(width: 16), },
Expanded( child: Text(L10n.of(context)!.cancel),
child: Column( ),
mainAxisSize: MainAxisSize.min, TextButton(
crossAxisAlignment: CrossAxisAlignment.start, onPressed: _send,
children: [ child: Text(L10n.of(context)!.send),
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),
),
],
); );
} }
} }

View file

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

View file

@ -10,8 +10,9 @@ import '../../utils/matrix_sdk_extensions/event_extension.dart';
class ImageViewer extends StatefulWidget { class ImageViewer extends StatefulWidget {
final Event event; final Event event;
final BuildContext outerContext;
const ImageViewer(this.event, {super.key}); const ImageViewer(this.event, {required this.outerContext, super.key});
@override @override
ImageViewerController createState() => ImageViewerController(); ImageViewerController createState() => ImageViewerController();
@ -20,8 +21,9 @@ class ImageViewer extends StatefulWidget {
class ImageViewerController extends State<ImageViewer> { class ImageViewerController extends State<ImageViewer> {
/// Forward this image to another room. /// Forward this image to another room.
void forwardAction() { void forwardAction() {
Matrix.of(context).shareContent = widget.event.content; Matrix.of(widget.outerContext).shareContent = widget.event.content;
context.go('/rooms'); Navigator.of(context).pop();
widget.outerContext.go('/rooms');
} }
/// Save this file with a system call. /// Save this file with a system call.

View file

@ -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 {
// will throw an error e.g. on Android SDK < 18 if (PlatformInfos.isMobile) {
mediaInfo = await VideoCompress.compressVideo(tmpFile.path); // will throw an error e.g. on Android SDK < 18
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,

View file

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

View file

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