TF-296 Fix upload file on browser
This commit is contained in:
@@ -1 +1 @@
|
||||
SERVER_URL=http://localhost
|
||||
SERVER_URL=https://dev.open-paas.org
|
||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
||||
import 'package:core/core.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:model/model.dart';
|
||||
import 'package:tmail_ui_user/main/utils/app_logger.dart';
|
||||
|
||||
class ComposerAPI {
|
||||
|
||||
@@ -13,15 +14,16 @@ class ComposerAPI {
|
||||
ComposerAPI(this._dioClient);
|
||||
|
||||
Future<UploadResponse> uploadAttachment(UploadRequest uploadRequest) async {
|
||||
final file = File(uploadRequest.fileInfo.filePath);
|
||||
log('uploadAttachment: fileInfo: ${uploadRequest.fileInfo.props}');
|
||||
final headerParam = _dioClient.getHeaders();
|
||||
headerParam[HttpHeaders.contentTypeHeader] = uploadRequest.fileInfo.mimeType;
|
||||
headerParam[HttpHeaders.contentLengthHeader] = file.readAsBytesSync().length;
|
||||
headerParam[HttpHeaders.contentLengthHeader] = uploadRequest.fileInfo.fileSize;
|
||||
|
||||
final resultJson = await _dioClient.post(
|
||||
Uri.decodeFull(uploadRequest.uploadUrl.toString()),
|
||||
options: Options(headers: headerParam),
|
||||
data: file.openRead());
|
||||
data: uploadRequest.fileInfo.readStream);
|
||||
|
||||
return UploadResponse.fromJson(jsonDecode(resultJson));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tmail_ui_user/features/base/base_bindings.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource/autocomplete_datasource.dart';
|
||||
@@ -121,7 +120,6 @@ class ComposerBindings extends BaseBindings {
|
||||
|
||||
@override
|
||||
void bindingsController() {
|
||||
Get.lazyPut(() => TextEditingController());
|
||||
Get.lazyPut(() => ComposerController(
|
||||
Get.find<SendEmailInteractor>(),
|
||||
Get.find<SaveEmailAddressesInteractor>(),
|
||||
@@ -131,7 +129,6 @@ class ComposerBindings extends BaseBindings {
|
||||
Get.find<ImagePaths>(),
|
||||
Get.find<Uuid>(),
|
||||
Get.find<DeviceInfoPlugin>(),
|
||||
Get.find<TextEditingController>(),
|
||||
Get.find<LocalFilePickerInteractor>(),
|
||||
Get.find<UploadMultipleAttachmentInteractor>(),
|
||||
Get.find<SaveEmailAsDraftsInteractor>(),
|
||||
|
||||
@@ -7,8 +7,8 @@ import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:enough_html_editor/enough_html_editor.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fk_user_agent/fk_user_agent.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:html_editor_enhanced/html_editor.dart' as HtmlEditorBrowser;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
@@ -64,7 +64,6 @@ class ComposerController extends BaseController {
|
||||
final AppToast _appToast;
|
||||
final ImagePaths _imagePaths;
|
||||
final Uuid _uuid;
|
||||
final TextEditingController subjectEmailInputController;
|
||||
final LocalFilePickerInteractor _localFilePickerInteractor;
|
||||
final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor;
|
||||
final DeviceInfoPlugin _deviceInfoPlugin;
|
||||
@@ -80,6 +79,7 @@ class ComposerController extends BaseController {
|
||||
HtmlEditorApi? htmlEditorApi;
|
||||
final HtmlEditorBrowser.HtmlEditorController htmlControllerBrowser = HtmlEditorBrowser.HtmlEditorController();
|
||||
|
||||
final subjectEmailInputController = TextEditingController();
|
||||
final toEmailAddressController = TextEditingController();
|
||||
final ccEmailAddressController = TextEditingController();
|
||||
final bccEmailAddressController = TextEditingController();
|
||||
@@ -109,7 +109,6 @@ class ComposerController extends BaseController {
|
||||
this._imagePaths,
|
||||
this._uuid,
|
||||
this._deviceInfoPlugin,
|
||||
this.subjectEmailInputController,
|
||||
this._localFilePickerInteractor,
|
||||
this._uploadMultipleAttachmentInteractor,
|
||||
this._saveEmailAsDraftsInteractor,
|
||||
@@ -136,12 +135,9 @@ class ComposerController extends BaseController {
|
||||
@override
|
||||
void onClose() {
|
||||
subjectEmailInputController.dispose();
|
||||
|
||||
toEmailAddressController.dispose();
|
||||
ccEmailAddressController.dispose();
|
||||
bccEmailAddressController.dispose();
|
||||
|
||||
htmlControllerBrowser.clearFocus();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@@ -359,6 +355,8 @@ class ComposerController extends BaseController {
|
||||
}
|
||||
|
||||
void sendEmailAction(BuildContext context) async {
|
||||
clearFocusEditor(context);
|
||||
|
||||
if (isEnableEmailSendButton.value) {
|
||||
final arguments = composerArguments.value;
|
||||
if (arguments != null) {
|
||||
@@ -416,13 +414,25 @@ class ComposerController extends BaseController {
|
||||
}
|
||||
|
||||
void openPickAttachmentMenu(BuildContext context, List<Widget> actionTiles) {
|
||||
(ContextMenuBuilder(context)
|
||||
..addHeader(
|
||||
(ContextMenuHeaderBuilder(Key('attachment_picker_context_menu_header_builder'))
|
||||
clearFocusEditor(context);
|
||||
|
||||
(ContextMenuBuilder(context)
|
||||
..addHeader((ContextMenuHeaderBuilder(Key('attachment_picker_context_menu_header_builder'))
|
||||
..addLabel(AppLocalizations.of(context).pick_attachments))
|
||||
.build())
|
||||
..addTiles(actionTiles))
|
||||
.build();
|
||||
..addTiles(actionTiles)
|
||||
..addOnCloseContextMenuAction(() => popBack()))
|
||||
.build();
|
||||
}
|
||||
|
||||
void openPickAttachmentsForWeb(BuildContext context, RelativeRect? position, List<PopupMenuEntry> popupMenuItems) async {
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position ?? RelativeRect.fromLTRB(16, 40, 16, 16),
|
||||
color: Colors.white,
|
||||
elevation: 5,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
items: popupMenuItems);
|
||||
}
|
||||
|
||||
void openFilePickerByType(BuildContext context, FileType fileType) async {
|
||||
@@ -525,7 +535,9 @@ class ComposerController extends BaseController {
|
||||
return false;
|
||||
}
|
||||
|
||||
void saveEmailAsDrafts() async {
|
||||
void saveEmailAsDrafts(BuildContext context) async {
|
||||
clearFocusEditor(context);
|
||||
|
||||
final arguments = composerArguments.value;
|
||||
if (arguments != null && Get.context != null) {
|
||||
final isChanged = await _isEmailChanged(arguments);
|
||||
@@ -583,7 +595,14 @@ class ComposerController extends BaseController {
|
||||
return '';
|
||||
}
|
||||
|
||||
void backToEmailViewAction() {
|
||||
void clearFocusEditor(BuildContext context) {
|
||||
if (!kIsWeb) {
|
||||
htmlEditorApi?.unfocus(context);
|
||||
}
|
||||
}
|
||||
|
||||
void backToEmailViewAction(BuildContext context) {
|
||||
clearFocusEditor(context);
|
||||
popBack();
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,12 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
controller.htmlEditorApi?.unfocus(context);
|
||||
controller.saveEmailAsDrafts();
|
||||
controller.saveEmailAsDrafts(context);
|
||||
return true;
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
controller.htmlEditorApi?.unfocus(context);
|
||||
controller.clearFocusEditor(context);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.primaryLightColor,
|
||||
@@ -60,19 +59,20 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
Widget _buildTopBar(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Obx(() => (TopBarComposerWidgetBuilder(imagePaths, controller.isEnableEmailSendButton.value)
|
||||
child: Obx(() => (TopBarComposerWidgetBuilder(context, imagePaths, controller.isEnableEmailSendButton.value)
|
||||
..addSendEmailActionClick(() {
|
||||
controller.htmlEditorApi?.unfocus(context);
|
||||
controller.sendEmailAction(context);
|
||||
})
|
||||
..addAttachFileActionClick(() {
|
||||
controller.htmlEditorApi?.unfocus(context);
|
||||
controller.openPickAttachmentMenu(context, _pickAttachmentsActionTiles(context));
|
||||
..addAttachFileActionClick((position) {
|
||||
if (kIsWeb) {
|
||||
controller.openPickAttachmentsForWeb(context,position, _pickAttachmentsActionTilesForWeb(context));
|
||||
} else {
|
||||
controller.openPickAttachmentMenu(context, _pickAttachmentsActionTiles(context));
|
||||
}
|
||||
})
|
||||
..addBackActionClick(() {
|
||||
controller.htmlEditorApi?.unfocus(context);
|
||||
controller.saveEmailAsDrafts();
|
||||
controller.backToEmailViewAction();
|
||||
controller.saveEmailAsDrafts(context);
|
||||
controller.backToEmailViewAction(context);
|
||||
}))
|
||||
.build()),
|
||||
);
|
||||
@@ -82,7 +82,15 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
return [
|
||||
_pickPhotoAndVideoAction(context),
|
||||
_browseFileAction(context),
|
||||
SizedBox(height: 30),
|
||||
SizedBox(height: kIsWeb ? 16 : 30),
|
||||
];
|
||||
}
|
||||
|
||||
List<PopupMenuEntry> _pickAttachmentsActionTilesForWeb(BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem(padding: EdgeInsets.symmetric(horizontal: 8), child: _pickPhotoAndVideoAction(context)),
|
||||
PopupMenuDivider(height: 0.5),
|
||||
PopupMenuItem(padding: EdgeInsets.symmetric(horizontal: 8), child: _browseFileAction(context)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -262,10 +270,11 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
alignment: Alignment.topCenter,
|
||||
child: Column(
|
||||
children: [
|
||||
if (kIsWeb) _buildAttachmentsLoadingView(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 24),
|
||||
child: _buildComposerEditor(context)),
|
||||
_buildAttachmentsLoadingView(),
|
||||
if (!kIsWeb) _buildAttachmentsLoadingView(),
|
||||
_buildAttachments(context),
|
||||
],
|
||||
),
|
||||
@@ -290,7 +299,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
htmlToolbarOptions: HtmlEditorBrowser.HtmlToolbarOptions(
|
||||
toolbarPosition: HtmlEditorBrowser.ToolbarPosition.custom
|
||||
),
|
||||
otherOptions: HtmlEditorBrowser.OtherOptions(height: 550),
|
||||
otherOptions: HtmlEditorBrowser.OtherOptions(height: 400),
|
||||
);
|
||||
} else {
|
||||
if (controller.composerArguments.value?.emailActionType == EmailActionType.compose) {
|
||||
@@ -317,7 +326,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
(failure) => SizedBox.shrink(),
|
||||
(success) => success is UploadingAttachmentState
|
||||
? Center(child: Padding(
|
||||
padding: EdgeInsets.only(bottom: controller.attachments.isNotEmpty ? 0 : 24),
|
||||
padding: EdgeInsets.only(bottom: controller.attachments.isNotEmpty ? 0 : 24, top: kIsWeb ? 20 : 0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
typedef OnBackActionClick = void Function();
|
||||
typedef OnSendEmailActionClick = void Function();
|
||||
typedef OnAttachFileActionClick = void Function();
|
||||
typedef OnAttachFileActionClick = void Function(RelativeRect? position);
|
||||
|
||||
class TopBarComposerWidgetBuilder {
|
||||
OnBackActionClick? _onBackActionClick;
|
||||
OnSendEmailActionClick? _onSendEmailActionClick;
|
||||
OnAttachFileActionClick? _onAttachFileActionClick;
|
||||
|
||||
final BuildContext _context;
|
||||
final ImagePaths _imagePaths;
|
||||
final bool _isEnableEmailSendButton;
|
||||
|
||||
TopBarComposerWidgetBuilder(this._imagePaths, this._isEnableEmailSendButton);
|
||||
TopBarComposerWidgetBuilder(
|
||||
this._context,
|
||||
this._imagePaths,
|
||||
this._isEnableEmailSendButton,
|
||||
);
|
||||
|
||||
void addBackActionClick(OnBackActionClick onBackActionClick) {
|
||||
_onBackActionClick = onBackActionClick;
|
||||
@@ -68,14 +74,32 @@ class TopBarComposerWidgetBuilder {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
key: Key('button_attachment'),
|
||||
icon: SvgPicture.asset(_imagePaths.icShare, fit: BoxFit.fill),
|
||||
onPressed: () {
|
||||
if (_onAttachFileActionClick != null) {
|
||||
_onAttachFileActionClick!();
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (!kIsWeb) {
|
||||
_onAttachFileActionClick?.call(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: kIsWeb ? 16 : 0),
|
||||
child: SvgPicture.asset(_imagePaths.icShare, fit: BoxFit.fill),
|
||||
),
|
||||
onTapDown: (detail) {
|
||||
if (kIsWeb) {
|
||||
final screenSize = MediaQuery.of(_context).size;
|
||||
final offset = detail.globalPosition;
|
||||
final position = RelativeRect.fromLTRB(
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
screenSize.width - offset.dx,
|
||||
screenSize.height - offset.dy,
|
||||
);
|
||||
_onAttachFileActionClick?.call(position);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: Key('button_send_email'),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:model/model.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart';
|
||||
|
||||
@@ -11,13 +12,14 @@ class LocalFilePickerInteractor {
|
||||
|
||||
Stream<Either<Failure, Success>> execute({FileType fileType = FileType.any}) async* {
|
||||
try {
|
||||
final filesResult = await FilePicker.platform.pickFiles(type: fileType, allowMultiple: true);
|
||||
final filesResult = await FilePicker.platform.pickFiles(type: fileType, allowMultiple: true, withReadStream: true);
|
||||
if (filesResult != null && filesResult.files.isNotEmpty) {
|
||||
final fileInfoResults = filesResult.files
|
||||
.map((platformFile) => FileInfo(
|
||||
platformFile.name,
|
||||
platformFile.path ?? '',
|
||||
platformFile.size)).toList();
|
||||
kIsWeb ? '' : platformFile.path ?? '',
|
||||
platformFile.size,
|
||||
readStream: platformFile.readStream)).toList();
|
||||
yield Right<Failure, Success>(LocalFilePickerSuccess(fileInfoResults));
|
||||
} else {
|
||||
yield Left<Failure, Success>(LocalFilePickerCancel());
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class FileInfo with EquatableMixin {
|
||||
final String fileName;
|
||||
final String filePath;
|
||||
final int fileSize;
|
||||
final Stream<List<int>>? readStream;
|
||||
|
||||
FileInfo(this.fileName, this.filePath, this.fileSize);
|
||||
FileInfo(this.fileName, this.filePath, this.fileSize, {this.readStream});
|
||||
|
||||
factory FileInfo.empty() {
|
||||
return FileInfo('', '', 0);
|
||||
}
|
||||
|
||||
String get mimeType => lookupMimeType(filePath) ?? 'application/json; charset=UTF-8';
|
||||
String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/json; charset=UTF-8';
|
||||
|
||||
@override
|
||||
List<Object> get props => [fileName, filePath, fileSize];
|
||||
List<Object?> get props => [fileName, filePath, fileSize, readStream];
|
||||
}
|
||||
@@ -130,6 +130,9 @@ dependencies:
|
||||
# fk_user_agent
|
||||
fk_user_agent: 2.1.0
|
||||
|
||||
# pointer_interceptor
|
||||
pointer_interceptor: 0.9.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user