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, }); } 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, required List allowedMimeTypes, int? imageMaxWidth, int? imageMaxHeight, CropAspectRatio? aspectRatio, }) async { // 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().pickImage( 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().pickVideo( 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}) : 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), ), ), ), ), ); }