You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			440 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			440 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
import 'package:flutter/cupertino.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
 | 
						|
import 'package:cross_file/cross_file.dart';
 | 
						|
import 'package:matrix/matrix.dart';
 | 
						|
import 'package:mime/mime.dart';
 | 
						|
 | 
						|
import 'package:fluffychat/config/app_config.dart';
 | 
						|
import 'package:fluffychat/l10n/l10n.dart';
 | 
						|
import 'package:fluffychat/utils/localized_exception_extension.dart';
 | 
						|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
 | 
						|
import 'package:fluffychat/utils/other_party_can_receive.dart';
 | 
						|
import 'package:fluffychat/utils/platform_infos.dart';
 | 
						|
import 'package:fluffychat/utils/size_string.dart';
 | 
						|
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
 | 
						|
import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.dart';
 | 
						|
import '../../utils/resize_video.dart';
 | 
						|
 | 
						|
class SendFileDialog extends StatefulWidget {
 | 
						|
  final Room room;
 | 
						|
  final List<XFile> files;
 | 
						|
  final BuildContext outerContext;
 | 
						|
 | 
						|
  const SendFileDialog({
 | 
						|
    required this.room,
 | 
						|
    required this.files,
 | 
						|
    required this.outerContext,
 | 
						|
    super.key,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  SendFileDialogState createState() => SendFileDialogState();
 | 
						|
}
 | 
						|
 | 
						|
class SendFileDialogState extends State<SendFileDialog> {
 | 
						|
  bool compress = true;
 | 
						|
 | 
						|
  /// Images smaller than 20kb don't need compression.
 | 
						|
  static const int minSizeToCompress = 20 * 1000;
 | 
						|
 | 
						|
  final TextEditingController _labelTextController = TextEditingController();
 | 
						|
 | 
						|
  Future<void> _send() async {
 | 
						|
    final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext);
 | 
						|
    final l10n = L10n.of(context);
 | 
						|
 | 
						|
    try {
 | 
						|
      if (!widget.room.otherPartyCanReceiveMessages) {
 | 
						|
        throw OtherPartyCanNotReceiveMessages();
 | 
						|
      }
 | 
						|
      scaffoldMessenger.showLoadingSnackBar(l10n.prepareSendingAttachment);
 | 
						|
      Navigator.of(context, rootNavigator: false).pop();
 | 
						|
      final clientConfig = await widget.room.client.getConfig();
 | 
						|
      final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1000 * 1000;
 | 
						|
 | 
						|
      for (final xfile in widget.files) {
 | 
						|
        final MatrixFile file;
 | 
						|
        MatrixImageFile? thumbnail;
 | 
						|
        final length = await xfile.length();
 | 
						|
        final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path);
 | 
						|
 | 
						|
        // Generate video thumbnail
 | 
						|
        if (PlatformInfos.isMobile &&
 | 
						|
            mimeType != null &&
 | 
						|
            mimeType.startsWith('video')) {
 | 
						|
          scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail);
 | 
						|
          thumbnail = await xfile.getVideoThumbnail();
 | 
						|
        }
 | 
						|
 | 
						|
        // If file is a video, shrink it!
 | 
						|
        if (PlatformInfos.isMobile &&
 | 
						|
            mimeType != null &&
 | 
						|
            mimeType.startsWith('video')) {
 | 
						|
          scaffoldMessenger.showLoadingSnackBar(l10n.compressVideo);
 | 
						|
          file = await xfile.getVideoInfo(
 | 
						|
            compress: length > minSizeToCompress && compress,
 | 
						|
          );
 | 
						|
        } else {
 | 
						|
          if (length > maxUploadSize) {
 | 
						|
            throw FileTooBigMatrixException(length, maxUploadSize);
 | 
						|
          }
 | 
						|
          // Else we just create a MatrixFile
 | 
						|
          file = MatrixFile(
 | 
						|
            bytes: await xfile.readAsBytes(),
 | 
						|
            name: xfile.name,
 | 
						|
            mimeType: mimeType,
 | 
						|
          ).detectFileType;
 | 
						|
        }
 | 
						|
 | 
						|
        if (file.bytes.length > maxUploadSize) {
 | 
						|
          throw FileTooBigMatrixException(length, maxUploadSize);
 | 
						|
        }
 | 
						|
 | 
						|
        if (widget.files.length > 1) {
 | 
						|
          scaffoldMessenger.showLoadingSnackBar(
 | 
						|
            l10n.sendingAttachmentCountOfCount(
 | 
						|
              widget.files.indexOf(xfile) + 1,
 | 
						|
              widget.files.length,
 | 
						|
            ),
 | 
						|
          );
 | 
						|
        }
 | 
						|
 | 
						|
        final label = _labelTextController.text.trim();
 | 
						|
 | 
						|
        try {
 | 
						|
          await widget.room.sendFileEvent(
 | 
						|
            file,
 | 
						|
            thumbnail: thumbnail,
 | 
						|
            shrinkImageMaxDimension: compress ? 1600 : null,
 | 
						|
            extraContent: label.isEmpty ? null : {'body': label},
 | 
						|
          );
 | 
						|
        } on MatrixException catch (e) {
 | 
						|
          final retryAfterMs = e.retryAfterMs;
 | 
						|
          if (e.error != MatrixError.M_LIMIT_EXCEEDED || retryAfterMs == null) {
 | 
						|
            rethrow;
 | 
						|
          }
 | 
						|
          final retryAfterDuration =
 | 
						|
              Duration(milliseconds: retryAfterMs + 1000);
 | 
						|
 | 
						|
          scaffoldMessenger.showSnackBar(
 | 
						|
            SnackBar(
 | 
						|
              content: Text(
 | 
						|
                l10n.serverLimitReached(retryAfterDuration.inSeconds),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          );
 | 
						|
          await Future.delayed(retryAfterDuration);
 | 
						|
 | 
						|
          scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment);
 | 
						|
 | 
						|
          await widget.room.sendFileEvent(
 | 
						|
            file,
 | 
						|
            thumbnail: thumbnail,
 | 
						|
            shrinkImageMaxDimension: compress ? 1600 : null,
 | 
						|
            extraContent: label.isEmpty ? null : {'body': label},
 | 
						|
          );
 | 
						|
        }
 | 
						|
      }
 | 
						|
      scaffoldMessenger.clearSnackBars();
 | 
						|
    } catch (e) {
 | 
						|
      scaffoldMessenger.clearSnackBars();
 | 
						|
      final theme = Theme.of(context);
 | 
						|
      scaffoldMessenger.showSnackBar(
 | 
						|
        SnackBar(
 | 
						|
          backgroundColor: theme.colorScheme.errorContainer,
 | 
						|
          closeIconColor: theme.colorScheme.onErrorContainer,
 | 
						|
          content: Text(
 | 
						|
            e.toLocalizedString(widget.outerContext),
 | 
						|
            style: TextStyle(color: theme.colorScheme.onErrorContainer),
 | 
						|
          ),
 | 
						|
          duration: const Duration(seconds: 30),
 | 
						|
          showCloseIcon: true,
 | 
						|
        ),
 | 
						|
      );
 | 
						|
      rethrow;
 | 
						|
    }
 | 
						|
 | 
						|
    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 uniqueFileType = widget.files
 | 
						|
        .map((file) => file.mimeType ?? lookupMimeType(file.name))
 | 
						|
        .map((mimeType) => mimeType?.split('/').first)
 | 
						|
        .toSet()
 | 
						|
        .singleOrNull;
 | 
						|
 | 
						|
    final fileName = widget.files.length == 1
 | 
						|
        ? widget.files.single.name
 | 
						|
        : L10n.of(context).countFiles(widget.files.length);
 | 
						|
    final fileTypes = widget.files
 | 
						|
        .map((file) => file.name.split('.').last)
 | 
						|
        .toSet()
 | 
						|
        .join(', ')
 | 
						|
        .toUpperCase();
 | 
						|
 | 
						|
    if (uniqueFileType == 'image') {
 | 
						|
      if (widget.files.length == 1) {
 | 
						|
        sendStr = L10n.of(context).sendImage;
 | 
						|
      } else {
 | 
						|
        sendStr = L10n.of(context).sendImages(widget.files.length);
 | 
						|
      }
 | 
						|
    } else if (uniqueFileType == 'audio') {
 | 
						|
      sendStr = L10n.of(context).sendAudio;
 | 
						|
    } else if (uniqueFileType == 'video') {
 | 
						|
      sendStr = L10n.of(context).sendVideo;
 | 
						|
    }
 | 
						|
 | 
						|
    final compressionSupported =
 | 
						|
        uniqueFileType != 'video' || PlatformInfos.isMobile;
 | 
						|
 | 
						|
    return FutureBuilder<String>(
 | 
						|
      future: _calcCombinedFileSize(),
 | 
						|
      builder: (context, snapshot) {
 | 
						|
        final sizeString =
 | 
						|
            snapshot.data ?? L10n.of(context).calculatingFileSize;
 | 
						|
 | 
						|
        return AlertDialog.adaptive(
 | 
						|
          title: Text(sendStr),
 | 
						|
          content: SizedBox(
 | 
						|
            width: 256,
 | 
						|
            child: SingleChildScrollView(
 | 
						|
              child: Column(
 | 
						|
                mainAxisSize: MainAxisSize.min,
 | 
						|
                children: [
 | 
						|
                  const SizedBox(height: 12),
 | 
						|
                  if (uniqueFileType == 'image')
 | 
						|
                    Padding(
 | 
						|
                      padding: const EdgeInsets.only(bottom: 16.0),
 | 
						|
                      child: SizedBox(
 | 
						|
                        height: 256,
 | 
						|
                        child: Center(
 | 
						|
                          child: ListView.builder(
 | 
						|
                            shrinkWrap: true,
 | 
						|
                            itemCount: widget.files.length,
 | 
						|
                            scrollDirection: Axis.horizontal,
 | 
						|
                            itemBuilder: (context, i) => Padding(
 | 
						|
                              padding: const EdgeInsets.only(right: 8.0),
 | 
						|
                              child: Material(
 | 
						|
                                borderRadius: BorderRadius.circular(
 | 
						|
                                  AppConfig.borderRadius / 2,
 | 
						|
                                ),
 | 
						|
                                color: Colors.black,
 | 
						|
                                clipBehavior: Clip.hardEdge,
 | 
						|
                                child: FutureBuilder(
 | 
						|
                                  future: widget.files[i].readAsBytes(),
 | 
						|
                                  builder: (context, snapshot) {
 | 
						|
                                    final bytes = snapshot.data;
 | 
						|
                                    if (bytes == null) {
 | 
						|
                                      return const Center(
 | 
						|
                                        child: CircularProgressIndicator
 | 
						|
                                            .adaptive(),
 | 
						|
                                      );
 | 
						|
                                    }
 | 
						|
                                    if (snapshot.error != null) {
 | 
						|
                                      Logs().w(
 | 
						|
                                        'Unable to preview image',
 | 
						|
                                        snapshot.error,
 | 
						|
                                        snapshot.stackTrace,
 | 
						|
                                      );
 | 
						|
                                      return const Center(
 | 
						|
                                        child: SizedBox(
 | 
						|
                                          width: 256,
 | 
						|
                                          height: 256,
 | 
						|
                                          child: Icon(
 | 
						|
                                            Icons.broken_image_outlined,
 | 
						|
                                            size: 64,
 | 
						|
                                          ),
 | 
						|
                                        ),
 | 
						|
                                      );
 | 
						|
                                    }
 | 
						|
                                    return Image.memory(
 | 
						|
                                      bytes,
 | 
						|
                                      height: 256,
 | 
						|
                                      width: widget.files.length == 1
 | 
						|
                                          ? 256 - 36
 | 
						|
                                          : null,
 | 
						|
                                      fit: BoxFit.contain,
 | 
						|
                                      errorBuilder: (context, e, s) {
 | 
						|
                                        Logs()
 | 
						|
                                            .w('Unable to preview image', e, s);
 | 
						|
                                        return const Center(
 | 
						|
                                          child: SizedBox(
 | 
						|
                                            width: 256,
 | 
						|
                                            height: 256,
 | 
						|
                                            child: Icon(
 | 
						|
                                              Icons.broken_image_outlined,
 | 
						|
                                              size: 64,
 | 
						|
                                            ),
 | 
						|
                                          ),
 | 
						|
                                        );
 | 
						|
                                      },
 | 
						|
                                    );
 | 
						|
                                  },
 | 
						|
                                ),
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  if (uniqueFileType != 'image')
 | 
						|
                    Padding(
 | 
						|
                      padding: const EdgeInsets.only(bottom: 16.0),
 | 
						|
                      child: Row(
 | 
						|
                        children: [
 | 
						|
                          Icon(
 | 
						|
                            uniqueFileType == null
 | 
						|
                                ? Icons.description_outlined
 | 
						|
                                : uniqueFileType == 'video'
 | 
						|
                                    ? Icons.video_file_outlined
 | 
						|
                                    : uniqueFileType == 'audio'
 | 
						|
                                        ? Icons.audio_file_outlined
 | 
						|
                                        : Icons.description_outlined,
 | 
						|
                            size: 32,
 | 
						|
                          ),
 | 
						|
                          const SizedBox(width: 8),
 | 
						|
                          Expanded(
 | 
						|
                            child: Column(
 | 
						|
                              mainAxisSize: MainAxisSize.min,
 | 
						|
                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                              children: [
 | 
						|
                                Text(
 | 
						|
                                  fileName,
 | 
						|
                                  maxLines: 1,
 | 
						|
                                  overflow: TextOverflow.ellipsis,
 | 
						|
                                ),
 | 
						|
                                Text(
 | 
						|
                                  '$sizeString - $fileTypes',
 | 
						|
                                  style: theme.textTheme.labelSmall,
 | 
						|
                                  maxLines: 1,
 | 
						|
                                  overflow: TextOverflow.ellipsis,
 | 
						|
                                ),
 | 
						|
                              ],
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  if (widget.files.length == 1)
 | 
						|
                    Padding(
 | 
						|
                      padding: const EdgeInsets.only(bottom: 8.0),
 | 
						|
                      child: DialogTextField(
 | 
						|
                        controller: _labelTextController,
 | 
						|
                        labelText: L10n.of(context).optionalMessage,
 | 
						|
                        minLines: 1,
 | 
						|
                        maxLines: 3,
 | 
						|
                        maxLength: 255,
 | 
						|
                        counterText: '',
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
 | 
						|
                  if ({'image', 'video'}.contains(uniqueFileType))
 | 
						|
                    Row(
 | 
						|
                      crossAxisAlignment: CrossAxisAlignment.center,
 | 
						|
                      children: [
 | 
						|
                        if ({TargetPlatform.iOS, TargetPlatform.macOS}
 | 
						|
                            .contains(theme.platform))
 | 
						|
                          CupertinoSwitch(
 | 
						|
                            value: compressionSupported && compress,
 | 
						|
                            onChanged: compressionSupported
 | 
						|
                                ? (v) => setState(() => compress = v)
 | 
						|
                                : null,
 | 
						|
                          )
 | 
						|
                        else
 | 
						|
                          Switch.adaptive(
 | 
						|
                            value: compressionSupported && compress,
 | 
						|
                            onChanged: compressionSupported
 | 
						|
                                ? (v) => setState(() => compress = v)
 | 
						|
                                : null,
 | 
						|
                          ),
 | 
						|
                        const SizedBox(width: 16),
 | 
						|
                        Expanded(
 | 
						|
                          child: Column(
 | 
						|
                            mainAxisSize: MainAxisSize.min,
 | 
						|
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                            children: [
 | 
						|
                              Row(
 | 
						|
                                mainAxisSize: MainAxisSize.min,
 | 
						|
                                children: [
 | 
						|
                                  Text(
 | 
						|
                                    L10n.of(context).compress,
 | 
						|
                                    style: theme.textTheme.titleMedium,
 | 
						|
                                    textAlign: TextAlign.left,
 | 
						|
                                  ),
 | 
						|
                                ],
 | 
						|
                              ),
 | 
						|
                              if (!compress)
 | 
						|
                                Text(
 | 
						|
                                  ' ($sizeString)',
 | 
						|
                                  style: theme.textTheme.labelSmall,
 | 
						|
                                ),
 | 
						|
                              if (!compressionSupported)
 | 
						|
                                Text(
 | 
						|
                                  L10n.of(context).notSupportedOnThisDevice,
 | 
						|
                                  style: theme.textTheme.labelSmall,
 | 
						|
                                ),
 | 
						|
                            ],
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          actions: <Widget>[
 | 
						|
            AdaptiveDialogAction(
 | 
						|
              onPressed: () =>
 | 
						|
                  Navigator.of(context, rootNavigator: false).pop(),
 | 
						|
              child: Text(L10n.of(context).cancel),
 | 
						|
            ),
 | 
						|
            AdaptiveDialogAction(
 | 
						|
              onPressed: _send,
 | 
						|
              child: Text(L10n.of(context).send),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        );
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
extension on ScaffoldMessengerState {
 | 
						|
  ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showLoadingSnackBar(
 | 
						|
    String title,
 | 
						|
  ) {
 | 
						|
    clearSnackBars();
 | 
						|
    return showSnackBar(
 | 
						|
      SnackBar(
 | 
						|
        duration: const Duration(minutes: 5),
 | 
						|
        dismissDirection: DismissDirection.none,
 | 
						|
        content: Row(
 | 
						|
          children: [
 | 
						|
            const SizedBox(
 | 
						|
              width: 16,
 | 
						|
              height: 16,
 | 
						|
              child: CircularProgressIndicator.adaptive(
 | 
						|
                strokeWidth: 2,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            const SizedBox(width: 16),
 | 
						|
            Text(title),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |