1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-22 12:59:21 +00:00

Extend the possibilities of file picker

This commit is contained in:
Pierre HUBERT 2021-03-12 18:54:15 +01:00
parent 701d5d3c27
commit 19d4e1d31c
6 changed files with 199 additions and 39 deletions

View File

@ -70,3 +70,6 @@ class ServerConfigurationHelper {
return _config;
}
}
/// Shortcut for server configuration
ServerConfig get srvConfig => ServerConfigurationHelper.config;

View 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),
),
),
);
}

View File

@ -6,16 +6,15 @@ import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/helpers/users_helper.dart';
import 'package:comunic/lists/conversation_messages_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_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/tiles/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/scroll_watcher.dart';
import 'package:comunic/utils/files_utils.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/list_utils.dart';
import 'package:comunic/utils/log_utils.dart';
@ -221,49 +220,42 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
_setError(ErrorLevel.NONE);
}
/// Pick and send an image
Future<void> _sendImage(BuildContext context) async {
/// Send a file message
Future<void> _sendFileMessage() async {
try {
final image = await pickImage(context);
final file = await showPickFileDialog(
context: context,
maxFileSize: srvConfig.conversationsPolicy.filesMaxSize,
allowedMimeTypes: srvConfig.conversationsPolicy.allowedFilesType,
);
if (image == null) return;
if (file == null) return;
await _sendFileMessage(BytesFile(
image.path.split("/").last,
await image.readAsBytes(),
));
await _submitMessage(
NewConversationMessage(
conversationID: widget.conversationID,
message: null,
file: file,
),
);
} catch (e, s) {
logError(e, s);
showSimpleSnack(context, tr("Failed to send image!"));
showSimpleSnack(context, tr("Failed to send a file!"));
}
}
/// Send a file message
Future<void> _sendFileMessage(BytesFile file) async {
await _submitMessage(
context,
NewConversationMessage(
conversationID: widget.conversationID,
message: null,
file: file,
),
);
}
/// Send a new text message
Future<void> _submitTextMessage(BuildContext context, String content) async {
if (await _submitMessage(
context,
NewConversationMessage(
conversationID: widget.conversationID,
message: content,
)) ==
Future<void> _submitTextMessage() async {
if (await _submitMessage(NewConversationMessage(
conversationID: widget.conversationID,
message: textMessage,
)) ==
SendMessageResult.SUCCESS) _clearSendMessageForm();
}
/// Submit a new message
Future<SendMessageResult> _submitMessage(
BuildContext context, NewConversationMessage message) async {
NewConversationMessage message) async {
//Send the message
_setSending(true);
final result = await _conversationsHelper.sendMessage(message);
@ -368,7 +360,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
? Theme.of(context).disabledColor
: Theme.of(context).accentColor,
),
onPressed: () => _sendImage(context),
onPressed: () => _sendFileMessage(),
),
),
@ -387,9 +379,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
enabled: !_isSendingMessage,
controller: _textEditingController,
onChanged: (s) => setState(() {}),
onSubmitted: _isMessageValid
? (s) => _submitTextMessage(context, s)
: null,
onSubmitted: _isMessageValid ? (s) => _submitTextMessage() : null,
decoration: new InputDecoration.collapsed(
hintText: tr("Send a message"),
),
@ -407,8 +397,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
: Theme.of(context).disabledColor,
),
onPressed: !_isSendingMessage && _isMessageValid
? () =>
_submitTextMessage(context, _textEditingController.text)
? () => _submitTextMessage()
: null,
),
),

View File

@ -45,3 +45,6 @@ Future<PickedFile> pickImage(BuildContext context) async {
? ImageSource.camera
: ImageSource.gallery);
}
/// Check if a mime type maps to an image or not
bool isImage(String mimeType) => mimeType.startsWith("image/");

View File

@ -191,7 +191,7 @@ packages:
source: hosted
version: "0.1.6"
file_picker:
dependency: transitive
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
@ -346,7 +346,7 @@ packages:
source: hosted
version: "1.3.0-nullsafety.3"
mime:
dependency: transitive
dependency: "direct main"
description:
name: mime
url: "https://pub.dartlang.org"

View File

@ -83,6 +83,7 @@ dependencies:
wakelock: ^0.2.1+1
# Pick any kind of file
file_picker: ^2.1.6
file_picker_cross: ^4.2.8
# Get information about current version
@ -105,6 +106,9 @@ dependencies:
chewie_audio: ^1.1.2
chewie: ^0.12.2
# Determine file mime type
mime: ^0.9.7
dev_dependencies:
flutter_test:
sdk: flutter