TF-761 Implement reduce image size when add inline image

This commit is contained in:
dab246
2022-07-28 14:36:37 +07:00
committed by Dat H. Pham
parent 8e22bec3f1
commit e050042e2c
12 changed files with 129 additions and 110 deletions
@@ -256,16 +256,14 @@ class ComposerController extends BaseController {
ImageSource.local,
fileInfo: success.fileInfo,
cid: success.cid,
base64: success.imageAsBase64),
maxWithEditor: maxWithEditor);
base64Uri: success.base64Uri));
} else {
richTextMobileTabletController.insertImage(
InlineImage(
ImageSource.local,
fileInfo: success.fileInfo,
cid: success.cid,
base64: success.imageAsBase64),
maxWithEditor: maxWithEditor);
base64Uri: success.base64Uri));
}
maxWithEditor = null;
}
@@ -337,7 +335,8 @@ class ComposerController extends BaseController {
void _initAttachments(ComposerArguments arguments) {
if (arguments.attachments?.isNotEmpty == true) {
initialAttachments = arguments.attachments!;
uploadController.initializeUploadAttachments(arguments.attachments!);
uploadController.initializeUploadAttachments(
arguments.attachments!.listAttachmentsDisplayedOutSide);
}
if (BuildUtils.isWeb) {
expandModeAttachments.value = ExpandMode.EXPAND;
@@ -538,19 +537,22 @@ class ComposerController extends BaseController {
final generatePartId = PartId(_uuid.v1());
final generateBlobId = Id(_uuid.v1());
var emailBodyText = await _getEmailBodyText(context);
final mapContents = await _getMapContent(emailBodyText);
log('ComposerController::_generateEmail(): mapContents: $mapContents');
emailBodyText = mapContents.value1;
final listInlineAttachment = mapContents.value2;
final listInlineEmailBodyPart = listInlineAttachment
.map((attachment) => attachment.toEmailBodyPart(charset: 'base64'))
.toSet();
final attachments = <EmailBodyPart>{};
attachments.addAll(uploadController.generateAttachments() ?? []);
attachments.addAll(listInlineEmailBodyPart);
log('ComposerController::_generateEmail(): listInlineEmailBodyPart: $listInlineEmailBodyPart');
var emailBodyText = await _getEmailBodyText(context);
if (uploadController.mapInlineAttachments.isNotEmpty) {
final mapContents = await _getMapContent(emailBodyText);
log('ComposerController::_generateEmail(): mapContents: $mapContents');
emailBodyText = mapContents.value1;
final listInlineAttachment = mapContents.value2;
final listInlineEmailBodyPart = listInlineAttachment
.map((attachment) => attachment.toEmailBodyPart(charset: 'base64'))
.toSet();
attachments.addAll(listInlineEmailBodyPart);
log('ComposerController::_generateEmail(): listInlineEmailBodyPart: $listInlineEmailBodyPart');
}
final userAgent = await userAgentPlatform;
log('ComposerController::_generateEmail(): userAgent: $userAgent');
@@ -915,7 +917,8 @@ class ComposerController extends BaseController {
void _getEmailContentSuccess(GetEmailContentSuccess success) {
if (success.attachments.isNotEmpty) {
initialAttachments = success.attachments;
uploadController.initializeUploadAttachments(success.attachments);
uploadController.initializeUploadAttachments(
success.attachments.listAttachmentsDisplayedOutSide);
}
emailContentsViewState.value = Right(success);
_emailContents = success.emailContents;
@@ -1133,11 +1136,16 @@ class ComposerController extends BaseController {
}
}
void insertImage(BuildContext context, {double? maxWithEditor}) async {
void insertImage(BuildContext context, double maxWith) async {
if (_responsiveUtils.isMobile(context)) {
maxWithEditor = maxWith - 40;
} else {
maxWithEditor = maxWith - 120;
}
final inlineImage = await _selectFromFile();
if(inlineImage != null) {
this.maxWithEditor = maxWithEditor;
if(kIsWeb) {
log('ComposerController::insertImage(): $inlineImage');
if (inlineImage != null) {
if (BuildUtils.isWeb) {
_insertImageOnWeb(inlineImage);
} else {
_insertImageOnMobileAndTablet(inlineImage);
@@ -1148,15 +1156,17 @@ class ComposerController extends BaseController {
Future<InlineImage?> _selectFromFile() async {
final filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.image,
withReadStream: true);
withData: !BuildUtils.isWeb,
withReadStream: BuildUtils.isWeb);
final platformFile = filePickerResult?.files.single;
if (platformFile != null) {
final fileSelected = FileInfo(
final fileSelected = FileInfo(
platformFile.name,
BuildUtils.isWeb ? '' : platformFile.path ?? '',
platformFile.size,
bytes: platformFile.bytes,
readStream: platformFile.readStream);
return InlineImage(ImageSource.local, fileInfo: fileSelected);
return InlineImage(ImageSource.local, fileInfo: fileSelected);
}
return null;
@@ -1212,7 +1222,8 @@ class ComposerController extends BaseController {
consumeState(_downloadImageAsBase64Interactor.execute(
imageUrl,
uploadState.attachment.cid!,
uploadState.fileInfo));
uploadState.fileInfo,
maxWidth: maxWithEditor));
}
}
@@ -11,21 +11,21 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart';
import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/composer_loading_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/rich_text_style_type.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_file_composer_builder.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart';
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin, RichTextButtonMixin {
class ComposerView extends GetWidget<ComposerController>
with AppLoaderMixin, RichTextButtonMixin, ComposerLoadingMixin {
final responsiveUtils = Get.find<ResponsiveUtils>();
final imagePaths = Get.find<ImagePaths>();
@@ -390,9 +390,7 @@ class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin, Ri
color: AppColor.colorTextButton,
fit: BoxFit.fill),
tooltip: AppLocalizations.of(context).insertImage,
onTap: () => controller.insertImage(
context,
maxWithEditor: constraints.maxWidth - 120))
onTap: () => controller.insertImage(context, constraints.maxWidth))
])
)
);
@@ -417,7 +415,7 @@ class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin, Ri
const Divider(color: AppColor.colorDividerComposer, height: 1),
_buildAttachmentsWidget(context),
_buildToolbarMobileWidget(context),
_buildInlineLoadingView(),
buildInlineLoadingView(controller),
_buildComposerEditor(context),
]),
)
@@ -462,7 +460,7 @@ class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin, Ri
child: Column(children: [
_buildToolbarRichTextWidget(context),
_buildAttachmentsWidget(context),
_buildInlineLoadingView(),
buildInlineLoadingView(controller),
_buildComposerEditor(context),
])
)
@@ -493,21 +491,6 @@ class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin, Ri
});
}
Widget _buildInlineLoadingView() {
return Obx(() => controller.uploadController.uploadInlineViewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is UploadingAttachmentUploadState || success is DownloadingImageAsBase64) {
return Padding(
padding: const EdgeInsets.all(5),
child: loadingWidgetWithSizeColor(
size: 30,
color: AppColor.primaryColor));
}
return const SizedBox.shrink();
}));
}
Widget _buildToolbarRichTextWidget(BuildContext context) {
return Obx(() {
final richTextMobileTabletController = controller.richTextMobileTabletController;
@@ -788,7 +771,6 @@ class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin, Ri
],
),
);
},
);
});
}
}
@@ -11,9 +11,9 @@ import 'package:model/model.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu_overlay_widget.dart';
import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart';
import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/composer_loading_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu_font_status.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart';
@@ -25,14 +25,13 @@ import 'package:tmail_ui_user/features/composer/presentation/model/screen_displa
import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_file_composer_builder.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart';
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class ComposerView extends GetWidget<ComposerController>
with AppLoaderMixin, RichTextButtonMixin {
with AppLoaderMixin, RichTextButtonMixin, ComposerLoadingMixin {
final responsiveUtils = Get.find<ResponsiveUtils>();
final imagePaths = Get.find<ImagePaths>();
@@ -74,7 +73,7 @@ class ComposerView extends GetWidget<ComposerController>
children: [
_buildAttachmentsWidget(context),
_buildToolbarRichTextWidget(context),
_buildInlineLoadingView(),
buildInlineLoadingView(controller),
_buildEditorForm(context)
]
)),
@@ -308,7 +307,7 @@ class ComposerView extends GetWidget<ComposerController>
children: [
_buildAttachmentsWidget(context),
_buildToolbarRichTextWidget(context),
_buildInlineLoadingView(),
buildInlineLoadingView(controller),
_buildEditorForm(context)
]
))),
@@ -525,9 +524,7 @@ class ComposerView extends GetWidget<ComposerController>
color: AppColor.colorTextButton.withOpacity(opacity),
fit: BoxFit.fill),
tooltip: AppLocalizations.of(context).insertImage,
onTap: () => controller.insertImage(
context,
maxWithEditor: constraints.maxWidth - 120)),
onTap: () => controller.insertImage(context, constraints.maxWidth)),
);
}),
const SizedBox(width: 4),
@@ -955,20 +952,4 @@ class ComposerView extends GetWidget<ComposerController>
);
});
}
Widget _buildInlineLoadingView() {
return Obx(() => controller.uploadController.uploadInlineViewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is UploadingAttachmentUploadState ||
success is DownloadingImageAsBase64) {
return Padding(
padding: const EdgeInsets.all(5),
child: loadingWidgetWithSizeColor(
size: 30,
color: AppColor.primaryColor));
}
return const SizedBox.shrink();
}));
}
}
@@ -19,7 +19,7 @@ class RichTextMobileTabletController extends BaseRichTextController {
final selectedTextColor = Colors.black.obs;
final selectedTextBackgroundColor = Colors.white.obs;
final selectedFontName = SafeFont.sansSerif.obs;
final menuHeaderStyleStatus = DropdownMenuFontStatus.closed.obs;
bool get isMenuHeaderStyleOpen => menuHeaderStyleStatus.value == DropdownMenuFontStatus.open;
@@ -106,7 +106,7 @@ class RichTextMobileTabletController extends BaseRichTextController {
Future<Tuple2<String, List<Attachment>>> refactorContentHasInlineImage(
String emailContent,
Map<String, Attachment> mapInlineAttachments
) async {
) async {
final document = parse(emailContent);
final listImgTag = document.querySelectorAll('img[src^="data:image/"]');
final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async {
@@ -145,9 +145,7 @@ class RichTextMobileTabletController extends BaseRichTextController {
if (image.source == ImageSource.network) {
htmlEditorApi?.insertImageLink(image.link!);
} else {
final htmlContent = await image.generateImgTagHtml(maxWithEditor: maxWithEditor);
log('RichTextMobileTabletController::insertImage(): $htmlContent');
htmlEditorApi?.insertHtml(htmlContent);
htmlEditorApi?.insertHtml(image.base64Uri ?? '');
}
}
@@ -156,4 +154,4 @@ class RichTextMobileTabletController extends BaseRichTextController {
htmlEditorApi?.formatHeader(styleSelected.styleValue);
}
}
}
@@ -155,14 +155,12 @@ class RichTextWebController extends BaseRichTextController {
bool isTextStyleTypeSelected(RichTextStyleType richTextStyleType) =>
listTextStyleApply.contains(richTextStyleType);
void insertImage(InlineImage image, {double? maxWithEditor}) async {
log('RichTextWebController::insertImage(): $image | maxWithEditor: $maxWithEditor');
void insertImage(InlineImage image) async {
log('RichTextWebController::insertImage(): $image');
if (image.source == ImageSource.network) {
editorController.insertNetworkImage(image.link!);
} else {
final htmlContent = await image.generateImgTagHtml(maxWithEditor: maxWithEditor);
log('RichTextWebController::insertImage(): $htmlContent');
editorController.insertHtml(htmlContent);
editorController.insertHtml(image.base64Uri ?? '');
}
}
@@ -0,0 +1,45 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
mixin ComposerLoadingMixin {
Widget _loadingWidgetWithSizeColor({double? size, Color? color}) {
return Center(child: Container(
margin: const EdgeInsets.all(10),
width: size ?? 24,
height: size ?? 24,
child: CircularProgressIndicator(color: color ?? AppColor.primaryColor)));
}
Widget buildInlineLoadingView(ComposerController controller) {
return Obx(() => controller.uploadController.uploadInlineViewState.value.fold(
(failure) {
return controller.viewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is DownloadingImageAsBase64) {
return _loadingWidgetWithSizeColor();
}
return const SizedBox.shrink();
});
},
(success) {
if (success is UploadingAttachmentUploadState) {
return _loadingWidgetWithSizeColor();
}
return controller.viewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is DownloadingImageAsBase64) {
return _loadingWidgetWithSizeColor();
}
return const SizedBox.shrink();
});
}));
}
}
@@ -1,5 +1,4 @@
import 'package:core/presentation/extensions/html_extension.dart';
import 'package:equatable/equatable.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart';
@@ -9,7 +8,7 @@ class InlineImage with EquatableMixin {
final String? link;
final FileInfo? fileInfo;
final String? cid;
final String? base64;
final String? base64Uri;
InlineImage(
this.source,
@@ -17,21 +16,10 @@ class InlineImage with EquatableMixin {
this.link,
this.fileInfo,
this.cid,
this.base64,
this.base64Uri,
}
);
Future<String> generateImgTagHtml({double? maxWithEditor}) async {
if (source == ImageSource.local && base64 != null) {
return base64!.generateImageBase64(
cid!,
fileInfo!.fileExtension,
fileInfo!.fileName,
maxWithEditor: maxWithEditor);
}
return '';
}
@override
List<Object?> get props => [source, link, fileInfo, cid];
List<Object?> get props => [source, link, fileInfo, cid, base64Uri];
}
@@ -71,7 +71,7 @@ class FileUploader {
final resultJson = await argsUpload.dioClient.post(
Uri.decodeFull(uploadUri.toString()),
options: Options(headers: headerParam),
data: fileInfo.readStream,
data: fileInfo.readStream ?? File(fileInfo.filePath).openRead(),
cancelToken: cancelToken,
onSendProgress: (progress, total) {
log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $progress');
@@ -112,11 +112,7 @@ class UploadController extends GetxController {
);
_refreshListUploadAttachmentState();
if (success.attachment.disposition == ContentDisposition.attachment) {
_handleUploadAttachmentsSuccess(success);
} else {
_handleUploadInlineAttachmentsSuccess(success);
}
_handleUploadAttachmentsSuccess(success);
}
}
);
@@ -261,7 +257,9 @@ class UploadController extends GetxController {
bool hasEnoughMaxAttachmentSize({List<FileInfo>? listFiles}) {
final currentTotalAttachmentsSize = attachmentsUploaded.totalSize();
final totalInlineAttachmentsSize = inlineAttachmentsUploaded.totalSize();
log('ComposerController::_validateAttachmentsSize(): $currentTotalAttachmentsSize');
log('ComposerController::_validateAttachmentsSize(): totalInlineAttachmentsSize: $totalInlineAttachmentsSize');
num uploadedTotalSize = 0;
if (listFiles != null && listFiles.isNotEmpty) {
final uploadedListSize = listFiles.map((file) => file.fileSize).toList();
@@ -269,7 +267,9 @@ class UploadController extends GetxController {
log('ComposerController::_validateAttachmentsSize(): uploadedTotalSize: $uploadedTotalSize');
}
final totalSizeReadyToUpload = currentTotalAttachmentsSize + uploadedTotalSize;
final totalSizeReadyToUpload = currentTotalAttachmentsSize +
totalInlineAttachmentsSize +
uploadedTotalSize;
log('ComposerController::_validateAttachmentsSize(): totalSizeReadyToUpload: $totalSizeReadyToUpload');
final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value;
@@ -318,6 +318,18 @@ class UploadController extends GetxController {
uploadInlineViewState.value = Right(UIState.idle);
}
List<Attachment> get inlineAttachmentsUploaded {
if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) {
return List.empty();
}
return _uploadingStateInlineFiles.uploadingStateFiles
.whereNotNull()
.map((fileState) => fileState.attachment)
.whereNotNull()
.where((attachment) => attachment.cid != null)
.toList();
}
Map<String, Attachment> get mapInlineAttachments {
final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles
.whereNotNull()
@@ -58,5 +58,6 @@ class CoreBindings extends Bindings {
void _bindingUtils() {
Get.put(const Uuid());
Get.put(CompressFileUtils());
}
}
@@ -69,7 +69,7 @@ class NetworkBindings extends Bindings {
void _bindingApi() {
Get.put(JmapHttpClient.HttpClient(Get.find<Dio>()));
Get.put(DownloadClient(Get.find<DioClient>()));
Get.put(DownloadClient(Get.find<DioClient>(), Get.find<CompressFileUtils>()));
Get.put(DownloadManager(Get.find<DownloadClient>()));
Get.put(MailboxAPI(Get.find<JmapHttpClient.HttpClient>()));
Get.put(SessionAPI(Get.find<JmapHttpClient.HttpClient>()));
+5 -2
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:mime/mime.dart';
@@ -8,8 +10,9 @@ class FileInfo with EquatableMixin {
final String filePath;
final int fileSize;
final Stream<List<int>>? readStream;
final Uint8List? bytes;
FileInfo(this.fileName, this.filePath, this.fileSize, {this.readStream});
FileInfo(this.fileName, this.filePath, this.fileSize, {this.readStream, this.bytes});
factory FileInfo.empty() {
return FileInfo('', '', 0);
@@ -20,5 +23,5 @@ class FileInfo with EquatableMixin {
String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/json; charset=UTF-8';
@override
List<Object?> get props => [fileName, filePath, fileSize, readStream];
List<Object?> get props => [fileName, filePath, fileSize, readStream, bytes];
}