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:
parent
701d5d3c27
commit
19d4e1d31c
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user