2021-03-12 17:54:15 +00:00
|
|
|
import 'package:comunic/models/api_request.dart';
|
2021-03-12 19:52:26 +00:00
|
|
|
import 'package:comunic/ui/dialogs/record_audio_dialog.dart';
|
2021-03-12 17:54:15 +00:00
|
|
|
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';
|
2021-03-13 10:48:33 +00:00
|
|
|
import 'package:flutter/rendering.dart';
|
2021-03-12 17:54:15 +00:00
|
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
import 'package:mime/mime.dart';
|
|
|
|
|
|
|
|
/// Pick file dialog
|
|
|
|
///
|
|
|
|
/// @author Pierre Hubert
|
|
|
|
|
|
|
|
enum _FileChoices {
|
|
|
|
PICK_IMAGE,
|
|
|
|
TAKE_PICTURE,
|
2021-03-12 18:10:10 +00:00
|
|
|
PICK_VIDEO,
|
|
|
|
TAKE_VIDEO,
|
2021-03-12 19:52:26 +00:00
|
|
|
RECORD_AUDIO,
|
2021-03-12 17:54:15 +00:00
|
|
|
PICK_OTHER_FILE,
|
|
|
|
}
|
|
|
|
|
|
|
|
typedef _CanEnable = bool Function(List<String>);
|
|
|
|
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"),
|
2021-03-12 18:10:10 +00:00
|
|
|
icon: Icons.image,
|
2021-03-12 17:54:15 +00:00
|
|
|
canEnable: (l) => l.any(isImage)),
|
|
|
|
_PickFileOption(
|
|
|
|
value: _FileChoices.TAKE_PICTURE,
|
|
|
|
label: tr("Take a picture"),
|
|
|
|
icon: Icons.camera_alt,
|
|
|
|
canEnable: (l) => l.any(isImage)),
|
2021-03-12 18:10:10 +00:00
|
|
|
|
|
|
|
// 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)),
|
|
|
|
|
2021-03-12 19:52:26 +00:00
|
|
|
// Audio
|
|
|
|
_PickFileOption(
|
|
|
|
value: _FileChoices.RECORD_AUDIO,
|
|
|
|
label: tr("Record audio"),
|
|
|
|
icon: Icons.mic,
|
|
|
|
canEnable: (l) => l.any(isAudio)),
|
|
|
|
|
2021-03-12 18:10:10 +00:00
|
|
|
// Other
|
2021-03-12 17:54:15 +00:00
|
|
|
_PickFileOption(
|
|
|
|
value: _FileChoices.PICK_OTHER_FILE,
|
|
|
|
label: tr("Browse files"),
|
|
|
|
icon: Icons.folder_open,
|
2021-03-12 19:52:26 +00:00
|
|
|
canEnable: (l) =>
|
|
|
|
l.any((el) => !isImage(el) && !isVideo(el) && !isAudio(el))),
|
2021-03-12 17:54:15 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
Future<BytesFile> showPickFileDialog({
|
|
|
|
@required BuildContext context,
|
|
|
|
int maxFileSize,
|
|
|
|
List<String> allowedMimeTypes,
|
2021-03-12 18:36:42 +00:00
|
|
|
int imageMaxWidth,
|
|
|
|
int imageMaxHeight,
|
2021-03-12 17:54:15 +00:00
|
|
|
}) async {
|
|
|
|
assert(allowedMimeTypes != null);
|
|
|
|
|
|
|
|
// Get the list of allowed extension
|
2021-03-13 14:14:54 +00:00
|
|
|
final allowedExtensions = <String>[];
|
2021-03-12 17:54:15 +00:00
|
|
|
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,
|
2021-03-12 18:36:42 +00:00
|
|
|
maxWidth: imageMaxWidth.toDouble(),
|
|
|
|
maxHeight: imageMaxHeight.toDouble(),
|
2021-03-12 17:54:15 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (image == null) return null;
|
|
|
|
|
|
|
|
file = BytesFile(image.path.split("/").last, await image.readAsBytes());
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
2021-03-12 18:10:10 +00:00
|
|
|
// 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;
|
|
|
|
|
2021-03-12 19:52:26 +00:00
|
|
|
// Record audio file
|
|
|
|
case _FileChoices.RECORD_AUDIO:
|
|
|
|
final bytes = await showRecordAudioDialog(context);
|
|
|
|
if (bytes == null) return null;
|
|
|
|
file = BytesFile("record.mp3", bytes);
|
|
|
|
break;
|
|
|
|
|
2021-03-12 17:54:15 +00:00
|
|
|
// 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
|
2021-03-13 10:33:25 +00:00
|
|
|
if (maxFileSize != null && file.bytes.length > maxFileSize) {
|
2021-03-12 17:54:15 +00:00
|
|
|
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(
|
2021-03-12 19:52:26 +00:00
|
|
|
height: 255,
|
2021-03-13 10:48:33 +00:00
|
|
|
child: Center(
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(maxWidth: 400),
|
|
|
|
child: ListView.builder(
|
|
|
|
itemCount: options.length,
|
|
|
|
itemBuilder: (c, i) => ListTile(
|
|
|
|
leading: Icon(options[i].icon),
|
|
|
|
title: Text(options[i].label),
|
|
|
|
onTap: () => onOptionSelected(options[i].value),
|
|
|
|
),
|
|
|
|
),
|
2021-03-12 17:54:15 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|