Browse Source

Merge remote-tracking branch 'origin/main' into tekdel/main

# Conflicts:
#	frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart
Lucas.Xu 2 years ago
parent
commit
e9c0956c51
100 changed files with 2030 additions and 503 deletions
  1. 3 1
      .github/workflows/appflowy_editor_test.yml
  2. 30 10
      frontend/app_flowy/assets/translations/fr-FR.json
  3. 2 0
      frontend/app_flowy/ios/Runner/Info.plist
  4. 81 28
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  5. 4 4
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  6. 31 24
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  7. 4 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart
  8. 8 1
      frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart
  9. 10 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  10. 6 6
      frontend/app_flowy/lib/plugins/doc/document.dart
  11. 42 6
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  12. 4 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart
  13. 14 20
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  14. 6 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  15. 6 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/common/text_field.dart
  16. 8 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart
  17. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart
  18. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart
  19. 39 5
      frontend/app_flowy/lib/workspace/application/home/home_bloc.dart
  20. 1 2
      frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart
  21. 15 6
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  22. 1 0
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  23. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  24. 4 3
      frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart
  25. 1 1
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart
  26. 1 3
      frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart
  27. 6 0
      frontend/app_flowy/packages/appflowy_board/CHANGELOG.md
  28. 1 1
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  29. 4 0
      frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
  30. 26 30
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  31. 17 1
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  32. 2 2
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart
  33. 29 4
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart
  34. 6 6
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart
  35. 11 7
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
  36. 68 55
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
  37. 11 8
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
  38. 1 1
      frontend/app_flowy/packages/appflowy_board/pubspec.yaml
  39. 10 0
      frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md
  40. 8 1
      frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
  41. 7 3
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart
  42. 167 0
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart
  43. 193 0
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart
  44. 1 1
      frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock
  45. 1 0
      frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml
  46. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_cs_CZ.arb
  47. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb
  48. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb
  49. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb
  50. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb
  51. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb
  52. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ml_IN.arb
  53. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb
  54. 8 6
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart
  55. 4 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart
  56. 12 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart
  57. 44 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_cs-CZ.dart
  58. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart
  59. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart
  60. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart
  61. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart
  62. 16 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart
  63. 45 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ml_IN.dart
  64. 43 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_nl-NL.dart
  65. 17 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart
  66. 17 16
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart
  67. 3 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart
  68. 0 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart
  69. 25 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/cursor_widget.dart
  70. 9 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart
  71. 8 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
  72. 5 5
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart
  73. 4 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
  74. 7 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart
  75. 5 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
  76. 0 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
  77. 0 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart
  78. 119 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
  79. 2 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
  80. 11 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  81. 3 1
      frontend/app_flowy/packages/appflowy_editor/pubspec.yaml
  82. 201 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/attributes_extension_test.dart
  83. 40 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/color_extension_test.dart
  84. 57 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/node_extension_test.dart
  85. 20 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/object_extension_test.dart
  86. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart
  87. 7 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/text_node_extensions_test.dart
  88. 43 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/text_style_extension_test.dart
  89. 10 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/url_launcher_extension_test.dart
  90. 3 0
      frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart
  91. 0 2
      frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
  92. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart
  93. 7 7
      frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
  94. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart
  95. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart
  96. 106 0
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart
  97. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart
  98. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart
  99. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart
  100. 6 0
      frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart

+ 3 - 1
.github/workflows/flowy_editor_test.yml → .github/workflows/appflowy_editor_test.yml

@@ -1,4 +1,4 @@
-name: FlowyEditor test
+name: AppFlowyEditor test
 
 on:
   push:
@@ -37,6 +37,8 @@ jobs:
         working-directory: frontend/app_flowy/packages/appflowy_editor
         run: |
           flutter pub get
+          flutter format --set-exit-if-changed .
+          flutter analyze .
           flutter test --coverage
 
       - uses: codecov/codecov-action@v3

+ 30 - 10
frontend/app_flowy/assets/translations/fr-FR.json

@@ -94,7 +94,20 @@
   },
   "tooltip": {
     "lightMode": "Passer en mode clair",
-    "darkMode": "Passer en mode sombre"
+    "darkMode": "Passer en mode sombre",
+    "openAsPage": "Ouvrir en tant que page",
+    "addNewRow": "Ajouter une ligne",
+    "openMenu": "Cliquer pour ouvrir le menu"
+  },
+  "sideBar": {
+    "closeSidebar": "Fermer le menu latéral",
+    "openSidebar": "Ouvrir le menu latéral"
+  },
+  "notifications": {
+    "export": {
+      "markdown": "Note exportée en Markdown",
+      "path": "Documents/flowy"
+    }
   },
   "contactsPage": {
     "title": "Contacts",
@@ -123,7 +136,7 @@
       "failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur."
     },
     "google": {
-      "title": "CONNEXION GOOGLE",
+      "title": "CONNEXION VIA GOOGLE",
       "instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.",
       "instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:",
       "instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:",
@@ -135,6 +148,7 @@
     "menu": {
       "appearance": "Apparence",
       "language": "Langue",
+      "user": "Utilisateur",
       "open": "Ouvrir les paramètres"
     },
     "appearance": {
@@ -142,15 +156,12 @@
       "darkLabel": "Mode sombre"
     }
   },
-  "sideBar": {
-    "openSidebar": "Open sidebar",
-    "closeSidebar": "Close sidebar"
-  },
   "grid": {
     "settings": {
       "filter": "Filtrer",
       "sortBy": "Filtrer par",
-      "Properties": "Propriétés"
+      "Properties": "Propriétés",
+      "group": "Groupe"
     },
     "field": {
       "hide": "Cacher",
@@ -179,13 +190,17 @@
       "addSelectOption": "Ajouter une option",
       "optionTitle": "Options",
       "addOption": "Ajouter une option",
-      "editProperty": "Modifier la propriété"
+      "editProperty": "Modifier la propriété",
+      "newColumn": "Nouvelle colonne",
+      "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?"
     },
     "row": {
       "duplicate": "Dupliquer",
       "delete": "Supprimer",
       "textPlaceholder": "Vide",
-      "copyProperty": "Copie de la propriété dans le presse-papiers"
+      "copyProperty": "Copie de la propriété dans le presse-papiers",
+      "count": "Nombre",
+      "newRow": "Nouvelle ligne"
     },
     "selectOption": {
       "create": "Créer",
@@ -211,5 +226,10 @@
       "timeHintTextInTwelveHour": "01:00 PM",
       "timeHintTextInTwentyFourHour": "13:00"
     }
+  },
+  "board": {
+    "column": {
+      "create_new_card": "Nouveau"
+    }
   }
-}
+}

+ 2 - 0
frontend/app_flowy/ios/Runner/Info.plist

@@ -45,5 +45,7 @@
 	<array>
 		<string>en</string>
 	</array>
+	<key>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
 </dict>
 </plist>

+ 81 - 28
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -89,18 +89,30 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
               (err) => Log.error(err),
             );
           },
-          didCreateRow: (String groupId, RowPB row, int? index) {
+          didCreateRow: (group, row, int? index) {
             emit(state.copyWith(
               editingRow: Some(BoardEditingRow(
-                columnId: groupId,
+                group: group,
                 row: row,
                 index: index,
               )),
             ));
+            _groupItemStartEditing(group, row, true);
           },
-          endEditRow: (rowId) {
+          startEditingRow: (group, row) {
+            emit(state.copyWith(
+              editingRow: Some(BoardEditingRow(
+                group: group,
+                row: row,
+                index: null,
+              )),
+            ));
+            _groupItemStartEditing(group, row, true);
+          },
+          endEditingRow: (rowId) {
             state.editingRow.fold(() => null, (editingRow) {
               assert(editingRow.row.id == rowId);
+              _groupItemStartEditing(editingRow.group, editingRow.row, false);
               emit(state.copyWith(editingRow: none()));
             });
           },
@@ -122,6 +134,24 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
+  void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
+    final fieldContext = fieldController.getField(group.fieldId);
+    if (fieldContext == null) {
+      Log.warn("FieldContext should not be null");
+      return;
+    }
+
+    boardController.enableGroupDragging(!isEdit);
+    // boardController.updateGroupItem(
+    //   group.groupId,
+    //   GroupItem(
+    //     row: row,
+    //     fieldContext: fieldContext,
+    //     isDraggable: !isEdit,
+    //   ),
+    // );
+  }
+
   void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
     if (fromRow != null) {
       _rowService
@@ -136,11 +166,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     }
   }
 
-  void _moveGroup(String fromColumnId, String toColumnId) {
+  void _moveGroup(String fromGroupId, String toGroupId) {
     _rowService
         .moveGroup(
-      fromGroupId: fromColumnId,
-      toGroupId: toColumnId,
+      fromGroupId: fromGroupId,
+      toGroupId: toGroupId,
     )
         .then((result) {
       result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
@@ -156,7 +186,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     return super.close();
   }
 
-  void initializeGroups(List<GroupPB> groups) {
+  void initializeGroups(List<GroupPB> groupsData) {
     for (var controller in groupControllers.values) {
       controller.dispose();
     }
@@ -164,27 +194,27 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     boardController.clear();
 
     //
-    List<AppFlowyGroupData> columns = groups
+    List<AppFlowyGroupData> groups = groupsData
         .where((group) => fieldController.getField(group.fieldId) != null)
         .map((group) {
       return AppFlowyGroupData(
         id: group.groupId,
         name: group.desc,
-        items: _buildRows(group),
-        customData: BoardCustomData(
+        items: _buildGroupItems(group),
+        customData: GroupData(
           group: group,
           fieldContext: fieldController.getField(group.fieldId)!,
         ),
       );
     }).toList();
-    boardController.addGroups(columns);
+    boardController.addGroups(groups);
 
-    for (final group in groups) {
+    for (final group in groupsData) {
       final delegate = GroupControllerDelegateImpl(
         controller: boardController,
         fieldController: fieldController,
         onNewColumnItem: (groupId, row, index) {
-          add(BoardEvent.didCreateRow(groupId, row, index));
+          add(BoardEvent.didCreateRow(group, row, index));
         },
       );
       final controller = GroupController(
@@ -242,10 +272,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
-  List<AppFlowyGroupItem> _buildRows(GroupPB group) {
+  List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
     final items = group.rows.map((row) {
       final fieldContext = fieldController.getField(group.fieldId);
-      return BoardColumnItem(row: row, fieldContext: fieldContext!);
+      return GroupItem(
+        row: row,
+        fieldContext: fieldContext!,
+      );
     }).toList();
 
     return <AppFlowyGroupItem>[...items];
@@ -270,11 +303,15 @@ class BoardEvent with _$BoardEvent {
   const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
   const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
   const factory BoardEvent.didCreateRow(
-    String groupId,
+    GroupPB group,
     RowPB row,
     int? index,
   ) = _DidCreateRow;
-  const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
+  const factory BoardEvent.startEditingRow(
+    GroupPB group,
+    RowPB row,
+  ) = _StartEditRow;
+  const factory BoardEvent.endEditingRow(String rowId) = _EndEditRow;
   const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
   const factory BoardEvent.didReceiveGridUpdate(
     GridPB grid,
@@ -334,14 +371,17 @@ class GridFieldEquatable extends Equatable {
   UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
 }
 
-class BoardColumnItem extends AppFlowyGroupItem {
+class GroupItem extends AppFlowyGroupItem {
   final RowPB row;
   final GridFieldContext fieldContext;
 
-  BoardColumnItem({
+  GroupItem({
     required this.row,
     required this.fieldContext,
-  });
+    bool draggable = true,
+  }) {
+    super.draggable = draggable;
+  }
 
   @override
   String get id => row.id;
@@ -367,10 +407,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
     }
 
     if (index != null) {
-      final item = BoardColumnItem(row: row, fieldContext: fieldContext);
+      final item = GroupItem(
+        row: row,
+        fieldContext: fieldContext,
+      );
       controller.insertGroupItem(group.groupId, index, item);
     } else {
-      final item = BoardColumnItem(row: row, fieldContext: fieldContext);
+      final item = GroupItem(
+        row: row,
+        fieldContext: fieldContext,
+      );
       controller.addGroupItem(group.groupId, item);
     }
   }
@@ -389,7 +435,10 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
     }
     controller.updateGroupItem(
       group.groupId,
-      BoardColumnItem(row: row, fieldContext: fieldContext),
+      GroupItem(
+        row: row,
+        fieldContext: fieldContext,
+      ),
     );
   }
 
@@ -400,7 +449,11 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
       Log.warn("FieldContext should not be null");
       return;
     }
-    final item = BoardColumnItem(row: row, fieldContext: fieldContext);
+    final item = GroupItem(
+      row: row,
+      fieldContext: fieldContext,
+      draggable: false,
+    );
 
     if (index != null) {
       controller.insertGroupItem(group.groupId, index, item);
@@ -412,21 +465,21 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
 }
 
 class BoardEditingRow {
-  String columnId;
+  GroupPB group;
   RowPB row;
   int? index;
 
   BoardEditingRow({
-    required this.columnId,
+    required this.group,
     required this.row,
     required this.index,
   });
 }
 
-class BoardCustomData {
+class GroupData {
   final GroupPB group;
   final GridFieldContext fieldContext;
-  BoardCustomData({
+  GroupData({
     required this.group,
     required this.fieldContext,
   });

+ 4 - 4
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -87,13 +87,13 @@ class BoardDataController {
               onUpdatedGroup.call(changeset.updateGroups);
             }
 
-            if (changeset.insertedGroups.isNotEmpty) {
-              onInsertedGroup.call(changeset.insertedGroups);
-            }
-
             if (changeset.deletedGroups.isNotEmpty) {
               onDeletedGroup.call(changeset.deletedGroups);
             }
+
+            if (changeset.insertedGroups.isNotEmpty) {
+              onInsertedGroup.call(changeset.insertedGroups);
+            }
           },
           (e) => _onError?.call(e),
         );

+ 31 - 24
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -83,7 +83,7 @@ class _BoardContentState extends State<BoardContent> {
   @override
   Widget build(BuildContext context) {
     return BlocListener<BoardBloc, BoardState>(
-      listener: (context, state) => _handleEditState(state, context),
+      listener: (context, state) => _handleEditStateChanged(state, context),
       child: BlocBuilder<BoardBloc, BoardState>(
         buildWhen: (previous, current) => previous.groupIds != current.groupIds,
         builder: (context, state) {
@@ -128,21 +128,14 @@ class _BoardContentState extends State<BoardContent> {
     );
   }
 
-  void _handleEditState(BoardState state, BuildContext context) {
+  void _handleEditStateChanged(BoardState state, BuildContext context) {
     state.editingRow.fold(
       () => null,
       (editingRow) {
         WidgetsBinding.instance.addPostFrameCallback((_) {
           if (editingRow.index != null) {
-            context
-                .read<BoardBloc>()
-                .add(BoardEvent.endEditRow(editingRow.row.id));
           } else {
-            scrollManager.scrollToBottom(editingRow.columnId, (boardContext) {
-              context
-                  .read<BoardBloc>()
-                  .add(BoardEvent.endEditRow(editingRow.row.id));
-            });
+            scrollManager.scrollToBottom(editingRow.group.groupId);
           }
         });
       },
@@ -156,14 +149,14 @@ class _BoardContentState extends State<BoardContent> {
 
   Widget _buildHeader(
     BuildContext context,
-    AppFlowyGroupData columnData,
+    AppFlowyGroupData groupData,
   ) {
-    final boardCustomData = columnData.customData as BoardCustomData;
+    final boardCustomData = groupData.customData as GroupData;
     return AppFlowyGroupHeader(
       title: Flexible(
         fit: FlexFit.tight,
         child: FlowyText.medium(
-          columnData.headerData.groupName,
+          groupData.headerData.groupName,
           fontSize: 14,
           overflow: TextOverflow.clip,
           color: context.read<AppTheme>().textColor,
@@ -180,7 +173,7 @@ class _BoardContentState extends State<BoardContent> {
       ),
       onAddButtonClick: () {
         context.read<BoardBloc>().add(
-              BoardEvent.createHeaderRow(columnData.id),
+              BoardEvent.createHeaderRow(groupData.id),
             );
       },
       height: 50,
@@ -218,15 +211,16 @@ class _BoardContentState extends State<BoardContent> {
 
   Widget _buildCard(
     BuildContext context,
-    AppFlowyGroupData group,
-    AppFlowyGroupItem columnItem,
+    AppFlowyGroupData afGroupData,
+    AppFlowyGroupItem afGroupItem,
   ) {
-    final boardColumnItem = columnItem as BoardColumnItem;
-    final rowPB = boardColumnItem.row;
+    final groupItem = afGroupItem as GroupItem;
+    final groupData = afGroupData.customData as GroupData;
+    final rowPB = groupItem.row;
     final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
 
     /// Return placeholder widget if the rowCache is null.
-    if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
+    if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
 
     final fieldController = context.read<BoardBloc>().fieldController;
     final gridId = context.read<BoardBloc>().gridId;
@@ -241,19 +235,19 @@ class _BoardContentState extends State<BoardContent> {
     context.read<BoardBloc>().state.editingRow.fold(
       () => null,
       (editingRow) {
-        isEditing = editingRow.row.id == columnItem.row.id;
+        isEditing = editingRow.row.id == groupItem.row.id;
       },
     );
 
-    final groupItemId = columnItem.id + group.id;
+    final groupItemId = groupItem.row.id + groupData.group.groupId;
     return AppFlowyGroupCard(
       key: ValueKey(groupItemId),
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
       child: BoardCard(
         gridId: gridId,
-        groupId: group.id,
-        fieldId: boardColumnItem.fieldContext.id,
+        groupId: groupData.group.groupId,
+        fieldId: groupItem.fieldContext.id,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
         dataController: cardController,
@@ -264,6 +258,19 @@ class _BoardContentState extends State<BoardContent> {
           rowCache,
           context,
         ),
+        onStartEditing: () {
+          context.read<BoardBloc>().add(
+                BoardEvent.startEditingRow(
+                  groupData.group,
+                  groupItem.row,
+                ),
+              );
+        },
+        onEndEditing: () {
+          context
+              .read<BoardBloc>()
+              .add(BoardEvent.endEditingRow(groupItem.row.id));
+        },
       ),
     );
   }
@@ -345,7 +352,7 @@ extension HexColor on Color {
   }
 }
 
-Widget? _buildHeaderIcon(BoardCustomData customData) {
+Widget? _buildHeaderIcon(GroupData customData) {
   Widget? widget;
   switch (customData.fieldType) {
     case FieldType.Checkbox:

+ 4 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart

@@ -76,6 +76,10 @@ class EditableRowNotifier {
 }
 
 abstract class EditableCell {
+  // Each cell notifier will be bind to the [EditableRowNotifier], which enable
+  // the row notifier receive its cells event. For example: begin editing the
+  // cell or end editing the cell.
+  //
   EditableCellNotifier? get editableNotifier;
 }
 

+ 8 - 1
frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart

@@ -42,6 +42,9 @@ class _BoardTextCellState extends State<BoardTextCell> {
       focusNode.requestFocus();
     }
 
+    // If the focusNode lost its focus, the widget's editableNotifier will
+    // set to false, which will cause the [EditableRowNotifier] to receive
+    // end edit event.
     focusNode.addListener(() {
       if (!focusNode.hasFocus) {
         focusWhenInit = false;
@@ -131,7 +134,11 @@ class _BoardTextCellState extends State<BoardTextCell> {
       padding: EdgeInsets.symmetric(
         vertical: BoardSizes.cardCellVPadding,
       ),
-      child: FlowyText.medium(state.content, fontSize: 14),
+      child: FlowyText.medium(
+        state.content,
+        fontSize: 14,
+        maxLines: null, // Enable multiple lines
+      ),
     );
   }
 

+ 10 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -21,6 +21,8 @@ class BoardCard extends StatefulWidget {
   final CardDataController dataController;
   final BoardCellBuilder cellBuilder;
   final void Function(BuildContext) openCard;
+  final VoidCallback onStartEditing;
+  final VoidCallback onEndEditing;
 
   const BoardCard({
     required this.gridId,
@@ -30,6 +32,8 @@ class BoardCard extends StatefulWidget {
     required this.dataController,
     required this.cellBuilder,
     required this.openCard,
+    required this.onStartEditing,
+    required this.onEndEditing,
     Key? key,
   }) : super(key: key);
 
@@ -56,6 +60,12 @@ class _BoardCardState extends State<BoardCard> {
     rowNotifier.isEditing.addListener(() {
       if (!mounted) return;
       _cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
+
+      if (rowNotifier.isEditing.value) {
+        widget.onStartEditing();
+      } else {
+        widget.onEndEditing();
+      }
     });
 
     popoverController = PopoverController();

+ 6 - 6
frontend/app_flowy/lib/plugins/doc/document.dart

@@ -15,6 +15,7 @@ import 'package:clipboard/clipboard.dart';
 import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_sdk/log.dart';
@@ -112,7 +113,6 @@ class DocumentShareButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    double buttonWidth = 60;
     return BlocProvider(
       create: (context) => getIt<DocShareBloc>(param1: view),
       child: BlocListener<DocShareBloc, DocShareState>(
@@ -130,6 +130,7 @@ class DocumentShareButton extends StatelessWidget {
         },
         child: BlocBuilder<DocShareBloc, DocShareState>(
           builder: (context, state) {
+            final theme = context.watch<AppTheme>();
             return ChangeNotifierProvider.value(
               value: Provider.of<AppearanceSetting>(context, listen: true),
               child: Selector<AppearanceSetting, Locale>(
@@ -137,16 +138,15 @@ class DocumentShareButton extends StatelessWidget {
                 builder: (ctx, _, child) => ConstrainedBox(
                   constraints: const BoxConstraints.expand(
                     height: 30,
-                    // minWidth: buttonWidth,
                     width: 100,
                   ),
                   child: RoundedTextButton(
                     title: LocaleKeys.shareAction_buttonText.tr(),
                     fontSize: 12,
                     borderRadius: Corners.s6Border,
-                    color: Colors.lightBlue,
-                    onPressed: () => _showActionList(
-                        context, Offset(-(buttonWidth / 2), 10)),
+                    color: theme.main1,
+                    onPressed: () =>
+                        _showActionList(context, const Offset(0, 10)),
                   ),
                 ),
               ),
@@ -193,7 +193,7 @@ class DocumentShareButton extends StatelessWidget {
     });
     actionList.show(
       context,
-      anchorDirection: AnchorDirection.bottomWithCenterAligned,
+      anchorDirection: AnchorDirection.bottomWithRightAligned,
       anchorOffset: offset,
     );
   }

+ 42 - 6
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart

@@ -1,6 +1,8 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle.dart';
+import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:dartz/dartz.dart' show Either;
 import 'package:easy_localization/easy_localization.dart';
@@ -157,6 +159,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
           focusedDay: state.focusedDay,
           rowHeight: 40,
           calendarFormat: state.format,
+          daysOfWeekHeight: 40,
           headerStyle: HeaderStyle(
             formatButtonVisible: false,
             titleCentered: true,
@@ -166,15 +169,46 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
             rightChevronPadding: EdgeInsets.zero,
             rightChevronMargin: EdgeInsets.zero,
             rightChevronIcon: svgWidget("home/arrow_right"),
+            headerMargin: const EdgeInsets.only(bottom: 8.0),
+          ),
+          daysOfWeekStyle: DaysOfWeekStyle(
+            dowTextFormatter: (date, locale) =>
+                DateFormat.E(locale).format(date).toUpperCase(),
+            weekdayStyle: TextStyle(
+              fontSize: 13,
+              color: theme.shader3,
+            ),
+            weekendStyle: TextStyle(
+              fontSize: 13,
+              color: theme.shader3,
+            ),
           ),
           calendarStyle: CalendarStyle(
+            cellMargin: const EdgeInsets.all(3),
+            defaultDecoration: BoxDecoration(
+              color: theme.surface,
+              shape: BoxShape.rectangle,
+              borderRadius: const BorderRadius.all(Radius.circular(6)),
+            ),
             selectedDecoration: BoxDecoration(
               color: theme.main1,
-              shape: BoxShape.circle,
+              shape: BoxShape.rectangle,
+              borderRadius: const BorderRadius.all(Radius.circular(6)),
             ),
             todayDecoration: BoxDecoration(
               color: theme.shader4,
-              shape: BoxShape.circle,
+              shape: BoxShape.rectangle,
+              borderRadius: const BorderRadius.all(Radius.circular(6)),
+            ),
+            weekendDecoration: BoxDecoration(
+              color: theme.surface,
+              shape: BoxShape.rectangle,
+              borderRadius: const BorderRadius.all(Radius.circular(6)),
+            ),
+            outsideDecoration: BoxDecoration(
+              color: theme.surface,
+              shape: BoxShape.rectangle,
+              borderRadius: const BorderRadius.all(Radius.circular(6)),
             ),
             selectedTextStyle: TextStyle(
               color: theme.surface,
@@ -230,11 +264,13 @@ class _IncludeTimeButton extends StatelessWidget {
                 FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(),
                     fontSize: 14),
                 const Spacer(),
-                Switch(
+                Toggle(
                   value: includeTime,
-                  onChanged: (newValue) => context
+                  onChanged: (value) => context
                       .read<DateCalBloc>()
-                      .add(DateCalEvent.setIncludeTime(newValue)),
+                      .add(DateCalEvent.setIncludeTime(!value)),
+                  style: ToggleStyle.big(theme),
+                  padding: EdgeInsets.zero,
                 ),
               ],
             ),
@@ -350,7 +386,7 @@ class _DateTypeOptionButton extends StatelessWidget {
           offset: const Offset(20, 0),
           constraints: BoxConstraints.loose(const Size(140, 100)),
           child: FlowyButton(
-            text: FlowyText.medium(title, fontSize: 12),
+            text: FlowyText.medium(title, fontSize: 14),
             hoverColor: theme.hover,
             margin: kMargin,
             rightIcon: svgWidget("grid/more", color: theme.iconColor),

+ 4 - 8
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart

@@ -130,14 +130,10 @@ class SelectOptionTagCell extends StatelessWidget {
               child: Row(
                 mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
-                  Flexible(
-                    fit: FlexFit.loose,
-                    flex: 2,
-                    child: SelectOptionTag.fromOption(
-                      context: context,
-                      option: option,
-                      onSelected: () => onSelected(option),
-                    ),
+                  SelectOptionTag.fromOption(
+                    context: context,
+                    option: option,
+                    onSelected: () => onSelected(option),
                   ),
                   const Spacer(),
                   ...children,

+ 14 - 20
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart

@@ -142,11 +142,12 @@ class _TextField extends StatelessWidget {
             value: (option) => option);
 
         return SizedBox(
-          height: 42,
+          height: 62,
           child: SelectOptionTextField(
             options: state.options,
             selectedOptionMap: optionMap,
             distanceToText: _editorPanelWidth * 0.7,
+            maxLength: 30,
             tagController: _tagController,
             onClick: () => popoverMutex.close(),
             newText: (text) {
@@ -248,32 +249,25 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
       mutex: widget.popoverMutex,
       child: SizedBox(
         height: GridSize.typeOptionItemHeight,
-        child: Row(
+        child: SelectOptionTagCell(
+          option: widget.option,
+          onSelected: (option) {
+            context
+                .read<SelectOptionCellEditorBloc>()
+                .add(SelectOptionEditorEvent.selectOption(option.id));
+          },
           children: [
-            Flexible(
-              fit: FlexFit.loose,
-              child: SelectOptionTagCell(
-                option: widget.option,
-                onSelected: (option) {
-                  context
-                      .read<SelectOptionCellEditorBloc>()
-                      .add(SelectOptionEditorEvent.selectOption(option.id));
-                },
-                children: [
-                  if (widget.isSelected)
-                    Padding(
-                      padding: const EdgeInsets.only(right: 6),
-                      child: svgWidget("grid/checkmark"),
-                    ),
-                ],
+            if (widget.isSelected)
+              Padding(
+                padding: const EdgeInsets.only(right: 6),
+                child: svgWidget("grid/checkmark"),
               ),
-            ),
             FlowyIconButton(
               width: 30,
               onPressed: () => _popoverController.show(),
               iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
               icon: svgWidget("editor/details", color: theme.iconColor),
-            )
+            ),
           ],
         ),
       ),

+ 6 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart

@@ -6,6 +6,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:textfield_tags/textfield_tags.dart';
 
@@ -20,6 +21,7 @@ class SelectOptionTextField extends StatefulWidget {
   final Function(String) onSubmitted;
   final Function(String) newText;
   final VoidCallback? onClick;
+  final int? maxLength;
 
   const SelectOptionTextField({
     required this.options,
@@ -29,6 +31,7 @@ class SelectOptionTextField extends StatefulWidget {
     required this.onSubmitted,
     required this.newText,
     this.onClick,
+    this.maxLength,
     TextEditingController? textController,
     FocusNode? focusNode,
     Key? key,
@@ -93,6 +96,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
               }
             },
             maxLines: 1,
+            maxLength: widget.maxLength,
+            maxLengthEnforcement:
+                MaxLengthEnforcement.truncateAfterCompositionEnds,
             style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
             decoration: InputDecoration(
               enabledBorder: OutlineInputBorder(

+ 6 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/common/text_field.dart

@@ -9,6 +9,7 @@ class InputTextField extends StatefulWidget {
   final void Function() onCanceled;
   final bool autoClearWhenDone;
   final String text;
+  final int? maxLength;
 
   const InputTextField({
     required this.text,
@@ -16,6 +17,7 @@ class InputTextField extends StatefulWidget {
     required this.onCanceled,
     this.onChanged,
     this.autoClearWhenDone = false,
+    this.maxLength,
     Key? key,
   }) : super(key: key);
 
@@ -41,11 +43,14 @@ class _InputTextFieldState extends State<InputTextField> {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
 
+    final height = widget.maxLength == null ? 36.0 : 56.0;
+
     return RoundedInputField(
       controller: _controller,
       focusNode: _focusNode,
       autoFocus: true,
-      height: 36,
+      height: height,
+      maxLength: widget.maxLength,
       style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
       normalBorderColor: theme.shader4,
       focusBorderColor: theme.main1,

+ 8 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart

@@ -1,5 +1,7 @@
 import 'package:app_flowy/plugins/grid/application/field/type_option/date_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle.dart';
+import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart';
 import 'package:easy_localization/easy_localization.dart' hide DateFormat;
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:flowy_infra/image.dart';
@@ -161,6 +163,7 @@ class _IncludeTimeButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
     return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>(
       selector: (state) => state.typeOption.includeTime,
       builder: (context, includeTime) {
@@ -173,13 +176,15 @@ class _IncludeTimeButton extends StatelessWidget {
                 FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(),
                     fontSize: 12),
                 const Spacer(),
-                Switch(
+                Toggle(
                   value: includeTime,
-                  onChanged: (newValue) {
+                  onChanged: (value) {
                     context
                         .read<DateTypeOptionBloc>()
-                        .add(DateTypeOptionEvent.includeTime(newValue));
+                        .add(DateTypeOptionEvent.includeTime(!value));
                   },
+                  style: ToggleStyle.big(theme),
+                  padding: EdgeInsets.zero,
                 ),
               ],
             ),

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart

@@ -256,6 +256,7 @@ class _CreateOptionTextField extends StatelessWidget {
         final text = state.newOptionName.foldRight("", (a, previous) => a);
         return InputTextField(
           autoClearWhenDone: true,
+          maxLength: 30,
           text: text,
           onCanceled: () {
             context

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart

@@ -106,6 +106,7 @@ class _OptionNameTextField extends StatelessWidget {
   Widget build(BuildContext context) {
     return InputTextField(
       text: name,
+      maxLength: 30,
       onCanceled: () {},
       onDone: (optionName) {
         if (name != optionName) {

+ 39 - 5
frontend/app_flowy/lib/workspace/application/home/home_bloc.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/user/application/user_listener.dart';
 import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
+import 'package:flowy_infra/time/duration.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@@ -50,13 +51,24 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
           unauthorized: (_Unauthorized value) {
             emit(state.copyWith(unauthorized: true));
           },
-          collapseMenu: (e) {
+          collapseMenu: (_CollapseMenu e) {
             emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
           },
-          editPanelResized: (e) {
-            final newOffset =
-                (state.resizeOffset + e.offset).clamp(-50, 200).toDouble();
-            emit(state.copyWith(resizeOffset: newOffset));
+          editPanelResizeStart: (_EditPanelResizeStart e) {
+            emit(state.copyWith(
+              resizeType: MenuResizeType.drag,
+              resizeStart: state.resizeOffset,
+            ));
+          },
+          editPanelResized: (_EditPanelResized e) {
+            final newPosition =
+                (e.offset + state.resizeStart).clamp(-50, 200).toDouble();
+            if (state.resizeOffset != newPosition) {
+              emit(state.copyWith(resizeOffset: newPosition));
+            }
+          },
+          editPanelResizeEnd: (_EditPanelResizeEnd e) {
+            emit(state.copyWith(resizeType: MenuResizeType.slide));
           },
         );
       },
@@ -78,6 +90,22 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
   }
 }
 
+enum MenuResizeType {
+  slide,
+  drag,
+}
+
+extension MenuResizeTypeExtension on MenuResizeType {
+  Duration duration() {
+    switch (this) {
+      case MenuResizeType.drag:
+        return 30.milliseconds;
+      case MenuResizeType.slide:
+        return 350.milliseconds;
+    }
+  }
+}
+
 @freezed
 class HomeEvent with _$HomeEvent {
   const factory HomeEvent.initial() = _Initial;
@@ -91,6 +119,8 @@ class HomeEvent with _$HomeEvent {
   const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
   const factory HomeEvent.collapseMenu() = _CollapseMenu;
   const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized;
+  const factory HomeEvent.editPanelResizeStart() = _EditPanelResizeStart;
+  const factory HomeEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
 }
 
 @freezed
@@ -103,6 +133,8 @@ class HomeState with _$HomeState {
     required bool unauthorized,
     required bool isMenuCollapsed,
     required double resizeOffset,
+    required double resizeStart,
+    required MenuResizeType resizeType,
   }) = _HomeState;
 
   factory HomeState.initial(CurrentWorkspaceSettingPB workspaceSetting) =>
@@ -114,5 +146,7 @@ class HomeState with _$HomeState {
         unauthorized: false,
         isMenuCollapsed: false,
         resizeOffset: 0,
+        resizeStart: 0,
+        resizeType: MenuResizeType.slide,
       );
 }

+ 1 - 2
frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart

@@ -2,7 +2,6 @@ import 'dart:io' show Platform;
 
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
 import 'package:flowy_infra/size.dart';
-import 'package:flowy_infra/time/duration.dart';
 import 'package:flutter/material.dart';
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:sized_context/sized_context.dart';
@@ -44,7 +43,7 @@ class HomeLayout {
     homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0;
 
     menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0;
-    animDuration = .35.seconds;
+    animDuration = homeBlocState.resizeType.duration();
 
     editPanelWidth = HomeSizes.editPanelWidth;
     homePageROffset = showEditPanel ? editPanelWidth : 0;

+ 15 - 6
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -176,11 +176,18 @@ class _HomeScreenState extends State<HomeScreen> {
       cursor: SystemMouseCursors.resizeLeftRight,
       child: GestureDetector(
           dragStartBehavior: DragStartBehavior.down,
-          onPanUpdate: ((details) {
-            context
-                .read<HomeBloc>()
-                .add(HomeEvent.editPanelResized(details.delta.dx));
-          }),
+          onHorizontalDragStart: (details) => context
+              .read<HomeBloc>()
+              .add(const HomeEvent.editPanelResizeStart()),
+          onHorizontalDragUpdate: (details) => context
+              .read<HomeBloc>()
+              .add(HomeEvent.editPanelResized(details.localPosition.dx)),
+          onHorizontalDragEnd: (details) => context
+              .read<HomeBloc>()
+              .add(const HomeEvent.editPanelResizeEnd()),
+          onHorizontalDragCancel: () => context
+              .read<HomeBloc>()
+              .add(const HomeEvent.editPanelResizeEnd()),
           behavior: HitTestBehavior.translucent,
           child: SizedBox(
             width: 10,
@@ -208,7 +215,6 @@ class _HomeScreenState extends State<HomeScreen> {
                 top: 0,
                 animate: true)
             .animate(layout.animDuration, Curves.easeOut),
-        homeMenuResizer.positioned(left: layout.homePageLOffset - 5),
         bubble
             .positioned(
               right: 20,
@@ -236,6 +242,9 @@ class _HomeScreenState extends State<HomeScreen> {
                 bottom: 0,
                 animate: true)
             .animate(layout.animDuration, Curves.easeOut),
+        homeMenuResizer
+            .positioned(left: layout.homePageLOffset - 5)
+            .animate(layout.animDuration, Curves.easeOut),
       ],
     );
   }

+ 1 - 0
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -99,6 +99,7 @@ class MenuAppHeader extends StatelessWidget {
               app.name,
               fontSize: 12,
               color: theme.textColor,
+              overflow: TextOverflow.ellipsis,
             ),
           ),
         ),

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -82,7 +82,7 @@ class ViewSectionItem extends StatelessWidget {
         child: FlowyText.regular(
           state.view.name,
           fontSize: 12,
-          overflow: TextOverflow.clip,
+          overflow: TextOverflow.ellipsis,
         ),
       ),
     ];

+ 4 - 3
frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart

@@ -28,8 +28,9 @@ class MenuUser extends StatelessWidget {
           children: [
             _renderAvatar(context),
             const HSpace(10),
-            _renderUserName(context),
-            const Spacer(),
+            Expanded(
+              child: _renderUserName(context),
+            ),
             _renderSettingsButton(context),
             //ToDo: when the user is allowed to create another workspace,
             //we get the below block back
@@ -63,7 +64,7 @@ class MenuUser extends StatelessWidget {
     if (name.isEmpty) {
       name = context.read<MenuUserBloc>().state.userProfile.email;
     }
-    return FlowyText(name, fontSize: 12);
+    return FlowyText(name, fontSize: 12, overflow: TextOverflow.ellipsis);
   }
 
   Widget _renderSettingsButton(BuildContext context) {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart

@@ -13,7 +13,7 @@ class SettingsAppearanceView extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final theme = context.read<AppTheme>();
+    final theme = context.watch<AppTheme>();
 
     return SingleChildScrollView(
       child: Column(

+ 1 - 3
frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -97,10 +97,8 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
         child: SizedBox(
           height: itemHeight,
           child: Row(
-            crossAxisAlignment: CrossAxisAlignment.start,
             children: [
-              if (icon != null) icon,
-              HSpace(ActionListSizes.itemHPadding),
+              if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)],
               FlowyText.medium(action.name, fontSize: 12),
             ],
           ),

+ 6 - 0
frontend/app_flowy/packages/appflowy_board/CHANGELOG.md

@@ -1,8 +1,14 @@
+# 0.0.9
+* Enable slide to select text in card
+* Fix some bugs
+
 # 0.0.8
 * Enable drag and drop group  
+
 # 0.0.7
 * Rename some classes
 * Add documentation
+
 # 0.0.6
 * Support scroll to bottom
 * Fix some bugs

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart

@@ -78,7 +78,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
             height: 50,
             margin: config.groupItemPadding,
             onAddButtonClick: () {
-              boardController.scrollToBottom(columnData.id, (p0) {});
+              boardController.scrollToBottom(columnData.id);
             },
           );
         },

+ 4 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart

@@ -32,4 +32,8 @@ class Log {
           'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message');
     }
   }
+
+  static void error(String? message) {
+    debugPrint('AppFlowyBoard: ❌[Error] - ${DateTime.now().second}=> $message');
+  }
 }

+ 26 - 30
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -11,10 +11,11 @@ import 'reorder_phantom/phantom_controller.dart';
 import '../rendering/board_overlay.dart';
 
 class AppFlowyBoardScrollController {
-  AppFlowyBoardState? _groupState;
+  AppFlowyBoardState? _boardState;
 
-  void scrollToBottom(String groupId, void Function(BuildContext)? completed) {
-    _groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
+  void scrollToBottom(String groupId,
+      {void Function(BuildContext)? completed}) {
+    _boardState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
   }
 }
 
@@ -39,9 +40,6 @@ class AppFlowyBoardConfig {
 }
 
 class AppFlowyBoard extends StatelessWidget {
-  /// The direction to use as the main axis.
-  final Axis direction = Axis.vertical;
-
   /// The widget that will be rendered as the background of the board.
   final Widget? background;
 
@@ -94,11 +92,7 @@ class AppFlowyBoard extends StatelessWidget {
   ///
   final AppFlowyBoardScrollController? boardScrollController;
 
-  final AppFlowyBoardState _groupState = AppFlowyBoardState();
-
-  late final BoardPhantomController _phantomController;
-
-  AppFlowyBoard({
+  const AppFlowyBoard({
     required this.controller,
     required this.cardBuilder,
     this.background,
@@ -109,12 +103,7 @@ class AppFlowyBoard extends StatelessWidget {
     this.groupConstraints = const BoxConstraints(maxWidth: 200),
     this.config = const AppFlowyBoardConfig(),
     Key? key,
-  }) : super(key: key) {
-    _phantomController = BoardPhantomController(
-      delegate: controller,
-      groupsState: _groupState,
-    );
-  }
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -122,8 +111,14 @@ class AppFlowyBoard extends StatelessWidget {
       value: controller,
       child: Consumer<AppFlowyBoardController>(
         builder: (context, notifier, child) {
+          final boardState = AppFlowyBoardState();
+          BoardPhantomController phantomController = BoardPhantomController(
+            delegate: controller,
+            groupsState: boardState,
+          );
+
           if (boardScrollController != null) {
-            boardScrollController!._groupState = _groupState;
+            boardScrollController!._boardState = boardState;
           }
 
           return _AppFlowyBoardContent(
@@ -131,14 +126,14 @@ class AppFlowyBoard extends StatelessWidget {
             dataController: controller,
             scrollController: scrollController,
             scrollManager: boardScrollController,
-            groupState: _groupState,
+            boardState: boardState,
             background: background,
-            delegate: _phantomController,
+            delegate: phantomController,
             groupConstraints: groupConstraints,
             cardBuilder: cardBuilder,
             footerBuilder: footerBuilder,
             headerBuilder: headerBuilder,
-            phantomController: _phantomController,
+            phantomController: phantomController,
             onReorder: controller.moveGroup,
           );
         },
@@ -156,7 +151,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
   final ReorderFlexConfig reorderFlexConfig;
   final BoxConstraints groupConstraints;
   final AppFlowyBoardScrollController? scrollManager;
-  final AppFlowyBoardState groupState;
+  final AppFlowyBoardState boardState;
   final AppFlowyBoardCardBuilder cardBuilder;
   final AppFlowyBoardHeaderBuilder? headerBuilder;
   final AppFlowyBoardFooterBuilder? footerBuilder;
@@ -169,7 +164,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
     required this.delegate,
     required this.dataController,
     required this.scrollManager,
-    required this.groupState,
+    required this.boardState,
     this.scrollController,
     this.background,
     required this.groupConstraints,
@@ -178,7 +173,10 @@ class _AppFlowyBoardContent extends StatefulWidget {
     this.headerBuilder,
     required this.phantomController,
     Key? key,
-  })  : reorderFlexConfig = const ReorderFlexConfig(),
+  })  : reorderFlexConfig = const ReorderFlexConfig(
+          direction: Axis.horizontal,
+          dragDirection: Axis.horizontal,
+        ),
         super(key: key);
 
   @override
@@ -198,7 +196,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           reorderFlexId: widget.dataController.identifier,
           acceptedReorderFlexId: widget.dataController.groupIds,
           delegate: widget.delegate,
-          columnsState: widget.groupState,
+          columnsState: widget.boardState,
         );
 
         final reorderFlex = ReorderFlex(
@@ -206,9 +204,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
           scrollController: widget.scrollController,
           onReorder: widget.onReorder,
           dataSource: widget.dataController,
-          direction: Axis.horizontal,
           interceptor: interceptor,
-          reorderable: true,
           children: _buildColumns(),
         );
 
@@ -254,7 +250,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
         );
 
         final reorderFlexAction = ReorderFlexActionImpl();
-        widget.groupState.reorderFlexActionMap[columnData.id] =
+        widget.boardState.reorderFlexActionMap[columnData.id] =
             reorderFlexAction;
 
         return ChangeNotifierProvider.value(
@@ -275,8 +271,8 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
                 onReorder: widget.dataController.moveGroupItem,
                 cornerRadius: widget.config.cornerRadius,
                 backgroundColor: widget.config.groupBackgroundColor,
-                dragStateStorage: widget.groupState,
-                dragTargetKeys: widget.groupState,
+                dragStateStorage: widget.boardState,
+                dragTargetKeys: widget.boardState,
                 reorderFlexAction: reorderFlexAction,
               );
 

+ 17 - 1
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -138,7 +138,11 @@ class AppFlowyBoardController extends ChangeNotifier
   /// groups or get ready to reinitialize the [AppFlowyBoard].
   void clear() {
     _groupDatas.clear();
+    for (final group in _groupControllers.values) {
+      group.dispose();
+    }
     _groupControllers.clear();
+
     notifyListeners();
   }
 
@@ -202,6 +206,14 @@ class AppFlowyBoardController extends ChangeNotifier
     getGroupController(groupId)?.replaceOrInsertItem(item);
   }
 
+  void enableGroupDragging(bool isEnable) {
+    for (var groupController in _groupControllers.values) {
+      groupController.enableDragging(isEnable);
+    }
+
+    notifyListeners();
+  }
+
   /// Moves the item at [fromGroupIndex] in group with id [fromGroupId] to
   /// group with id [toGroupId] at [toGroupIndex]
   @override
@@ -215,6 +227,8 @@ class AppFlowyBoardController extends ChangeNotifier
     final fromGroupController = getGroupController(fromGroupId)!;
     final toGroupController = getGroupController(toGroupId)!;
     final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
+    if (fromGroupItem == null) return;
+
     if (toGroupController.items.length > toGroupIndex) {
       assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
 
@@ -275,7 +289,9 @@ class AppFlowyBoardController extends ChangeNotifier
         Log.trace(
             '[$BoardPhantomController] update $groupId:$index to $groupId:$newIndex');
         final item = groupController.removeAt(index, notify: false);
-        groupController.insert(newIndex, item, notify: false);
+        if (item != null) {
+          groupController.insert(newIndex, item, notify: false);
+        }
       }
     }
   }

+ 2 - 2
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart

@@ -156,9 +156,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
             widget.onDragStarted?.call(index);
           },
           onReorder: ((fromIndex, toIndex) {
-            if (widget.phantomController.isFromGroup(widget.groupId)) {
+            if (widget.phantomController.shouldReorder(widget.groupId)) {
               widget.onReorder(widget.groupId, fromIndex, toIndex);
-              widget.phantomController.transformIndex(fromIndex, toIndex);
+              widget.phantomController.updateIndex(fromIndex, toIndex);
             }
           }),
           onDragEnded: () {

+ 29 - 4
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart

@@ -5,6 +5,8 @@ import 'package:appflowy_board/src/widgets/reorder_flex/reorder_flex.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flutter/material.dart';
 
+typedef IsDraggable = bool;
+
 /// A item represents the generic data model of each group card.
 ///
 /// Each item displayed in the group required to implement this class.
@@ -50,8 +52,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
   /// * [notify] the default value of [notify] is true, it will notify the
   /// listener. Set to false if you do not want to notify the listeners.
   ///
-  AppFlowyGroupItem removeAt(int index, {bool notify = true}) {
-    assert(index >= 0);
+  AppFlowyGroupItem? removeAt(int index, {bool notify = true}) {
+    if (groupData._items.length <= index) {
+      Log.error(
+          'Fatal error, index is out of bounds. Index: $index,  len: ${groupData._items.length}');
+      return null;
+    }
+
+    if (index < 0) {
+      Log.error('Invalid index:$index');
+      return null;
+    }
 
     Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
     final item = groupData._items.removeAt(index);
@@ -71,12 +82,17 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
   /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
   /// [fromIndex] equal to the [toIndex].
   bool move(int fromIndex, int toIndex) {
-    assert(fromIndex >= 0);
     assert(toIndex >= 0);
+    if (groupData._items.length < fromIndex) {
+      Log.error(
+          'Out of bounds error. index: $fromIndex should not greater than ${groupData._items.length}');
+      return false;
+    }
 
     if (fromIndex == toIndex) {
       return false;
     }
+
     Log.debug(
         '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
     final item = groupData._items.removeAt(fromIndex);
@@ -124,7 +140,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
       Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
     } else {
       if (index >= groupData._items.length) {
-        Log.warn(
+        Log.error(
             '[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}');
         return;
       }
@@ -155,6 +171,15 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
         -1;
   }
 
+  void enableDragging(bool isEnable) {
+    groupData.draggable = isEnable;
+
+    for (var item in groupData._items) {
+      item.draggable = isEnable;
+    }
+    _notify();
+  }
+
   void _notify() {
     notifyListeners();
   }

+ 6 - 6
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart

@@ -16,13 +16,13 @@ class FlexDragTargetData extends DragTargetData {
   @override
   final int draggingIndex;
 
-  final DraggingState _state;
+  final DraggingState _draggingState;
 
-  Widget? get draggingWidget => _state.draggingWidget;
+  Widget? get draggingWidget => _draggingState.draggingWidget;
 
-  Size? get feedbackSize => _state.feedbackSize;
+  Size? get feedbackSize => _draggingState.feedbackSize;
 
-  bool get isDragging => _state.isDragging();
+  bool get isDragging => _draggingState.isDragging();
 
   final String dragTargetId;
 
@@ -40,8 +40,8 @@ class FlexDragTargetData extends DragTargetData {
     required this.reorderFlexId,
     required this.reorderFlexItem,
     required this.dragTargetIndexKey,
-    required DraggingState state,
-  }) : _state = state;
+    required DraggingState draggingState,
+  }) : _draggingState = draggingState;
 
   @override
   String toString() {

+ 11 - 7
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy_board/appflowy_board.dart';
 import 'package:appflowy_board/src/utils/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
@@ -78,10 +79,12 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
 
   final bool useMoveAnimation;
 
-  final bool draggable;
+  final IsDraggable draggable;
 
   final double draggingOpacity;
 
+  final Axis? dragDirection;
+
   const ReorderDragTarget({
     Key? key,
     required this.child,
@@ -99,6 +102,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
     this.onLeave,
     this.draggableTargetBuilder,
     this.draggingOpacity = 0.3,
+    this.dragDirection,
   }) : super(key: key);
 
   @override
@@ -115,8 +119,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
     Widget dragTarget = DragTarget<T>(
       builder: _buildDraggableWidget,
       onWillAccept: (dragTargetData) {
-        assert(dragTargetData != null);
-        if (dragTargetData == null) return false;
+        if (dragTargetData == null) {
+          return false;
+        }
+
         return widget.onWillAccept(dragTargetData);
       },
       onAccept: widget.onAccept,
@@ -140,9 +146,6 @@ class _ReorderDragTargetState<T extends DragTargetData>
     List<T?> acceptedCandidates,
     List<dynamic> rejectedCandidates,
   ) {
-    if (!widget.draggable) {
-      return widget.child;
-    }
     Widget feedbackBuilder = Builder(builder: (BuildContext context) {
       BoxConstraints contentSizeConstraints =
           BoxConstraints.loose(_draggingFeedbackSize!);
@@ -163,7 +166,8 @@ class _ReorderDragTargetState<T extends DragTargetData>
           widget.deleteAnimationController,
         ) ??
         Draggable<DragTargetData>(
-          maxSimultaneousDrags: 1,
+          axis: widget.dragDirection,
+          maxSimultaneousDrags: widget.draggable ? 1 : 0,
           data: widget.dragTargetData,
           ignoringFeedbackSemantics: false,
           feedback: feedbackBuilder,

+ 68 - 55
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart

@@ -1,6 +1,7 @@
 import 'dart:collection';
 import 'dart:math';
 
+import 'package:appflowy_board/appflowy_board.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
 import '../../utils/log.dart';
@@ -29,6 +30,8 @@ abstract class ReoderFlexDataSource {
 abstract class ReoderFlexItem {
   /// [id] is used to identify the item. It must be unique.
   String get id;
+
+  IsDraggable draggable = true;
 }
 
 /// Cache each dragTarget's key.
@@ -73,8 +76,15 @@ class ReorderFlexConfig {
 
   final bool useMovePlaceholder;
 
+  /// [direction] How to place the children, default is Axis.vertical
+  final Axis direction;
+
+  final Axis? dragDirection;
+
   const ReorderFlexConfig({
     this.useMoveAnimation = true,
+    this.direction = Axis.vertical,
+    this.dragDirection,
   }) : useMovePlaceholder = !useMoveAnimation;
 }
 
@@ -82,8 +92,6 @@ class ReorderFlex extends StatefulWidget {
   final ReorderFlexConfig config;
   final List<Widget> children;
 
-  /// [direction] How to place the children, default is Axis.vertical
-  final Axis direction;
   final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start;
 
   final ScrollController? scrollController;
@@ -108,8 +116,6 @@ class ReorderFlex extends StatefulWidget {
 
   final ReorderFlexAction? reorderFlexAction;
 
-  final bool reorderable;
-
   ReorderFlex({
     Key? key,
     this.scrollController,
@@ -117,14 +123,12 @@ class ReorderFlex extends StatefulWidget {
     required this.children,
     required this.config,
     required this.onReorder,
-    this.reorderable = true,
     this.dragStateStorage,
     this.dragTargetKeys,
     this.onDragStarted,
     this.onDragEnded,
     this.interceptor,
     this.reorderFlexAction,
-    this.direction = Axis.vertical,
   })  : assert(children.every((Widget w) => w.key != null),
             'All child must have a key.'),
         super(key: key);
@@ -146,8 +150,8 @@ class ReorderFlexState extends State<ReorderFlex>
   /// Whether or not we are currently scrolling this view to show a widget.
   bool _scrolling = false;
 
-  /// [dragState] records the dragging state including dragStartIndex, and phantomIndex, etc.
-  late DraggingState dragState;
+  /// [draggingState] records the dragging state including dragStartIndex, and phantomIndex, etc.
+  late DraggingState draggingState;
 
   /// [_animation] controls the dragging animations
   late DragTargetAnimation _animation;
@@ -158,9 +162,9 @@ class ReorderFlexState extends State<ReorderFlex>
   void initState() {
     _notifier = ReorderFlexNotifier();
     final flexId = widget.reorderFlexId;
-    dragState = widget.dragStateStorage?.readState(flexId) ??
+    draggingState = widget.dragStateStorage?.readState(flexId) ??
         DraggingState(widget.reorderFlexId);
-    Log.trace('[DragTarget] init dragState: $dragState');
+    Log.trace('[DragTarget] init dragState: $draggingState');
 
     widget.dragStateStorage?.removeState(flexId);
 
@@ -168,7 +172,7 @@ class ReorderFlexState extends State<ReorderFlex>
       reorderAnimationDuration: widget.config.reorderAnimationDuration,
       entranceAnimateStatusChanged: (status) {
         if (status == AnimationStatus.completed) {
-          if (dragState.nextIndex == -1) return;
+          if (draggingState.nextIndex == -1) return;
           setState(() => _requestAnimationToNextIndex());
         }
       },
@@ -225,7 +229,7 @@ class ReorderFlexState extends State<ReorderFlex>
         indexKey,
       );
 
-      children.add(_wrap(child, i, indexKey));
+      children.add(_wrap(child, i, indexKey, item.draggable));
 
       // if (widget.config.useMovePlaceholder) {
       //   children.add(DragTargeMovePlaceholder(
@@ -256,64 +260,70 @@ class ReorderFlexState extends State<ReorderFlex>
     /// when the animation finish.
 
     if (_animation.entranceController.isCompleted) {
-      dragState.removePhantom();
+      draggingState.removePhantom();
 
-      if (!isAcceptingNewTarget && dragState.didDragTargetMoveToNext()) {
+      if (!isAcceptingNewTarget && draggingState.didDragTargetMoveToNext()) {
         return;
       }
 
-      dragState.moveDragTargetToNext();
+      draggingState.moveDragTargetToNext();
       _animation.animateToNext();
     }
   }
 
   /// [child]: the child will be wrapped with dartTarget
   /// [childIndex]: the index of the child in a list
-  Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
+  Widget _wrap(
+    Widget child,
+    int childIndex,
+    GlobalObjectKey indexKey,
+    IsDraggable draggable,
+  ) {
     return Builder(builder: (context) {
       final ReorderDragTarget dragTarget = _buildDragTarget(
         context,
         child,
         childIndex,
         indexKey,
+        draggable,
       );
       int shiftedIndex = childIndex;
 
-      if (dragState.isOverlapWithPhantom()) {
-        shiftedIndex = dragState.calculateShiftedIndex(childIndex);
+      if (draggingState.isOverlapWithPhantom()) {
+        shiftedIndex = draggingState.calculateShiftedIndex(childIndex);
       }
 
       Log.trace(
-          'Rebuild: Group:[${dragState.reorderFlexId}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
-      final currentIndex = dragState.currentIndex;
-      final dragPhantomIndex = dragState.phantomIndex;
+          'Rebuild: Group:[${draggingState.reorderFlexId}] ${draggingState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
+      final currentIndex = draggingState.currentIndex;
+      final dragPhantomIndex = draggingState.phantomIndex;
 
       if (shiftedIndex == currentIndex || childIndex == dragPhantomIndex) {
         Widget dragSpace;
-        if (dragState.draggingWidget != null) {
-          if (dragState.draggingWidget is PhantomWidget) {
-            dragSpace = dragState.draggingWidget!;
+        if (draggingState.draggingWidget != null) {
+          if (draggingState.draggingWidget is PhantomWidget) {
+            dragSpace = draggingState.draggingWidget!;
           } else {
             dragSpace = PhantomWidget(
               opacity: widget.config.draggingWidgetOpacity,
-              child: dragState.draggingWidget,
+              child: draggingState.draggingWidget,
             );
           }
         } else {
-          dragSpace = SizedBox.fromSize(size: dragState.dropAreaSize);
+          dragSpace = SizedBox.fromSize(size: draggingState.dropAreaSize);
         }
 
         /// Returns the dragTarget it is not start dragging. The size of the
         /// dragTarget is the same as the the passed in child.
         ///
-        if (dragState.isNotDragging()) {
+        if (draggingState.isNotDragging()) {
           return _buildDraggingContainer(children: [dragTarget]);
         }
 
         /// Determine the size of the drop area to show under the dragging widget.
         Size? feedbackSize = Size.zero;
         if (widget.config.useMoveAnimation) {
-          feedbackSize = dragState.feedbackSize;
+          feedbackSize = draggingState.feedbackSize;
         }
 
         Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
@@ -321,7 +331,7 @@ class ReorderFlexState extends State<ReorderFlex>
 
         /// When start dragging, the dragTarget, [ReorderDragTarget], will
         /// return a [IgnorePointerWidget] which size is zero.
-        if (dragState.isPhantomAboveDragTarget()) {
+        if (draggingState.isPhantomAboveDragTarget()) {
           _notifier.updateDragTargetIndex(currentIndex);
           if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
             return _buildDraggingContainer(children: [
@@ -343,7 +353,7 @@ class ReorderFlexState extends State<ReorderFlex>
         }
 
         ///
-        if (dragState.isPhantomBelowDragTarget()) {
+        if (draggingState.isPhantomBelowDragTarget()) {
           _notifier.updateDragTargetIndex(currentIndex);
           if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
             return _buildDraggingContainer(children: [
@@ -364,10 +374,10 @@ class ReorderFlexState extends State<ReorderFlex>
           }
         }
 
-        assert(!dragState.isOverlapWithPhantom());
+        assert(!draggingState.isOverlapWithPhantom());
 
         List<Widget> children = [];
-        if (dragState.isDragTargetMovingDown()) {
+        if (draggingState.isDragTargetMovingDown()) {
           children.addAll([dragTarget, appearSpace]);
         } else {
           children.addAll([appearSpace, dragTarget]);
@@ -395,15 +405,17 @@ class ReorderFlexState extends State<ReorderFlex>
     Widget child,
     int dragTargetIndex,
     GlobalObjectKey indexKey,
+    IsDraggable draggable,
   ) {
     final reorderFlexItem = widget.dataSource.items[dragTargetIndex];
     return ReorderDragTarget<FlexDragTargetData>(
       indexGlobalKey: indexKey,
+      draggable: draggable,
       dragTargetData: FlexDragTargetData(
         draggingIndex: dragTargetIndex,
         reorderFlexId: widget.reorderFlexId,
         reorderFlexItem: reorderFlexItem,
-        state: dragState,
+        draggingState: draggingState,
         dragTargetId: reorderFlexItem.id,
         dragTargetIndexKey: indexKey,
       ),
@@ -432,11 +444,11 @@ class ReorderFlexState extends State<ReorderFlex>
         setState(() {
           if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
             _onReordered(
-              dragState.dragStartIndex,
-              dragState.currentIndex,
+              draggingState.dragStartIndex,
+              draggingState.currentIndex,
             );
           }
-          dragState.endDragging();
+          draggingState.endDragging();
           widget.onDragEnded?.call();
         });
       },
@@ -482,8 +494,8 @@ class ReorderFlexState extends State<ReorderFlex>
       deleteAnimationController: _animation.deleteController,
       draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
       useMoveAnimation: widget.config.useMoveAnimation,
-      draggable: widget.reorderable,
       draggingOpacity: widget.config.draggingWidgetOpacity,
+      dragDirection: widget.config.dragDirection,
       child: child,
     );
   }
@@ -506,7 +518,7 @@ class ReorderFlexState extends State<ReorderFlex>
       child,
       _animation.entranceController,
       feedbackSize,
-      widget.direction,
+      widget.config.direction,
     );
   }
 
@@ -515,7 +527,7 @@ class ReorderFlexState extends State<ReorderFlex>
       child,
       _animation.phantomController,
       feedbackSize,
-      widget.direction,
+      widget.config.direction,
     );
   }
 
@@ -525,7 +537,7 @@ class ReorderFlexState extends State<ReorderFlex>
     Size? feedbackSize,
   ) {
     setState(() {
-      dragState.startDragging(draggingWidget, dragIndex, feedbackSize);
+      draggingState.startDragging(draggingWidget, dragIndex, feedbackSize);
       _animation.startDragging();
     });
   }
@@ -535,34 +547,34 @@ class ReorderFlexState extends State<ReorderFlex>
       return;
     }
 
-    dragState.setStartDraggingIndex(dragTargetIndex);
+    draggingState.setStartDraggingIndex(dragTargetIndex);
     widget.dragStateStorage?.insertState(
       widget.reorderFlexId,
-      dragState,
+      draggingState,
     );
   }
 
   bool handleOnWillAccept(BuildContext context, int dragTargetIndex) {
-    final dragIndex = dragState.dragStartIndex;
+    final dragIndex = draggingState.dragStartIndex;
 
     /// The [willAccept] will be true if the dargTarget is the widget that gets
     /// dragged and it is dragged on top of the other dragTargets.
     ///
 
-    bool willAccept =
-        dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex;
+    bool willAccept = draggingState.dragStartIndex == dragIndex &&
+        dragIndex != dragTargetIndex;
     setState(() {
       if (willAccept) {
-        int shiftedIndex = dragState.calculateShiftedIndex(dragTargetIndex);
-        dragState.updateNextIndex(shiftedIndex);
+        int shiftedIndex = draggingState.calculateShiftedIndex(dragTargetIndex);
+        draggingState.updateNextIndex(shiftedIndex);
       } else {
-        dragState.updateNextIndex(dragTargetIndex);
+        draggingState.updateNextIndex(dragTargetIndex);
       }
       _requestAnimationToNextIndex(isAcceptingNewTarget: true);
     });
 
     Log.trace(
-        '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}');
+        '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $draggingState}');
 
     _scrollTo(context);
 
@@ -587,7 +599,7 @@ class ReorderFlexState extends State<ReorderFlex>
       return child;
     } else {
       return SingleChildScrollView(
-        scrollDirection: widget.direction,
+        scrollDirection: widget.config.direction,
         controller: _scrollController,
         child: child,
       );
@@ -595,7 +607,7 @@ class ReorderFlexState extends State<ReorderFlex>
   }
 
   Widget _wrapContainer(List<Widget> children) {
-    switch (widget.direction) {
+    switch (widget.config.direction) {
       case Axis.horizontal:
         return Row(
           crossAxisAlignment: CrossAxisAlignment.start,
@@ -613,7 +625,7 @@ class ReorderFlexState extends State<ReorderFlex>
   }
 
   Widget _buildDraggingContainer({required List<Widget> children}) {
-    switch (widget.direction) {
+    switch (widget.config.direction) {
       case Axis.horizontal:
         return Row(
           crossAxisAlignment: CrossAxisAlignment.start,
@@ -660,6 +672,7 @@ class ReorderFlexState extends State<ReorderFlex>
             .ensureVisible(
           dragTargetRenderObject,
           alignment: 0.5,
+          alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
           duration: const Duration(milliseconds: 120),
         )
             .then((value) {
@@ -683,9 +696,9 @@ class ReorderFlexState extends State<ReorderFlex>
     // If and only if the current scroll offset falls in-between the offsets
     // necessary to reveal the selected context at the top or bottom of the
     // screen, then it is already on-screen.
-    final double margin = widget.direction == Axis.horizontal
-        ? dragState.dropAreaSize.width
-        : dragState.dropAreaSize.height / 2.0;
+    final double margin = widget.config.direction == Axis.horizontal
+        ? draggingState.dropAreaSize.width
+        : draggingState.dropAreaSize.height / 2.0;
     if (_scrollController.hasClients) {
       final double scrollOffset = _scrollController.offset;
       final double topOffset = max(

+ 11 - 8
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart

@@ -46,15 +46,23 @@ class BoardPhantomController extends OverlapDragTargetDelegate
     required this.groupsState,
   });
 
-  bool isFromGroup(String groupId) {
+  /// Determines whether the group should perform reorder
+  ///
+  /// Returns `true` if the fromGroupId and toGroupId of the phantomRecord
+  /// equal to the passed in groupId.
+  ///
+  /// Returns `true` if the phantomRecord is null
+  ///
+  bool shouldReorder(String groupId) {
     if (phantomRecord != null) {
-      return phantomRecord!.fromGroupId == groupId;
+      return phantomRecord!.toGroupId == groupId &&
+          phantomRecord!.fromGroupId == groupId;
     } else {
       return true;
     }
   }
 
-  void transformIndex(int fromIndex, int toIndex) {
+  void updateIndex(int fromIndex, int toIndex) {
     if (phantomRecord == null) {
       return;
     }
@@ -69,7 +77,6 @@ class BoardPhantomController extends OverlapDragTargetDelegate
   /// Remove the phantom in the group when the group is end dragging.
   void groupEndDragging(String groupId) {
     phantomState.setGroupIsDragging(groupId, false);
-
     if (phantomRecord == null) return;
 
     final fromGroupId = phantomRecord!.fromGroupId;
@@ -246,10 +253,6 @@ class PhantomRecord {
   });
 
   void updateFromGroupIndex(int index) {
-    if (fromGroupIndex == index) {
-      return;
-    }
-
     fromGroupIndex = index;
   }
 

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/pubspec.yaml

@@ -1,6 +1,6 @@
 name: appflowy_board
 description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups. 
-version: 0.0.8
+version: 0.0.9
 homepage: https://github.com/AppFlowy-IO/AppFlowy
 repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board
 

+ 10 - 0
frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md

@@ -1,3 +1,13 @@
+## 0.0.6
+* Add three plugins: Code Block, LateX, and Horizontal rule.
+* Support web platform.
+* Support more markdown syntax conversions.
+    * `~ ~` to format text as strikethrough
+    * `_ _` to format text as italic
+    * \` \` to format text as code
+    * `[]()` to format text as link
+* Fix some bugs.
+
 ## 0.0.5
 * Support customize the hotkeys for a shortcut on different platforms.
 * Support customize a theme.

+ 8 - 1
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -2,6 +2,8 @@ import 'dart:convert';
 import 'dart:io';
 
 import 'package:example/plugin/code_block_node_widget.dart';
+import 'package:example/plugin/horizontal_rule_node_widget.dart';
+import 'package:example/plugin/tex_block_node_widget.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -119,14 +121,19 @@ class _MyHomePageState extends State<MyHomePage> {
               editable: true,
               customBuilders: {
                 'text/code_block': CodeBlockNodeWidgetBuilder(),
+                'tex': TeXBlockNodeWidgetBuidler(),
+                'horizontal_rule': HorizontalRuleWidgetBuilder(),
               },
               shortcutEvents: [
                 enterInCodeBlock,
                 ignoreKeysInCodeBlock,
                 underscoreToItalic,
+                insertHorizontalRule,
               ],
               selectionMenuItems: [
-                codeBlockItem,
+                codeBlockMenuItem,
+                teXBlockMenuItem,
+                horizontalRuleMenuItem,
               ],
             ),
           );

+ 7 - 3
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart

@@ -44,9 +44,13 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
   return KeyEventResult.ignored;
 };
 
-SelectionMenuItem codeBlockItem = SelectionMenuItem(
-  name: 'Code Block',
-  icon: const Icon(Icons.abc),
+SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
+  name: () => 'Code Block',
+  icon: const Icon(
+    Icons.abc,
+    color: Colors.black,
+    size: 18.0,
+  ),
   keywords: ['code block'],
   handler: (editorState, _, __) {
     final selection =

+ 167 - 0
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart

@@ -0,0 +1,167 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+ShortcutEvent insertHorizontalRule = ShortcutEvent(
+  key: 'Horizontal rule',
+  command: 'Minus',
+  handler: _insertHorzaontalRule,
+);
+
+ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final textNodes = editorState.service.selectionService.currentSelectedNodes
+      .whereType<TextNode>();
+  if (textNodes.length != 1 || selection == null) {
+    return KeyEventResult.ignored;
+  }
+  final textNode = textNodes.first;
+  if (textNode.toRawString() == '--') {
+    TransactionBuilder(editorState)
+      ..deleteText(textNode, 0, 2)
+      ..insertNode(
+        textNode.path,
+        Node(
+          type: 'horizontal_rule',
+          children: LinkedList(),
+          attributes: {},
+        ),
+      )
+      ..afterSelection =
+          Selection.single(path: textNode.path.next, startOffset: 0)
+      ..commit();
+    return KeyEventResult.handled;
+  }
+  return KeyEventResult.ignored;
+};
+
+SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
+  name: () => 'Horizontal rule',
+  icon: const Icon(
+    Icons.horizontal_rule,
+    color: Colors.black,
+    size: 18.0,
+  ),
+  keywords: ['horizontal rule'],
+  handler: (editorState, _, __) {
+    final selection =
+        editorState.service.selectionService.currentSelection.value;
+    final textNodes = editorState.service.selectionService.currentSelectedNodes
+        .whereType<TextNode>();
+    if (selection == null || textNodes.isEmpty) {
+      return;
+    }
+    final textNode = textNodes.first;
+    if (textNode.toRawString().isEmpty) {
+      TransactionBuilder(editorState)
+        ..insertNode(
+          textNode.path,
+          Node(
+            type: 'horizontal_rule',
+            children: LinkedList(),
+            attributes: {},
+          ),
+        )
+        ..afterSelection =
+            Selection.single(path: textNode.path.next, startOffset: 0)
+        ..commit();
+    } else {
+      TransactionBuilder(editorState)
+        ..insertNode(
+          selection.end.path.next,
+          TextNode(
+            type: 'text',
+            children: LinkedList(),
+            attributes: {
+              'subtype': 'horizontal_rule',
+            },
+            delta: Delta()..insert('---'),
+          ),
+        )
+        ..afterSelection = selection
+        ..commit();
+    }
+  },
+);
+
+class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
+  @override
+  Widget build(NodeWidgetContext<Node> context) {
+    return _HorizontalRuleWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => (node) {
+        return true;
+      };
+}
+
+class _HorizontalRuleWidget extends StatefulWidget {
+  const _HorizontalRuleWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+
+  @override
+  State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
+}
+
+class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
+    with SelectableMixin {
+  RenderBox get _renderBox => context.findRenderObject() as RenderBox;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.symmetric(vertical: 10),
+      child: Container(
+        height: 1,
+        color: Colors.grey,
+      ),
+    );
+  }
+
+  @override
+  Position start() => Position(path: widget.node.path, offset: 0);
+
+  @override
+  Position end() => Position(path: widget.node.path, offset: 1);
+
+  @override
+  Position getPositionInOffset(Offset start) => end();
+
+  @override
+  bool get shouldCursorBlink => false;
+
+  @override
+  CursorStyle get cursorStyle => CursorStyle.borderLine;
+
+  @override
+  Rect? getCursorRectInPosition(Position position) {
+    final size = _renderBox.size;
+    return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
+  }
+
+  @override
+  List<Rect> getRectsInSelection(Selection selection) =>
+      [Offset.zero & _renderBox.size];
+
+  @override
+  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
+        path: widget.node.path,
+        startOffset: 0,
+        endOffset: 1,
+      );
+
+  @override
+  Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
+}

+ 193 - 0
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart

@@ -0,0 +1,193 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_math_fork/flutter_math.dart';
+
+SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
+  name: () => 'Tex',
+  icon: const Icon(
+    Icons.text_fields_rounded,
+    color: Colors.black,
+    size: 18.0,
+  ),
+  keywords: ['tex, latex, katex'],
+  handler: (editorState, _, __) {
+    final selection =
+        editorState.service.selectionService.currentSelection.value;
+    final textNodes = editorState.service.selectionService.currentSelectedNodes
+        .whereType<TextNode>();
+    if (selection == null || !selection.isCollapsed || textNodes.isEmpty) {
+      return;
+    }
+    final Path texNodePath;
+    if (textNodes.first.toRawString().isEmpty) {
+      texNodePath = selection.end.path;
+      TransactionBuilder(editorState)
+        ..insertNode(
+          selection.end.path,
+          Node(
+            type: 'tex',
+            children: LinkedList(),
+            attributes: {'tex': ''},
+          ),
+        )
+        ..deleteNode(textNodes.first)
+        ..afterSelection = selection
+        ..commit();
+    } else {
+      texNodePath = selection.end.path.next;
+      TransactionBuilder(editorState)
+        ..insertNode(
+          selection.end.path.next,
+          Node(
+            type: 'tex',
+            children: LinkedList(),
+            attributes: {'tex': ''},
+          ),
+        )
+        ..afterSelection = selection
+        ..commit();
+    }
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      final texState =
+          editorState.document.nodeAtPath(texNodePath)?.key?.currentState;
+      if (texState != null && texState is __TeXBlockNodeWidgetState) {
+        texState.showEditingDialog();
+      }
+    });
+  },
+);
+
+class TeXBlockNodeWidgetBuidler extends NodeWidgetBuilder<Node> {
+  @override
+  Widget build(NodeWidgetContext<Node> context) {
+    return _TeXBlockNodeWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => (node) {
+        return node.attributes['tex'] is String;
+      };
+}
+
+class _TeXBlockNodeWidget extends StatefulWidget {
+  const _TeXBlockNodeWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+
+  @override
+  State<_TeXBlockNodeWidget> createState() => __TeXBlockNodeWidgetState();
+}
+
+class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
+  String get _tex => widget.node.attributes['tex'] as String;
+  bool _isHover = false;
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      onHover: (value) {
+        setState(() {
+          _isHover = value;
+        });
+      },
+      onTap: () {
+        showEditingDialog();
+      },
+      child: Stack(
+        children: [
+          _buildTex(context),
+          if (_isHover) _buildDeleteButton(context),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildTex(BuildContext context) {
+    return Container(
+      width: MediaQuery.of(context).size.width,
+      padding: const EdgeInsets.symmetric(vertical: 20),
+      decoration: BoxDecoration(
+        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
+        color: _isHover ? Colors.grey[200] : Colors.transparent,
+      ),
+      child: Center(
+        child: Math.tex(
+          _tex,
+          textStyle: const TextStyle(fontSize: 20),
+          mathStyle: MathStyle.display,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildDeleteButton(BuildContext context) {
+    return Positioned(
+      top: -5,
+      right: -5,
+      child: IconButton(
+        icon: Icon(
+          Icons.delete_outline,
+          color: Colors.blue[400],
+          size: 16,
+        ),
+        onPressed: () {
+          TransactionBuilder(widget.editorState)
+            ..deleteNode(widget.node)
+            ..commit();
+        },
+      ),
+    );
+  }
+
+  void showEditingDialog() {
+    showDialog(
+      context: context,
+      builder: (context) {
+        final controller = TextEditingController(text: _tex);
+        return AlertDialog(
+          title: const Text('Edit Katex'),
+          content: TextField(
+            controller: controller,
+            maxLines: null,
+            decoration: const InputDecoration(
+              border: OutlineInputBorder(),
+            ),
+          ),
+          actions: [
+            TextButton(
+              onPressed: () {
+                Navigator.of(context).pop();
+              },
+              child: const Text('Cancel'),
+            ),
+            TextButton(
+              onPressed: () {
+                Navigator.of(context).pop();
+                if (controller.text != _tex) {
+                  TransactionBuilder(widget.editorState)
+                    ..updateNode(
+                      widget.node,
+                      {'tex': controller.text},
+                    )
+                    ..commit();
+                }
+              },
+              child: const Text('OK'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock

@@ -24,7 +24,7 @@ EXTERNAL SOURCES:
     :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
 
 SPEC CHECKSUMS:
-  FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
+  FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
   path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
   rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
   url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml

@@ -44,6 +44,7 @@ dependencies:
   file_picker: ^5.0.1
   universal_html: ^2.0.8
   highlight: ^0.7.0
+  flutter_math_fork: ^0.6.3+1
 
 dev_dependencies:
   flutter_test:

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_cs_CZ.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "cs-CZ",
+  "bold": "Tučně",
+  "@bold": {},
+  "bulletedList": "Odrážkový seznam",
+  "@bulletedList": {},
+  "checkbox": "Zaškrtávací políčko",
+  "@checkbox": {},
+  "embedCode": "Vložit kód",
+  "@embedCode": {},
+  "heading1": "Nadpis 1",
+  "@heading1": {},
+  "heading2": "Nadpis 2",
+  "@heading2": {},
+  "heading3": "Nadpis 3",
+  "@heading3": {},
+  "highlight": "Zvýraznění",
+  "@highlight": {},
+  "image": "Obrázek",
+  "@image": {},
+  "italic": "Kurzíva",
+  "@italic": {},
+  "link": "Odkaz",
+  "@link": {},
+  "numberedList": "Číslovaný seznam",
+  "@numberedList": {},
+  "quote": "Citace",
+  "@quote": {},
+  "strikethrough": "Přeškrtnutí",
+  "@strikethrough": {},
+  "text": "Text",
+  "@text": {},
+  "underline": "Podtržení",
+  "@underline": {}
+}

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb

@@ -1,35 +1,35 @@
 {
   "@@locale": "fr-CA",
-  "bold": "",
+  "bold": "gras",
   "@bold": {},
-  "bulletedList": "",
+  "bulletedList": "liste à puces",
   "@bulletedList": {},
-  "checkbox": "",
+  "checkbox": "case à cocher",
   "@checkbox": {},
-  "embedCode": "",
+  "embedCode": "incorporer Code",
   "@embedCode": {},
-  "heading1": "",
+  "heading1": "en-tête1",
   "@heading1": {},
-  "heading2": "",
+  "heading2": "en-tête2",
   "@heading2": {},
-  "heading3": "",
+  "heading3": "en-tête3",
   "@heading3": {},
-  "highlight": "",
+  "highlight": "mettre en évidence",
   "@highlight": {},
-  "image": "",
+  "image": "l’image",
   "@image": {},
-  "italic": "",
+  "italic": "italique",
   "@italic": {},
-  "link": "",
+  "link": "lien",
   "@link": {},
-  "numberedList": "",
+  "numberedList": "liste numérotée",
   "@numberedList": {},
-  "quote": "",
+  "quote": "citation",
   "@quote": {},
-  "strikethrough": "",
+  "strikethrough": "barré",
   "@strikethrough": {},
-  "text": "",
+  "text": "texte",
   "@text": {},
-  "underline": "",
+  "underline": "souligner",
   "@underline": {}
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb

@@ -1,35 +1,35 @@
 {
   "@@locale": "fr-FR",
-  "bold": "",
+  "bold": "Gras",
   "@bold": {},
-  "bulletedList": "",
+  "bulletedList": "List à puces",
   "@bulletedList": {},
-  "checkbox": "",
+  "checkbox": "Case à cocher",
   "@checkbox": {},
-  "embedCode": "",
+  "embedCode": "Incorporer code",
   "@embedCode": {},
-  "heading1": "",
+  "heading1": "Titre 1",
   "@heading1": {},
-  "heading2": "",
+  "heading2": "Titre 2",
   "@heading2": {},
-  "heading3": "",
+  "heading3": "Titre 3",
   "@heading3": {},
-  "highlight": "",
+  "highlight": "Surligné",
   "@highlight": {},
-  "image": "",
+  "image": "Image",
   "@image": {},
-  "italic": "",
+  "italic": "Italique",
   "@italic": {},
-  "link": "",
+  "link": "Lien",
   "@link": {},
-  "numberedList": "",
+  "numberedList": "Liste numérotée",
   "@numberedList": {},
-  "quote": "",
+  "quote": "Citation",
   "@quote": {},
-  "strikethrough": "",
+  "strikethrough": "Barré",
   "@strikethrough": {},
-  "text": "",
+  "text": "Texte",
   "@text": {},
-  "underline": "",
+  "underline": "Souligné",
   "@underline": {}
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb

@@ -1,35 +1,35 @@
 {
   "@@locale": "hu-HU",
-  "bold": "",
+  "bold": "bátor",
   "@bold": {},
-  "bulletedList": "",
+  "bulletedList": "pontozott lista",
   "@bulletedList": {},
-  "checkbox": "",
+  "checkbox": "jelölőnégyzetet",
   "@checkbox": {},
-  "embedCode": "",
+  "embedCode": "Beágyazás",
   "@embedCode": {},
-  "heading1": "",
+  "heading1": "címsor1",
   "@heading1": {},
-  "heading2": "",
+  "heading2": "címsor2",
   "@heading2": {},
-  "heading3": "",
+  "heading3": "címsor3",
   "@heading3": {},
-  "highlight": "",
+  "highlight": "Kiemel",
   "@highlight": {},
-  "image": "",
+  "image": "kép",
   "@image": {},
-  "italic": "",
+  "italic": "dőlt",
   "@italic": {},
-  "link": "",
+  "link": "link",
   "@link": {},
-  "numberedList": "",
+  "numberedList": "számozottLista",
   "@numberedList": {},
-  "quote": "",
+  "quote": "idézet",
   "@quote": {},
-  "strikethrough": "",
+  "strikethrough": "áthúzott",
   "@strikethrough": {},
-  "text": "",
+  "text": "szöveg",
   "@text": {},
-  "underline": "",
+  "underline": "aláhúzás",
   "@underline": {}
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb

@@ -1,35 +1,35 @@
 {
   "@@locale": "id-ID",
-  "bold": "",
+  "bold": "berani",
   "@bold": {},
-  "bulletedList": "",
+  "bulletedList": "daftar berpoin",
   "@bulletedList": {},
-  "checkbox": "",
+  "checkbox": "kotak centang",
   "@checkbox": {},
-  "embedCode": "",
+  "embedCode": "menyematkan Kode",
   "@embedCode": {},
-  "heading1": "",
+  "heading1": "pos1",
   "@heading1": {},
-  "heading2": "",
+  "heading2": "pos2",
   "@heading2": {},
-  "heading3": "",
+  "heading3": "pos3",
   "@heading3": {},
-  "highlight": "",
+  "highlight": "menyorot",
   "@highlight": {},
-  "image": "",
+  "image": "gambar",
   "@image": {},
-  "italic": "",
+  "italic": "miring",
   "@italic": {},
-  "link": "",
+  "link": "tautan",
   "@link": {},
-  "numberedList": "",
+  "numberedList": "daftar bernomor",
   "@numberedList": {},
-  "quote": "",
+  "quote": "mengutip",
   "@quote": {},
-  "strikethrough": "",
+  "strikethrough": "coret",
   "@strikethrough": {},
-  "text": "",
+  "text": "teks",
   "@text": {},
-  "underline": "",
+  "underline": "menggarisbawahi",
   "@underline": {}
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb

@@ -1,35 +1,35 @@
 {
   "@@locale": "it-IT",
-  "bold": "",
+  "bold": "Grassetto",
   "@bold": {},
-  "bulletedList": "",
+  "bulletedList": "Elenco puntato",
   "@bulletedList": {},
-  "checkbox": "",
+  "checkbox": "Casella di spunta",
   "@checkbox": {},
-  "embedCode": "",
+  "embedCode": "Incorpora codice",
   "@embedCode": {},
-  "heading1": "",
+  "heading1": "H1",
   "@heading1": {},
-  "heading2": "",
+  "heading2": "H2",
   "@heading2": {},
-  "heading3": "",
+  "heading3": "H3",
   "@heading3": {},
-  "highlight": "",
+  "highlight": "Evidenzia",
   "@highlight": {},
-  "image": "",
+  "image": "Immagine",
   "@image": {},
-  "italic": "",
+  "italic": "Corsivo",
   "@italic": {},
-  "link": "",
+  "link": "Collegamento",
   "@link": {},
-  "numberedList": "",
+  "numberedList": "Elenco numerato",
   "@numberedList": {},
-  "quote": "",
+  "quote": "Cita",
   "@quote": {},
-  "strikethrough": "",
+  "strikethrough": "Barrato",
   "@strikethrough": {},
-  "text": "",
+  "text": "Testo",
   "@text": {},
-  "underline": "",
+  "underline": "Sottolineato",
   "@underline": {}
 }

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ml_IN.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "ml_IN",
+  "bold": "ബോൾഡ്",
+  "@bold": {},
+  "bulletedList": "ബുള്ളറ്റഡ് പട്ടിക",
+  "@bulletedList": {},
+  "checkbox": "ചെക്ക്ബോക്സ്",
+  "@checkbox": {},
+  "embedCode": "എംബെഡഡ് കോഡ്",
+  "@embedCode": {},
+  "heading1": "തലക്കെട്ട് 1",
+  "@heading1": {},
+  "heading2": "തലക്കെട്ട് 2",
+  "@heading2": {},
+  "heading3": "തലക്കെട്ട് 3",
+  "@heading3": {},
+  "highlight": "പ്രമുഖമാക്കിക്കാട്ടുക",
+  "@highlight": {},
+  "image": "ചിത്രം",
+  "@image": {},
+  "italic": "ഇറ്റാലിക്",
+  "@italic": {},
+  "link": "ലിങ്ക്",
+  "@link": {},
+  "numberedList": "അക്കമിട്ട പട്ടിക",
+  "@numberedList": {},
+  "quote": "ഉദ്ധരണി",
+  "@quote": {},
+  "strikethrough": "സ്ട്രൈക്ക്ത്രൂ",
+  "@strikethrough": {},
+  "text": "വചനം",
+  "@text": {},
+  "underline": "അടിവരയിടുക",
+  "@underline": {}
+}

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb

@@ -1,35 +1,35 @@
 {
   "@@locale": "pt-PT",
-  "bold": "",
+  "bold": "negrito",
   "@bold": {},
-  "bulletedList": "",
+  "bulletedList": "lista com marcadores",
   "@bulletedList": {},
-  "checkbox": "",
+  "checkbox": "caixa de seleção",
   "@checkbox": {},
-  "embedCode": "",
+  "embedCode": "Código embutido",
   "@embedCode": {},
-  "heading1": "",
+  "heading1": "Cabeçallho 1",
   "@heading1": {},
-  "heading2": "",
+  "heading2": "Cabeçallho 2",
   "@heading2": {},
-  "heading3": "",
+  "heading3": "Cabeçallho 3",
   "@heading3": {},
-  "highlight": "",
+  "highlight": "realçar",
   "@highlight": {},
-  "image": "",
+  "image": "imagem",
   "@image": {},
-  "italic": "",
+  "italic": "itálico",
   "@italic": {},
-  "link": "",
+  "link": "link",
   "@link": {},
-  "numberedList": "",
+  "numberedList": "lista numerada",
   "@numberedList": {},
-  "quote": "",
+  "quote": "citar",
   "@quote": {},
-  "strikethrough": "",
+  "strikethrough": "tachado",
   "@strikethrough": {},
-  "text": "",
+  "text": "texto",
   "@text": {},
-  "underline": "",
+  "underline": "sublinhado",
   "@underline": {}
 }

+ 8 - 6
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart

@@ -17,9 +17,9 @@ extension NodeAttributesExtensions on Attributes {
     return containsKey(BuiltInAttributeKey.quote);
   }
 
-  int? get number {
+  num? get number {
     if (containsKey(BuiltInAttributeKey.number) &&
-        this[BuiltInAttributeKey.number] is int) {
+        this[BuiltInAttributeKey.number] is num) {
       return this[BuiltInAttributeKey.number];
     }
     return null;
@@ -27,7 +27,7 @@ extension NodeAttributesExtensions on Attributes {
 
   bool get code {
     if (containsKey(BuiltInAttributeKey.code) &&
-        this[BuiltInAttributeKey.code] == true) {
+        this[BuiltInAttributeKey.code] is bool) {
       return this[BuiltInAttributeKey.code];
     }
     return false;
@@ -63,11 +63,14 @@ extension DeltaAttributesExtensions on Attributes {
         this[BuiltInAttributeKey.strikethrough] == true);
   }
 
+  static const whiteInt = 0XFFFFFFFF;
+
   Color? get color {
     if (containsKey(BuiltInAttributeKey.color) &&
         this[BuiltInAttributeKey.color] is String) {
       return Color(
-        int.parse(this[BuiltInAttributeKey.color]),
+        // If the parse fails returns white by default
+        int.tryParse(this[BuiltInAttributeKey.color]) ?? whiteInt,
       );
     }
     return null;
@@ -77,8 +80,7 @@ extension DeltaAttributesExtensions on Attributes {
     if (containsKey(BuiltInAttributeKey.backgroundColor) &&
         this[BuiltInAttributeKey.backgroundColor] is String) {
       return Color(
-        int.parse(this[BuiltInAttributeKey.backgroundColor]),
-      );
+          int.tryParse(this[BuiltInAttributeKey.backgroundColor]) ?? whiteInt);
     }
     return null;
   }

+ 4 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart

@@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart';
 import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 extension TextNodeExtension on TextNode {
-  dynamic getAttributeInSelection(Selection selection, String styleKey) {
+  T? getAttributeInSelection<T>(Selection selection, String styleKey) {
     final ops = delta.whereType<TextInsert>();
     final startOffset =
         selection.isBackward ? selection.start.offset : selection.end.offset;
@@ -19,8 +19,9 @@ extension TextNodeExtension on TextNode {
       }
       final length = op.length;
       if (start < endOffset && start + length > startOffset) {
-        if (op.attributes?.containsKey(styleKey) == true) {
-          return op.attributes![styleKey];
+        final attributes = op.attributes;
+        if (attributes != null && attributes[styleKey] is T?) {
+          return attributes[styleKey];
         }
       }
       start += length;

+ 12 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart

@@ -16,6 +16,7 @@ import 'package:intl/message_lookup_by_library.dart';
 import 'package:intl/src/intl_helpers.dart';
 
 import 'messages_ca.dart' as messages_ca;
+import 'messages_cs-CZ.dart' as messages_cs_cz;
 import 'messages_de-DE.dart' as messages_de_de;
 import 'messages_en.dart' as messages_en;
 import 'messages_es-VE.dart' as messages_es_ve;
@@ -25,6 +26,8 @@ import 'messages_hu-HU.dart' as messages_hu_hu;
 import 'messages_id-ID.dart' as messages_id_id;
 import 'messages_it-IT.dart' as messages_it_it;
 import 'messages_ja-JP.dart' as messages_ja_jp;
+import 'messages_ml_IN.dart' as messages_ml_in;
+import 'messages_nl-NL.dart' as messages_nl_nl;
 import 'messages_pl-PL.dart' as messages_pl_pl;
 import 'messages_pt-BR.dart' as messages_pt_br;
 import 'messages_pt-PT.dart' as messages_pt_pt;
@@ -36,6 +39,7 @@ import 'messages_zh-TW.dart' as messages_zh_tw;
 typedef Future<dynamic> LibraryLoader();
 Map<String, LibraryLoader> _deferredLibraries = {
   'ca': () => new Future.value(null),
+  'cs_CZ': () => new Future.value(null),
   'de_DE': () => new Future.value(null),
   'en': () => new Future.value(null),
   'es_VE': () => new Future.value(null),
@@ -45,6 +49,8 @@ Map<String, LibraryLoader> _deferredLibraries = {
   'id_ID': () => new Future.value(null),
   'it_IT': () => new Future.value(null),
   'ja_JP': () => new Future.value(null),
+  'ml_IN': () => new Future.value(null),
+  'nl_NL': () => new Future.value(null),
   'pl_PL': () => new Future.value(null),
   'pt_BR': () => new Future.value(null),
   'pt_PT': () => new Future.value(null),
@@ -58,6 +64,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
   switch (localeName) {
     case 'ca':
       return messages_ca.messages;
+    case 'cs_CZ':
+      return messages_cs_cz.messages;
     case 'de_DE':
       return messages_de_de.messages;
     case 'en':
@@ -76,6 +84,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
       return messages_it_it.messages;
     case 'ja_JP':
       return messages_ja_jp.messages;
+    case 'ml_IN':
+      return messages_ml_in.messages;
+    case 'nl_NL':
+      return messages_nl_nl.messages;
     case 'pl_PL':
       return messages_pl_pl.messages;
     case 'pt_BR':

+ 44 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_cs-CZ.dart

@@ -0,0 +1,44 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a cs_CZ locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
+// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'cs_CZ';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage("Tučně"),
+        "bulletedList":
+            MessageLookupByLibrary.simpleMessage("Odrážkový seznam"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("Zaškrtávací políčko"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Vložit kód"),
+        "heading1": MessageLookupByLibrary.simpleMessage("Nadpis 1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("Nadpis 2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("Nadpis 3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Zvýraznění"),
+        "image": MessageLookupByLibrary.simpleMessage("Obrázek"),
+        "italic": MessageLookupByLibrary.simpleMessage("Kurzíva"),
+        "link": MessageLookupByLibrary.simpleMessage("Odkaz"),
+        "numberedList":
+            MessageLookupByLibrary.simpleMessage("Číslovaný seznam"),
+        "quote": MessageLookupByLibrary.simpleMessage("Citace"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("Přeškrtnutí"),
+        "text": MessageLookupByLibrary.simpleMessage("Text"),
+        "underline": MessageLookupByLibrary.simpleMessage("Podtržení")
+      };
+}

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart

@@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("gras"),
+        "bulletedList": MessageLookupByLibrary.simpleMessage("liste à puces"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("case à cocher"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("incorporer Code"),
+        "heading1": MessageLookupByLibrary.simpleMessage("en-tête1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("en-tête2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("en-tête3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("mettre en évidence"),
+        "image": MessageLookupByLibrary.simpleMessage("l’image"),
+        "italic": MessageLookupByLibrary.simpleMessage("italique"),
+        "link": MessageLookupByLibrary.simpleMessage("lien"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("liste numérotée"),
+        "quote": MessageLookupByLibrary.simpleMessage("citation"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("barré"),
+        "text": MessageLookupByLibrary.simpleMessage("texte"),
+        "underline": MessageLookupByLibrary.simpleMessage("souligner")
       };
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart

@@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("Gras"),
+        "bulletedList": MessageLookupByLibrary.simpleMessage("List à puces"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("Case à cocher"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Incorporer code"),
+        "heading1": MessageLookupByLibrary.simpleMessage("Titre 1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("Titre 2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("Titre 3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Surligné"),
+        "image": MessageLookupByLibrary.simpleMessage("Image"),
+        "italic": MessageLookupByLibrary.simpleMessage("Italique"),
+        "link": MessageLookupByLibrary.simpleMessage("Lien"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("Liste numérotée"),
+        "quote": MessageLookupByLibrary.simpleMessage("Citation"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("Barré"),
+        "text": MessageLookupByLibrary.simpleMessage("Texte"),
+        "underline": MessageLookupByLibrary.simpleMessage("Souligné")
       };
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart

@@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("bátor"),
+        "bulletedList": MessageLookupByLibrary.simpleMessage("pontozott lista"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("jelölőnégyzetet"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Beágyazás"),
+        "heading1": MessageLookupByLibrary.simpleMessage("címsor1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("címsor2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("címsor3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Kiemel"),
+        "image": MessageLookupByLibrary.simpleMessage("kép"),
+        "italic": MessageLookupByLibrary.simpleMessage("dőlt"),
+        "link": MessageLookupByLibrary.simpleMessage("link"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("számozottLista"),
+        "quote": MessageLookupByLibrary.simpleMessage("idézet"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("áthúzott"),
+        "text": MessageLookupByLibrary.simpleMessage("szöveg"),
+        "underline": MessageLookupByLibrary.simpleMessage("aláhúzás")
       };
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart

@@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("berani"),
+        "bulletedList": MessageLookupByLibrary.simpleMessage("daftar berpoin"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("kotak centang"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("menyematkan Kode"),
+        "heading1": MessageLookupByLibrary.simpleMessage("pos1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("pos2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("pos3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("menyorot"),
+        "image": MessageLookupByLibrary.simpleMessage("gambar"),
+        "italic": MessageLookupByLibrary.simpleMessage("miring"),
+        "link": MessageLookupByLibrary.simpleMessage("tautan"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("daftar bernomor"),
+        "quote": MessageLookupByLibrary.simpleMessage("mengutip"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("coret"),
+        "text": MessageLookupByLibrary.simpleMessage("teks"),
+        "underline": MessageLookupByLibrary.simpleMessage("menggarisbawahi")
       };
 }

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart

@@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("Grassetto"),
+        "bulletedList": MessageLookupByLibrary.simpleMessage("Elenco puntato"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("Casella di spunta"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Incorpora codice"),
+        "heading1": MessageLookupByLibrary.simpleMessage("H1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("H2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("H3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Evidenzia"),
+        "image": MessageLookupByLibrary.simpleMessage("Immagine"),
+        "italic": MessageLookupByLibrary.simpleMessage("Corsivo"),
+        "link": MessageLookupByLibrary.simpleMessage("Collegamento"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("Elenco numerato"),
+        "quote": MessageLookupByLibrary.simpleMessage("Cita"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("Barrato"),
+        "text": MessageLookupByLibrary.simpleMessage("Testo"),
+        "underline": MessageLookupByLibrary.simpleMessage("Sottolineato")
       };
 }

+ 45 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ml_IN.dart

@@ -0,0 +1,45 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a ml_IN locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
+// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'ml_IN';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage("ബോൾഡ്"),
+        "bulletedList":
+            MessageLookupByLibrary.simpleMessage("ബുള്ളറ്റഡ് പട്ടിക"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("ചെക്ക്ബോക്സ്"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("എംബെഡഡ് കോഡ്"),
+        "heading1": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 3"),
+        "highlight":
+            MessageLookupByLibrary.simpleMessage("പ്രമുഖമാക്കിക്കാട്ടുക"),
+        "image": MessageLookupByLibrary.simpleMessage("ചിത്രം"),
+        "italic": MessageLookupByLibrary.simpleMessage("ഇറ്റാലിക്"),
+        "link": MessageLookupByLibrary.simpleMessage("ലിങ്ക്"),
+        "numberedList":
+            MessageLookupByLibrary.simpleMessage("അക്കമിട്ട പട്ടിക"),
+        "quote": MessageLookupByLibrary.simpleMessage("ഉദ്ധരണി"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("സ്ട്രൈക്ക്ത്രൂ"),
+        "text": MessageLookupByLibrary.simpleMessage("വചനം"),
+        "underline": MessageLookupByLibrary.simpleMessage("അടിവരയിടുക")
+      };
+}

+ 43 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_nl-NL.dart

@@ -0,0 +1,43 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a nl_NL locale. All the
+// messages from the main program should be duplicated here with the same
+// function name.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
+// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
+// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
+// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
+// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+
+final messages = new MessageLookup();
+
+typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'nl_NL';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage("Vet"),
+        "bulletedList":
+            MessageLookupByLibrary.simpleMessage("Opsommingstekens"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("Selectievakje"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Invoegcode"),
+        "heading1": MessageLookupByLibrary.simpleMessage("H1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("H2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("H3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Highlight"),
+        "image": MessageLookupByLibrary.simpleMessage("Afbeelding"),
+        "italic": MessageLookupByLibrary.simpleMessage("Cursief"),
+        "link": MessageLookupByLibrary.simpleMessage(""),
+        "numberedList": MessageLookupByLibrary.simpleMessage("Nummering"),
+        "quote": MessageLookupByLibrary.simpleMessage("Quote"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("Doorhalen"),
+        "text": MessageLookupByLibrary.simpleMessage("Tekst"),
+        "underline": MessageLookupByLibrary.simpleMessage("Onderstrepen")
+      };
+}

+ 17 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart

@@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("Negrito"),
+        "bulletedList":
+            MessageLookupByLibrary.simpleMessage("Lista de marcadores"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("Caixa de seleção"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Código incorporado"),
+        "heading1": MessageLookupByLibrary.simpleMessage("H1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("H2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("H3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Destacar"),
+        "image": MessageLookupByLibrary.simpleMessage("Imagem"),
+        "italic": MessageLookupByLibrary.simpleMessage("Itálico"),
+        "link": MessageLookupByLibrary.simpleMessage("Link"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("Lista numerada"),
+        "quote": MessageLookupByLibrary.simpleMessage("Citar"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("Rasurar"),
+        "text": MessageLookupByLibrary.simpleMessage("Texto"),
+        "underline": MessageLookupByLibrary.simpleMessage("Sublinhar")
       };
 }

+ 17 - 16
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart

@@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary {
 
   final messages = _notInlinedMessages(_notInlinedMessages);
   static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
-        "bold": MessageLookupByLibrary.simpleMessage(""),
-        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
-        "checkbox": MessageLookupByLibrary.simpleMessage(""),
-        "embedCode": MessageLookupByLibrary.simpleMessage(""),
-        "heading1": MessageLookupByLibrary.simpleMessage(""),
-        "heading2": MessageLookupByLibrary.simpleMessage(""),
-        "heading3": MessageLookupByLibrary.simpleMessage(""),
-        "highlight": MessageLookupByLibrary.simpleMessage(""),
-        "image": MessageLookupByLibrary.simpleMessage(""),
-        "italic": MessageLookupByLibrary.simpleMessage(""),
-        "link": MessageLookupByLibrary.simpleMessage(""),
-        "numberedList": MessageLookupByLibrary.simpleMessage(""),
-        "quote": MessageLookupByLibrary.simpleMessage(""),
-        "strikethrough": MessageLookupByLibrary.simpleMessage(""),
-        "text": MessageLookupByLibrary.simpleMessage(""),
-        "underline": MessageLookupByLibrary.simpleMessage("")
+        "bold": MessageLookupByLibrary.simpleMessage("negrito"),
+        "bulletedList":
+            MessageLookupByLibrary.simpleMessage("lista com marcadores"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("caixa de seleção"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Código embutido"),
+        "heading1": MessageLookupByLibrary.simpleMessage("Cabeçallho 1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("Cabeçallho 2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("Cabeçallho 3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("realçar"),
+        "image": MessageLookupByLibrary.simpleMessage("imagem"),
+        "italic": MessageLookupByLibrary.simpleMessage("itálico"),
+        "link": MessageLookupByLibrary.simpleMessage("link"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("lista numerada"),
+        "quote": MessageLookupByLibrary.simpleMessage("citar"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("tachado"),
+        "text": MessageLookupByLibrary.simpleMessage("texto"),
+        "underline": MessageLookupByLibrary.simpleMessage("sublinhado")
       };
 }

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart

@@ -220,6 +220,7 @@ class AppLocalizationDelegate
     return const <Locale>[
       Locale.fromSubtags(languageCode: 'en'),
       Locale.fromSubtags(languageCode: 'ca'),
+      Locale.fromSubtags(languageCode: 'cs', countryCode: 'CZ'),
       Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'),
       Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'),
       Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'),
@@ -228,6 +229,8 @@ class AppLocalizationDelegate
       Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
       Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
       Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
+      Locale.fromSubtags(languageCode: 'ml', countryCode: 'IN'),
+      Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'),
       Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
       Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),
       Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:flutter/material.dart';
 
 abstract class BuiltInTextWidget extends StatefulWidget {

+ 25 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/cursor_widget.dart

@@ -1,5 +1,6 @@
 import 'dart:async';
 
+import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:flutter/material.dart';
 
 class CursorWidget extends StatefulWidget {
@@ -9,9 +10,13 @@ class CursorWidget extends StatefulWidget {
     required this.rect,
     required this.color,
     this.blinkingInterval = 0.5,
+    this.shouldBlink = true,
+    this.cursorStyle = CursorStyle.verticalLine,
   }) : super(key: key);
 
   final double blinkingInterval; // milliseconds
+  final bool shouldBlink;
+  final CursorStyle cursorStyle;
   final Color color;
   final Rect rect;
   final LayerLink layerLink;
@@ -67,11 +72,28 @@ class CursorWidgetState extends State<CursorWidget> {
         // Ignore the gestures in cursor
         //  to solve the problem that cursor area cannot be selected.
         child: IgnorePointer(
-          child: Container(
-            color: showCursor ? widget.color : Colors.transparent,
-          ),
+          child: _buildCursor(context),
         ),
       ),
     );
   }
+
+  Widget _buildCursor(BuildContext context) {
+    var color = widget.color;
+    if (widget.shouldBlink && !showCursor) {
+      color = Colors.transparent;
+    }
+    switch (widget.cursorStyle) {
+      case CursorStyle.verticalLine:
+        return Container(
+          color: color,
+        );
+      case CursorStyle.borderLine:
+        return Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: color, width: 2),
+          ),
+        );
+    }
+  }
 }

+ 9 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart

@@ -2,6 +2,11 @@ import 'package:appflowy_editor/src/document/position.dart';
 import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:flutter/material.dart';
 
+enum CursorStyle {
+  verticalLine,
+  borderLine,
+}
+
 /// [SelectableMixin] is used for the editor to calculate the position
 ///   and size of the selection.
 ///
@@ -53,4 +58,8 @@ mixin SelectableMixin<T extends StatefulWidget> on State<T> {
   Selection? getWorldBoundaryInOffset(Offset start) {
     return null;
   }
+
+  bool get shouldCursorBlink => true;
+
+  CursorStyle get cursorStyle => CursorStyle.verticalLine;
 }

+ 8 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart

@@ -169,6 +169,14 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
       insertBulletedListAfterSelection(editorState);
     },
   ),
+  SelectionMenuItem(
+    name: () => AppFlowyEditorLocalizations.current.numberedList,
+    icon: _selectionMenuIcon('number'),
+    keywords: ['numbered list', 'list', 'ordered list'],
+    handler: (editorState, _, __) {
+      insertNumberedListAfterSelection(editorState);
+    },
+  ),
   SelectionMenuItem(
     name: () => AppFlowyEditorLocalizations.current.checkbox,
     icon: _selectionMenuIcon('checkbox'),

+ 5 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart

@@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
 typedef SelectionMenuItemHandler = void Function(
-    EditorState editorState,
-    SelectionMenuService menuService,
-    BuildContext context,
-    );
+  EditorState editorState,
+  SelectionMenuService menuService,
+  BuildContext context,
+);
 
 /// Selection Menu Item
 class SelectionMenuItem {
@@ -23,7 +23,7 @@ class SelectionMenuItem {
     this.handler = (editorState, menuService, context) {
       _deleteToSlash(editorState);
       WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
-            handler(editorState, menuService, context);
+        handler(editorState, menuService, context);
       });
     };
   }

+ 4 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -333,8 +333,10 @@ void showLinkMenu(
   final textNode = node.first as TextNode;
   String? linkText;
   if (textNode.allSatisfyLinkInSelection(selection)) {
-    linkText =
-        textNode.getAttributeInSelection(selection, BuiltInAttributeKey.href);
+    linkText = textNode.getAttributeInSelection<String>(
+      selection,
+      BuiltInAttributeKey.href,
+    );
   }
   _linkMenuOverlay = OverlayEntry(builder: (context) {
     return Positioned(

+ 7 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart

@@ -34,6 +34,13 @@ void insertBulletedListAfterSelection(EditorState editorState) {
   });
 }
 
+void insertNumberedListAfterSelection(EditorState editorState) {
+  insertTextNodeAfterSelection(editorState, {
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
+    BuiltInAttributeKey.number: 1,
+  });
+}
+
 bool insertTextNodeAfterSelection(
     EditorState editorState, Attributes attributes) {
   final selection = editorState.service.selectionService.currentSelection.value;

+ 5 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart

@@ -3,7 +3,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_l
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/extensions/path_extensions.dart';
 
 // Handle delete text.
 ShortcutEventHandler deleteTextHandler = (editorState, event) {
@@ -84,6 +83,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
     }
   } else {
     if (textNodes.isEmpty) {
+      if (nonTextNodes.isNotEmpty) {
+        transactionBuilder.afterSelection =
+            Selection.collapsed(selection.start);
+      }
+      transactionBuilder.commit();
       return KeyEventResult.handled;
     }
     final startPosition = selection.start;

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -1,7 +1,6 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/infra/html_converter.dart';
 import 'package:appflowy_editor/src/document/node_iterator.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
 import 'package:flutter/material.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart

@@ -3,7 +3,6 @@ import 'dart:collection';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
 
-import 'package:appflowy_editor/src/extensions/path_extensions.dart';
 import './number_list_helper.dart';
 
 /// Handle some cases where enter is pressed and shift is not pressed.

+ 119 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart

@@ -1,4 +1,3 @@
-import "dart:math";
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
@@ -49,7 +48,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
       .substring(selection.start.offset, selection.end.offset);
 
   // toggle code style when selected some text
-  if (selectionText.length > 0) {
+  if (selectionText.isNotEmpty) {
     formatEmbedCode(editorState);
     return KeyEventResult.handled;
   }
@@ -124,3 +123,121 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
 
   return KeyEventResult.handled;
 };
+
+// convert ~~abc~~ to strikethrough abc.
+ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
+  final selectionService = editorState.service.selectionService;
+  final selection = selectionService.currentSelection.value;
+  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
+  if (selection == null || !selection.isSingle || textNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = textNodes.first;
+  final text = textNode.toRawString().substring(0, selection.end.offset);
+
+  // make sure the last two characters are ~~.
+  if (text.length < 2 || text[selection.end.offset - 1] != '~') {
+    return KeyEventResult.ignored;
+  }
+
+  // find all the index of `~`.
+  final tildeIndexes = <int>[];
+  for (var i = 0; i < text.length; i++) {
+    if (text[i] == '~') {
+      tildeIndexes.add(i);
+    }
+  }
+
+  if (tildeIndexes.length < 3) {
+    return KeyEventResult.ignored;
+  }
+
+  // make sure the second to last and third to last tildes are connected.
+  final thirdToLastTildeIndex = tildeIndexes[tildeIndexes.length - 3];
+  final secondToLastTildeIndex = tildeIndexes[tildeIndexes.length - 2];
+  final lastTildeIndex = tildeIndexes[tildeIndexes.length - 1];
+  if (secondToLastTildeIndex != thirdToLastTildeIndex + 1 ||
+      lastTildeIndex == secondToLastTildeIndex + 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // delete the last three tildes.
+  // update the style of the text surround by `~~ ~~` to strikethrough.
+  // and update the cursor position.
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, lastTildeIndex, 1)
+    ..deleteText(textNode, thirdToLastTildeIndex, 2)
+    ..formatText(
+      textNode,
+      thirdToLastTildeIndex,
+      selection.end.offset - thirdToLastTildeIndex - 2,
+      {
+        BuiltInAttributeKey.strikethrough: true,
+      },
+    )
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: selection.end.offset - 3,
+      ),
+    )
+    ..commit();
+
+  return KeyEventResult.handled;
+};
+
+/// To create a link, enclose the link text in brackets (e.g., [link text]).
+/// Then, immediately follow it with the URL in parentheses (e.g., (https://example.com)).
+ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
+  final selectionService = editorState.service.selectionService;
+  final selection = selectionService.currentSelection.value;
+  final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
+  if (selection == null || !selection.isSingle || textNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  // find all of the indexs for important characters
+  final textNode = textNodes.first;
+  final text = textNode.toRawString();
+  final firstOpeningBracket = text.indexOf('[');
+  final firstClosingBracket = text.indexOf(']');
+
+  // use regex to validate the format of the link
+  // note: this enforces that the link has http or https
+  final regexp = RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d./?=#]+)$');
+  final match = regexp.firstMatch(text);
+  if (match == null) {
+    return KeyEventResult.ignored;
+  }
+
+  // extract the text and the url of the link
+  final linkText = match.group(1);
+  final linkUrl = match.group(2);
+
+  // Delete the initial opening bracket,
+  // update the href attribute of the text surrounded by [ ] to the url,
+  // delete everything after the text,
+  // and update the cursor position.
+  TransactionBuilder(editorState)
+    ..deleteText(textNode, firstOpeningBracket, 1)
+    ..formatText(
+      textNode,
+      firstOpeningBracket,
+      firstClosingBracket - firstOpeningBracket - 1,
+      {
+        BuiltInAttributeKey.href: linkUrl,
+      },
+    )
+    ..deleteText(textNode, firstClosingBracket - 1,
+        selection.end.offset - firstClosingBracket)
+    ..afterSelection = Selection.collapsed(
+      Position(
+        path: textNode.path,
+        offset: firstOpeningBracket + linkText!.length,
+      ),
+    )
+    ..commit();
+
+  return KeyEventResult.handled;
+};

+ 2 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -457,6 +457,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
           rect: cursorRect,
           color: widget.cursorColor,
           layerLink: node.layerLink,
+          shouldBlink: selectable.shouldCursorBlink,
+          cursorStyle: selectable.cursorStyle,
         ),
       );
 

+ 11 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -263,10 +263,21 @@ List<ShortcutEvent> builtInShortcutEvents = [
     command: 'shift+underscore',
     handler: doubleUnderscoresToBold,
   ),
+  ShortcutEvent(
     key: 'Backquote to code',
     command: 'backquote',
     handler: backquoteToCodeHandler,
   ),
+  ShortcutEvent(
+    key: 'Double tilde to strikethrough',
+    command: 'shift+tilde',
+    handler: doubleTildeToStrikethrough,
+  ),
+  ShortcutEvent(
+    key: 'Markdown link to link',
+    command: 'shift+parenthesis right',
+    handler: markdownLinkToLinkHandler,
+  ),
   // https://github.com/flutter/flutter/issues/104944
   // Workaround: Using space editing on the web platform often results in errors,
   //  so adding a shortcut event to handle the space input instead of using the

+ 3 - 1
frontend/app_flowy/packages/appflowy_editor/pubspec.yaml

@@ -1,12 +1,13 @@
 name: appflowy_editor
 description: A highly customizable rich-text editor for Flutter
-version: 0.0.5
+version: 0.0.6
 homepage: https://github.com/AppFlowy-IO/AppFlowy
 
 platforms:
   linux:
   macos:
   windows:
+  web:
 
 environment:
   sdk: ">=2.17.0 <3.0.0"
@@ -32,6 +33,7 @@ dev_dependencies:
     sdk: flutter
   flutter_lints: ^2.0.1
   network_image_mock: ^2.1.1
+  mockito: ^5.3.2
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec

+ 201 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/attributes_extension_test.dart

@@ -0,0 +1,201 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('NodeAttributesExtensions::', () {
+    test('heading', () {
+      final Attributes attribute = {
+        'subtype': 'heading',
+        'heading': 'AppFlowy',
+      };
+      expect(attribute.heading, 'AppFlowy');
+    });
+
+    test('heading - text is not String return null', () {
+      final Attributes attribute = {
+        'subtype': 'heading',
+        'heading': 123,
+      };
+      expect(attribute.heading, null);
+    });
+
+    test('heading - subtype is not "heading" return null', () {
+      final Attributes attribute = {
+        'subtype': 'code',
+        'heading': 'Hello World!',
+      };
+      expect(attribute.heading, null);
+    });
+
+    test('quote', () {
+      final Attributes attribute = {
+        'quote': 'quote text',
+      };
+      expect(attribute.quote, true);
+    });
+
+    test('number - int', () {
+      final Attributes attribute = {
+        'number': 99,
+      };
+      expect(attribute.number, 99);
+    });
+
+    test('number - double', () {
+      final Attributes attribute = {
+        'number': 12.34,
+      };
+      expect(attribute.number, 12.34);
+    });
+
+    test('number - return null', () {
+      final Attributes attribute = {
+        'code': 12.34,
+      };
+      expect(attribute.number, null);
+    });
+
+    test('code', () {
+      final Attributes attribute = {
+        'code': true,
+      };
+      expect(attribute.code, true);
+    });
+
+    test('code - return false', () {
+      final Attributes attribute = {
+        'quote': true,
+      };
+      expect(attribute.code, false);
+    });
+
+    test('check', () {
+      final Attributes attribute = {
+        'checkbox': true,
+      };
+      expect(attribute.check, true);
+    });
+
+    test('check - return false', () {
+      final Attributes attribute = {
+        'quote': true,
+      };
+      expect(attribute.check, false);
+    });
+  });
+
+  group('DeltaAttributesExtensions::', () {
+    test('bold', () {
+      final Attributes attribute = {
+        'bold': true,
+      };
+      expect(attribute.bold, true);
+    });
+
+    test('bold - return false', () {
+      final Attributes attribute = {
+        'bold': 123,
+      };
+      expect(attribute.bold, false);
+    });
+
+    test('italic', () {
+      final Attributes attribute = {
+        'italic': true,
+      };
+      expect(attribute.italic, true);
+    });
+
+    test('italic - return false', () {
+      final Attributes attribute = {
+        'italic': 123,
+      };
+      expect(attribute.italic, false);
+    });
+
+    test('underline', () {
+      final Attributes attribute = {
+        'underline': true,
+      };
+      expect(attribute.underline, true);
+    });
+
+    test('underline - return false', () {
+      final Attributes attribute = {
+        'underline': 123,
+      };
+      expect(attribute.underline, false);
+    });
+
+    test('strikethrough', () {
+      final Attributes attribute = {
+        'strikethrough': true,
+      };
+      expect(attribute.strikethrough, true);
+    });
+
+    test('strikethrough - return false', () {
+      final Attributes attribute = {
+        'strikethrough': 123,
+      };
+      expect(attribute.strikethrough, false);
+    });
+
+    test('color', () {
+      final Attributes attribute = {
+        'color': '0xff212fff',
+      };
+      expect(attribute.color, const Color(0XFF212FFF));
+    });
+
+    test('color - return null', () {
+      final Attributes attribute = {
+        'color': 123,
+      };
+      expect(attribute.color, null);
+    });
+
+    test('color - parse failure return white', () {
+      final Attributes attribute = {
+        'color': 'hello123',
+      };
+      expect(attribute.color, const Color(0XFFFFFFFF));
+    });
+
+    test('backgroundColor', () {
+      final Attributes attribute = {
+        'backgroundColor': '0xff678fff',
+      };
+      expect(attribute.backgroundColor, const Color(0XFF678FFF));
+    });
+
+    test('backgroundColor - return null', () {
+      final Attributes attribute = {
+        'backgroundColor': 123,
+      };
+      expect(attribute.backgroundColor, null);
+    });
+
+    test('backgroundColor - parse failure return white', () {
+      final Attributes attribute = {
+        'backgroundColor': 'hello123',
+      };
+      expect(attribute.backgroundColor, const Color(0XFFFFFFFF));
+    });
+
+    test('href', () {
+      final Attributes attribute = {
+        'href': '/app/flowy',
+      };
+      expect(attribute.href, '/app/flowy');
+    });
+
+    test('href - return null', () {
+      final Attributes attribute = {
+        'href': 123,
+      };
+      expect(attribute.href, null);
+    });
+  });
+}

+ 40 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/color_extension_test.dart

@@ -0,0 +1,40 @@
+import 'package:appflowy_editor/src/extensions/color_extension.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('ColorExtension::', () {
+    const white = Color(0XFFFFFFFF);
+    const black = Color(0XFF000000);
+    const blue = Color(0XFF000FFF);
+    const blueRgba = 'rgba(0, 15, 255, 255)';
+    test('ToRgbaString', () {
+      expect(blue.toRgbaString(), 'rgba(0, 15, 255, 255)');
+      expect(white.toRgbaString(), 'rgba(255, 255, 255, 255)');
+      expect(black.toRgbaString(), 'rgba(0, 0, 0, 255)');
+    });
+
+    test('tryFromRgbaString', () {
+      final color = ColorExtension.tryFromRgbaString(blueRgba);
+      expect(color, const Color.fromARGB(255, 0, 15, 255));
+    });
+
+    test('tryFromRgbaString - wrong rgba format return null', () {
+      const wrongRgba = 'abc(1,2,3,4)';
+      final color = ColorExtension.tryFromRgbaString(wrongRgba);
+      expect(color, null);
+    });
+
+    test('tryFromRgbaString - wrong length return null', () {
+      const wrongRgba = 'rgba(0, 15, 255)';
+      final color = ColorExtension.tryFromRgbaString(wrongRgba);
+      expect(color, null);
+    });
+
+    test('tryFromRgbaString - wrong values return null', () {
+      const wrongRgba = 'rgba(-12, 999, 1234, 619)';
+      final color = ColorExtension.tryFromRgbaString(wrongRgba);
+      expect(color, null);
+    });
+  });
+}

+ 57 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/node_extension_test.dart

@@ -0,0 +1,57 @@
+import 'dart:collection';
+import 'dart:ui';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:appflowy_editor/src/extensions/node_extensions.dart';
+
+class MockNode extends Mock implements Node {}
+
+void main() {
+  final mockNode = MockNode();
+
+  group('NodeExtensions::', () {
+    final selection = Selection(
+      start: Position(path: [0]),
+      end: Position(path: [1]),
+    );
+
+    test('rect - renderBox is null', () {
+      when(mockNode.renderBox).thenReturn(null);
+      final result = mockNode.rect;
+      expect(result, Rect.zero);
+    });
+
+    test('inSelection', () {
+      // I use an empty implementation instead of mock, because the mocked
+      // version throws error trying to access the path.
+
+      final subLinkedList = LinkedList<Node>()
+        ..addAll([
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+          Node(type: 'type', children: LinkedList(), attributes: {}),
+        ]);
+
+      final linkedList = LinkedList<Node>()
+        ..addAll([
+          Node(
+            type: 'type',
+            children: subLinkedList,
+            attributes: {},
+          ),
+        ]);
+
+      final node = Node(
+        type: 'type',
+        children: linkedList,
+        attributes: {},
+      );
+      final result = node.inSelection(selection);
+      expect(result, false);
+    });
+  });
+}

+ 20 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/object_extension_test.dart

@@ -0,0 +1,20 @@
+import 'dart:io';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy_editor/src/extensions/object_extensions.dart';
+
+void main() {
+  group('FlowyObjectExtensions::', () {
+    test('unwrapOrNull', () {
+      final result = const TextSpan().unwrapOrNull<HitTestTarget>();
+      assert(result is TextSpan);
+    });
+
+    test('unwrapOrNull - return null', () {
+      final result = const TextSpan().unwrapOrNull<ServerSocket>();
+      expect(result, null);
+    });
+  });
+}

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart

@@ -1,6 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:appflowy_editor/src/extensions/path_extensions.dart';
 
 void main() async {
   setUpAll(() {

+ 7 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/text_node_extensions_test.dart

@@ -0,0 +1,7 @@
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('TextNodeExtension::', () {
+    test('description', () {});
+  });
+}

+ 43 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/text_style_extension_test.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+
+void main() {
+  group('TextStyleExtensions::', () {
+    const style = TextStyle(
+      color: Colors.blue,
+      backgroundColor: Colors.white,
+      fontSize: 14,
+      height: 100,
+      wordSpacing: 2,
+      fontWeight: FontWeight.w700,
+    );
+
+    const otherStyle = TextStyle(
+      color: Colors.red,
+      backgroundColor: Colors.black,
+      fontSize: 12,
+      height: 10,
+      wordSpacing: 1,
+    );
+    test('combine', () {
+      final result = style.combine(otherStyle);
+      expect(result.color, Colors.red);
+      expect(result.backgroundColor, Colors.black);
+      expect(result.fontSize, 12);
+      expect(result.height, 10);
+      expect(result.wordSpacing, 1);
+    });
+
+    test('combine - return this', () {
+      final result = style.combine(null);
+      expect(result, style);
+    });
+
+    test('combine - return null with inherit', () {
+      final styleCopy = otherStyle.copyWith(inherit: false);
+      final result = style.combine(styleCopy);
+      expect(result, styleCopy);
+    });
+  });
+}

+ 10 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/url_launcher_extension_test.dart

@@ -0,0 +1,10 @@
+import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('safeLaunchUrl without scheme', () async {
+    const href = null;
+    final result = await safeLaunchUrl(href);
+    expect(result, false);
+  });
+}

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart

@@ -145,6 +145,9 @@ extension on LogicalKeyboardKey {
     if (this == LogicalKeyboardKey.underscore) {
       return PhysicalKeyboardKey.minus;
     }
+    if (this == LogicalKeyboardKey.tilde) {
+      return PhysicalKeyboardKey.backquote;
+    }
     throw UnimplementedError();
   }
 }

+ 0 - 2
frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart

@@ -1,6 +1,4 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
-import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';

+ 7 - 7
frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart

@@ -1,7 +1,6 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
-import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
@@ -13,8 +12,8 @@ void main() async {
 
   group('selection_menu_widget.dart', () {
     for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) {
-      testWidgets('Selects number.$i item in selection menu with enter', (
-          tester) async {
+      testWidgets('Selects number.$i item in selection menu with enter',
+          (tester) async {
         final editor = await _prepare(tester);
         for (var j = 0; j < i; j++) {
           await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
@@ -30,8 +29,8 @@ void main() async {
         }
       });
 
-      testWidgets('Selects number.$i item in selection menu with click', (
-          tester) async {
+      testWidgets('Selects number.$i item in selection menu with click',
+          (tester) async {
         final editor = await _prepare(tester);
 
         await tester.tap(find.byType(SelectionMenuItemWidget).at(i));
@@ -59,7 +58,7 @@ void main() async {
       await editor.pressLogicKey(LogicalKeyboardKey.backspace);
       expect(
         find.byType(SelectionMenuItemWidget, skipOffstage: false),
-        findsNWidgets(4),
+        findsNWidgets(5),
       );
       await editor.pressLogicKey(LogicalKeyboardKey.keyE);
       expect(
@@ -148,7 +147,8 @@ Future<void> _testDefaultSelectionMenuItems(
     int index, EditorWidgetTester editor) async {
   expect(editor.documentLength, 4);
   expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0));
-  expect((editor.nodeAtPath([1]) as TextNode).toRawString(), 'Welcome to Appflowy 😁');
+  expect((editor.nodeAtPath([1]) as TextNode).toRawString(),
+      'Welcome to Appflowy 😁');
   final node = editor.nodeAtPath([2]);
   final item = defaultSelectionMenuItems[index];
   final itemName = item.name();

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart

@@ -2,7 +2,6 @@ import 'dart:collection';
 
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
-import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:network_image_mock/network_image_mock.dart';

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart

@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {

+ 106 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart

@@ -150,5 +150,111 @@ void main() async {
         expect(textNode.toRawString(), text);
       });
     });
+
+    group('convert double tilde to strikethrough', () {
+      Future<void> insertTilde(
+        EditorWidgetTester editor, {
+        int repeat = 1,
+      }) async {
+        for (var i = 0; i < repeat; i++) {
+          await editor.pressLogicKey(
+            LogicalKeyboardKey.tilde,
+            isShiftPressed: true,
+          );
+        }
+      }
+
+      testWidgets('~~AppFlowy~~ to strikethrough AppFlowy', (tester) async {
+        const text = '~~AppFlowy~';
+        final editor = tester.editor..insertTextNode('');
+        await editor.startTesting();
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        final textNode = editor.nodeAtPath([0]) as TextNode;
+        for (var i = 0; i < text.length; i++) {
+          await editor.insertText(textNode, text[i], i);
+        }
+        await insertTilde(editor);
+        final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allStrikethrough, true);
+        expect(textNode.toRawString(), 'AppFlowy');
+      });
+
+      testWidgets('App~~Flowy~~ to strikethrough AppFlowy', (tester) async {
+        const text = 'App~~Flowy~';
+        final editor = tester.editor..insertTextNode('');
+        await editor.startTesting();
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        final textNode = editor.nodeAtPath([0]) as TextNode;
+        for (var i = 0; i < text.length; i++) {
+          await editor.insertText(textNode, text[i], i);
+        }
+        await insertTilde(editor);
+        final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 3,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allStrikethrough, true);
+        expect(textNode.toRawString(), 'AppFlowy');
+      });
+
+      testWidgets('~~~AppFlowy~~ to bold ~AppFlowy', (tester) async {
+        const text = '~~~AppFlowy~';
+        final editor = tester.editor..insertTextNode('');
+        await editor.startTesting();
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        final textNode = editor.nodeAtPath([0]) as TextNode;
+        for (var i = 0; i < text.length; i++) {
+          await editor.insertText(textNode, text[i], i);
+        }
+        await insertTilde(editor);
+        final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 1,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allStrikethrough, true);
+        expect(textNode.toRawString(), '~AppFlowy');
+      });
+
+      testWidgets('~~~~ nothing changes', (tester) async {
+        const text = '~~~';
+        final editor = tester.editor..insertTextNode('');
+        await editor.startTesting();
+        await editor.updateSelection(
+          Selection.single(path: [0], startOffset: 0),
+        );
+        final textNode = editor.nodeAtPath([0]) as TextNode;
+        for (var i = 0; i < text.length; i++) {
+          await editor.insertText(textNode, text[i], i);
+        }
+        await insertTilde(editor);
+        final allStrikethrough = textNode.allSatisfyStrikethroughInSelection(
+          Selection.single(
+            path: [0],
+            startOffset: 0,
+            endOffset: textNode.toRawString().length,
+          ),
+        );
+        expect(allStrikethrough, false);
+        expect(textNode.toRawString(), text);
+      });
+    });
   });
 }

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart

@@ -1,7 +1,6 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
-import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart

@@ -3,7 +3,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespa
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart

@@ -4,7 +4,6 @@ import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../infra/test_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {

+ 6 - 0
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart

@@ -2,6 +2,7 @@ import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_infra/time/duration.dart';
+import 'package:flutter/services.dart';
 
 class RoundedInputField extends StatefulWidget {
   final String? hintText;
@@ -24,6 +25,7 @@ class RoundedInputField extends StatefulWidget {
   final FocusNode? focusNode;
   final TextEditingController? controller;
   final bool autoFocus;
+  final int? maxLength;
 
   const RoundedInputField({
     Key? key,
@@ -47,6 +49,7 @@ class RoundedInputField extends StatefulWidget {
     this.focusNode,
     this.controller,
     this.autoFocus = false,
+    this.maxLength,
   }) : super(key: key);
 
   @override
@@ -89,6 +92,9 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
           initialValue: widget.initialValue,
           focusNode: widget.focusNode,
           autofocus: widget.autoFocus,
+          maxLength: widget.maxLength,
+          maxLengthEnforcement:
+              MaxLengthEnforcement.truncateAfterCompositionEnds,
           onChanged: (value) {
             inputText = value;
             if (widget.onChanged != null) {

Some files were not shown because too many files changed in this diff