mirror of
				https://gitlab.com/comunic/comunicmobile
				synced 2025-11-03 19:54:12 +00:00 
			
		
		
		
	Extend the possibilities of file picker
This commit is contained in:
		@@ -70,3 +70,6 @@ class ServerConfigurationHelper {
 | 
				
			|||||||
    return _config;
 | 
					    return _config;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Shortcut for server configuration
 | 
				
			||||||
 | 
					ServerConfig get srvConfig => ServerConfigurationHelper.config;
 | 
				
			||||||
							
								
								
									
										161
									
								
								lib/ui/dialogs/pick_file_dialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								lib/ui/dialogs/pick_file_dialog.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
				
			|||||||
 | 
					import 'package:comunic/models/api_request.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_picker/image_picker.dart';
 | 
				
			||||||
 | 
					import 'package:mime/mime.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Pick file dialog
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// @author Pierre Hubert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum _FileChoices {
 | 
				
			||||||
 | 
					  PICK_IMAGE,
 | 
				
			||||||
 | 
					  TAKE_PICTURE,
 | 
				
			||||||
 | 
					  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"),
 | 
				
			||||||
 | 
					          icon: Icons.image_search,
 | 
				
			||||||
 | 
					          canEnable: (l) => l.any(isImage)),
 | 
				
			||||||
 | 
					      _PickFileOption(
 | 
				
			||||||
 | 
					          value: _FileChoices.TAKE_PICTURE,
 | 
				
			||||||
 | 
					          label: tr("Take a picture"),
 | 
				
			||||||
 | 
					          icon: Icons.camera_alt,
 | 
				
			||||||
 | 
					          canEnable: (l) => l.any(isImage)),
 | 
				
			||||||
 | 
					      _PickFileOption(
 | 
				
			||||||
 | 
					          value: _FileChoices.PICK_OTHER_FILE,
 | 
				
			||||||
 | 
					          label: tr("Browse files"),
 | 
				
			||||||
 | 
					          icon: Icons.folder_open,
 | 
				
			||||||
 | 
					          canEnable: (l) => l.any((el) => !isImage(el))),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<BytesFile> showPickFileDialog({
 | 
				
			||||||
 | 
					  @required BuildContext context,
 | 
				
			||||||
 | 
					  int maxFileSize,
 | 
				
			||||||
 | 
					  List<String> allowedMimeTypes,
 | 
				
			||||||
 | 
					  double imageMaxWidth,
 | 
				
			||||||
 | 
					  double imageMaxHeight,
 | 
				
			||||||
 | 
					}) async {
 | 
				
			||||||
 | 
					  assert(allowedMimeTypes != null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the list of allowed extension
 | 
				
			||||||
 | 
					  final allowedExtensions = List<String>();
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
 | 
					        maxHeight: imageMaxHeight,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (image == null) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      file = BytesFile(image.path.split("/").last, await image.readAsBytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      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 (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(
 | 
				
			||||||
 | 
					        height: 300,
 | 
				
			||||||
 | 
					        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),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,16 +6,15 @@ import 'package:comunic/helpers/server_config_helper.dart';
 | 
				
			|||||||
import 'package:comunic/helpers/users_helper.dart';
 | 
					import 'package:comunic/helpers/users_helper.dart';
 | 
				
			||||||
import 'package:comunic/lists/conversation_messages_list.dart';
 | 
					import 'package:comunic/lists/conversation_messages_list.dart';
 | 
				
			||||||
import 'package:comunic/lists/users_list.dart';
 | 
					import 'package:comunic/lists/users_list.dart';
 | 
				
			||||||
import 'package:comunic/models/api_request.dart';
 | 
					 | 
				
			||||||
import 'package:comunic/models/conversation.dart';
 | 
					import 'package:comunic/models/conversation.dart';
 | 
				
			||||||
import 'package:comunic/models/conversation_message.dart';
 | 
					import 'package:comunic/models/conversation_message.dart';
 | 
				
			||||||
import 'package:comunic/models/new_conversation_message.dart';
 | 
					import 'package:comunic/models/new_conversation_message.dart';
 | 
				
			||||||
 | 
					import 'package:comunic/ui/dialogs/pick_file_dialog.dart';
 | 
				
			||||||
import 'package:comunic/ui/routes/main_route/main_route.dart';
 | 
					import 'package:comunic/ui/routes/main_route/main_route.dart';
 | 
				
			||||||
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
 | 
					import 'package:comunic/ui/tiles/conversation_message_tile.dart';
 | 
				
			||||||
import 'package:comunic/ui/tiles/server_conversation_message_tile.dart';
 | 
					import 'package:comunic/ui/tiles/server_conversation_message_tile.dart';
 | 
				
			||||||
import 'package:comunic/ui/widgets/safe_state.dart';
 | 
					import 'package:comunic/ui/widgets/safe_state.dart';
 | 
				
			||||||
import 'package:comunic/ui/widgets/scroll_watcher.dart';
 | 
					import 'package:comunic/ui/widgets/scroll_watcher.dart';
 | 
				
			||||||
import 'package:comunic/utils/files_utils.dart';
 | 
					 | 
				
			||||||
import 'package:comunic/utils/intl_utils.dart';
 | 
					import 'package:comunic/utils/intl_utils.dart';
 | 
				
			||||||
import 'package:comunic/utils/list_utils.dart';
 | 
					import 'package:comunic/utils/list_utils.dart';
 | 
				
			||||||
import 'package:comunic/utils/log_utils.dart';
 | 
					import 'package:comunic/utils/log_utils.dart';
 | 
				
			||||||
@@ -221,49 +220,42 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
 | 
				
			|||||||
    _setError(ErrorLevel.NONE);
 | 
					    _setError(ErrorLevel.NONE);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Pick and send an image
 | 
					 | 
				
			||||||
  Future<void> _sendImage(BuildContext context) async {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final image = await pickImage(context);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (image == null) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await _sendFileMessage(BytesFile(
 | 
					 | 
				
			||||||
        image.path.split("/").last,
 | 
					 | 
				
			||||||
        await image.readAsBytes(),
 | 
					 | 
				
			||||||
      ));
 | 
					 | 
				
			||||||
    } catch (e, s) {
 | 
					 | 
				
			||||||
      logError(e, s);
 | 
					 | 
				
			||||||
      showSimpleSnack(context, tr("Failed to send image!"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Send a file message
 | 
					  /// Send a file message
 | 
				
			||||||
  Future<void> _sendFileMessage(BytesFile file) async {
 | 
					  Future<void> _sendFileMessage() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final file = await showPickFileDialog(
 | 
				
			||||||
 | 
					        context: context,
 | 
				
			||||||
 | 
					        maxFileSize: srvConfig.conversationsPolicy.filesMaxSize,
 | 
				
			||||||
 | 
					        allowedMimeTypes: srvConfig.conversationsPolicy.allowedFilesType,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (file == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await _submitMessage(
 | 
					      await _submitMessage(
 | 
				
			||||||
      context,
 | 
					 | 
				
			||||||
        NewConversationMessage(
 | 
					        NewConversationMessage(
 | 
				
			||||||
          conversationID: widget.conversationID,
 | 
					          conversationID: widget.conversationID,
 | 
				
			||||||
          message: null,
 | 
					          message: null,
 | 
				
			||||||
          file: file,
 | 
					          file: file,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (e, s) {
 | 
				
			||||||
 | 
					      logError(e, s);
 | 
				
			||||||
 | 
					      showSimpleSnack(context, tr("Failed to send a file!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Send a new text message
 | 
					  /// Send a new text message
 | 
				
			||||||
  Future<void> _submitTextMessage(BuildContext context, String content) async {
 | 
					  Future<void> _submitTextMessage() async {
 | 
				
			||||||
    if (await _submitMessage(
 | 
					    if (await _submitMessage(NewConversationMessage(
 | 
				
			||||||
            context,
 | 
					 | 
				
			||||||
            NewConversationMessage(
 | 
					 | 
				
			||||||
          conversationID: widget.conversationID,
 | 
					          conversationID: widget.conversationID,
 | 
				
			||||||
              message: content,
 | 
					          message: textMessage,
 | 
				
			||||||
        )) ==
 | 
					        )) ==
 | 
				
			||||||
        SendMessageResult.SUCCESS) _clearSendMessageForm();
 | 
					        SendMessageResult.SUCCESS) _clearSendMessageForm();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Submit a new message
 | 
					  /// Submit a new message
 | 
				
			||||||
  Future<SendMessageResult> _submitMessage(
 | 
					  Future<SendMessageResult> _submitMessage(
 | 
				
			||||||
      BuildContext context, NewConversationMessage message) async {
 | 
					      NewConversationMessage message) async {
 | 
				
			||||||
    //Send the message
 | 
					    //Send the message
 | 
				
			||||||
    _setSending(true);
 | 
					    _setSending(true);
 | 
				
			||||||
    final result = await _conversationsHelper.sendMessage(message);
 | 
					    final result = await _conversationsHelper.sendMessage(message);
 | 
				
			||||||
@@ -368,7 +360,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
 | 
				
			|||||||
                    ? Theme.of(context).disabledColor
 | 
					                    ? Theme.of(context).disabledColor
 | 
				
			||||||
                    : Theme.of(context).accentColor,
 | 
					                    : Theme.of(context).accentColor,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onPressed: () => _sendImage(context),
 | 
					              onPressed: () => _sendFileMessage(),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -387,9 +379,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
 | 
				
			|||||||
              enabled: !_isSendingMessage,
 | 
					              enabled: !_isSendingMessage,
 | 
				
			||||||
              controller: _textEditingController,
 | 
					              controller: _textEditingController,
 | 
				
			||||||
              onChanged: (s) => setState(() {}),
 | 
					              onChanged: (s) => setState(() {}),
 | 
				
			||||||
              onSubmitted: _isMessageValid
 | 
					              onSubmitted: _isMessageValid ? (s) => _submitTextMessage() : null,
 | 
				
			||||||
                  ? (s) => _submitTextMessage(context, s)
 | 
					 | 
				
			||||||
                  : null,
 | 
					 | 
				
			||||||
              decoration: new InputDecoration.collapsed(
 | 
					              decoration: new InputDecoration.collapsed(
 | 
				
			||||||
                hintText: tr("Send a message"),
 | 
					                hintText: tr("Send a message"),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@@ -407,8 +397,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
 | 
				
			|||||||
                    : Theme.of(context).disabledColor,
 | 
					                    : Theme.of(context).disabledColor,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onPressed: !_isSendingMessage && _isMessageValid
 | 
					              onPressed: !_isSendingMessage && _isMessageValid
 | 
				
			||||||
                  ? () =>
 | 
					                  ? () => _submitTextMessage()
 | 
				
			||||||
                      _submitTextMessage(context, _textEditingController.text)
 | 
					 | 
				
			||||||
                  : null,
 | 
					                  : null,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,3 +45,6 @@ Future<PickedFile> pickImage(BuildContext context) async {
 | 
				
			|||||||
          ? ImageSource.camera
 | 
					          ? ImageSource.camera
 | 
				
			||||||
          : ImageSource.gallery);
 | 
					          : ImageSource.gallery);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Check if a mime type maps to an image or not
 | 
				
			||||||
 | 
					bool isImage(String mimeType) => mimeType.startsWith("image/");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -191,7 +191,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.1.6"
 | 
					    version: "0.1.6"
 | 
				
			||||||
  file_picker:
 | 
					  file_picker:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: file_picker
 | 
					      name: file_picker
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
@@ -346,7 +346,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.0-nullsafety.3"
 | 
					    version: "1.3.0-nullsafety.3"
 | 
				
			||||||
  mime:
 | 
					  mime:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: mime
 | 
					      name: mime
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -83,6 +83,7 @@ dependencies:
 | 
				
			|||||||
  wakelock: ^0.2.1+1
 | 
					  wakelock: ^0.2.1+1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Pick any kind of file
 | 
					  # Pick any kind of file
 | 
				
			||||||
 | 
					  file_picker: ^2.1.6
 | 
				
			||||||
  file_picker_cross: ^4.2.8
 | 
					  file_picker_cross: ^4.2.8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Get information about current version
 | 
					  # Get information about current version
 | 
				
			||||||
@@ -105,6 +106,9 @@ dependencies:
 | 
				
			|||||||
  chewie_audio: ^1.1.2
 | 
					  chewie_audio: ^1.1.2
 | 
				
			||||||
  chewie: ^0.12.2
 | 
					  chewie: ^0.12.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Determine file mime type
 | 
				
			||||||
 | 
					  mime: ^0.9.7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
    sdk: flutter
 | 
					    sdk: flutter
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user