[LLC, UI-KIT] Add support for custom attachment upload delegate
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -223,19 +223,27 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send a file to this channel
|
/// Send a file to this channel
|
||||||
Future<SendFileResponse> sendFile(MultipartFile file) async {
|
Future<SendFileResponse> sendFile(
|
||||||
|
MultipartFile file, {
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
|
}) async {
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'$_channelURL/file',
|
'$_channelURL/file',
|
||||||
data: FormData.fromMap({'file': file}),
|
data: FormData.fromMap({'file': file}),
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
);
|
);
|
||||||
return _client.decode(response.data, SendFileResponse.fromJson);
|
return _client.decode(response.data, SendFileResponse.fromJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send an image to this channel
|
/// Send an image to this channel
|
||||||
Future<SendImageResponse> sendImage(MultipartFile file) async {
|
Future<SendImageResponse> sendImage(
|
||||||
|
MultipartFile file, {
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
|
}) async {
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'$_channelURL/image',
|
'$_channelURL/image',
|
||||||
data: FormData.fromMap({'file': file}),
|
data: FormData.fromMap({'file': file}),
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
);
|
);
|
||||||
return _client.decode(response.data, SendImageResponse.fromJson);
|
return _client.decode(response.data, SendImageResponse.fromJson);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -794,9 +794,14 @@ class StreamChatClient {
|
|||||||
Future<Response<String>> post(
|
Future<Response<String>> post(
|
||||||
String path, {
|
String path, {
|
||||||
dynamic data,
|
dynamic data,
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final response = await httpClient.post<String>(path, data: data);
|
final response = await httpClient.post<String>(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
} on DioError catch (error) {
|
} on DioError catch (error) {
|
||||||
throw _parseError(error);
|
throw _parseError(error);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ library stream_chat;
|
|||||||
|
|
||||||
export 'package:dio/src/dio_error.dart';
|
export 'package:dio/src/dio_error.dart';
|
||||||
export 'package:dio/src/multipart_file.dart';
|
export 'package:dio/src/multipart_file.dart';
|
||||||
|
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||||
export 'package:logging/logging.dart' show Logger, Level;
|
export 'package:logging/logging.dart' show Logger, Level;
|
||||||
|
|
||||||
export './src/api/channel.dart';
|
export './src/api/channel.dart';
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
import 'extension.dart';
|
||||||
|
|
||||||
|
abstract class AttachmentUploader {
|
||||||
|
Future<String> uploadImage(
|
||||||
|
PlatformFile file, {
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<String> uploadFile(
|
||||||
|
PlatformFile file, {
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamAttachmentUploader implements AttachmentUploader {
|
||||||
|
final Channel _channel;
|
||||||
|
|
||||||
|
const StreamAttachmentUploader(this._channel);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> uploadImage(
|
||||||
|
PlatformFile file, {
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
|
}) async {
|
||||||
|
final filename = file.path?.split('/')?.last;
|
||||||
|
final mimeType = filename.mimeType;
|
||||||
|
final res = await _channel.sendImage(
|
||||||
|
await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: filename,
|
||||||
|
contentType: mimeType,
|
||||||
|
),
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
);
|
||||||
|
return res.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> uploadFile(
|
||||||
|
PlatformFile file, {
|
||||||
|
ProgressCallback onSendProgress,
|
||||||
|
}) async {
|
||||||
|
final filename = file.path?.split('/')?.last;
|
||||||
|
final mimeType = filename.mimeType;
|
||||||
|
final res = await _channel.sendFile(
|
||||||
|
await MultipartFile.fromFile(
|
||||||
|
file.path,
|
||||||
|
filename: filename,
|
||||||
|
contentType: mimeType,
|
||||||
|
),
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
);
|
||||||
|
return res.file;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@ import 'package:video_compress/video_compress.dart';
|
|||||||
class ICompressVideoService {
|
class ICompressVideoService {
|
||||||
static final ICompressVideoService instance = ICompressVideoService._();
|
static final ICompressVideoService instance = ICompressVideoService._();
|
||||||
final _lock = Lock();
|
final _lock = Lock();
|
||||||
|
|
||||||
ICompressVideoService._();
|
ICompressVideoService._();
|
||||||
|
|
||||||
Future<MediaInfo> compressVideo(String path) async {
|
Future<MediaInfo> compress(String path) async {
|
||||||
return _lock.synchronized(() {
|
return _lock.synchronized(() {
|
||||||
return VideoCompress.compressVideo(
|
return VideoCompress.compressVideo(
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:characters/characters.dart';
|
import 'package:characters/characters.dart';
|
||||||
import 'package:emojis/emoji.dart';
|
import 'package:emojis/emoji.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
final _emojis = Emoji.all();
|
final _emojis = Emoji.all();
|
||||||
|
|
||||||
@@ -10,15 +12,27 @@ extension StringExtension on String {
|
|||||||
return '${this[0].toUpperCase()}${substring(1)}';
|
return '${this[0].toUpperCase()}${substring(1)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emojis guidelines
|
/// Returns whether the string contains only emoji's or not.
|
||||||
// 1 to 3 emojis: big size with no text bubble.
|
///
|
||||||
// 4+ emojis or emojis+text: standard size with text bubble.
|
/// Emojis guidelines
|
||||||
|
/// 1 to 3 emojis: big size with no text bubble.
|
||||||
|
/// 4+ emojis or emojis+text: standard size with text bubble.
|
||||||
bool get isOnlyEmoji {
|
bool get isOnlyEmoji {
|
||||||
final characters = trim().characters;
|
final characters = trim().characters;
|
||||||
if (characters.isEmpty) return false;
|
if (characters.isEmpty) return false;
|
||||||
if (characters.length > 3) return false;
|
if (characters.length > 3) return false;
|
||||||
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
|
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the mime type from the passed file name.
|
||||||
|
http_parser.MediaType get mimeType {
|
||||||
|
if (this == null) return null;
|
||||||
|
if (toLowerCase().endsWith('heic')) {
|
||||||
|
return http_parser.MediaType.parse('image/heic');
|
||||||
|
} else {
|
||||||
|
return http_parser.MediaType.parse(lookupMimeType(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List extension
|
/// List extension
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class _FileAttachmentState extends State<FileAttachment> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 3.0),
|
SizedBox(height: 3.0),
|
||||||
Text(
|
Text(
|
||||||
'${getSizeText(widget.attachment.extraData['file_size'])}',
|
'${filesize(widget.attachment.extraData['file_size'])}',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.footnote
|
.footnote
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mime/mime.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:stream_chat_flutter/src/compress_video_service.dart';
|
import 'package:stream_chat_flutter/src/compress_video_service.dart';
|
||||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||||
@@ -24,10 +22,10 @@ import 'package:substring_highlight/substring_highlight.dart';
|
|||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
|
import 'attachment_uploader.dart';
|
||||||
import 'extension.dart';
|
import 'extension.dart';
|
||||||
import 'quoted_message_widget.dart';
|
import 'quoted_message_widget.dart';
|
||||||
|
|
||||||
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
|
|
||||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
_SendingAttachment,
|
_SendingAttachment,
|
||||||
@@ -101,8 +99,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.maxHeight = 150,
|
this.maxHeight = 150,
|
||||||
this.keyboardType = TextInputType.multiline,
|
this.keyboardType = TextInputType.multiline,
|
||||||
this.disableAttachments = false,
|
this.disableAttachments = false,
|
||||||
this.doImageUploadRequest,
|
this.attachmentUploader,
|
||||||
this.doFileUploadRequest,
|
|
||||||
this.initialMessage,
|
this.initialMessage,
|
||||||
this.textEditingController,
|
this.textEditingController,
|
||||||
this.actions,
|
this.actions,
|
||||||
@@ -138,11 +135,8 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// If true the attachments button will not be displayed
|
/// If true the attachments button will not be displayed
|
||||||
final bool disableAttachments;
|
final bool disableAttachments;
|
||||||
|
|
||||||
/// Override image upload request
|
/// A delegate to upload attachments
|
||||||
final FileUploader doImageUploadRequest;
|
final AttachmentUploader attachmentUploader;
|
||||||
|
|
||||||
/// Override file upload request
|
|
||||||
final FileUploader doFileUploadRequest;
|
|
||||||
|
|
||||||
/// The text controller of the TextField
|
/// The text controller of the TextField
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
@@ -184,7 +178,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MessageInputState extends State<MessageInput> {
|
class MessageInputState extends State<MessageInput> {
|
||||||
final List<_SendingAttachment> _attachments = [];
|
final _attachments = <String, _SendingAttachment>{};
|
||||||
final List<User> _mentionedUsers = [];
|
final List<User> _mentionedUsers = [];
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
@@ -210,6 +204,40 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||||
|
|
||||||
|
AttachmentUploader _attachmentUploader;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
_emojiNames = Emoji.all().map((e) => e.name);
|
||||||
|
|
||||||
|
if (!kIsWeb) {
|
||||||
|
_keyboardListener =
|
||||||
|
_keyboardVisibilityController.onChange.listen((visible) {
|
||||||
|
if (_focusNode.hasFocus) {
|
||||||
|
_onChanged(context, textEditingController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
textEditingController =
|
||||||
|
widget.textEditingController ?? TextEditingController();
|
||||||
|
if (widget.editMessage != null || widget.initialMessage != null) {
|
||||||
|
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
textEditingController.addListener(() {
|
||||||
|
_onChanged(context, textEditingController.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
_focusNode.addListener(() {
|
||||||
|
if (_focusNode.hasFocus) {
|
||||||
|
_openFilePickerSection = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child = SafeArea(
|
Widget child = SafeArea(
|
||||||
@@ -364,7 +392,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: AnimatedCrossFade(
|
child: AnimatedCrossFade(
|
||||||
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
|
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
|
||||||
_attachments.every((a) => a.uploaded == true))
|
_attachments.values.every((a) => a.isUploaded == true))
|
||||||
? CrossFadeState.showFirst
|
? CrossFadeState.showFirst
|
||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: _buildSendButton(context),
|
firstChild: _buildSendButton(context),
|
||||||
@@ -757,8 +785,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilePickerSection() {
|
Widget _buildFilePickerSection() {
|
||||||
var _attachmentContainsFile =
|
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||||
_attachments.any((element) => element?.attachment?.type == 'file');
|
return it.attachmentType == 'file';
|
||||||
|
});
|
||||||
|
|
||||||
Color _getIconColor(int index) {
|
Color _getIconColor(int index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
@@ -913,8 +942,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPickerSection() {
|
Widget _buildPickerSection() {
|
||||||
var _attachmentContainsFile =
|
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||||
_attachments.any((element) => element.attachment?.type == 'file');
|
return it.attachmentType == 'file';
|
||||||
|
});
|
||||||
|
|
||||||
switch (_filePickerIndex) {
|
switch (_filePickerIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -949,16 +979,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return MediaListView(
|
return MediaListView(
|
||||||
selectedIds: _attachments.map((e) => e.id).toList(),
|
selectedIds: _attachments.keys.toList(),
|
||||||
onSelect: (media) async {
|
onSelect: (media) async {
|
||||||
if (!_attachments
|
if (!_attachments.containsKey(media.id)) {
|
||||||
.any((element) => element.id == media.id)) {
|
|
||||||
_addAttachment(media);
|
_addAttachment(media);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() => _attachments.remove(media.id));
|
||||||
_attachments
|
|
||||||
.removeWhere((element) => element.id == media.id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1019,14 +1045,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _addAttachment(AssetEntity medium) async {
|
void _addAttachment(AssetEntity medium) async {
|
||||||
final attachment = _SendingAttachment(
|
final attachmentId = medium.id;
|
||||||
id: medium.id,
|
_attachments[attachmentId] = _SendingAttachment(id: attachmentId);
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
setState(() {
|
|
||||||
_attachments.add(attachment);
|
|
||||||
});
|
|
||||||
|
|
||||||
final mediaFile = await medium.originFile.timeout(
|
final mediaFile = await medium.originFile.timeout(
|
||||||
Duration(seconds: 5),
|
Duration(seconds: 5),
|
||||||
onTimeout: () => medium.originFile,
|
onTimeout: () => medium.originFile,
|
||||||
@@ -1040,15 +1061,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
if (file.size > _kMaxAttachmentSize) {
|
if (file.size > _kMaxAttachmentSize) {
|
||||||
if (medium?.type == AssetType.video) {
|
if (medium?.type == AssetType.video) {
|
||||||
final mediaInfo = await compressVideoService.compressVideo(file.path);
|
final mediaInfo = await compressVideoService.compress(file.path);
|
||||||
|
|
||||||
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
||||||
_showErrorAlert(
|
_showErrorAlert(
|
||||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||||
);
|
);
|
||||||
setState(() {
|
_attachments.remove(attachmentId);
|
||||||
_attachments.remove(attachment);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file = PlatformFile(
|
file = PlatformFile(
|
||||||
@@ -1064,54 +1083,59 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final channel = StreamChannel.of(context).channel;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
attachment
|
_attachments.update(attachmentId, (it) {
|
||||||
..file = file
|
return it.copyWith(
|
||||||
..attachment = Attachment(
|
file: file,
|
||||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
attachment: Attachment(
|
||||||
type: medium?.type == AssetType.image ? 'image' : 'video',
|
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||||
|
type: medium?.type == AssetType.image ? 'image' : 'video',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
final url = await _uploadAttachment(
|
|
||||||
file,
|
|
||||||
medium.type == AssetType.image
|
|
||||||
? DefaultAttachmentTypes.image
|
|
||||||
: DefaultAttachmentTypes.video,
|
|
||||||
channel);
|
|
||||||
|
|
||||||
final fileType = medium.type == AssetType.image
|
final fileType = medium.type == AssetType.image
|
||||||
? DefaultAttachmentTypes.image
|
? DefaultAttachmentTypes.image
|
||||||
: DefaultAttachmentTypes.video;
|
: DefaultAttachmentTypes.video;
|
||||||
|
|
||||||
|
final url = await _uploadAttachment(
|
||||||
|
file,
|
||||||
|
fileType,
|
||||||
|
onSendProgress: (sent, total) {
|
||||||
|
setState(() {
|
||||||
|
_attachments.update(
|
||||||
|
attachmentId,
|
||||||
|
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (fileType == DefaultAttachmentTypes.image) {
|
if (fileType == DefaultAttachmentTypes.image) {
|
||||||
attachment.attachment = attachment.attachment.copyWith(
|
_attachments.update(attachmentId, (it) {
|
||||||
imageUrl: url,
|
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||||
);
|
});
|
||||||
} else {
|
} else {
|
||||||
attachment.attachment = attachment.attachment.copyWith(
|
_attachments.update(attachmentId, (it) {
|
||||||
assetUrl: url,
|
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
attachment.uploaded = true;
|
// Marking as upload complete
|
||||||
|
_attachments.update(
|
||||||
|
attachmentId,
|
||||||
|
(it) => it.copyWith(totalUploaded: it.totalSize),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
setState(() {
|
setState(() => _attachments.remove(attachmentId));
|
||||||
_attachments.remove(attachment);
|
|
||||||
});
|
|
||||||
print(e);
|
print(e);
|
||||||
print(s);
|
print(s);
|
||||||
// ignore: deprecated_member_use
|
_showErrorAlert('Error adding the attachment: $e');
|
||||||
Scaffold.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Error adding the attachment: $e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1536,9 +1560,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
if (_attachments.isEmpty) return Offstage();
|
if (_attachments.isEmpty) return Offstage();
|
||||||
|
final fileAttachments = _attachments.values
|
||||||
|
.where((it) => it.attachmentType == 'file')
|
||||||
|
.toList(growable: false);
|
||||||
|
final remainingAttachments = _attachments.values
|
||||||
|
.where((it) => it.attachmentType != 'file')
|
||||||
|
.toList(growable: false);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (_attachments.any((e) => e.attachment?.type == 'file'))
|
if (fileAttachments.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
child: LimitedBox(
|
child: LimitedBox(
|
||||||
@@ -1546,8 +1576,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: _attachments.reversed
|
children: fileAttachments.reversed
|
||||||
.where((e) => e.attachment?.type == 'file')
|
|
||||||
.map<Widget>(
|
.map<Widget>(
|
||||||
(e) => ClipRRect(
|
(e) => ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
@@ -1575,8 +1604,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
.white,
|
.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () =>
|
onTap: () {
|
||||||
setState(() => _attachments.remove(e)),
|
setState(() => _attachments.remove(e.id));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1586,15 +1616,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_attachments.any((e) => e.attachment?.type != 'file'))
|
if (remainingAttachments.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
child: LimitedBox(
|
child: LimitedBox(
|
||||||
maxHeight: 104.0,
|
maxHeight: 104.0,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: _attachments
|
children: remainingAttachments
|
||||||
.where((e) => e.attachment?.type != 'file')
|
|
||||||
.map<Widget>(
|
.map<Widget>(
|
||||||
(attachment) => ClipRRect(
|
(attachment) => ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
@@ -1610,16 +1639,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildRemoveButton(attachment),
|
_buildRemoveButton(attachment),
|
||||||
attachment.uploaded
|
if (!attachment.isUploaded)
|
||||||
? SizedBox()
|
Positioned.fill(
|
||||||
: Positioned.fill(
|
child: Center(
|
||||||
child: Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(16.0),
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: CircularProgressIndicator(),
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1632,6 +1660,38 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildUploadProgressIndicator(int uploaded, int total) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
valueColor: AlwaysStoppedAnimation(Color(0xffb2b2b2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'${filesize(uploaded)} / ${filesize(total)}',
|
||||||
|
style: theme.textTheme.footnote.copyWith(
|
||||||
|
color: theme.colorTheme.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Positioned _buildRemoveButton(_SendingAttachment attachment) {
|
Positioned _buildRemoveButton(_SendingAttachment attachment) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
height: 24,
|
height: 24,
|
||||||
@@ -1648,9 +1708,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
disabledElevation: 0,
|
disabledElevation: 0,
|
||||||
hoverElevation: 0,
|
hoverElevation: 0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() => _attachments.remove(attachment.id));
|
||||||
_attachments.remove(attachment);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
fillColor: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5),
|
fillColor: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -1665,9 +1723,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildAttachment(_SendingAttachment attachment) {
|
Widget _buildAttachment(_SendingAttachment attachment) {
|
||||||
if (widget.attachmentThumbnailBuilders
|
if (widget.attachmentThumbnailBuilders
|
||||||
?.containsKey(attachment.attachment?.type) ==
|
?.containsKey(attachment.attachmentType) ==
|
||||||
true) {
|
true) {
|
||||||
return widget.attachmentThumbnailBuilders[attachment.attachment?.type](
|
return widget.attachmentThumbnailBuilders[attachment.attachmentType](
|
||||||
context,
|
context,
|
||||||
attachment,
|
attachment,
|
||||||
);
|
);
|
||||||
@@ -1677,7 +1735,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return SizedBox();
|
return SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (attachment.attachment?.type) {
|
switch (attachment.attachmentType) {
|
||||||
case 'image':
|
case 'image':
|
||||||
case 'giphy':
|
case 'giphy':
|
||||||
return attachment.file != null
|
return attachment.file != null
|
||||||
@@ -1895,19 +1953,18 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
/// Use this to add custom type attachments
|
/// Use this to add custom type attachments
|
||||||
void addAttachment(Attachment attachment) {
|
void addAttachment(Attachment attachment) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachments.add(_SendingAttachment(
|
final _attachment = _SendingAttachment(
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
uploaded: true,
|
totalUploaded: attachment.extraData['file_size'],
|
||||||
));
|
);
|
||||||
|
_attachments[_attachment.id] = _attachment;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick a file from the device
|
/// Pick a file from the device
|
||||||
/// If [camera] is true then the camera will open
|
/// If [camera] is true then the camera will open
|
||||||
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
||||||
setState(() {
|
setState(() => _inputEnabled = false);
|
||||||
_inputEnabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
PlatformFile file;
|
PlatformFile file;
|
||||||
String attachmentType;
|
String attachmentType;
|
||||||
@@ -1954,15 +2011,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() => _inputEnabled = true);
|
||||||
_inputEnabled = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final mimeType = _getMimeType(file.path.split('/').last);
|
final mimeType = file.path.split('/').last.mimeType;
|
||||||
|
|
||||||
var extraDataMap = <String, dynamic>{};
|
var extraDataMap = <String, dynamic>{};
|
||||||
|
|
||||||
@@ -1982,7 +2035,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
extraDataMap['file_size'] = file.size;
|
extraDataMap['file_size'] = file.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
final channel = StreamChannel.of(context).channel;
|
|
||||||
final attachment = _SendingAttachment(
|
final attachment = _SendingAttachment(
|
||||||
file: file,
|
file: file,
|
||||||
attachment: Attachment(
|
attachment: Attachment(
|
||||||
@@ -1992,14 +2044,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
title: file.name,
|
title: file.name,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final attachmentId = attachment.id;
|
||||||
|
|
||||||
setState(() {
|
setState(() => _attachments[attachmentId] = attachment);
|
||||||
_attachments.add(attachment);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (file.size / 1024 > _kMaxAttachmentSize) {
|
if (file.size / 1024 > _kMaxAttachmentSize) {
|
||||||
if (attachmentType == 'video') {
|
if (attachmentType == 'video') {
|
||||||
final mediaInfo = await compressVideoService.compressVideo(file.path);
|
final mediaInfo = await compressVideoService.compress(file.path);
|
||||||
file = PlatformFile(
|
file = PlatformFile(
|
||||||
name: mediaInfo.title,
|
name: mediaInfo.title,
|
||||||
size: (mediaInfo.filesize / 1024).ceil(),
|
size: (mediaInfo.filesize / 1024).ceil(),
|
||||||
@@ -2007,98 +2058,74 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
path: mediaInfo.path,
|
path: mediaInfo.path,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
attachment.file = file;
|
_attachments.update(attachmentId, (it) => it.copyWith(file: file));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// ignore: deprecated_member_use
|
|
||||||
_showErrorAlert(
|
_showErrorAlert(
|
||||||
'The file is too large to upload. The file size limit is 20MB.',
|
'The file is too large to upload. The file size limit is 20MB.',
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() => _attachments.remove(attachmentId));
|
||||||
_attachments.remove(attachment);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = await _uploadAttachment(file, fileType, channel);
|
try {
|
||||||
|
final url = await _uploadAttachment(
|
||||||
|
file,
|
||||||
|
fileType,
|
||||||
|
onSendProgress: (sent, total) {
|
||||||
|
setState(() {
|
||||||
|
_attachments.update(
|
||||||
|
attachmentId,
|
||||||
|
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (fileType == DefaultAttachmentTypes.image) {
|
if (fileType == DefaultAttachmentTypes.image) {
|
||||||
attachment.attachment = attachment.attachment.copyWith(
|
_attachments.update(attachmentId, (it) {
|
||||||
imageUrl: url,
|
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||||
);
|
});
|
||||||
} else {
|
} else {
|
||||||
attachment.attachment = attachment.attachment.copyWith(
|
_attachments.update(attachmentId, (it) {
|
||||||
assetUrl: url,
|
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
// Marking as upload complete
|
||||||
|
_attachments.update(
|
||||||
|
attachmentId,
|
||||||
|
(it) => it.copyWith(totalUploaded: it.totalSize),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
setState(() => _attachments.remove(attachmentId));
|
||||||
|
print(e);
|
||||||
|
print(s);
|
||||||
|
_showErrorAlert('Error adding the attachment: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
|
||||||
attachment.uploaded = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _uploadAttachment(
|
Future<String> _uploadAttachment(
|
||||||
PlatformFile file,
|
PlatformFile file,
|
||||||
DefaultAttachmentTypes type,
|
DefaultAttachmentTypes type, {
|
||||||
Channel channel,
|
ProgressCallback onSendProgress,
|
||||||
) async {
|
}) {
|
||||||
String url;
|
|
||||||
if (type == DefaultAttachmentTypes.image) {
|
if (type == DefaultAttachmentTypes.image) {
|
||||||
if (widget.doImageUploadRequest != null) {
|
return _attachmentUploader.uploadImage(
|
||||||
url = await widget.doImageUploadRequest(file, channel);
|
file,
|
||||||
} else {
|
onSendProgress: onSendProgress,
|
||||||
url = await _uploadImage(file, channel);
|
);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (widget.doFileUploadRequest != null) {
|
return _attachmentUploader.uploadFile(
|
||||||
url = await widget.doFileUploadRequest(file, channel);
|
file,
|
||||||
} else {
|
onSendProgress: onSendProgress,
|
||||||
url = await _uploadFile(file, channel);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _uploadImage(PlatformFile file, Channel channel) async {
|
|
||||||
final filename = file.path?.split('/')?.last;
|
|
||||||
final mimeType = _getMimeType(filename);
|
|
||||||
final bytes = file.bytes;
|
|
||||||
final res = await channel.sendImage(
|
|
||||||
MultipartFile.fromBytes(
|
|
||||||
bytes,
|
|
||||||
filename: filename,
|
|
||||||
contentType: mimeType,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return res.file;
|
|
||||||
}
|
|
||||||
|
|
||||||
http_parser.MediaType _getMimeType(String filename) {
|
|
||||||
http_parser.MediaType mimeType;
|
|
||||||
if (filename != null) {
|
|
||||||
if (filename.toLowerCase().endsWith('heic')) {
|
|
||||||
mimeType = http_parser.MediaType.parse('image/heic');
|
|
||||||
} else {
|
|
||||||
mimeType = http_parser.MediaType.parse(lookupMimeType(filename));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _uploadFile(PlatformFile file, Channel channel) async {
|
|
||||||
final filename = file.path?.split('/')?.last;
|
|
||||||
final mimeType = _getMimeType(filename);
|
|
||||||
final bytes = file.bytes;
|
|
||||||
final res = await channel.sendFile(
|
|
||||||
MultipartFile.fromBytes(
|
|
||||||
bytes,
|
|
||||||
filename: filename,
|
|
||||||
contentType: mimeType,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return res.file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIdleSendButton(BuildContext context) {
|
Widget _buildIdleSendButton(BuildContext context) {
|
||||||
@@ -2153,7 +2180,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
text = '/${_chosenCommand.name} ' + text;
|
text = '/${_chosenCommand.name} ' + text;
|
||||||
}
|
}
|
||||||
|
|
||||||
final attachments = List<_SendingAttachment>.from(_attachments);
|
final attachments = [..._attachments.values];
|
||||||
|
|
||||||
textEditingController.clear();
|
textEditingController.clear();
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
@@ -2234,39 +2261,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
StreamSubscription _keyboardListener;
|
StreamSubscription _keyboardListener;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusNode = widget.focusNode ?? FocusNode();
|
|
||||||
|
|
||||||
_emojiNames = Emoji.all().map((e) => e.name);
|
|
||||||
|
|
||||||
if (!kIsWeb) {
|
|
||||||
_keyboardListener =
|
|
||||||
_keyboardVisibilityController.onChange.listen((visible) {
|
|
||||||
if (_focusNode.hasFocus) {
|
|
||||||
_onChanged(context, textEditingController.text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
textEditingController =
|
|
||||||
widget.textEditingController ?? TextEditingController();
|
|
||||||
if (widget.editMessage != null || widget.initialMessage != null) {
|
|
||||||
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
textEditingController.addListener(() {
|
|
||||||
_onChanged(context, textEditingController.text);
|
|
||||||
});
|
|
||||||
|
|
||||||
_focusNode.addListener(() {
|
|
||||||
if (_focusNode.hasFocus) {
|
|
||||||
_openFilePickerSection = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorAlert(String description) {
|
void _showErrorAlert(String description) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||||
@@ -2344,10 +2338,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_messageIsPresent = true;
|
_messageIsPresent = true;
|
||||||
|
|
||||||
message.attachments?.forEach((attachment) {
|
message.attachments?.forEach((attachment) {
|
||||||
_attachments.add(_SendingAttachment(
|
final _attachment = _SendingAttachment(
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
uploaded: true,
|
totalUploaded: attachment.extraData['file_size'],
|
||||||
));
|
);
|
||||||
|
_attachments[_attachment.id] = _attachment;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2368,22 +2363,61 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
FocusScope.of(context).requestFocus(_focusNode);
|
FocusScope.of(context).requestFocus(_focusNode);
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
if (_attachmentUploader == null) {
|
||||||
|
_attachmentUploader =
|
||||||
|
widget.attachmentUploader ?? StreamAttachmentUploader(channel);
|
||||||
|
} else if (_attachmentUploader is StreamAttachmentUploader) {
|
||||||
|
_attachmentUploader = StreamAttachmentUploader(channel);
|
||||||
|
}
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SendingAttachment {
|
class _SendingAttachment {
|
||||||
PlatformFile file;
|
|
||||||
Attachment attachment;
|
|
||||||
bool uploaded;
|
|
||||||
String id;
|
|
||||||
|
|
||||||
_SendingAttachment({
|
_SendingAttachment({
|
||||||
|
String id,
|
||||||
this.file,
|
this.file,
|
||||||
this.attachment,
|
this.attachment,
|
||||||
this.uploaded = false,
|
this.totalUploaded = 0,
|
||||||
this.id,
|
int totalSize,
|
||||||
});
|
}) : id = id ?? shortHash(DateTime.now().millisecondsSinceEpoch),
|
||||||
|
attachmentType = attachment?.type,
|
||||||
|
totalSize =
|
||||||
|
totalSize ?? file?.size ?? attachment.extraData['file_size'];
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final PlatformFile file;
|
||||||
|
final Attachment attachment;
|
||||||
|
final String attachmentType;
|
||||||
|
|
||||||
|
final int totalUploaded;
|
||||||
|
final int totalSize;
|
||||||
|
|
||||||
|
// Progress while the attachment is uploading to the server
|
||||||
|
// 0 -> 100
|
||||||
|
double get uploadPercentage {
|
||||||
|
if (totalSize == null) return null;
|
||||||
|
return (totalUploaded / totalSize) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isUploaded => uploadPercentage == 100;
|
||||||
|
|
||||||
|
_SendingAttachment copyWith({
|
||||||
|
String id,
|
||||||
|
PlatformFile file,
|
||||||
|
Attachment attachment,
|
||||||
|
int totalUploaded,
|
||||||
|
int totalSize,
|
||||||
|
}) {
|
||||||
|
return _SendingAttachment(
|
||||||
|
id: id ?? this.id,
|
||||||
|
file: file ?? this.file,
|
||||||
|
attachment: attachment ?? this.attachment,
|
||||||
|
totalUploaded: totalUploaded ?? this.totalUploaded,
|
||||||
|
totalSize: totalSize ?? this.totalSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a 2-tuple, or pair.
|
/// Represents a 2-tuple, or pair.
|
||||||
|
|||||||
@@ -214,18 +214,70 @@ String getWebsiteName(String hostName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// A method returns a human readable string representing a file _size
|
||||||
String getSizeText(int bytes) {
|
String filesize(dynamic size, [int round = 2]) {
|
||||||
if (bytes == null) {
|
if (size == null) return 'Size N/A';
|
||||||
return 'Size N/A';
|
|
||||||
|
/**
|
||||||
|
* [size] can be passed as number or as string
|
||||||
|
*
|
||||||
|
* the optional parameter [round] specifies the number
|
||||||
|
* of digits after comma/point (default is 2)
|
||||||
|
*/
|
||||||
|
final divider = 1024;
|
||||||
|
int _size;
|
||||||
|
try {
|
||||||
|
_size = int.parse(size.toString());
|
||||||
|
} catch (e) {
|
||||||
|
throw ArgumentError('Can not parse the size parameter: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes <= 1000) {
|
if (_size < divider) {
|
||||||
return '$bytes bytes';
|
return '$_size B';
|
||||||
} else if (bytes <= 100000) {
|
}
|
||||||
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
|
||||||
|
if (_size < divider * divider && _size % divider == 0) {
|
||||||
|
return '${(_size / divider).toStringAsFixed(0)} KB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider) {
|
||||||
|
return '${(_size / divider).toStringAsFixed(round)} KB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider && _size % divider == 0) {
|
||||||
|
return '${(_size / (divider * divider)).toStringAsFixed(0)} MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider) {
|
||||||
|
return '${(_size / divider / divider).toStringAsFixed(round)} MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider * divider && _size % divider == 0) {
|
||||||
|
return '${(_size / (divider * divider * divider)).toStringAsFixed(0)} GB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider * divider) {
|
||||||
|
return '${(_size / divider / divider / divider).toStringAsFixed(round)} GB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider * divider * divider &&
|
||||||
|
_size % divider == 0) {
|
||||||
|
num r = _size / divider / divider / divider / divider;
|
||||||
|
return '${r.toStringAsFixed(0)} TB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider * divider * divider) {
|
||||||
|
num r = _size / divider / divider / divider / divider;
|
||||||
|
return '${r.toStringAsFixed(round)} TB';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_size < divider * divider * divider * divider * divider * divider &&
|
||||||
|
_size % divider == 0) {
|
||||||
|
num r = _size / divider / divider / divider / divider / divider;
|
||||||
|
return '${r.toStringAsFixed(0)} PB';
|
||||||
} else {
|
} else {
|
||||||
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
num r = _size / divider / divider / divider / divider / divider;
|
||||||
|
return '${r.toStringAsFixed(round)} PB';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter_core: ^1.0.1-beta
|
stream_chat_flutter_core:
|
||||||
|
path: ../stream_chat_flutter_core
|
||||||
flutter_app_badger: ^1.1.2
|
flutter_app_badger: ^1.1.2
|
||||||
photo_view: ^0.10.3
|
photo_view: ^0.10.3
|
||||||
rxdart: ^0.25.0
|
rxdart: ^0.25.0
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ environment:
|
|||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
stream_chat: ^1.0.2-beta
|
stream_chat:
|
||||||
|
path: ../stream_chat
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
rxdart: ^0.25.0
|
rxdart: ^0.25.0
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ dependencies:
|
|||||||
path: ^1.7.0
|
path: ^1.7.0
|
||||||
path_provider: ^1.6.27
|
path_provider: ^1.6.27
|
||||||
sqlite3_flutter_libs: ^0.3.0
|
sqlite3_flutter_libs: ^0.3.0
|
||||||
stream_chat: ^1.0.2-beta
|
stream_chat:
|
||||||
|
path: ../stream_chat
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
test: ^1.15.7
|
test: ^1.15.7
|
||||||
|
|||||||
Reference in New Issue
Block a user