import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/config.dart'; import 'package:comunic/ui/dialogs/record_audio_dialog.dart'; import 'package:comunic/ui/routes/image_editor_route.dart'; import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; /// Pick file dialog /// /// @author Pierre Hubert enum _FileChoices { PICK_IMAGE, TAKE_PICTURE, PICK_VIDEO, TAKE_VIDEO, RECORD_AUDIO, PICK_OTHER_FILE, } typedef _CanEnable = bool Function(List); typedef _OnOptionSelected = void Function(_FileChoices); class _PickFileOption { final _FileChoices value; final String label; final IconData icon; final _CanEnable canEnable; const _PickFileOption({ @required this.value, @required this.label, @required this.icon, @required this.canEnable, }) : assert(value != null), assert(label != null), assert(icon != null), assert(canEnable != null); } List<_PickFileOption> get _optionsList => [ // Image _PickFileOption( value: _FileChoices.PICK_IMAGE, label: tr("Choose an image"), icon: Icons.image, canEnable: (l) => l.any(isImage)), _PickFileOption( value: _FileChoices.TAKE_PICTURE, label: tr("Take a picture"), icon: Icons.camera_alt, canEnable: (l) => l.any(isImage)), // Video _PickFileOption( value: _FileChoices.PICK_VIDEO, label: tr("Choose a video"), icon: Icons.video_library, canEnable: (l) => l.any(isVideo)), _PickFileOption( value: _FileChoices.TAKE_VIDEO, label: tr("Take a video"), icon: Icons.videocam, canEnable: (l) => l.any(isVideo)), // Audio _PickFileOption( value: _FileChoices.RECORD_AUDIO, label: tr("Record audio"), icon: Icons.mic, canEnable: (l) => l.any(isAudio)), // Other _PickFileOption( value: _FileChoices.PICK_OTHER_FILE, label: tr("Browse files"), icon: Icons.folder_open, canEnable: (l) => l.any((el) => !isImage(el) && !isVideo(el) && !isAudio(el))), ]; Future showPickFileDialog({ @required BuildContext context, int maxFileSize, List allowedMimeTypes, int imageMaxWidth, int imageMaxHeight, CropAspectRatio aspectRatio, }) async { assert(allowedMimeTypes != null); // Get the list of allowed extension final allowedExtensions = []; for (var mime in allowedExtensions) { final ext = extensionFromMime(mime); if (ext != mime) allowedExtensions.add(ext); } // Display bottom sheet final choice = await showModalBottomSheet( context: context, builder: (c) => BottomSheet( onClosing: () {}, builder: (c) => _BottomSheetPickOption( options: _optionsList .where((element) => element.canEnable(allowedMimeTypes)) .toList(), onOptionSelected: (v) => Navigator.pop(c, v), ), )); if (choice == null) return null; BytesFile file; switch (choice) { // Pick an image case _FileChoices.PICK_IMAGE: case _FileChoices.TAKE_PICTURE: final image = await ImagePicker().getImage( source: choice == _FileChoices.PICK_IMAGE ? ImageSource.gallery : ImageSource.camera, maxWidth: imageMaxWidth.toDouble(), maxHeight: imageMaxHeight.toDouble(), ); if (image == null) return null; file = BytesFile(image.path.split("/").last, await image.readAsBytes()); file = await showImageCropper(context, file, aspectRatio: aspectRatio); break; // Pick an video case _FileChoices.PICK_VIDEO: case _FileChoices.TAKE_VIDEO: final image = await ImagePicker().getVideo( source: choice == _FileChoices.PICK_VIDEO ? ImageSource.gallery : ImageSource.camera, ); if (image == null) return null; file = BytesFile(image.path.split("/").last, await image.readAsBytes()); break; // Record audio file case _FileChoices.RECORD_AUDIO: final bytes = await showRecordAudioDialog(context); if (bytes == null) return null; file = BytesFile("record.mp3", bytes); break; // Pick other files case _FileChoices.PICK_OTHER_FILE: final pickedFile = await FilePicker.platform.pickFiles( type: FileType.any, allowedExtensions: allowedExtensions, allowMultiple: false, withData: true, ); if (pickedFile == null || pickedFile.files.length == 0) return null; file = BytesFile(pickedFile.files[0].name, pickedFile.files[0].bytes); break; } if (file == null) return null; // Check file size if (maxFileSize != null && file.bytes.length > maxFileSize) { showSimpleSnack( context, tr("This file could not be sent: it is too big! (Max allowed size: %1%)", args: {"1": filesize(file.bytes.length)})); return null; } return file; } class _BottomSheetPickOption extends StatelessWidget { final List<_PickFileOption> options; final _OnOptionSelected onOptionSelected; const _BottomSheetPickOption( {Key key, @required this.options, @required this.onOptionSelected}) : assert(options != null), assert(onOptionSelected != null), super(key: key); @override Widget build(BuildContext context) => Container( color: config().splashBackgroundColor, height: 255, child: Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 400), child: ListView.builder( itemCount: options.length, itemBuilder: (c, i) => ListTile( leading: Icon(options[i].icon, color: Colors.white), title: Text(options[i].label, style: TextStyle(color: Colors.white)), onTap: () => onOptionSelected(options[i].value), ), ), ), ), ); }