diff --git a/contact/pubspec.lock b/contact/pubspec.lock index e16cc22376..96e76e89a6 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -640,6 +640,15 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + languagetool_textfield: + dependency: transitive + description: + path: "." + ref: twake-supported + resolved-ref: "03f67e3ea7739e3cfbe17a2fd814ab817dd803dd" + url: "https://github.com/dab246/languagetool_textfield.git" + source: git + version: "0.1.0" leak_tracker: dependency: transitive description: @@ -1036,6 +1045,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + throttling: + dependency: transitive + description: + name: throttling + sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 + url: "https://pub.dev" + source: hosted + version: "2.0.1" timing: dependency: transitive description: diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index c639226f4f..f8eef5a02d 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -4,6 +4,7 @@ extension AppColor on Color { static const primaryColor = Color(0xFF007AFF); static const primaryDarkColor = Color(0xFF1C1C1C); static const primaryLightColor = Color(0xFFFFFFFF); + static const primarySelectedColor = Color(0xFFDFEEFF); static const baseTextColor = Color(0xFF7E869B); static const textFieldTextColor = Color(0xFF7E869B); static const textFieldLabelColor = Color(0xFF7E869B); @@ -16,11 +17,8 @@ extension AppColor on Color { static const loginTextFieldFocusedBorder = Color(0xFF007AFF); static const loginTextFieldHintColor = Color(0xff818C99); static const loginTextFieldBackgroundColor = Color(0xFFF2F3F5); - static const loginTextFieldBackgroundErrorColor = Color(0xFFFAEBEB); - static const buttonColor = Color(0xFF837DFF); static const appColor = Color(0xFF3840F7); static const nameUserColor = Color(0xFF182952); - static const emailUserColor = Color(0xFF7E869B); static const userInformationBackgroundColor = Color(0xFFF5F5F7); static const searchBorderColor = Color(0xFFEAEAEA); static const searchHintTextColor = Color(0xFF7E869B); @@ -29,7 +27,6 @@ extension AppColor on Color { static const mailboxSelectedTextColor = Color(0xFF3840F7); static const mailboxTextColor = Color(0xFF182952); static const mailboxSelectedTextNumberColor = Color(0xFF182952); - static const mailboxTextNumberColor = Color(0xFF837DFF); static const mailboxSelectedIconColor = Color(0xFF3840F7); static const mailboxIconColor = Color(0xFF7E869B); static const storageBackgroundColor = Color(0xFFF5F5F7); diff --git a/core/lib/presentation/utils/theme_utils.dart b/core/lib/presentation/utils/theme_utils.dart index ab5bf3722b..d6bc7e4e41 100644 --- a/core/lib/presentation/utils/theme_utils.dart +++ b/core/lib/presentation/utils/theme_utils.dart @@ -11,6 +11,7 @@ class ThemeUtils { fontFamily: ConstantsUI.fontApp, appBarTheme: _appBarTheme, textTheme: _textTheme, + textSelectionTheme: _textSelectionTheme, dividerTheme: _dividerTheme, visualDensity: VisualDensity.adaptivePlatformDensity, scrollbarTheme: ScrollbarThemeData( @@ -27,6 +28,14 @@ class ThemeUtils { ); } + static TextSelectionThemeData get _textSelectionTheme { + return const TextSelectionThemeData( + cursorColor: AppColor.primaryColor, + selectionHandleColor: AppColor.primaryColor, + selectionColor: AppColor.primarySelectedColor, + ); + } + static AppBarTheme get _appBarTheme { return const AppBarTheme( color: Colors.white, diff --git a/core/lib/presentation/views/text/text_field_builder.dart b/core/lib/presentation/views/text/text_field_builder.dart index fbff332b4b..049b59e18d 100644 --- a/core/lib/presentation/views/text/text_field_builder.dart +++ b/core/lib/presentation/views/text/text_field_builder.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/languagetool_textfield.dart'; class TextFieldBuilder extends StatefulWidget { @@ -10,7 +11,7 @@ class TextFieldBuilder extends StatefulWidget { final TapRegionCallback? onTapOutside; final TextStyle? textStyle; final TextInputAction? textInputAction; - final InputDecoration? decoration; + final InputDecoration decoration; final bool obscureText; final int? maxLines; final int? minLines; @@ -24,7 +25,9 @@ class TextFieldBuilder extends StatefulWidget { final bool autocorrect; final TextDirection textDirection; final bool readOnly; + final bool spellCheck; final MouseCursor? mouseCursor; + final LanguageToolController? languageToolController; const TextFieldBuilder({ super.key, @@ -33,13 +36,15 @@ class TextFieldBuilder extends StatefulWidget { this.obscureText = false, this.autoFocus = false, this.readOnly = false, + this.spellCheck = false, this.textStyle = const TextStyle(color: AppColor.textFieldTextColor), this.textDirection = TextDirection.ltr, this.textInputAction, - this.decoration, + this.decoration = const InputDecoration(), this.maxLines, this.minLines, this.controller, + this.languageToolController, this.keyboardType, this.focusNode, this.fromValue, @@ -49,7 +54,7 @@ class TextFieldBuilder extends StatefulWidget { this.onTapOutside, this.onTextChange, this.onTextSubmitted, - }); + }) : assert(spellCheck == true), assert(languageToolController != null); @override State createState() => _TextFieldBuilderState(); @@ -57,22 +62,71 @@ class TextFieldBuilder extends StatefulWidget { class _TextFieldBuilderState extends State { - late TextEditingController _controller; + TextEditingController? _controller; + LanguageToolController? _languageToolController; + late TextDirection _textDirection; @override void initState() { super.initState(); - if (widget.fromValue != null) { - _controller = TextEditingController.fromValue(TextEditingValue(text: widget.fromValue!)); + if (widget.spellCheck) { + _languageToolController = widget.languageToolController ?? LanguageToolController( + delay: const Duration(milliseconds: 200) + ); + if (widget.fromValue != null) { + _languageToolController?.value = TextEditingValue(text: widget.fromValue!); + } } else { - _controller = widget.controller ?? TextEditingController(); + if (widget.fromValue != null) { + _controller = TextEditingController.fromValue(TextEditingValue(text: widget.fromValue!)); + } else { + _controller = widget.controller ?? TextEditingController(); + } } _textDirection = widget.textDirection; } @override Widget build(BuildContext context) { + if (widget.spellCheck) { + return LanguageToolTextField( + key: widget.key, + controller: _languageToolController ?? LanguageToolController(), + 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, + alignCenter: false, + padding: null, + 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; + }); + } + } + }, + onTextSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, + onTapOutside: widget.onTapOutside, + ); + } + return TextField( key: widget.key, controller: _controller, @@ -111,7 +165,11 @@ class _TextFieldBuilderState extends State { @override void dispose() { if (widget.fromValue == null && widget.controller == null) { - _controller.dispose(); + if (widget.spellCheck) { + _languageToolController?.dispose(); + } else { + _controller?.dispose(); + } } super.dispose(); } diff --git a/core/pubspec.lock b/core/pubspec.lock index 4f8ae6a181..823b7bc12c 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -448,6 +448,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + languagetool_textfield: + dependency: "direct main" + description: + path: "." + ref: twake-supported + resolved-ref: "9a7574a51cc779f103f3ac8f053897899c3e136c" + url: "https://github.com/dab246/languagetool_textfield.git" + source: git + version: "0.1.0" leak_tracker: dependency: transitive description: @@ -733,6 +742,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + throttling: + dependency: transitive + description: + name: throttling + sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 + url: "https://pub.dev" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 6192956797..2c02089586 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -26,6 +26,12 @@ dependencies: flutter: sdk: flutter + ### Dependencies from git ### + languagetool_textfield: + git: + url: https://github.com/dab246/languagetool_textfield.git + ref: twake-supported + ### Dependencies from pub.dev ### cupertino_icons: 1.0.6 diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index edd79e591e..afc1ca9b7e 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -20,6 +20,7 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:languagetool_textfield/languagetool_textfield.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -143,7 +144,9 @@ class ComposerController extends BaseController with DragDropFileMixin implement List listBccEmailAddress = []; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; - final subjectEmailInputController = TextEditingController(); + final subjectEmailInputController = LanguageToolController( + delay: const Duration(milliseconds: 200), + ); final toEmailAddressController = TextEditingController(); final ccEmailAddressController = TextEditingController(); final bccEmailAddressController = TextEditingController(); @@ -1887,6 +1890,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement void handleOnFocusHtmlEditorWeb() { FocusManager.instance.primaryFocus?.unfocus(); + subjectEmailInputController.popupWidget?.popupRenderer.dismiss(); richTextWebController?.editorController.setFocus(); richTextWebController?.closeAllMenuPopup(); } diff --git a/lib/features/composer/presentation/styles/composer_style.dart b/lib/features/composer/presentation/styles/composer_style.dart index ca246c3c0e..26bf5e3e15 100644 --- a/lib/features/composer/presentation/styles/composer_style.dart +++ b/lib/features/composer/presentation/styles/composer_style.dart @@ -19,17 +19,17 @@ class ComposerStyle { static const EdgeInsetsGeometry desktopRecipientPadding = EdgeInsetsDirectional.only(end: 24); static const EdgeInsetsGeometry desktopRecipientMargin = EdgeInsetsDirectional.only(start: 24); static const EdgeInsetsGeometry desktopSubjectMargin = EdgeInsetsDirectional.only(start: 24); - static const EdgeInsetsGeometry desktopSubjectPadding = EdgeInsetsDirectional.only(end: 24, top: 12, bottom: 12); + static const EdgeInsetsGeometry desktopSubjectPadding = EdgeInsetsDirectional.only(end: 24); static const EdgeInsetsGeometry desktopEditorPadding = EdgeInsetsDirectional.symmetric(horizontal: 20); static const EdgeInsetsGeometry tabletRecipientPadding = EdgeInsetsDirectional.only(end: 24); static const EdgeInsetsGeometry tabletRecipientMargin = EdgeInsetsDirectional.only(start: 24); static const EdgeInsetsGeometry tabletSubjectMargin = EdgeInsetsDirectional.only(start: 24); - static const EdgeInsetsGeometry tabletSubjectPadding = EdgeInsetsDirectional.only(end: 24, top: 12, bottom: 12); + static const EdgeInsetsGeometry tabletSubjectPadding = EdgeInsetsDirectional.only(end: 24); static const EdgeInsetsGeometry tabletEditorPadding = EdgeInsetsDirectional.symmetric(horizontal: 20); static const EdgeInsetsGeometry mobileRecipientPadding = EdgeInsetsDirectional.only(end: 16); static const EdgeInsetsGeometry mobileRecipientMargin = EdgeInsetsDirectional.only(start: 16); static const EdgeInsetsGeometry mobileSubjectMargin = EdgeInsetsDirectional.only(start: 16); - static const EdgeInsetsGeometry mobileSubjectPadding = EdgeInsetsDirectional.only(end: 16, top: 12, bottom: 12); + static const EdgeInsetsGeometry mobileSubjectPadding = EdgeInsetsDirectional.only(end: 16); static const EdgeInsetsGeometry mobileEditorPadding = EdgeInsetsDirectional.symmetric(horizontal: 12); static const EdgeInsetsGeometry popupItemPadding = EdgeInsetsDirectional.symmetric(horizontal: 12); static const EdgeInsetsGeometry insertImageLoadingBarPadding = EdgeInsetsDirectional.only(top: 12); diff --git a/lib/features/composer/presentation/widgets/subject_composer_widget.dart b/lib/features/composer/presentation/widgets/subject_composer_widget.dart index cfc4d8a7a4..7d5ebe9f31 100644 --- a/lib/features/composer/presentation/widgets/subject_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/subject_composer_widget.dart @@ -1,13 +1,14 @@ import 'package:core/presentation/views/text/text_field_builder.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/languagetool_textfield.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/subject_composer_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class SubjectComposerWidget extends StatelessWidget { final FocusNode? focusNode; - final TextEditingController textController; + final LanguageToolController textController; final ValueChanged? onTextChange; final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? padding; @@ -47,9 +48,11 @@ class SubjectComposerWidget extends StatelessWidget { focusNode: focusNode, onTextChange: onTextChange, maxLines: 1, + spellCheck: true, + decoration: const InputDecoration(border: InputBorder.none), textDirection: DirectionUtils.getDirectionByLanguage(context), textStyle: SubjectComposerWidgetStyle.inputTextStyle, - controller: textController, + languageToolController: textController, ) ) ] diff --git a/model/pubspec.lock b/model/pubspec.lock index 428bc03bac..3aa8de61df 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -632,6 +632,15 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + languagetool_textfield: + dependency: transitive + description: + path: "." + ref: twake-supported + resolved-ref: "03f67e3ea7739e3cfbe17a2fd814ab817dd803dd" + url: "https://github.com/dab246/languagetool_textfield.git" + source: git + version: "0.1.0" leak_tracker: dependency: transitive description: @@ -1013,6 +1022,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + throttling: + dependency: transitive + description: + name: throttling + sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 + url: "https://pub.dev" + source: hosted + version: "2.0.1" timing: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index f38644f000..3d92e36bdd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1224,6 +1224,15 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + languagetool_textfield: + dependency: "direct main" + description: + path: "." + ref: twake-supported + resolved-ref: "9a7574a51cc779f103f3ac8f053897899c3e136c" + url: "https://github.com/dab246/languagetool_textfield.git" + source: git + version: "0.1.0" leak_tracker: dependency: transitive description: @@ -1877,6 +1886,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + throttling: + dependency: transitive + description: + name: throttling + sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 + url: "https://pub.dev" + source: hosted + version: "2.0.1" timeago: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9fcaff4fa6..104fbfc93b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -108,6 +108,11 @@ dependencies: url: https://github.com/linagora/flutter_pdf_render.git ref: twake_supported + languagetool_textfield: + git: + url: https://github.com/dab246/languagetool_textfield.git + ref: twake-supported + ### Dependencies from pub.dev ### cupertino_icons: 1.0.6