diff --git a/env.file b/env.file index 9bf93db70..152bde72d 100644 --- a/env.file +++ b/env.file @@ -1 +1 @@ -SERVER_URL=http://localhost \ No newline at end of file +SERVER_URL=https://dev.open-paas.org \ No newline at end of file diff --git a/lib/features/composer/data/network/composer_api.dart b/lib/features/composer/data/network/composer_api.dart index f83f0151e..46bf98880 100644 --- a/lib/features/composer/data/network/composer_api.dart +++ b/lib/features/composer/data/network/composer_api.dart @@ -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 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)); } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 0dfc78afd..ef8d7b908 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -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(), Get.find(), @@ -131,7 +129,6 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 955fe583e..c9d057eca 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -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 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 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(); } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index a03647bc6..518090b5b 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -25,13 +25,12 @@ class ComposerView extends GetWidget { 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 { 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 { return [ _pickPhotoAndVideoAction(context), _browseFileAction(context), - SizedBox(height: 30), + SizedBox(height: kIsWeb ? 16 : 30), + ]; + } + + List _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 { 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 { 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 { (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, diff --git a/lib/features/composer/presentation/widgets/top_bar_composer_widget_builder.dart b/lib/features/composer/presentation/widgets/top_bar_composer_widget_builder.dart index a6dde007d..6056b2f31 100644 --- a/lib/features/composer/presentation/widgets/top_bar_composer_widget_builder.dart +++ b/lib/features/composer/presentation/widgets/top_bar_composer_widget_builder.dart @@ -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'), diff --git a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart index f0088e74f..faf13c2e3 100644 --- a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart +++ b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart @@ -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> 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(LocalFilePickerSuccess(fileInfoResults)); } else { yield Left(LocalFilePickerCancel()); diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index 69f7b7523..c2fdcaefb 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -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>? 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 get props => [fileName, filePath, fileSize]; + List get props => [fileName, filePath, fileSize, readStream]; } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 0eb28f40a..3180f76fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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