From c9f0f827a27cee390a153753f8219c18528fd51e Mon Sep 17 00:00:00 2001 From: DatDang Date: Wed, 4 Dec 2024 16:11:44 +0700 Subject: [PATCH] TF-3308 Implement super_drag_and_drop --- .../views/text/text_field_builder.dart | 94 ++++++----- .../presentation/composer_controller.dart | 7 +- .../presentation/composer_view_web.dart | 21 ++- .../mixin/drag_drog_file_mixin.dart | 33 ---- .../widgets/recipient_composer_widget.dart | 158 ++++++++++-------- .../widgets/subject_composer_widget.dart | 3 + .../identity_creator_controller.dart | 6 +- .../presentation/identity_creator_view.dart | 6 +- 8 files changed, 162 insertions(+), 166 deletions(-) diff --git a/core/lib/presentation/views/text/text_field_builder.dart b/core/lib/presentation/views/text/text_field_builder.dart index 3f162bc2db..60ef6be1fe 100644 --- a/core/lib/presentation/views/text/text_field_builder.dart +++ b/core/lib/presentation/views/text/text_field_builder.dart @@ -1,4 +1,5 @@ import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/text_drop_zone_web.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:languagetool_textfield/languagetool_textfield.dart'; @@ -27,6 +28,7 @@ class TextFieldBuilder extends StatefulWidget { final bool readOnly; final MouseCursor? mouseCursor; final LanguageToolController? languageToolController; + final bool dropTextEnabled; const TextFieldBuilder({ super.key, @@ -52,6 +54,7 @@ class TextFieldBuilder extends StatefulWidget { this.onTapOutside, this.onTextChange, this.onTextSubmitted, + this.dropTextEnabled = false, }); @override @@ -85,8 +88,10 @@ class _TextFieldBuilderState extends State { @override Widget build(BuildContext context) { + Widget child; + if (_languageToolController != null) { - return LanguageToolTextField( + child = LanguageToolTextField( key: widget.key, controller: _languageToolController!, cursorColor: widget.cursorColor, @@ -104,55 +109,46 @@ class _TextFieldBuilderState extends State { textDirection: _textDirection, readOnly: widget.readOnly, mouseCursor: widget.mouseCursor, - onTextChange: (value) { - widget.onTextChange?.call(value); - if (value.isNotEmpty) { - final directionByText = DirectionUtils.getDirectionByEndsText(value); - if (directionByText != _textDirection) { - setState(() { - _textDirection = directionByText; - }); - } - } - }, + onTextChange: _onChanged, onTextSubmitted: widget.onTextSubmitted, onTap: widget.onTap, onTapOutside: widget.onTapOutside, ); + } else { + child = TextField( + key: widget.key, + controller: _controller, + cursorColor: widget.cursorColor, + autocorrect: widget.autocorrect, + textInputAction: widget.textInputAction, + decoration: widget.decoration, + maxLines: widget.maxLines, + minLines: widget.minLines, + keyboardAppearance: widget.keyboardAppearance, + style: widget.textStyle, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + autofocus: widget.autoFocus, + focusNode: widget.focusNode, + textDirection: _textDirection, + readOnly: widget.readOnly, + mouseCursor: widget.mouseCursor, + onChanged: _onChanged, + onSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, + onTapOutside: widget.onTapOutside, + ); } - return TextField( - key: widget.key, - controller: _controller, - cursorColor: widget.cursorColor, - autocorrect: widget.autocorrect, - textInputAction: widget.textInputAction, - decoration: widget.decoration, - maxLines: widget.maxLines, - minLines: widget.minLines, - keyboardAppearance: widget.keyboardAppearance, - style: widget.textStyle, - obscureText: widget.obscureText, - keyboardType: widget.keyboardType, - autofocus: widget.autoFocus, - focusNode: widget.focusNode, - textDirection: _textDirection, - readOnly: widget.readOnly, - mouseCursor: widget.mouseCursor, - onChanged: (value) { - widget.onTextChange?.call(value); - if (value.isNotEmpty) { - final directionByText = DirectionUtils.getDirectionByEndsText(value); - if (directionByText != _textDirection) { - setState(() { - _textDirection = directionByText; - }); - } - } + if (!widget.dropTextEnabled) return child; + + return TextDropZoneWeb( + onDrop: (value) { + (_languageToolController ?? _controller)?.text += value; + widget.focusNode?.requestFocus(); + _onChanged(value); }, - onSubmitted: widget.onTextSubmitted, - onTap: widget.onTap, - onTapOutside: widget.onTapOutside, + child: child, ); } @@ -166,4 +162,16 @@ class _TextFieldBuilderState extends State { } super.dispose(); } + + void _onChanged(String value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d90e26d85a..971435a0d3 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -5,7 +5,6 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:dio/dio.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; @@ -2295,15 +2294,13 @@ class ComposerController extends BaseController } } - void onLocalFileDropZoneListener({ + Future onSuperDropListener({ required BuildContext context, - required DropDoneDetails details, + required List listFileInfo, required double maxWidth }) async { _setUpMaxWidthInlineImage(context: context, maxWidth: maxWidth); - final listFileInfo = await onDragDone(context: context, details: details); - if (listFileInfo.isEmpty && context.mounted) { appToast.showToastErrorMessage( context, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index c27b1087b3..d60d6c6898 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -198,6 +198,7 @@ class ComposerView extends GetWidget { onTextChange: controller.setSubjectEmail, padding: ComposerStyle.mobileSubjectPadding, margin: ComposerStyle.mobileSubjectMargin, + dropTextEnabled: true, ), Expanded( child: LayoutBuilder( @@ -301,10 +302,10 @@ class ComposerView extends GetWidget { imagePaths: controller.imagePaths, width: constraintsEditor.maxWidth, height: constraintsEditor.maxHeight, - onLocalFileDropZoneListener: (details) => - controller.onLocalFileDropZoneListener( + onSuperDrop: (listFileInfo) => + controller.onSuperDropListener( context: context, - details: details, + listFileInfo: listFileInfo, maxWidth: constraintsEditor.maxWidth, ), ) @@ -472,6 +473,7 @@ class ComposerView extends GetWidget { onTextChange: controller.setSubjectEmail, padding: ComposerStyle.desktopSubjectPadding, margin: ComposerStyle.desktopSubjectMargin, + dropTextEnabled: true, ), Expanded( child: LayoutBuilder( @@ -607,10 +609,10 @@ class ComposerView extends GetWidget { imagePaths: controller.imagePaths, width: constraintsEditor.maxWidth, height: constraintsEditor.maxHeight, - onLocalFileDropZoneListener: (details) => - controller.onLocalFileDropZoneListener( + onSuperDrop: (listFileInfo) => + controller.onSuperDropListener( context: context, - details: details, + listFileInfo: listFileInfo, maxWidth: constraintsEditor.maxWidth, ), ) @@ -779,6 +781,7 @@ class ComposerView extends GetWidget { onTextChange: controller.setSubjectEmail, padding: ComposerStyle.tabletSubjectPadding, margin: ComposerStyle.tabletSubjectMargin, + dropTextEnabled: true, ), Expanded( child: LayoutBuilder( @@ -912,10 +915,10 @@ class ComposerView extends GetWidget { imagePaths: controller.imagePaths, width: constraintsBody.maxWidth, height: constraintsBody.maxHeight, - onLocalFileDropZoneListener: (details) => - controller.onLocalFileDropZoneListener( + onSuperDrop: (listFileInfo) => + controller.onSuperDropListener( context: context, - details: details, + listFileInfo: listFileInfo, maxWidth: constraintsBody.maxWidth, ), ) diff --git a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart index 1d27758307..ab0e5abb6c 100644 --- a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart +++ b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart @@ -1,10 +1,7 @@ import 'dart:async' as async; import 'package:async/async.dart'; -import 'package:core/data/constants/constant.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; mixin DragDropFileMixin { @@ -19,34 +16,4 @@ mixin DragDropFileMixin { future: future, ); } - - async.Future> onDragDone({ - required BuildContext context, - required DropDoneDetails details - }) async { - final bytesList = await showFutureLoadingDialogFullScreen( - context: context, - future: () => async.Future.wait( - details.files.map( - (xFile) => xFile.readAsBytes(), - ), - ), - ); - - if (bytesList.error != null) return []; - - final listFileInfo = []; - for (var i = 0; i < bytesList.result!.length; i++) { - listFileInfo.add( - FileInfo( - bytes: bytesList.result![i], - fileName: details.files[i].name, - type: details.files[i].mimeType, - fileSize: bytesList.result![i].length, - isInline: details.files[i].mimeType?.startsWith(Constant.imageType) == true - ), - ); - } - return listFileInfo; - } } diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 80a0163231..328bd5a996 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -17,6 +17,7 @@ import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:super_tag_editor/tag_editor.dart'; +import 'package:core/presentation/views/text/text_drop_zone_web.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/mail_address_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; @@ -160,79 +161,98 @@ class _RecipientComposerWidgetState extends State { if (PlatformInfo.isWeb || widget.isTestingForWeb) { return DragTarget( builder: (context, candidateData, rejectedData) { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - enableBorder: _isDragging, - focusNodeKeyboard: widget.focusNodeKeyboard, - borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, - enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - autoHideTextInputField: true, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - onFocusTextInput: () { - if (_isCollapse) { - widget.onShowFullListEmailAddressAction?.call(widget.prefix); + return TextDropZoneWeb( + onHover: () { + if (!_isDragging) { + stateSetter(() => _isDragging = true); } }, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - index: index, - imagePaths: widget.imagePaths, - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: widget.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); + onLeave: () { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - imagePaths: widget.imagePaths, - suggestionState: suggestionEmailAddress.state, - emailAddress: MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress).asEmailAddress(), - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); + onDrop: (value) { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + widget.controller?.text += value; + widget.focusNode?.requestFocus(); }, + child: TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + enableBorder: _isDragging, + focusNodeKeyboard: widget.focusNodeKeyboard, + borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, + enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + autoHideTextInputField: true, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: MailAddress.validateAddress(suggestionEmailAddress.emailAddress.emailAddress).asEmailAddress(), + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ), ); }, onAcceptWithDetails: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress.data, stateSetter), diff --git a/lib/features/composer/presentation/widgets/subject_composer_widget.dart b/lib/features/composer/presentation/widgets/subject_composer_widget.dart index 7dfc0e2bc1..7dfc5d2f33 100644 --- a/lib/features/composer/presentation/widgets/subject_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/subject_composer_widget.dart @@ -12,6 +12,7 @@ class SubjectComposerWidget extends StatelessWidget { final ValueChanged? onTextChange; final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? padding; + final bool dropTextEnabled; const SubjectComposerWidget({ super.key, @@ -20,6 +21,7 @@ class SubjectComposerWidget extends StatelessWidget { required this.onTextChange, this.margin, this.padding, + this.dropTextEnabled = false, }); @override @@ -52,6 +54,7 @@ class SubjectComposerWidget extends StatelessWidget { textDirection: DirectionUtils.getDirectionByLanguage(context), textStyle: SubjectComposerWidgetStyle.inputTextStyle, languageToolController: textController, + dropTextEnabled: dropTextEnabled, ) ) ] diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index c3e1d3b792..f6f762e399 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -11,7 +11,6 @@ import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/platform_info.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/scheduler.dart'; @@ -878,14 +877,13 @@ class IdentityCreatorController extends BaseController with DragDropFileMixin im AppLocalizations.of(context).thisImageCannotBePastedIntoTheEditor); } - void onLocalFileDropZoneListener({ + Future onSuperDropListener({ required BuildContext context, - required DropDoneDetails details, + required List listFileInfo, required double maxWidth }) async { clearFocusEditor(context); - final listFileInfo = await onDragDone(context: context, details: details); await _uploadMultipleFilesToPublicAsset( context, listFileInfo, diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index 2d633c0e0d..2a9eb9d83a 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -136,10 +136,10 @@ class IdentityCreatorView extends GetWidget width: constraintsEditor.maxWidth, height: constraintsEditor.maxHeight, margin: EdgeInsets.zero, - onLocalFileDropZoneListener: (details) => - controller.onLocalFileDropZoneListener( + onSuperDrop: (listFileInfo) => + controller.onSuperDropListener( context: context, - details: details, + listFileInfo: listFileInfo, maxWidth: constraintsEditor.maxWidth, ), )