Browse Source

feat: integrate new editor (#2536)

* feat: update editor

* feat: integrate new editor

* feat: integrate the document2 into folder2

* feat: integrate the new editor

* chore: cargo fix

* chore: active document feature for flowy-error

* feat: convert the editor action to collab action

* feat: migrate the grid and board

* feat: migrate the callout block

* feat: migrate the divider

* chore: migrate the callout and math equation

* feat: migrate the code block

* feat: add shift + enter command in code block

* feat: support tab and shift+tab in code block

* fix: cursor error after inserting divider

* feat: migrate the grid and board

* feat: migrate the emoji picker

* feat: migrate openai

* feat: integrate floating toolbar

* feat: migrate the smart editor

* feat: migrate the cover

* feat: add option block action

* chore: implement block selection and delete option

* feat: support background color

* feat: dismiss color picker afer setting color

* feat: migrate the cover block

* feat: resize the font

* chore: cutomsize the padding

* chore: wrap the option button with ignore widget

* feat: customize the heading style

* chore: optimize the divider line height

* fix: the option button can't dismiss

* ci: rust test

* chore: flutter analyze

* fix: code block selection

* fix: dismiss the emoji picker after selecting one

* chore: optimize the transaction adapter

* fix: can't save the new content

* feat: show export page when some errors happen

* feat: implement get_view_data for document

---------

Co-authored-by: nathan <[email protected]>
Lucas.Xu 2 years ago
parent
commit
2202326278
100 changed files with 4443 additions and 2770 deletions
  1. 1 0
      frontend/appflowy_flutter/assets/images/editor/option.svg
  2. 1 1
      frontend/appflowy_flutter/integration_test/empty_document_test.dart
  3. 1 1
      frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart
  4. 3 3
      frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart
  5. 22 12
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart
  6. 113 195
      frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart
  7. 22 41
      frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart
  8. 93 0
      frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart
  9. 133 0
      frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart
  10. 3 3
      frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart
  11. 81 186
      frontend/appflowy_flutter/lib/plugins/document/document_page.dart
  12. 0 124
      frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart
  13. 247 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  14. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/editor_action_wrapper.dart
  15. 171 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart
  16. 170 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart
  17. 15 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/build_context_extension.dart
  18. 41 42
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart
  19. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/color_extension.dart
  20. 56 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart
  21. 26 25
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart
  22. 45 57
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart
  23. 56 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart
  24. 48 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart
  25. 5 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart
  26. 14 9
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart
  27. 8 13
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_menu_item.dart
  28. 76 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_node_widget.dart
  29. 6 13
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_view_menu_item.dart
  30. 203 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart
  31. 348 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart
  32. 229 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart
  33. 9 18
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart
  34. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart
  35. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker.dart
  36. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart
  37. 47 30
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart
  38. 15 23
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart
  39. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart
  40. 65 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/divider/divider_character_shortcut_event.dart
  41. 107 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/divider/divider_node_widget.dart
  42. 120 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart
  43. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart
  44. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/config.dart
  45. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart
  46. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_lists.dart
  47. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart
  48. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker_builder.dart
  49. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_view_state.dart
  50. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/category_models.dart
  51. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/emoji_model.dart
  52. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/recent_emoji_model.dart
  53. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart
  54. 8 12
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_menu_item.dart
  55. 76 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart
  56. 6 12
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_view_menu_item.dart
  57. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart
  58. 214 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart
  59. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart
  60. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart
  61. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart
  62. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart
  63. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart
  64. 418 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart
  65. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart
  66. 47 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart
  67. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart
  68. 134 91
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart
  69. 31 39
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart
  70. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart
  71. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart
  72. 2 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart
  73. 4 5
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart
  74. 125 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart
  75. 41 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart
  76. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart
  77. 3 3
      frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart
  78. 0 54
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_node_widget.dart
  79. 0 300
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/callout/callout_node_widget.dart
  80. 0 224
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_node_widget.dart
  81. 0 125
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_shortcut_event.dart
  82. 0 84
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_node_widget.dart
  83. 0 72
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_shortcut_event.dart
  84. 0 180
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_menu_item.dart
  85. 0 54
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_node_widget.dart
  86. 0 224
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/math_equation/math_equation_node_widget.dart
  87. 0 366
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart
  88. 0 23
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart
  89. 0 59
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart
  90. 6 6
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  91. 4 2
      frontend/appflowy_flutter/lib/user/application/user_service.dart
  92. 5 0
      frontend/appflowy_flutter/lib/util/base64_string.dart
  93. 8 0
      frontend/appflowy_flutter/lib/util/json_print.dart
  94. 6 4
      frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart
  95. 92 5
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart
  96. 13 4
      frontend/appflowy_flutter/pubspec.lock
  97. 7 1
      frontend/appflowy_flutter/pubspec.yaml
  98. 4 4
      frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart
  99. 581 14
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  100. 1 0
      frontend/rust-lib/Cargo.lock

+ 1 - 0
frontend/appflowy_flutter/assets/images/editor/option.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-width="2.6" d="M9.41 7.3H9.4M14.6 7.3h-.01M9.31 12H9.3M14.6 12h-.01M9.41 16.7H9.4M14.6 16.7h-.01"/></svg>

+ 1 - 1
frontend/appflowy_flutter/integration_test/empty_document_test.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';

+ 1 - 1
frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';

+ 3 - 3
frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart

@@ -1,8 +1,8 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
 import 'package:mocktail/mocktail.dart';
 import 'dart:convert';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_completion.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
 import 'package:http/http.dart' as http;
 import 'dart:async';
 

+ 22 - 12
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart

@@ -66,7 +66,12 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
             if (showOptions) {
               cells.add(const TypeOptionSeparator());
               cells.add(
-                SelectOptionColorList(selectedColor: state.option.color),
+                SelectOptionColorList(
+                  selectedColor: state.option.color,
+                  onSelectedColor: (color) => context
+                      .read<EditSelectOptionBloc>()
+                      .add(EditSelectOptionEvent.updateColor(color)),
+                ),
               );
             }
 
@@ -147,16 +152,22 @@ class _OptionNameTextField extends StatelessWidget {
 }
 
 class SelectOptionColorList extends StatelessWidget {
-  final SelectOptionColorPB selectedColor;
-  const SelectOptionColorList({required this.selectedColor, Key? key})
-      : super(key: key);
+  const SelectOptionColorList({
+    super.key,
+    this.selectedColor,
+    required this.onSelectedColor,
+  });
+
+  final SelectOptionColorPB? selectedColor;
+  final void Function(SelectOptionColorPB color) onSelectedColor;
 
   @override
   Widget build(BuildContext context) {
     final cells = SelectOptionColorPB.values.map((color) {
       return _SelectOptionColorCell(
         color: color,
-        isSelected: selectedColor == color,
+        isSelected: selectedColor != null ? selectedColor == color : false,
+        onSelectedColor: onSelectedColor,
       );
     }).toList();
 
@@ -193,14 +204,17 @@ class SelectOptionColorList extends StatelessWidget {
 }
 
 class _SelectOptionColorCell extends StatelessWidget {
-  final SelectOptionColorPB color;
-  final bool isSelected;
   const _SelectOptionColorCell({
     required this.color,
     required this.isSelected,
+    required this.onSelectedColor,
     Key? key,
   }) : super(key: key);
 
+  final SelectOptionColorPB color;
+  final bool isSelected;
+  final void Function(SelectOptionColorPB color) onSelectedColor;
+
   @override
   Widget build(BuildContext context) {
     Widget? checkmark;
@@ -228,11 +242,7 @@ class _SelectOptionColorCell extends StatelessWidget {
         ),
         leftIcon: colorIcon,
         rightIcon: checkmark,
-        onTap: () {
-          context
-              .read<EditSelectOptionBloc>()
-              .add(EditSelectOptionEvent.updateColor(color));
-        },
+        onTap: () => onSelectedColor(color),
       ),
     );
   }

+ 113 - 195
frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart

@@ -1,172 +1,161 @@
-import 'dart:convert';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
+import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
+import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
 import 'package:appflowy/plugins/trash/application/trash_service.dart';
 import 'package:appflowy/user/application/user_service.dart';
+import 'package:appflowy/util/json_print.dart';
 import 'package:appflowy/workspace/application/view/view_listener.dart';
 import 'package:appflowy/workspace/application/doc/doc_listener.dart';
 import 'package:appflowy/plugins/document/application/doc_service.dart';
-import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
 import 'package:appflowy_editor/appflowy_editor.dart'
-    show EditorState, Document, Transaction, Node;
+    show EditorState, LogLevel;
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:appflowy_backend/log.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:dartz/dartz.dart';
 import 'dart:async';
-import 'package:appflowy/util/either_extension.dart';
 part 'doc_bloc.freezed.dart';
 
 class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
-  final ViewPB view;
-  final DocumentService _documentService;
-  final DocumentListener _docListener;
-
-  final ViewListener _listener;
-  final TrashService _trashService;
-  EditorState? editorState;
-  StreamSubscription? _subscription;
-
   DocumentBloc({
     required this.view,
-  })  : _documentService = DocumentService(),
-        _docListener = DocumentListener(id: view.id),
-        _listener = ViewListener(view: view),
+  })  : _documentListener = DocumentListener(id: view.id),
+        _viewListener = ViewListener(view: view),
+        _documentService = DocumentService(),
         _trashService = TrashService(),
         super(DocumentState.initial()) {
-    on<DocumentEvent>((event, emit) async {
-      await event.map(
-        initial: (Initial value) async {
-          _listenOnDocChange();
-          await _initial(value, emit);
-          _listenOnViewChange();
-        },
-        deleted: (Deleted value) async {
-          emit(state.copyWith(isDeleted: true));
-        },
-        restore: (Restore value) async {
-          emit(state.copyWith(isDeleted: false));
-        },
-        deletePermanently: (DeletePermanently value) async {
-          final result = await _trashService.deleteViews([view.id]);
-
-          final newState = result.fold(
-            (l) => state.copyWith(forceClose: true),
-            (r) => state,
-          );
-          emit(newState);
-        },
-        restorePage: (RestorePage value) async {
-          final result = await _trashService.putback(view.id);
-          final newState = result.fold(
-            (l) => state.copyWith(isDeleted: false),
-            (r) => state,
-          );
-          emit(newState);
-        },
-      );
-    });
+    _transactionAdapter = TransactionAdapter(
+      documentId: view.id,
+      documentService: _documentService,
+    );
+    on<DocumentEvent>(_onDocumentEvent);
   }
 
-  @override
-  Future<void> close() async {
-    await _listener.stop();
+  final ViewPB view;
 
-    if (_subscription != null) {
-      await _subscription?.cancel();
-    }
+  final DocumentListener _documentListener;
+  final ViewListener _viewListener;
 
-    await _documentService.closeDocument(docId: view.id);
-    await _documentService.closeDocumentV2(view: view);
+  final DocumentService _documentService;
+  final TrashService _trashService;
+
+  late final TransactionAdapter _transactionAdapter;
+
+  EditorState? editorState;
+  StreamSubscription? _subscription;
+
+  @override
+  Future<void> close() async {
+    await _viewListener.stop();
+    await _subscription?.cancel();
+    await _documentService.closeDocument(view: view);
+    editorState?.cancelSubscription();
     return super.close();
   }
 
-  Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
-    final userProfile = await UserBackendService.getCurrentUserProfile();
-    if (userProfile.isRight()) {
-      return emit(
-        state.copyWith(
-          loadingState: DocumentLoadingState.finish(
-            right(userProfile.asRight()),
-          ),
-        ),
-      );
-    }
-    final result = await _documentService.openDocument(view: view);
-
-    return result.fold(
-      (documentData) async {
-        await _initEditorState(documentData).whenComplete(() {
-          emit(
-            state.copyWith(
-              loadingState: DocumentLoadingState.finish(left(unit)),
-              userProfilePB: userProfile.asLeft(),
-            ),
-          );
-        });
+  Future<void> _onDocumentEvent(
+    DocumentEvent event,
+    Emitter<DocumentState> emit,
+  ) async {
+    await event.map(
+      initial: (Initial value) async {
+        final state = await _fetchDocumentState();
+        await _subscribe(state);
+        emit(state);
       },
-      (err) async {
-        emit(
-          state.copyWith(
-            loadingState: DocumentLoadingState.finish(right(err)),
-          ),
-        );
+      deleted: (Deleted value) async {
+        emit(state.copyWith(isDeleted: true));
+      },
+      restore: (Restore value) async {
+        emit(state.copyWith(isDeleted: false));
+      },
+      deletePermanently: (DeletePermanently value) async {
+        final result = await _trashService.deleteViews([view.id]);
+        emit(state.copyWith(forceClose: result.swap().isLeft()));
+      },
+      restorePage: (RestorePage value) async {
+        final result = await _trashService.putback(view.id);
+        emit(state.copyWith(isDeleted: result.swap().isRight()));
       },
     );
   }
 
-  void _listenOnViewChange() {
-    _listener.start(
-      onViewDeleted: (result) {
-        result.fold(
-          (view) => add(const DocumentEvent.deleted()),
-          (error) {},
-        );
-      },
-      onViewRestored: (result) {
-        result.fold(
-          (view) => add(const DocumentEvent.restore()),
-          (error) {},
-        );
+  Future<void> _subscribe(DocumentState state) async {
+    _onViewChanged();
+    _onDocumentChanged();
+
+    // create the editor state
+    await state.loadingState.whenOrNull(
+      finish: (data) async => data.map((r) {
+        _initAppFlowyEditorState(r);
+      }),
+    );
+  }
+
+  /// subscribe to the view(document page) change
+  void _onViewChanged() {
+    _viewListener.start(
+      onViewDeleted: (r) =>
+          r.swap().map((r) => add(const DocumentEvent.deleted())),
+      onViewRestored: (r) =>
+          r.swap().map((r) => add(const DocumentEvent.restore())),
+    );
+  }
+
+  /// subscribe to the document content change
+  void _onDocumentChanged() {
+    _documentListener.start(
+      didReceiveUpdate: (docEvent) {
+        // todo: integrate the document change to the editor
+        // prettyPrintJson(docEvent.toProto3Json());
       },
     );
   }
 
-  void _listenOnDocChange() {
-    _docListener.start(
-      didReceiveUpdate: () {},
+  /// Fetch document
+  Future<DocumentState> _fetchDocumentState() async {
+    final result = await UserBackendService.getCurrentUserProfile().then(
+      (value) async => value.andThen(
+        // open the document
+        await _documentService.openDocument(view: view),
+      ),
+    );
+    return state.copyWith(
+      loadingState: DocumentLoadingState.finish(result),
     );
   }
 
-  Future<void> _initEditorState(DocumentDataPB documentData) async {
-    final document = Document.fromJson(jsonDecode(documentData.content));
+  Future<void> _initAppFlowyEditorState(DocumentDataPB2 data) async {
+    if (kDebugMode) {
+      prettyPrintJson(data.toProto3Json());
+    }
+
+    final document = data.toDocument();
+    if (document == null) {
+      assert(false, 'document is null');
+      return;
+    }
+
     final editorState = EditorState(document: document);
     this.editorState = editorState;
 
-    // listen on document change
-    _subscription = editorState.transactionStream.listen((transaction) {
-      final json = jsonEncode(TransactionAdaptor(transaction).toJson());
-      _documentService
-          .applyEdit(docId: view.id, operations: json)
-          .then((result) {
-        result.fold(
-          (l) => null,
-          (err) => Log.error(err),
-        );
-      });
+    // subscribe to the document change from the editor
+    _subscription = editorState.transactionStream.listen((transaction) async {
+      await _transactionAdapter.apply(transaction, editorState);
     });
-    // log
+
+    // output the log from the editor when debug mode
     if (kDebugMode) {
-      editorState.logConfiguration.handler = (log) {
-        Log.debug(log);
-      };
+      editorState.logConfiguration
+        ..level = LogLevel.all
+        ..handler = (log) {
+          Log.debug(log);
+        };
     }
-    // migration
-    final migration = DocumentMigration(editorState: editorState);
-    await migration.apply();
   }
 }
 
@@ -200,77 +189,6 @@ class DocumentState with _$DocumentState {
 class DocumentLoadingState with _$DocumentLoadingState {
   const factory DocumentLoadingState.loading() = _Loading;
   const factory DocumentLoadingState.finish(
-    Either<Unit, FlowyError> successOrFail,
+    Either<FlowyError, DocumentDataPB2> successOrFail,
   ) = _Finish;
 }
-
-/// Uses to erase the different between appflowy editor and the backend
-class TransactionAdaptor {
-  final Transaction transaction;
-  TransactionAdaptor(this.transaction);
-
-  Map<String, dynamic> toJson() {
-    final json = <String, dynamic>{};
-    if (transaction.operations.isNotEmpty) {
-      // The backend uses [0,0] as the beginning path, but the editor uses [0].
-      // So it needs to extend the path by inserting `0` at the head for all
-      // operations before passing to the backend.
-      json['operations'] = transaction.operations
-          .map((e) => e.copyWith(path: [0, ...e.path]).toJson())
-          .toList();
-    }
-    if (transaction.afterSelection != null) {
-      final selection = transaction.afterSelection!;
-      final start = selection.start;
-      final end = selection.end;
-      json['after_selection'] = selection
-          .copyWith(
-            start: start.copyWith(path: [0, ...start.path]),
-            end: end.copyWith(path: [0, ...end.path]),
-          )
-          .toJson();
-    }
-    if (transaction.beforeSelection != null) {
-      final selection = transaction.beforeSelection!;
-      final start = selection.start;
-      final end = selection.end;
-      json['before_selection'] = selection
-          .copyWith(
-            start: start.copyWith(path: [0, ...start.path]),
-            end: end.copyWith(path: [0, ...end.path]),
-          )
-          .toJson();
-    }
-    return json;
-  }
-}
-
-class DocumentMigration {
-  const DocumentMigration({
-    required this.editorState,
-  });
-
-  final EditorState editorState;
-
-  /// Migrate the document to the latest version.
-  Future<void> apply() async {
-    final transaction = editorState.transaction;
-
-    // A temporary solution to migrate the document to the latest version.
-    // Once the editor is stable, we can remove this.
-
-    // cover plugin
-    if (editorState.document.nodeAtPath([0])?.type != kCoverType) {
-      transaction.insertNode(
-        [0],
-        Node(type: kCoverType),
-      );
-    }
-
-    transaction.afterSelection = null;
-
-    if (transaction.operations.isNotEmpty) {
-      editorState.apply(transaction);
-    }
-  }
-}

+ 22 - 41
frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart

@@ -3,69 +3,50 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
 
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
 
 class DocumentService {
-  Future<Either<DocumentDataPB, FlowyError>> openDocument({
+  // unused now.
+  Future<Either<FlowyError, Unit>> createDocument({
     required ViewPB view,
   }) async {
-    await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
-
-    final payload = OpenDocumentPayloadPB()
-      ..documentId = view.id
-      ..version = DocumentVersionPB.V1;
-    // switch (view.dataFormat) {
-    //   case ViewDataFormatPB.DeltaFormat:
-    //     payload.documentVersion = DocumentVersionPB.V0;
-    //     break;
-    //   default:
-    //     break;
-    // }
-
-    return DocumentEventGetDocument(payload).send();
-  }
-
-  Future<Either<Unit, FlowyError>> applyEdit({
-    required String docId,
-    required String operations,
-  }) {
-    final payload = EditPayloadPB.create()
-      ..docId = docId
-      ..operations = operations;
-    return DocumentEventApplyEdit(payload).send();
+    final canOpen = await openDocument(view: view);
+    if (canOpen.isRight()) {
+      return const Right(unit);
+    }
+    final payload = CreateDocumentPayloadPBV2()..documentId = view.id;
+    final result = await DocumentEvent2CreateDocument(payload).send();
+    return result.swap();
   }
 
-  Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
-    final payload = ViewIdPB(value: docId);
-    return FolderEventCloseView(payload).send();
-  }
-
-  Future<Either<DocumentDataPB2, FlowyError>> openDocumentV2({
+  Future<Either<FlowyError, DocumentDataPB2>> openDocument({
     required ViewPB view,
   }) async {
+    // set the latest view
     await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
 
     final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
-
-    return DocumentEvent2OpenDocument(payload).send();
+    final result = await DocumentEvent2OpenDocument(payload).send();
+    return result.swap();
   }
 
-  Future<Either<Unit, FlowyError>> closeDocumentV2({
+  Future<Either<FlowyError, Unit>> closeDocument({
     required ViewPB view,
   }) async {
     final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
-    return DocumentEvent2CloseDocument(payload).send();
+    final result = await DocumentEvent2CloseDocument(payload).send();
+    return result.swap();
   }
 
-  Future<Either<Unit, FlowyError>> applyAction({
-    required ViewPB view,
-    required List<BlockActionPB> actions,
+  Future<Either<FlowyError, Unit>> applyAction({
+    required String documentId,
+    required Iterable<BlockActionPB> actions,
   }) async {
     final payload = ApplyActionPayloadPBV2(
-      documentId: view.id,
+      documentId: documentId,
       actions: actions,
     );
-    return DocumentEvent2ApplyAction(payload).send();
+    final result = await DocumentEvent2ApplyAction(payload).send();
+    return result.swap();
   }
 }

+ 93 - 0
frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart

@@ -0,0 +1,93 @@
+import 'dart:convert';
+
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart'
+    show Document, Node, Attributes, Delta, ParagraphBlockKeys;
+
+extension AppFlowyEditor on DocumentDataPB2 {
+  Document? toDocument() {
+    final rootId = pageId;
+    try {
+      final root = buildNode(rootId);
+      return Document(root: root);
+    } catch (e) {
+      Log.error('create document error: $e');
+      return null;
+    }
+  }
+
+  Node buildNode(String id) {
+    final block = blocks[id]!; // TODO: don't use force unwrap
+    final childrenId = block.childrenId;
+    final childrenIds = meta.childrenMap[childrenId]?.children;
+    final children = <Node>[];
+    if (childrenIds != null && childrenIds.isNotEmpty) {
+      children.addAll(childrenIds.map((e) => buildNode(e)));
+    }
+    return block.toNode(children: children);
+  }
+}
+
+class _BackendKeys {
+  const _BackendKeys._();
+
+  static const String page = 'page';
+  static const String text = 'text';
+}
+
+extension BlockToNode on BlockPB {
+  Node toNode({
+    Iterable<Node>? children,
+  }) {
+    return Node(
+      id: id,
+      type: _typeAdapter(ty),
+      attributes: _dataAdapter(ty, data),
+      children: children ?? [],
+    );
+  }
+
+  String _typeAdapter(String ty) {
+    final adapter = {
+      _BackendKeys.page: 'document',
+      _BackendKeys.text: ParagraphBlockKeys.type,
+    };
+    return adapter[ty] ?? ty;
+  }
+
+  Attributes _dataAdapter(String ty, String data) {
+    final map = Attributes.from(jsonDecode(data));
+    final adapter = {
+      'text': (Attributes map) => map
+        ..putIfAbsent(
+          'delta',
+          () => Delta().toJson(),
+        ),
+    };
+    return adapter[ty]?.call(map) ?? map;
+  }
+}
+
+extension NodeToBlock on Node {
+  BlockPB toBlock() {
+    assert(id.isNotEmpty);
+    final block = BlockPB.create()
+      ..id = id
+      ..ty = _typeAdapter(type)
+      ..data = _dataAdapter(type, attributes);
+    return block;
+  }
+
+  String _typeAdapter(String type) {
+    final adapter = {
+      'document': 'page',
+      'paragraph': 'text',
+    };
+    return adapter[type] ?? type;
+  }
+
+  String _dataAdapter(String type, Attributes attributes) {
+    return jsonEncode(attributes);
+  }
+}

+ 133 - 0
frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart

@@ -0,0 +1,133 @@
+import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
+import 'package:appflowy/plugins/document/application/doc_service.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart'
+    show
+        EditorState,
+        Transaction,
+        Operation,
+        InsertOperation,
+        UpdateOperation,
+        DeleteOperation,
+        PathExtensions,
+        Node;
+import 'package:collection/collection.dart';
+import 'dart:async';
+
+/// Uses to adjust the data structure between the editor and the backend.
+///
+/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
+/// This adapter is used to convert the editor's transaction to the backend's transaction.
+class TransactionAdapter {
+  TransactionAdapter({
+    required this.documentId,
+    required this.documentService,
+  });
+
+  final DocumentService documentService;
+  final String documentId;
+
+  Future<void> apply(Transaction transaction, EditorState editorState) async {
+    final actions = transaction.operations
+        .map((op) => op.toBlockAction(editorState))
+        .whereNotNull()
+        .expand((element) => element);
+    Log.debug('actions => $actions');
+    await documentService.applyAction(
+      documentId: documentId,
+      actions: actions,
+    );
+  }
+}
+
+extension on Operation {
+  List<BlockActionPB> toBlockAction(EditorState editorState) {
+    final op = this;
+    if (op is InsertOperation) {
+      return op.toBlockAction(editorState);
+    } else if (op is UpdateOperation) {
+      return op.toBlockAction(editorState);
+    } else if (op is DeleteOperation) {
+      return op.toBlockAction(editorState);
+    }
+    throw UnimplementedError();
+  }
+}
+
+extension on InsertOperation {
+  List<BlockActionPB> toBlockAction(EditorState editorState) {
+    final List<BlockActionPB> actions = [];
+    // store the previous node for continuous insertion.
+    // because the backend needs to know the previous node's id.
+    Node? previousNode;
+    for (final node in nodes) {
+      final parentId =
+          node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
+      final prevId = previousNode?.id ??
+          node.previous?.id ??
+          editorState.getNodeAtPath(path.previous)?.id ??
+          '';
+      assert(parentId.isNotEmpty && prevId.isNotEmpty);
+      final payload = BlockActionPayloadPB()
+        ..block = node.toBlock()
+        ..parentId = parentId
+        ..prevId = prevId;
+      actions.add(
+        BlockActionPB()
+          ..action = BlockActionTypePB.Insert
+          ..payload = payload,
+      );
+      previousNode = node;
+    }
+    return actions;
+  }
+}
+
+extension on UpdateOperation {
+  List<BlockActionPB> toBlockAction(EditorState editorState) {
+    final List<BlockActionPB> actions = [];
+
+    // if the attributes are both empty, we don't need to update
+    if (const DeepCollectionEquality().equals(attributes, oldAttributes)) {
+      return actions;
+    }
+    final node = editorState.getNodeAtPath(path);
+    if (node == null) {
+      assert(false, 'node not found at path: $path');
+      return actions;
+    }
+    final parentId =
+        node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
+    assert(parentId.isNotEmpty);
+    final payload = BlockActionPayloadPB()
+      ..block = node.toBlock()
+      ..parentId = parentId;
+    actions.add(
+      BlockActionPB()
+        ..action = BlockActionTypePB.Update
+        ..payload = payload,
+    );
+    return actions;
+  }
+}
+
+extension on DeleteOperation {
+  List<BlockActionPB> toBlockAction(EditorState editorState) {
+    final List<BlockActionPB> actions = [];
+    for (final node in nodes) {
+      final parentId =
+          node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
+      final payload = BlockActionPayloadPB()
+        ..block = node.toBlock()
+        ..parentId = parentId;
+      assert(parentId.isNotEmpty);
+      actions.add(
+        BlockActionPB()
+          ..action = BlockActionTypePB.Delete
+          ..payload = payload,
+      );
+    }
+    return actions;
+  }
+}

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart

@@ -1,9 +1,9 @@
 import 'dart:convert';
 import 'dart:io';
 import 'package:appflowy/plugins/document/application/share_service.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart';
 import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';

+ 81 - 186
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -1,41 +1,51 @@
-import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:appflowy/plugins/document/application/doc_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/banner.dart';
+import 'package:appflowy/plugins/document/presentation/editor_page.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/util/base64_string.dart';
+import 'package:appflowy/util/file_picker/file_picker_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:dartz/dartz.dart' as dartz;
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:intl/intl.dart';
-
-import '../../startup/startup.dart';
-import 'application/doc_bloc.dart';
-import 'editor_styles.dart';
-import 'presentation/banner.dart';
+import 'package:path/path.dart' as p;
 
 class DocumentPage extends StatefulWidget {
+  const DocumentPage({
+    super.key,
+    required this.onDeleted,
+    required this.view,
+  });
+
   final VoidCallback onDeleted;
   final ViewPB view;
 
-  DocumentPage({
-    required this.view,
-    required this.onDeleted,
-    Key? key,
-  }) : super(key: ValueKey(view.id));
-
   @override
   State<DocumentPage> createState() => _DocumentPageState();
 }
 
 class _DocumentPageState extends State<DocumentPage> {
-  late DocumentBloc documentBloc;
+  late final DocumentBloc documentBloc;
+  EditorState? editorState;
 
   @override
   void initState() {
+    super.initState();
+
+    documentBloc = getIt<DocumentBloc>(param1: widget.view)
+      ..add(const DocumentEvent.initial());
+
     // The appflowy editor use Intl as localization, set the default language as fallback.
     Intl.defaultLocale = 'en_US';
-    documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
-      ..add(const DocumentEvent.initial());
-    super.initState();
   }
 
   @override
@@ -46,28 +56,29 @@ class _DocumentPageState extends State<DocumentPage> {
 
   @override
   Widget build(BuildContext context) {
-    return MultiBlocProvider(
-      providers: [
-        BlocProvider<DocumentBloc>.value(value: documentBloc),
-      ],
+    return BlocProvider.value(
+      value: documentBloc,
       child: BlocBuilder<DocumentBloc, DocumentState>(
         builder: (context, state) {
-          return state.loadingState.map(
-            loading: (_) => SizedBox.expand(
-              child: Container(color: Colors.transparent),
-            ),
-            finish: (result) => result.successOrFail.fold(
-              (_) {
+          return state.loadingState.when(
+            loading: () => const SizedBox.shrink(),
+            finish: (result) => result.fold(
+              (error) => FlowyErrorPage(error.toString()),
+              (data) {
                 if (state.forceClose) {
                   widget.onDeleted();
-                  return const SizedBox();
+                  return const SizedBox.shrink();
                 } else if (documentBloc.editorState == null) {
-                  return const SizedBox();
+                  return Center(
+                    child: ExportPageWidget(
+                      onTap: () async => await _exportPage(data),
+                    ),
+                  );
                 } else {
-                  return _renderDocument(context, state);
+                  editorState = documentBloc.editorState!;
+                  return _buildEditorPage(context, state);
                 }
               },
-              (err) => FlowyErrorPage(err.toString()),
             ),
           );
         },
@@ -75,177 +86,61 @@ class _DocumentPageState extends State<DocumentPage> {
     );
   }
 
-  Widget _renderDocument(BuildContext context, DocumentState state) {
+  Widget _buildEditorPage(BuildContext context, DocumentState state) {
+    final appflowyEditorPage = AppFlowyEditorPage(
+      editorState: editorState!,
+    );
     return Column(
       children: [
-        if (state.isDeleted) _renderBanner(context),
-        // AppFlowy Editor
-        const _AppFlowyEditorPage(),
+        if (state.isDeleted) _buildBanner(context),
+        _buildCoverAndIcon(context),
+        Expanded(
+          child: appflowyEditorPage,
+        ),
       ],
     );
   }
 
-  Widget _renderBanner(BuildContext context) {
+  Widget _buildBanner(BuildContext context) {
     return DocumentBanner(
-      onRestore: () =>
-          context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),
-      onDelete: () => context
-          .read<DocumentBloc>()
-          .add(const DocumentEvent.deletePermanently()),
+      onRestore: () => documentBloc.add(const DocumentEvent.restorePage()),
+      onDelete: () => documentBloc.add(const DocumentEvent.deletePermanently()),
     );
   }
-}
-
-class _AppFlowyEditorPage extends StatefulWidget {
-  const _AppFlowyEditorPage({
-    Key? key,
-  }) : super(key: key);
 
-  @override
-  State<_AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
-}
-
-class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
-  late DocumentBloc documentBloc;
-  late EditorState editorState;
-  String? get openAIKey => documentBloc.state.userProfilePB?.openaiKey;
-
-  @override
-  void initState() {
-    super.initState();
-    documentBloc = context.read<DocumentBloc>();
-    editorState = documentBloc.editorState ?? EditorState.empty();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = Theme.of(context);
-    final autoFocusParameters = _autoFocusParameters();
-    final editor = AppFlowyEditor(
-      editorState: editorState,
-      autoFocus: autoFocusParameters.value1,
-      focusedSelection: autoFocusParameters.value2,
-      customBuilders: {
-        // Divider
-        kDividerType: DividerWidgetBuilder(),
-        // Math Equation
-        kMathEquationType: MathEquationNodeWidgetBuidler(),
-        // Code Block
-        kCodeBlockType: CodeBlockNodeWidgetBuilder(),
-        // Board
-        kBoardType: BoardNodeWidgetBuilder(),
-        // Grid
-        kGridType: GridNodeWidgetBuilder(),
-        // Card
-        kCalloutType: CalloutNodeWidgetBuilder(),
-        // Auto Generator,
-        kAutoCompletionInputType: AutoCompletionInputBuilder(),
-        // Cover
-        kCoverType: CoverNodeWidgetBuilder(),
-        // Smart Edit,
-        kSmartEditType: SmartEditInputBuilder(),
-      },
-      shortcutEvents: [
-        // Divider
-        insertDividerEvent,
-        // Code Block
-        enterInCodeBlock,
-        ignoreKeysInCodeBlock,
-        pasteInCodeBlock,
-      ],
-      selectionMenuItems: [
-        // Divider
-        dividerMenuItem,
-        // Math Equation
-        mathEquationMenuItem,
-        // Code Block
-        codeBlockMenuItem,
-        // Emoji
-        emojiMenuItem,
-        // Board
-        boardMenuItem,
-        // Create Board
-        boardViewMenuItem(documentBloc),
-        // Grid
-        gridMenuItem,
-        // Create Grid
-        gridViewMenuItem(documentBloc),
-        // Callout
-        calloutMenuItem,
-        // AI
-        // enable open ai features if needed.
-        if (openAIKey != null && openAIKey!.isNotEmpty) ...[
-          autoGeneratorMenuItem,
-        ],
-      ],
-      toolbarItems: [
-        smartEditItem,
-      ],
-      themeData: theme.copyWith(
-        extensions: [
-          ...theme.extensions.values,
-          customEditorTheme(context),
-          ...customPluginTheme(context),
-        ],
-      ),
-    );
-    return Expanded(
-      child: Center(
-        child: Container(
-          constraints: const BoxConstraints(
-            maxWidth: double.infinity,
-          ),
-          child: editor,
-        ),
-      ),
+  Widget _buildCoverAndIcon(BuildContext context) {
+    if (editorState == null) {
+      return const Placeholder();
+    }
+    final page = editorState!.document.root;
+    return CoverImageNodeWidget(
+      node: page,
+      editorState: editorState!,
     );
   }
 
-  @override
-  void dispose() {
-    _clearTemporaryNodes();
-    super.dispose();
-  }
-
-  Future<void> _clearTemporaryNodes() async {
-    final document = editorState.document;
-    if (document.root.children.isEmpty) {
+  Future<void> _exportPage(DocumentDataPB2 data) async {
+    final picker = getIt<FilePickerService>();
+    final dir = await picker.getDirectoryPath();
+    if (dir == null) {
       return;
     }
-    final temporaryNodeTypes = [
-      kAutoCompletionInputType,
-      kSmartEditType,
-    ];
-    final iterator = NodeIterator(
-      document: document,
-      startNode: document.root.children.first,
-    );
-    final transaction = editorState.transaction;
-    while (iterator.moveNext()) {
-      final node = iterator.current;
-      if (temporaryNodeTypes.contains(node.type)) {
-        transaction.deleteNode(node);
-      }
-      if (kCoverType == node.type && !node.path.equals([0])) {
-        transaction.deleteNode(node);
-      }
-    }
-    if (transaction.operations.isNotEmpty) {
-      await editorState.apply(transaction, withUpdateCursor: false);
-    }
+    final path = p.join(dir, '${documentBloc.view.name}.json');
+    const encoder = JsonEncoder.withIndent('  ');
+    final json = encoder.convert(data.toProto3Json());
+    await File(path).writeAsString(json.base64.base64);
+
+    _showMessage('Export success to $path');
   }
 
-  dartz.Tuple2<bool, Selection?> _autoFocusParameters() {
-    if (editorState.document.isEmpty) {
-      return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
-    }
-    final texts = editorState.document.root.children.whereType<TextNode>();
-    if (texts.every((element) => element.toPlainText().isEmpty)) {
-      return dartz.Tuple2(
-        true,
-        Selection.single(path: texts.first.path, startOffset: 0),
-      );
+  void _showMessage(String message) {
+    if (!mounted) {
+      return;
     }
-    return const dartz.Tuple2(false, null);
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(
+        content: FlowyText(message),
+      ),
+    );
   }
 }

+ 0 - 124
frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart

@@ -1,124 +0,0 @@
-import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-import 'package:google_fonts/google_fonts.dart';
-import 'package:provider/provider.dart';
-
-EditorStyle customEditorTheme(BuildContext context) {
-  final documentStyle = context.watch<DocumentAppearanceCubit>().state;
-  final theme = Theme.of(context);
-
-  var editorStyle = EditorStyle(
-    // Editor styles
-    padding: const EdgeInsets.symmetric(horizontal: 100),
-    backgroundColor: theme.colorScheme.surface,
-    cursorColor: theme.colorScheme.primary,
-    // Text styles
-    textPadding: const EdgeInsets.symmetric(vertical: 8.0),
-    textStyle: TextStyle(
-      fontFamily: 'poppins',
-      fontSize: documentStyle.fontSize,
-      color: theme.colorScheme.onBackground,
-    ),
-    selectionColor: theme.colorScheme.tertiary.withOpacity(0.2),
-    // Selection menu
-    selectionMenuBackgroundColor: theme.cardColor,
-    selectionMenuItemTextColor: theme.iconTheme.color,
-    selectionMenuItemIconColor: theme.colorScheme.onBackground,
-    selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface,
-    selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface,
-    selectionMenuItemSelectedColor: theme.hoverColor,
-    // Toolbar and its item's style
-    toolbarColor: theme.colorScheme.onTertiary,
-    toolbarElevation: 0,
-    lineHeight: 1.5,
-    placeholderTextStyle:
-        TextStyle(fontSize: documentStyle.fontSize, color: theme.hintColor),
-    bold: const TextStyle(
-      fontFamily: 'poppins-Bold',
-      fontWeight: FontWeight.w600,
-    ),
-    italic: const TextStyle(fontStyle: FontStyle.italic),
-    underline: const TextStyle(decoration: TextDecoration.underline),
-    strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
-    href: TextStyle(
-      color: theme.colorScheme.primary,
-      decoration: TextDecoration.underline,
-    ),
-    highlightColorHex: '0x6000BCF0',
-    code: GoogleFonts.robotoMono(
-      textStyle: TextStyle(
-        fontSize: documentStyle.fontSize,
-        fontWeight: FontWeight.normal,
-        color: Colors.red,
-        backgroundColor: theme.colorScheme.inverseSurface,
-      ),
-    ),
-    popupMenuFGColor: theme.iconTheme.color,
-    popupMenuHoverColor: theme.colorScheme.tertiaryContainer,
-  );
-
-  return editorStyle;
-}
-
-Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
-  final documentStyle = context.watch<DocumentAppearanceCubit>().state;
-  final baseFontSize = documentStyle.fontSize;
-  const basePadding = 12.0;
-  var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
-      ? HeadingPluginStyle.dark
-      : HeadingPluginStyle.light;
-  headingPluginStyle = headingPluginStyle.copyWith(
-    textStyle: (EditorState editorState, Node node) {
-      final headingToFontSize = {
-        'h1': baseFontSize + 12,
-        'h2': baseFontSize + 8,
-        'h3': baseFontSize + 4,
-        'h4': baseFontSize,
-        'h5': baseFontSize,
-        'h6': baseFontSize,
-      };
-      final fontSize =
-          headingToFontSize[node.attributes.heading] ?? baseFontSize;
-      return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
-    },
-    padding: (EditorState editorState, Node node) {
-      final headingToPadding = {
-        'h1': basePadding + 6,
-        'h2': basePadding + 4,
-        'h3': basePadding + 2,
-        'h4': basePadding,
-        'h5': basePadding,
-        'h6': basePadding,
-      };
-      final padding = headingToPadding[node.attributes.heading] ?? basePadding;
-      return EdgeInsets.only(bottom: padding);
-    },
-  );
-  var numberListPluginStyle = Theme.of(context).brightness == Brightness.dark
-      ? NumberListPluginStyle.dark
-      : NumberListPluginStyle.light;
-
-  numberListPluginStyle = numberListPluginStyle.copyWith(
-    icon: (_, textNode) {
-      const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
-      return Container(
-        padding: iconPadding,
-        child: Text(
-          '${textNode.attributes.number.toString()}.',
-          style: customEditorTheme(context).textStyle,
-        ),
-      );
-    },
-  );
-  final pluginTheme = Theme.of(context).brightness == Brightness.dark
-      ? darkPluginStyleExtension
-      : lightPluginStyleExtension;
-  return pluginTheme.toList()
-    ..removeWhere(
-      (element) =>
-          element is HeadingPluginStyle || element is NumberListPluginStyle,
-    )
-    ..add(headingPluginStyle)
-    ..add(numberListPluginStyle);
-}

+ 247 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -0,0 +1,247 @@
+import 'package:appflowy/plugins/document/application/doc_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action_button.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy/plugins/document/presentation/editor_style.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:tuple/tuple.dart';
+
+/// Wrapper for the appflowy editor.
+class AppFlowyEditorPage extends StatefulWidget {
+  const AppFlowyEditorPage({
+    super.key,
+    required this.editorState,
+  });
+
+  final EditorState editorState;
+
+  @override
+  State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
+}
+
+class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
+  final scrollController = ScrollController();
+  final slashMenuItems = [
+    boardMenuItem,
+    gridMenuItem,
+    calloutItem,
+    dividerMenuItem,
+    mathEquationItem,
+    codeBlockItem,
+    emojiMenuItem,
+    autoGeneratorMenuItem,
+  ];
+
+  final List<CommandShortcutEvent> commandShortcutEvents = [
+    ...codeBlockCommands,
+    ...standardCommandShortcutEvents,
+  ];
+
+  final List<ToolbarItem> toolbarItems = [
+    smartEditItem,
+    placeholderItem,
+    paragraphItem,
+    ...headingItems,
+    placeholderItem,
+    ...markdownFormatItems,
+    placeholderItem,
+    quoteItem,
+    bulletedListItem,
+    numberedListItem,
+    placeholderItem,
+    linkItem,
+    textColorItem,
+    highlightColorItem,
+  ];
+
+  late final Map<String, BlockComponentBuilder> blockComponentBuilders =
+      _customAppFlowyBlockComponentBuilders();
+  late final List<CharacterShortcutEvent> characterShortcutEvents = [
+    // divider
+    convertMinusesToDivider,
+
+    // code block
+    ...codeBlockCharacterEvents,
+
+    ...standardCharacterShortcutEvents
+      ..removeWhere(
+        (element) => element == slashCommand,
+      ), // remove the default slash command.
+    customSlashCommand(slashMenuItems),
+  ];
+
+  late final styleCustomizer = EditorStyleCustomizer(context: context);
+  DocumentBloc get documentBloc => context.read<DocumentBloc>();
+
+  @override
+  Widget build(BuildContext context) {
+    final autoFocusParameters = _computeAutoFocusParameters();
+    final editor = AppFlowyEditor.custom(
+      editorState: widget.editorState,
+      editable: true,
+      scrollController: scrollController,
+      // setup the auto focus parameters
+      autoFocus: autoFocusParameters.item1,
+      focusedSelection: autoFocusParameters.item2,
+      // setup the theme
+      editorStyle: styleCustomizer.style(),
+      // customize the block builder
+      blockComponentBuilders: blockComponentBuilders,
+      // customize the shortcuts
+      characterShortcutEvents: characterShortcutEvents,
+      commandShortcutEvents: commandShortcutEvents,
+    );
+
+    return Center(
+      child: Container(
+        constraints: const BoxConstraints(
+          maxWidth: double.infinity,
+        ),
+        child: FloatingToolbar(
+          items: toolbarItems,
+          editorState: widget.editorState,
+          scrollController: scrollController,
+          child: editor,
+        ),
+      ),
+    );
+  }
+
+  Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {
+    final standardActions = [
+      OptionAction.delete,
+      OptionAction.duplicate,
+      OptionAction.divider,
+      OptionAction.moveUp,
+      OptionAction.moveDown,
+    ];
+
+    final configuration = BlockComponentConfiguration(
+      padding: (_) => const EdgeInsets.symmetric(vertical: 4.0),
+    );
+    final customBlockComponentBuilderMap = {
+      'document': DocumentComponentBuilder(),
+      ParagraphBlockKeys.type: TextBlockComponentBuilder(
+        configuration: configuration,
+      ),
+      TodoListBlockKeys.type: TodoListBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          placeholderText: (_) => 'To-do',
+        ),
+      ),
+      BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          placeholderText: (_) => 'List',
+        ),
+      ),
+      NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          placeholderText: (_) => 'List',
+        ),
+      ),
+      QuoteBlockKeys.type: QuoteBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          placeholderText: (_) => 'Quote',
+        ),
+      ),
+      HeadingBlockKeys.type: HeadingBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
+          placeholderText: (node) =>
+              'Heading ${node.attributes[HeadingBlockKeys.level]}',
+        ),
+        textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
+      ),
+      ImageBlockKeys.type: ImageBlockComponentBuilder(),
+      BoardBlockKeys.type: BoardBlockComponentBuilder(
+        configuration: configuration,
+      ),
+      GridBlockKeys.type: GridBlockComponentBuilder(
+        configuration: configuration,
+      ),
+      CalloutBlockKeys.type: CalloutBlockComponentBuilder(
+        configuration: configuration,
+      ),
+      DividerBlockKeys.type: DividerBlockComponentBuilder(),
+      MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          padding: (_) => const EdgeInsets.symmetric(vertical: 20),
+        ),
+      ),
+      CodeBlockKeys.type: CodeBlockComponentBuilder(
+        configuration: configuration.copyWith(
+          textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
+          placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
+        ),
+        padding: const EdgeInsets.only(
+          left: 30,
+          right: 30,
+          bottom: 36,
+        ),
+      ),
+      AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
+      SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
+    };
+
+    final builders = {
+      ...standardBlockComponentBuilderMap,
+      ...customBlockComponentBuilderMap,
+    };
+
+    // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
+    for (final entry in builders.entries) {
+      if (entry.key == 'document') {
+        continue;
+      }
+      final builder = entry.value;
+
+      // customize the action builder.
+      final supportColorBuilderTypes = [
+        ParagraphBlockKeys.type,
+        HeadingBlockKeys.type,
+        BulletedListBlockKeys.type,
+        NumberedListBlockKeys.type,
+        QuoteBlockKeys.type,
+        TodoListBlockKeys.type,
+        CalloutBlockKeys.type
+      ];
+      if (!supportColorBuilderTypes.contains(entry.key)) {
+        builder.actionBuilder = (context, state) => OptionActionList(
+              blockComponentContext: context,
+              blockComponentState: state,
+              editorState: widget.editorState,
+              actions: standardActions,
+            );
+        continue;
+      }
+      final colorAction = [
+        OptionAction.divider,
+        OptionAction.color,
+      ];
+      builder.actionBuilder = (context, state) => OptionActionList(
+            blockComponentContext: context,
+            blockComponentState: state,
+            editorState: widget.editorState,
+            actions: standardActions + colorAction,
+          );
+    }
+
+    return builders;
+  }
+
+  Tuple2<bool, Selection?> _computeAutoFocusParameters() {
+    if (widget.editorState.document.isEmpty) {
+      return Tuple2(true, Selection.collapse([0], 0));
+    }
+    final nodes = widget.editorState.document.root.children
+        .where((element) => element.delta != null);
+    final isAllEmpty =
+        nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty);
+    if (isAllEmpty) {
+      return Tuple2(true, Selection.collapse(nodes.first.path, 0));
+    }
+    return const Tuple2(false, null);
+  }
+}

+ 0 - 0
frontend/rust-lib/flowy-document2/tests/document_test.rs → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/editor_action_wrapper.dart


+ 171 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart

@@ -0,0 +1,171 @@
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
+import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+enum OptionAction {
+  delete,
+  duplicate,
+  turnInto,
+  moveUp,
+  moveDown,
+  color,
+  divider,
+}
+
+class DividerOptionAction extends CustomActionCell {
+  @override
+  Widget buildWithContext(BuildContext context) {
+    return const Divider(
+      height: 1.0,
+      thickness: 1.0,
+    );
+  }
+}
+
+class ColorOptionAction extends PopoverActionCell {
+  ColorOptionAction({
+    required this.editorState,
+  });
+
+  final EditorState editorState;
+
+  @override
+  Widget? leftIcon(Color iconColor) {
+    return svgWidget(
+      'editor/delete', // todo: add color icon
+      color: iconColor,
+    );
+  }
+
+  @override
+  String get name {
+    return 'Color'; // todo: l10n
+  }
+
+  @override
+  Widget Function(BuildContext context, PopoverController controller)
+      get builder => (context, controller) {
+            final selection = editorState.selection?.normalized;
+            if (selection == null) {
+              return const SizedBox.shrink();
+            }
+            // TODO: should we support multiple selection?
+            final node = editorState.getNodeAtPath(selection.start.path);
+            if (node == null) {
+              return const SizedBox.shrink();
+            }
+            final bgColor =
+                node.attributes[blockComponentBackgroundColor] as String?;
+            final selectedColor = convertHexToSelectOptionColorPB(
+              bgColor,
+              context,
+            );
+
+            return SelectOptionColorList(
+              selectedColor: selectedColor,
+              onSelectedColor: (color) {
+                final nodes = editorState.getNodesInSelection(selection);
+                final transaction = editorState.transaction;
+                for (final node in nodes) {
+                  transaction.updateNode(node, {
+                    blockComponentBackgroundColor: color.make(context).toHex(),
+                  });
+                }
+                editorState.apply(transaction);
+
+                controller.close();
+              },
+            );
+          };
+
+  SelectOptionColorPB? convertHexToSelectOptionColorPB(
+    String? hexColor,
+    BuildContext context,
+  ) {
+    if (hexColor == null) {
+      return null;
+    }
+    for (final value in SelectOptionColorPB.values) {
+      if (value.make(context).toHex() == hexColor) {
+        return value;
+      }
+    }
+    return null;
+  }
+}
+
+class OptionActionWrapper extends ActionCell {
+  final OptionAction inner;
+
+  OptionActionWrapper(this.inner);
+
+  @override
+  Widget? leftIcon(Color iconColor) {
+    var name = '';
+    // TODO: add icons.
+    switch (inner) {
+      case OptionAction.delete:
+        name = 'editor/delete';
+        break;
+      case OptionAction.duplicate:
+        name = 'editor/duplicate';
+        break;
+      case OptionAction.turnInto:
+        name = 'editor/turn_into';
+        break;
+      case OptionAction.moveUp:
+        name = 'editor/move_up';
+        break;
+      case OptionAction.moveDown:
+        name = 'editor/move_down';
+        break;
+      case OptionAction.color:
+        name = 'editor/color';
+        break;
+      case OptionAction.divider:
+        throw UnimplementedError();
+    }
+    if (name.isEmpty) {
+      return null;
+    }
+    name = 'editor/delete';
+    return svgWidget(
+      name,
+      color: iconColor,
+    );
+  }
+
+  @override
+  String get name {
+    var description = '';
+    switch (inner) {
+      // TODO: l10n
+      case OptionAction.delete:
+        description = 'Delete';
+        break;
+      case OptionAction.duplicate:
+        description = 'Duplicate';
+        break;
+      case OptionAction.turnInto:
+        description = 'Turn into';
+        break;
+      case OptionAction.moveUp:
+        description = 'Move up';
+        break;
+      case OptionAction.moveDown:
+        description = 'Move down';
+        break;
+      case OptionAction.color:
+        description = 'Color';
+        break;
+      case OptionAction.divider:
+        throw UnimplementedError();
+    }
+    return description;
+  }
+}

+ 170 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart

@@ -0,0 +1,170 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
+import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
+import 'package:flutter/material.dart';
+
+class OptionActionList extends StatelessWidget {
+  const OptionActionList({
+    Key? key,
+    required this.blockComponentContext,
+    required this.blockComponentState,
+    required this.actions,
+    required this.editorState,
+  }) : super(key: key);
+
+  final BlockComponentContext blockComponentContext;
+  final BlockComponentState blockComponentState;
+  final List<OptionAction> actions;
+  final EditorState editorState;
+
+  @override
+  Widget build(BuildContext context) {
+    final popoverActions = actions.map((e) {
+      if (e == OptionAction.divider) {
+        return DividerOptionAction();
+      } else if (e == OptionAction.color) {
+        return ColorOptionAction(
+          editorState: editorState,
+        );
+      } else {
+        return OptionActionWrapper(e);
+      }
+    }).toList();
+
+    return PopoverActionList<PopoverAction>(
+      direction: PopoverDirection.leftWithCenterAligned,
+      actions: popoverActions,
+      onPopupBuilder: () => blockComponentState.alwaysShowActions = true,
+      onClosed: () {
+        editorState.selectionType = null;
+        editorState.selection = null;
+        blockComponentState.alwaysShowActions = false;
+      },
+      onSelected: (action, controller) {
+        if (action is OptionActionWrapper) {
+          _onSelectAction(action.inner);
+          controller.close();
+        }
+      },
+      buildChild: (controller) => OptionActionButton(
+        onTap: () {
+          controller.show();
+
+          // update selection
+          _updateBlockSelection();
+        },
+      ),
+    );
+  }
+
+  void _updateBlockSelection() {
+    final startNode = blockComponentContext.node;
+    var endNode = startNode;
+    while (endNode.children.isNotEmpty) {
+      endNode = endNode.children.last;
+    }
+
+    final start = Position(path: startNode.path, offset: 0);
+    final end = endNode.selectable?.end() ??
+        Position(
+          path: endNode.path,
+          offset: endNode.delta?.length ?? 0,
+        );
+
+    editorState.selectionType = SelectionType.block;
+    editorState.selection = Selection(
+      start: start,
+      end: end,
+    );
+  }
+
+  void _onSelectAction(OptionAction action) {
+    final node = blockComponentContext.node;
+    final transaction = editorState.transaction;
+    switch (action) {
+      case OptionAction.delete:
+        transaction.deleteNode(node);
+        break;
+      case OptionAction.duplicate:
+        transaction.insertNode(
+          node.path.next,
+          node.copyWith(),
+        );
+        break;
+      case OptionAction.turnInto:
+        break;
+      case OptionAction.moveUp:
+        transaction.moveNode(node.path.previous, node);
+        break;
+      case OptionAction.moveDown:
+        transaction.moveNode(node.path.next.next, node);
+        break;
+      case OptionAction.color:
+        // show the color picker
+
+        break;
+      case OptionAction.divider:
+        throw UnimplementedError();
+    }
+    editorState.apply(transaction);
+  }
+}
+
+class BlockComponentActionButton extends StatelessWidget {
+  const BlockComponentActionButton({
+    super.key,
+    required this.icon,
+    required this.onTap,
+  });
+
+  final bool isHovering = false;
+  final Widget icon;
+  final VoidCallback onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return MouseRegion(
+      cursor: SystemMouseCursors.grab,
+      child: GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTap: onTap,
+        onTapDown: (details) {},
+        onTapUp: (details) {},
+        child: icon,
+      ),
+    );
+  }
+}
+
+class OptionActionButton extends StatelessWidget {
+  const OptionActionButton({
+    super.key,
+    required this.onTap,
+  });
+
+  final VoidCallback onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.center,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.grab,
+        child: IgnoreParentGestureWidget(
+          child: GestureDetector(
+            onTap: onTap,
+            behavior: HitTestBehavior.deferToChild,
+            child: svgWidget(
+              'editor/option',
+              size: const Size.square(24.0),
+              color: Theme.of(context).iconTheme.color,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 15 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/build_context_extension.dart

@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+extension BuildContextExtension on BuildContext {
+  /// returns a boolean value indicating whether the given offset is contained within the bounds of the specified RenderBox or not.
+  bool isOffsetInside(Offset offset) {
+    final box = findRenderObject() as RenderBox?;
+    if (box == null) {
+      return false;
+    }
+    var result = BoxHitTestResult();
+    box.hitTest(result, position: box.globalToLocal(offset));
+    return result.path.any((entry) => entry.target == box);
+  }
+}

+ 41 - 42
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/built_in_page_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@@ -35,61 +35,64 @@ class BuiltInPageWidget extends StatefulWidget {
 }
 
 class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
+  late Future<dartz.Either<FlowyError, ViewPB>> future;
   final focusNode = FocusNode();
 
-  String get gridID {
-    return widget.node.attributes[kViewID];
+  String get appId => widget.node.attributes[DatabaseBlockKeys.kAppID];
+  String get viewId => widget.node.attributes[DatabaseBlockKeys.kViewID];
+
+  @override
+  void initState() {
+    super.initState();
+    future = AppBackendService().getChildView(viewId, appId).then(
+          (value) => value.swap(),
+        );
   }
 
-  String get appID {
-    return widget.node.attributes[kAppID];
+  @override
+  void dispose() {
+    focusNode.dispose();
+    super.dispose();
   }
 
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
+    return FutureBuilder<dartz.Either<FlowyError, ViewPB>>(
       builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          final board = snapshot.data?.getLeftOrNull<ViewPB>();
-          if (board != null) {
-            return _build(context, board);
-          }
+        final page = snapshot.data?.toOption().toNullable();
+        if (snapshot.hasData && page != null) {
+          return _build(context, page);
+        }
+        if (snapshot.connectionState == ConnectionState.done) {
+          return const Center(
+            child: FlowyText('Cannot load the page'),
+          );
         }
         return const Center(
           child: CircularProgressIndicator(),
         );
       },
-      future: AppBackendService().getChildView(appID, gridID),
+      future: future,
     );
   }
 
-  @override
-  void dispose() {
-    focusNode.dispose();
-    super.dispose();
-  }
-
   Widget _build(BuildContext context, ViewPB viewPB) {
     return MouseRegion(
-      onEnter: (event) {
-        widget.editorState.service.scrollService?.disable();
-      },
-      onExit: (event) {
-        widget.editorState.service.scrollService?.enable();
-      },
+      onEnter: (_) => widget.editorState.service.scrollService?.disable(),
+      onExit: (_) => widget.editorState.service.scrollService?.enable(),
       child: SizedBox(
         height: 400,
         child: Stack(
           children: [
             _buildMenu(context, viewPB),
-            _buildGrid(context, viewPB),
+            _buildPage(context, viewPB),
           ],
         ),
       ),
     );
   }
 
-  Widget _buildGrid(BuildContext context, ViewPB viewPB) {
+  Widget _buildPage(BuildContext context, ViewPB viewPB) {
     return Focus(
       focusNode: focusNode,
       onFocusChange: (value) {
@@ -112,9 +115,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
           // information
           FlowyIconButton(
             tooltipText: LocaleKeys.tooltip_referencePage.tr(
-              namedArgs: {
-                'name': viewPB.layout.name,
-              },
+              namedArgs: {'name': viewPB.layout.name},
             ),
             width: 24,
             height: 24,
@@ -137,19 +138,17 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
             actions: _ActionType.values
                 .map((action) => _ActionWrapper(action))
                 .toList(),
-            buildChild: (controller) {
-              return FlowyIconButton(
-                tooltipText: LocaleKeys.tooltip_openMenu.tr(),
-                width: 24,
-                height: 24,
-                iconPadding: const EdgeInsets.all(3),
-                icon: svgWidget(
-                  'common/settings',
-                  color: Theme.of(context).iconTheme.color,
-                ),
-                onPressed: () => controller.show(),
-              );
-            },
+            buildChild: (controller) => FlowyIconButton(
+              tooltipText: LocaleKeys.tooltip_openMenu.tr(),
+              width: 24,
+              height: 24,
+              iconPadding: const EdgeInsets.all(3),
+              icon: svgWidget(
+                'common/settings',
+                color: Theme.of(context).iconTheme.color,
+              ),
+              onPressed: () => controller.show(),
+            ),
             onSelected: (action, controller) async {
               switch (action.inner) {
                 case _ActionType.viewDatabase:

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/color_extension.dart

@@ -0,0 +1 @@
+

+ 56 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart

@@ -0,0 +1,56 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class EmojiPickerButton extends StatelessWidget {
+  EmojiPickerButton({
+    super.key,
+    required this.emoji,
+    required this.onSubmitted,
+    this.emojiPickerSize = const Size(300, 250),
+    this.emojiSize = 18.0,
+  });
+
+  final String emoji;
+  final double emojiSize;
+  final Size emojiPickerSize;
+  final void Function(Emoji emoji, PopoverController controller) onSubmitted;
+  final PopoverController popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      controller: popoverController,
+      triggerActions: PopoverTriggerFlags.click,
+      constraints: BoxConstraints.expand(
+        width: emojiPickerSize.width,
+        height: emojiPickerSize.height,
+      ),
+      popupBuilder: (context) => _buildEmojiPicker(),
+      child: FlowyTextButton(
+        emoji,
+        fontSize: emojiSize,
+        padding: EdgeInsets.zero,
+        constraints: const BoxConstraints(minWidth: 35.0),
+        fillColor: Colors.transparent,
+        onPressed: () {
+          popoverController.show();
+        },
+      ),
+    );
+  }
+
+  Widget _buildEmojiPicker() {
+    return Container(
+      width: emojiPickerSize.width,
+      height: emojiPickerSize.height,
+      padding: const EdgeInsets.all(4.0),
+      child: EmojiSelectionMenu(
+        onSubmitted: (emoji) => onSubmitted(emoji, popoverController),
+        onExit: () {},
+      ),
+    );
+  }
+}

+ 26 - 25
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/insert_page_command.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart

@@ -1,38 +1,41 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/board/board_node_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
 
-const String kAppID = 'app_id';
-const String kViewID = 'view_id';
+class DatabaseBlockKeys {
+  const DatabaseBlockKeys._();
 
-extension InsertPage on EditorState {
+  static const String kAppID = 'app_id';
+  static const String kViewID = 'view_id';
+}
+
+extension InsertDatabase on EditorState {
   Future<void> insertPage(ViewPB appPB, ViewPB viewPB) async {
-    final selection = service.selectionService.currentSelection.value;
-    final textNodes =
-        service.selectionService.currentSelectedNodes.whereType<TextNode>();
-    if (selection == null || textNodes.isEmpty) {
+    final selection = this.selection;
+    if (selection == null || !selection.isCollapsed) {
+      return;
+    }
+    final node = getNodeAtPath(selection.end.path);
+    if (node == null) {
       return;
     }
 
     // get the database that the view is associated with
-    final database =
-        await DatabaseViewBackendService(viewId: viewPB.id).openGrid().then(
-              (value) => value.getLeftOrNull(),
-            );
-
+    final database = await DatabaseViewBackendService(viewId: viewPB.id)
+        .openGrid()
+        .then((value) => value.swap().toOption().toNullable());
     if (database == null) {
       throw StateError(
         'The database associated with ${viewPB.id} could not be found while attempting to create a referenced ${viewPB.layout.name}.',
       );
     }
 
-    final prefix = referencedBoardPrefix(viewPB.layout);
-
+    final prefix = _referencedDatabasePrefix(viewPB.layout);
     final ref = await AppBackendService().createView(
       appId: appPB.id,
       name: "$prefix ${viewPB.name}",
@@ -40,9 +43,7 @@ extension InsertPage on EditorState {
       ext: {
         'database_id': database.id,
       },
-    ).then(
-      (value) => value.getLeftOrNull(),
-    );
+    ).then((value) => value.swap().toOption().toNullable());
 
     // TODO(a-wallen): Show error dialog here.
     if (ref == null) {
@@ -55,15 +56,15 @@ extension InsertPage on EditorState {
       Node(
         type: _convertPageType(viewPB),
         attributes: {
-          kAppID: appPB.id,
-          kViewID: ref.id,
+          DatabaseBlockKeys.kAppID: appPB.id,
+          DatabaseBlockKeys.kViewID: ref.id,
         },
       ),
     );
-    apply(transaction);
+    await apply(transaction);
   }
 
-  String referencedBoardPrefix(ViewLayoutPB layout) {
+  String _referencedDatabasePrefix(ViewLayoutPB layout) {
     switch (layout) {
       case ViewLayoutPB.Grid:
         return LocaleKeys.grid_referencedGridPrefix.tr();
@@ -77,9 +78,9 @@ extension InsertPage on EditorState {
   String _convertPageType(ViewPB viewPB) {
     switch (viewPB.layout) {
       case ViewLayoutPB.Grid:
-        return kGridType;
+        return GridBlockKeys.type;
       case ViewLayoutPB.Board:
-        return kBoardType;
+        return BoardBlockKeys.type;
       default:
         throw Exception('Unknown layout type');
     }

+ 45 - 57
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
 import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -9,70 +10,37 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'insert_page_command.dart';
-
-EditorState? _editorState;
-OverlayEntry? _linkToPageMenu;
 
 void showLinkToPageMenu(
+  OverlayState container,
   EditorState editorState,
   SelectionMenuService menuService,
-  BuildContext context,
   ViewLayoutPB pageType,
 ) {
-  final alignment = menuService.alignment;
-  final offset = menuService.offset;
   menuService.dismiss();
 
-  _editorState = editorState;
-
-  String hintText = '';
-  switch (pageType) {
-    case ViewLayoutPB.Grid:
-      hintText = LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
-      break;
-    case ViewLayoutPB.Board:
-      hintText = LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
-      break;
-    default:
-      throw Exception('Unknown layout type');
-  }
-
-  _linkToPageMenu?.remove();
-  _linkToPageMenu = OverlayEntry(
-    builder: (context) {
-      return Positioned(
-        top: alignment == Alignment.bottomLeft ? offset.dy : null,
-        bottom: alignment == Alignment.topLeft ? offset.dy : null,
-        left: offset.dx,
-        child: Material(
-          color: Colors.transparent,
-          child: LinkToPageMenu(
-            editorState: editorState,
-            layoutType: pageType,
-            hintText: hintText,
-            onSelected: (appPB, viewPB) {
-              editorState.insertPage(appPB, viewPB);
-            },
-          ),
-        ),
-      );
-    },
-  );
-
-  Overlay.of(context).insert(_linkToPageMenu!);
-
-  editorState.service.selectionService.currentSelection
-      .addListener(dismissLinkToPageMenu);
-}
-
-void dismissLinkToPageMenu() {
-  _linkToPageMenu?.remove();
-  _linkToPageMenu = null;
+  final alignment = menuService.alignment;
+  final offset = menuService.offset;
+  final top = alignment == Alignment.bottomLeft ? offset.dy : null;
+  final bottom = alignment == Alignment.topLeft ? offset.dy : null;
 
-  _editorState?.service.selectionService.currentSelection
-      .removeListener(dismissLinkToPageMenu);
-  _editorState = null;
+  final linkToPageMenuEntry = FullScreenOverlayEntry(
+    top: top,
+    bottom: bottom,
+    left: offset.dx,
+    builder: (context) => Material(
+      color: Colors.transparent,
+      child: LinkToPageMenu(
+        editorState: editorState,
+        layoutType: pageType,
+        hintText: pageType.toHintText(),
+        onSelected: (appPB, viewPB) {
+          editorState.insertPage(appPB, viewPB);
+        },
+      ),
+    ),
+  ).build();
+  container.insert(linkToPageMenuEntry);
 }
 
 class LinkToPageMenu extends StatefulWidget {
@@ -133,6 +101,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
 
   @override
   Widget build(BuildContext context) {
+    final theme = Theme.of(context);
     return Focus(
       focusNode: _focusNode,
       onKey: _onKey,
@@ -140,7 +109,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
         width: 300,
         padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
         decoration: BoxDecoration(
-          color: style.selectionMenuBackgroundColor,
+          color: theme.cardColor,
           boxShadow: [
             BoxShadow(
               blurRadius: 5,
@@ -150,7 +119,11 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
           ],
           borderRadius: BorderRadius.circular(6.0),
         ),
-        child: _buildListWidget(context, _selectedIndex, _availableLayout),
+        child: _buildListWidget(
+          context,
+          _selectedIndex,
+          _availableLayout,
+        ),
       ),
     );
   }
@@ -272,3 +245,18 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
     }
   }
 }
+
+extension on ViewLayoutPB {
+  String toHintText() {
+    switch (this) {
+      case ViewLayoutPB.Grid:
+        return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
+
+      case ViewLayoutPB.Board:
+        return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
+
+      default:
+        throw Exception('Unknown layout type');
+    }
+  }
+}

+ 56 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart

@@ -0,0 +1,56 @@
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class SelectableItemListMenu extends StatelessWidget {
+  const SelectableItemListMenu({
+    super.key,
+    required this.items,
+    required this.selectedIndex,
+    required this.onSelected,
+  });
+
+  final List<String> items;
+  final int selectedIndex;
+  final void Function(int) onSelected;
+
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(
+      itemBuilder: (context, index) {
+        final item = items[index];
+        return SelectableItem(
+          isSelected: index == selectedIndex,
+          item: item,
+          onTap: () => onSelected(index),
+        );
+      },
+      itemCount: items.length,
+    );
+  }
+}
+
+class SelectableItem extends StatelessWidget {
+  const SelectableItem({
+    super.key,
+    required this.isSelected,
+    required this.item,
+    required this.onTap,
+  });
+
+  final bool isSelected;
+  final String item;
+  final VoidCallback onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 32,
+      child: FlowyButton(
+        text: FlowyText.medium(item),
+        rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null,
+        onTap: onTap,
+      ),
+    );
+  }
+}

+ 48 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart

@@ -0,0 +1,48 @@
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+class SelectableSvgWidget extends StatelessWidget {
+  const SelectableSvgWidget({
+    super.key,
+    required this.name,
+    required this.isSelected,
+  });
+
+  final String name;
+  final bool isSelected;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return svgWidget(
+      name,
+      size: const Size.square(18.0),
+      color: isSelected
+          ? theme.colorScheme.onSurface
+          : theme.colorScheme.onBackground,
+    );
+  }
+}
+
+class SelectableIconWidget extends StatelessWidget {
+  const SelectableIconWidget({
+    super.key,
+    required this.icon,
+    required this.isSelected,
+  });
+
+  final IconData icon;
+  final bool isSelected;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return Icon(
+      icon,
+      size: 18.0,
+      color: isSelected
+          ? theme.colorScheme.onSurface
+          : theme.colorScheme.onBackground,
+    );
+  }
+}

+ 5 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart

@@ -0,0 +1,5 @@
+extension Capitalize on String {
+  String capitalize() {
+    return "${this[0].toUpperCase()}${substring(1)}";
+  }
+}

+ 14 - 9
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart

@@ -5,30 +5,35 @@ enum TextRobotInputType {
   word,
 }
 
-extension TextRobot on EditorState {
+class TextRobot {
+  const TextRobot({
+    required this.editorState,
+  });
+
+  final EditorState editorState;
+
   Future<void> autoInsertText(
     String text, {
     TextRobotInputType inputType = TextRobotInputType.word,
     Duration delay = const Duration(milliseconds: 10),
   }) async {
     if (text == '\n') {
-      await insertNewLineAtCurrentSelection();
-      return;
+      return editorState.insertNewLine();
     }
     final lines = text.split('\n');
     for (final line in lines) {
       if (line.isEmpty) {
-        await insertNewLineAtCurrentSelection();
+        await editorState.insertNewLine();
         continue;
       }
       switch (inputType) {
         case TextRobotInputType.character:
           final iterator = line.runes.iterator;
           while (iterator.moveNext()) {
-            await insertTextAtCurrentSelection(
+            await editorState.insertTextAtCurrentSelection(
               iterator.currentAsString,
             );
-            await Future.delayed(delay, () {});
+            await Future.delayed(delay);
           }
           break;
         case TextRobotInputType.word:
@@ -36,17 +41,17 @@ extension TextRobot on EditorState {
           if (words.length == 1 ||
               (words.length == 2 &&
                   (words.first.isEmpty || words.last.isEmpty))) {
-            await insertTextAtCurrentSelection(
+            await editorState.insertTextAtCurrentSelection(
               line,
             );
           } else {
             for (final word in words.map((e) => '$e ')) {
-              await insertTextAtCurrentSelection(
+              await editorState.insertTextAtCurrentSelection(
                 word,
               );
             }
           }
-          await Future.delayed(delay, () {});
+          await Future.delayed(delay);
           break;
       }
     }

+ 8 - 13
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_menu_item.dart

@@ -1,29 +1,24 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
 import 'package:flutter/material.dart';
 
 SelectionMenuItem boardMenuItem = SelectionMenuItem(
   name: LocaleKeys.document_plugins_referencedBoard.tr(),
-  icon: (editorState, onSelected) {
-    return svgWidget(
-      'editor/board',
-      size: const Size.square(18.0),
-      color: onSelected
-          ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-          : editorState.editorStyle.selectionMenuItemIconColor,
-    );
-  },
-  // TODO(a-wallen): Translate keywords
+  icon: (editorState, onSelected) => SelectableSvgWidget(
+    name: 'editor/board',
+    isSelected: onSelected,
+  ),
   keywords: ['referenced', 'board', 'kanban'],
   handler: (editorState, menuService, context) {
+    final container = Overlay.of(context);
     showLinkToPageMenu(
+      container,
       editorState,
       menuService,
-      context,
       ViewLayoutPB.Board,
     );
   },

+ 76 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_node_widget.dart

@@ -0,0 +1,76 @@
+import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class BoardBlockKeys {
+  const BoardBlockKeys._();
+
+  static const String type = 'board';
+}
+
+class BoardBlockComponentBuilder extends BlockComponentBuilder {
+  BoardBlockComponentBuilder({
+    this.configuration = const BlockComponentConfiguration(),
+  });
+
+  @override
+  final BlockComponentConfiguration configuration;
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return BoardBlockComponentWidget(
+      key: node.key,
+      node: node,
+      configuration: configuration,
+    );
+  }
+
+  @override
+  bool validate(Node node) =>
+      node.children.isEmpty &&
+      node.attributes[DatabaseBlockKeys.kAppID] is String &&
+      node.attributes[DatabaseBlockKeys.kViewID] is String;
+}
+
+class BoardBlockComponentWidget extends StatefulWidget {
+  const BoardBlockComponentWidget({
+    super.key,
+    required this.configuration,
+    required this.node,
+  });
+
+  final Node node;
+  final BlockComponentConfiguration configuration;
+
+  @override
+  State<BoardBlockComponentWidget> createState() =>
+      _BoardBlockComponentWidgetState();
+}
+
+class _BoardBlockComponentWidgetState extends State<BoardBlockComponentWidget>
+    with BlockComponentConfigurable {
+  @override
+  Node get node => widget.node;
+
+  @override
+  BlockComponentConfiguration get configuration => widget.configuration;
+
+  @override
+  Widget build(BuildContext context) {
+    final editorState = Provider.of<EditorState>(context, listen: false);
+    return BuiltInPageWidget(
+      node: widget.node,
+      editorState: editorState,
+      builder: (viewPB) {
+        return BoardPage(
+          key: ValueKey(viewPB.id),
+          view: viewPB,
+        );
+      },
+    );
+  }
+}

+ 6 - 13
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_view_menu_item.dart

@@ -1,26 +1,19 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/application/prelude.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
 import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
-import 'package:flutter/material.dart';
 
 SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
     SelectionMenuItem(
       name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
-      icon: (editorState, onSelected) {
-        return svgWidget(
-          'editor/board',
-          size: const Size.square(18.0),
-          color: onSelected
-              ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-              : editorState.editorStyle.selectionMenuItemIconColor,
-        );
-      },
-      // TODO(a-wallen): Translate keywords.
+      icon: (editorState, onSelected) => SelectableSvgWidget(
+        name: 'editor/board',
+        isSelected: onSelected,
+      ),
       keywords: ['board', 'kanban'],
       handler: (editorState, menuService, context) async {
         if (!documentBloc.view.hasParentViewId()) {

+ 203 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart

@@ -0,0 +1,203 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../base/emoji_picker_button.dart';
+
+// defining the keys of the callout block's attributes for easy access
+class CalloutBlockKeys {
+  const CalloutBlockKeys._();
+
+  static const String type = 'callout';
+
+  /// The content of a code block.
+  ///
+  /// The value is a String.
+  static const String delta = 'delta';
+
+  /// The background color of a callout block.
+  ///
+  /// The value is a String.
+  static const String backgroundColor = blockComponentBackgroundColor;
+
+  /// The emoji icon of a callout block.
+  ///
+  /// The value is a String.
+  static const String icon = 'icon';
+}
+
+// creating a new callout node
+Node calloutNode({
+  Delta? delta,
+  String emoji = '📌',
+  String backgroundColor = '#F0F0F0',
+}) {
+  final attributes = {
+    CalloutBlockKeys.delta: (delta ?? Delta()).toJson(),
+    CalloutBlockKeys.icon: emoji,
+    CalloutBlockKeys.backgroundColor: backgroundColor,
+  };
+  return Node(
+    type: CalloutBlockKeys.type,
+    attributes: attributes,
+  );
+}
+
+// defining the callout block menu item for selection
+SelectionMenuItem calloutItem = SelectionMenuItem.node(
+  name: 'Callout',
+  iconData: Icons.note,
+  keywords: ['callout'],
+  nodeBuilder: (editorState) => calloutNode(),
+  replace: (_, node) => node.delta?.isEmpty ?? false,
+  updateSelection: (_, path, __, ___) {
+    return Selection.single(path: [...path, 0], startOffset: 0);
+  },
+);
+
+// building the callout block widget
+class CalloutBlockComponentBuilder extends BlockComponentBuilder {
+  CalloutBlockComponentBuilder({
+    this.configuration = const BlockComponentConfiguration(),
+  });
+
+  @override
+  final BlockComponentConfiguration configuration;
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return CalloutBlockComponentWidget(
+      key: node.key,
+      node: node,
+      configuration: configuration,
+    );
+  }
+
+  // validate the data of the node, if the result is false, the node will be rendered as a placeholder
+  @override
+  bool validate(Node node) =>
+      node.delta != null &&
+      node.children.isEmpty &&
+      node.attributes[CalloutBlockKeys.icon] is String &&
+      node.attributes[CalloutBlockKeys.backgroundColor] is String;
+}
+
+// the main widget for rendering the callout block
+class CalloutBlockComponentWidget extends StatefulWidget {
+  const CalloutBlockComponentWidget({
+    super.key,
+    required this.node,
+    required this.configuration,
+  });
+
+  final Node node;
+  final BlockComponentConfiguration configuration;
+
+  @override
+  State<CalloutBlockComponentWidget> createState() =>
+      _CalloutBlockComponentWidgetState();
+}
+
+class _CalloutBlockComponentWidgetState
+    extends State<CalloutBlockComponentWidget>
+    with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
+  // the key used to forward focus to the richtext child
+  @override
+  final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
+
+  // the key used to identify this component
+  @override
+  GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;
+
+  @override
+  BlockComponentConfiguration get configuration => widget.configuration;
+
+  @override
+  Node get node => widget.node;
+
+  // get the background color of the note block from the node's attributes
+  Color get backgroundColor {
+    final colorString =
+        node.attributes[CalloutBlockKeys.backgroundColor] as String?;
+    if (colorString == null) {
+      return Colors.transparent;
+    }
+    return colorString.toColor();
+  }
+
+  // get the emoji of the note block from the node's attributes or default to '📌'
+  String get emoji => node.attributes[CalloutBlockKeys.icon] ?? '📌';
+
+  // get access to the editor state via provider
+  late final editorState = Provider.of<EditorState>(context, listen: false);
+
+  // build the callout block widget
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
+        color: backgroundColor,
+      ),
+      width: double.infinity,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          // the emoji picker button for the note
+          Padding(
+            padding: const EdgeInsets.all(2.0),
+            child: EmojiPickerButton(
+              key: ValueKey(emoji), // force to refresh the popover state
+              emoji: emoji,
+              onSubmitted: (emoji, controller) {
+                setEmoji(emoji.emoji);
+                controller.close();
+              },
+            ),
+          ),
+          Expanded(
+            child: Padding(
+              padding: const EdgeInsets.symmetric(vertical: 6.0),
+              child: buildCalloutBlockComponent(context),
+            ),
+          ),
+          const SizedBox(
+            width: 10.0,
+          )
+        ],
+      ),
+    );
+  }
+
+  // build the richtext child
+  Widget buildCalloutBlockComponent(BuildContext context) {
+    return Padding(
+      padding: padding,
+      child: FlowyRichText(
+        key: forwardKey,
+        node: widget.node,
+        editorState: editorState,
+        placeholderText: placeholderText,
+        textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
+          textStyle,
+        ),
+        placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
+          placeholderTextStyle,
+        ),
+      ),
+    );
+  }
+
+  // set the emoji of the note block
+  Future<void> setEmoji(String emoji) async {
+    final transaction = editorState.transaction
+      ..updateNode(node, {
+        CalloutBlockKeys.icon: emoji,
+      })
+      ..afterSelection = Selection.collapse(
+        node.path,
+        node.delta?.length ?? 0,
+      );
+    await editorState.apply(transaction);
+  }
+}

+ 348 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart

@@ -0,0 +1,348 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:highlight/highlight.dart' as highlight;
+import 'package:highlight/languages/all.dart';
+import 'package:provider/provider.dart';
+
+class CodeBlockKeys {
+  const CodeBlockKeys._();
+
+  static const String type = 'code';
+
+  /// The content of a code block.
+  ///
+  /// The value is a String.
+  static const String delta = 'delta';
+
+  /// The language of a code block.
+  ///
+  /// The value is a String.
+  static const String language = 'language';
+}
+
+Node codeBlockNode({
+  Delta? delta,
+  String? language,
+}) {
+  final attributes = {
+    CodeBlockKeys.delta: (delta ?? Delta()).toJson(),
+    CodeBlockKeys.language: language,
+  };
+  return Node(
+    type: CodeBlockKeys.type,
+    attributes: attributes,
+  );
+}
+
+// defining the callout block menu item for selection
+SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
+  name: 'Code Block',
+  iconData: Icons.abc,
+  keywords: ['code', 'codeblock'],
+  nodeBuilder: (editorState) => codeBlockNode(),
+  replace: (_, node) => node.delta?.isEmpty ?? false,
+);
+
+class CodeBlockComponentBuilder extends BlockComponentBuilder {
+  CodeBlockComponentBuilder({
+    this.configuration = const BlockComponentConfiguration(),
+    this.padding = const EdgeInsets.all(0),
+  });
+
+  @override
+  final BlockComponentConfiguration configuration;
+
+  final EdgeInsets padding;
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return CodeBlockComponentWidget(
+      key: node.key,
+      node: node,
+      configuration: configuration,
+      padding: padding,
+    );
+  }
+
+  @override
+  bool validate(Node node) => node.delta != null;
+}
+
+class CodeBlockComponentWidget extends StatefulWidget {
+  const CodeBlockComponentWidget({
+    Key? key,
+    required this.node,
+    this.configuration = const BlockComponentConfiguration(),
+    this.padding = const EdgeInsets.all(0),
+  }) : super(key: key);
+
+  final Node node;
+  final BlockComponentConfiguration configuration;
+  final EdgeInsets padding;
+
+  @override
+  State<CodeBlockComponentWidget> createState() =>
+      _CodeBlockComponentWidgetState();
+}
+
+class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
+    with SelectableMixin, DefaultSelectable, BlockComponentConfigurable {
+  // the key used to forward focus to the richtext child
+  @override
+  final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
+
+  @override
+  BlockComponentConfiguration get configuration => widget.configuration;
+
+  @override
+  GlobalKey<State<StatefulWidget>> get containerKey => node.key;
+
+  @override
+  Node get node => widget.node;
+
+  final popoverController = PopoverController();
+
+  final supportedLanguages = [
+    'Assembly',
+    'Bash',
+    'BASIC',
+    'C',
+    'C#',
+    'C++',
+    'Clojure',
+    'CSS',
+    'Dart',
+    'Docker',
+    'Elixir',
+    'Elm',
+    'Erlang',
+    'Fortran',
+    'Go',
+    'GraphQL',
+    'Haskell',
+    'HTML',
+    'Java',
+    'JavaScript',
+    'JSON',
+    'Kotlin',
+    'LaTeX',
+    'Lisp',
+    'Lua',
+    'Markdown',
+    'MATLAB',
+    'Objective-C',
+    'OCaml',
+    'Perl',
+    'PHP',
+    'PowerShell',
+    'Python',
+    'R',
+    'Ruby',
+    'Rust',
+    'Scala',
+    'Shell',
+    'SQL',
+    'Swift',
+    'TypeScript',
+    'Visual Basic',
+    'XML',
+    'YAML',
+  ];
+  late final languages = supportedLanguages
+      .map((e) => e.toLowerCase())
+      .toSet()
+      .intersection(allLanguages.keys.toSet())
+      .toList();
+
+  late final editorState = context.read<EditorState>();
+
+  String? get language => node.attributes[CodeBlockKeys.language] as String?;
+  String? autoDetectLanguage;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
+        color: Colors.grey.withOpacity(0.1),
+      ),
+      width: MediaQuery.of(context).size.width,
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          _buildSwitchLanguageButton(context),
+          _buildCodeBlock(context),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildCodeBlock(BuildContext context) {
+    final delta = node.delta ?? Delta();
+    final content = delta.toPlainText();
+
+    final result = highlight.highlight.parse(
+      content,
+      language: language,
+      autoDetection: language == null,
+    );
+    autoDetectLanguage = language ?? result.language;
+
+    final codeNodes = result.nodes;
+    if (codeNodes == null) {
+      throw Exception('Code block parse error.');
+    }
+    final codeTextSpans = _convert(codeNodes);
+    return Padding(
+      padding: widget.padding,
+      child: FlowyRichText(
+        key: forwardKey,
+        node: widget.node,
+        editorState: editorState,
+        placeholderText: placeholderText,
+        textSpanDecorator: (textSpan) => TextSpan(
+          style: textStyle,
+          children: codeTextSpans,
+        ),
+        placeholderTextSpanDecorator: (textSpan) => TextSpan(
+          style: textStyle,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildSwitchLanguageButton(BuildContext context) {
+    return AppFlowyPopover(
+      controller: popoverController,
+      child: Container(
+        width: 100,
+        padding: const EdgeInsets.symmetric(horizontal: 4),
+        child: FlowyTextButton(
+          '${language?.capitalize() ?? 'auto'} ',
+          padding: const EdgeInsets.symmetric(
+            horizontal: 12.0,
+            vertical: 4.0,
+          ),
+          fontColor: Theme.of(context).colorScheme.onBackground,
+          fillColor: Colors.transparent,
+          onPressed: () {},
+        ),
+      ),
+      popupBuilder: (BuildContext context) {
+        return SelectableItemListMenu(
+          items: languages.map((e) => e.capitalize()).toList(),
+          selectedIndex: languages.indexOf(language ?? ''),
+          onSelected: (index) {
+            updateLanguage(languages[index]);
+            popoverController.close();
+          },
+        );
+      },
+    );
+  }
+
+  Future<void> updateLanguage(String language) async {
+    final transaction = editorState.transaction
+      ..updateNode(node, {
+        CodeBlockKeys.language: language,
+      })
+      ..afterSelection = Selection.collapse(
+        node.path,
+        node.delta?.length ?? 0,
+      );
+    await editorState.apply(transaction);
+  }
+
+  // Copy from flutter.highlight package.
+  // https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
+  List<TextSpan> _convert(List<highlight.Node> nodes) {
+    List<TextSpan> spans = [];
+    var currentSpans = spans;
+    List<List<TextSpan>> stack = [];
+
+    void traverse(highlight.Node node) {
+      if (node.value != null) {
+        currentSpans.add(
+          node.className == null
+              ? TextSpan(text: node.value)
+              : TextSpan(
+                  text: node.value,
+                  style: _builtInCodeBlockTheme[node.className!],
+                ),
+        );
+      } else if (node.children != null) {
+        List<TextSpan> tmp = [];
+        currentSpans.add(
+          TextSpan(
+            children: tmp,
+            style: _builtInCodeBlockTheme[node.className!],
+          ),
+        );
+        stack.add(currentSpans);
+        currentSpans = tmp;
+
+        for (var n in node.children!) {
+          traverse(n);
+          if (n == node.children!.last) {
+            currentSpans = stack.isEmpty ? spans : stack.removeLast();
+          }
+        }
+      }
+    }
+
+    for (var node in nodes) {
+      traverse(node);
+    }
+
+    return spans;
+  }
+}
+
+const _builtInCodeBlockTheme = {
+  'root':
+      TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)),
+  'comment': TextStyle(color: Color(0xff007400)),
+  'quote': TextStyle(color: Color(0xff007400)),
+  'tag': TextStyle(color: Color(0xffaa0d91)),
+  'attribute': TextStyle(color: Color(0xffaa0d91)),
+  'keyword': TextStyle(color: Color(0xffaa0d91)),
+  'selector-tag': TextStyle(color: Color(0xffaa0d91)),
+  'literal': TextStyle(color: Color(0xffaa0d91)),
+  'name': TextStyle(color: Color(0xffaa0d91)),
+  'variable': TextStyle(color: Color(0xff3F6E74)),
+  'template-variable': TextStyle(color: Color(0xff3F6E74)),
+  'code': TextStyle(color: Color(0xffc41a16)),
+  'string': TextStyle(color: Color(0xffc41a16)),
+  'meta-string': TextStyle(color: Color(0xffc41a16)),
+  'regexp': TextStyle(color: Color(0xff0E0EFF)),
+  'link': TextStyle(color: Color(0xff0E0EFF)),
+  'title': TextStyle(color: Color(0xff1c00cf)),
+  'symbol': TextStyle(color: Color(0xff1c00cf)),
+  'bullet': TextStyle(color: Color(0xff1c00cf)),
+  'number': TextStyle(color: Color(0xff1c00cf)),
+  'section': TextStyle(color: Color(0xff643820)),
+  'meta': TextStyle(color: Color(0xff643820)),
+  'type': TextStyle(color: Color(0xff5c2699)),
+  'built_in': TextStyle(color: Color(0xff5c2699)),
+  'builtin-name': TextStyle(color: Color(0xff5c2699)),
+  'params': TextStyle(color: Color(0xff5c2699)),
+  'attr': TextStyle(color: Color(0xff836C28)),
+  'subst': TextStyle(color: Color(0xff000000)),
+  'formula': TextStyle(
+    backgroundColor: Color(0xffeeeeee),
+    fontStyle: FontStyle.italic,
+  ),
+  'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
+  'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
+  'selector-id': TextStyle(color: Color(0xff9b703f)),
+  'selector-class': TextStyle(color: Color(0xff9b703f)),
+  'doctag': TextStyle(fontWeight: FontWeight.bold),
+  'strong': TextStyle(fontWeight: FontWeight.bold),
+  'emphasis': TextStyle(fontStyle: FontStyle.italic),
+};

+ 229 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart

@@ -0,0 +1,229 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+final List<CharacterShortcutEvent> codeBlockCharacterEvents = [
+  enterInCodeBlock,
+  ...ignoreKeysInCodeBlock,
+];
+
+final List<CommandShortcutEvent> codeBlockCommands = [
+  insertNewParagraphNextToCodeBlockCommand,
+  tabToInsertSpacesInCodeBlockCommand,
+  tabToDeleteSpacesInCodeBlockCommand,
+];
+
+/// press the enter key in code block to insert a new line in it.
+///
+/// - support
+///   - desktop
+///   - web
+///   - mobile
+///
+final CharacterShortcutEvent enterInCodeBlock = CharacterShortcutEvent(
+  key: 'press enter in code block',
+  character: '\n',
+  handler: _enterInCodeBlockCommandHandler,
+);
+
+/// ignore ' ', '/', '_', '*' in code block.
+///
+/// - support
+///   - desktop
+///   - web
+///   - mobile
+///
+final List<CharacterShortcutEvent> ignoreKeysInCodeBlock =
+    [' ', '/', '_', '*', '~']
+        .map(
+          (e) => CharacterShortcutEvent(
+            key: 'press enter in code block',
+            character: e,
+            handler: (editorState) => _ignoreKeysInCodeBlockCommandHandler(
+              editorState,
+              e,
+            ),
+          ),
+        )
+        .toList();
+
+/// shift + enter to insert a new node next to the code block.
+///
+/// - support
+///   - desktop
+///   - web
+///
+final CommandShortcutEvent insertNewParagraphNextToCodeBlockCommand =
+    CommandShortcutEvent(
+  key: 'insert a new paragraph next to the code block',
+  command: 'shift+enter',
+  handler: _insertNewParagraphNextToCodeBlockCommandHandler,
+);
+
+/// tab to insert two spaces at the line start in code block.
+///
+/// - support
+///   - desktop
+///   - web
+final CommandShortcutEvent tabToInsertSpacesInCodeBlockCommand =
+    CommandShortcutEvent(
+  key: 'tab to insert two spaces at the line start in code block',
+  command: 'tab',
+  handler: _tabToInsertSpacesInCodeBlockCommandHandler,
+);
+
+/// shift+tab to delete two spaces at the line start in code block if needed.
+///
+/// - support
+///   - desktop
+///   - web
+final CommandShortcutEvent tabToDeleteSpacesInCodeBlockCommand =
+    CommandShortcutEvent(
+  key: 'shift + tab to delete two spaces at the line start in code block',
+  command: 'shift+tab',
+  handler: _tabToDeleteSpacesInCodeBlockCommandHandler,
+);
+
+CharacterShortcutEventHandler _enterInCodeBlockCommandHandler =
+    (editorState) async {
+  final selection = editorState.selection;
+  if (selection == null || !selection.isCollapsed) {
+    return false;
+  }
+  final node = editorState.getNodeAtPath(selection.end.path);
+  if (node == null || node.type != CodeBlockKeys.type) {
+    return false;
+  }
+  final transaction = editorState.transaction
+    ..insertText(
+      node,
+      selection.end.offset,
+      '\n',
+    );
+  await editorState.apply(transaction);
+  return true;
+};
+
+Future<bool> _ignoreKeysInCodeBlockCommandHandler(
+  EditorState editorState,
+  String key,
+) async {
+  final selection = editorState.selection;
+  if (selection == null || !selection.isCollapsed) {
+    return false;
+  }
+  final node = editorState.getNodeAtPath(selection.end.path);
+  if (node == null || node.type != CodeBlockKeys.type) {
+    return false;
+  }
+  await editorState.insertTextAtCurrentSelection(key);
+  return true;
+}
+
+CommandShortcutEventHandler _insertNewParagraphNextToCodeBlockCommandHandler =
+    (editorState) {
+  final selection = editorState.selection;
+  if (selection == null || !selection.isCollapsed) {
+    return KeyEventResult.ignored;
+  }
+  final node = editorState.getNodeAtPath(selection.end.path);
+  final delta = node?.delta;
+  if (node == null || delta == null || node.type != CodeBlockKeys.type) {
+    return KeyEventResult.ignored;
+  }
+  final sliced = delta.slice(selection.startIndex);
+  final transaction = editorState.transaction
+    ..deleteText(
+      // delete the text after the cursor in the code block
+      node,
+      selection.startIndex,
+      delta.length - selection.startIndex,
+    )
+    ..insertNode(
+      // insert a new paragraph node with the sliced delta after the code block
+      selection.end.path.next,
+      paragraphNode(
+        attributes: {
+          'delta': sliced.toJson(),
+        },
+      ),
+    )
+    ..afterSelection = Selection.collapse(
+      selection.end.path.next,
+      0,
+    );
+  editorState.apply(transaction);
+  return KeyEventResult.handled;
+};
+
+CommandShortcutEventHandler _tabToInsertSpacesInCodeBlockCommandHandler =
+    (editorState) {
+  final selection = editorState.selection;
+  if (selection == null || !selection.isCollapsed) {
+    return KeyEventResult.ignored;
+  }
+  final node = editorState.getNodeAtPath(selection.end.path);
+  final delta = node?.delta;
+  if (node == null || delta == null || node.type != CodeBlockKeys.type) {
+    return KeyEventResult.ignored;
+  }
+  const spaces = '  ';
+  final lines = delta.toPlainText().split('\n');
+  var index = 0;
+  for (final line in lines) {
+    if (index <= selection.endIndex &&
+        selection.endIndex <= index + line.length) {
+      final transaction = editorState.transaction
+        ..insertText(
+          node,
+          index,
+          spaces, // two spaces
+        )
+        ..afterSelection = Selection.collapse(
+          selection.end.path,
+          selection.endIndex + spaces.length,
+        );
+      editorState.apply(transaction);
+      break;
+    }
+    index += line.length + 1;
+  }
+  return KeyEventResult.handled;
+};
+
+CommandShortcutEventHandler _tabToDeleteSpacesInCodeBlockCommandHandler =
+    (editorState) {
+  final selection = editorState.selection;
+  if (selection == null || !selection.isCollapsed) {
+    return KeyEventResult.ignored;
+  }
+  final node = editorState.getNodeAtPath(selection.end.path);
+  final delta = node?.delta;
+  if (node == null || delta == null || node.type != CodeBlockKeys.type) {
+    return KeyEventResult.ignored;
+  }
+  const spaces = '  ';
+  final lines = delta.toPlainText().split('\n');
+  var index = 0;
+  for (final line in lines) {
+    if (index <= selection.endIndex &&
+        selection.endIndex <= index + line.length) {
+      if (line.startsWith(spaces)) {
+        final transaction = editorState.transaction
+          ..deleteText(
+            node,
+            index,
+            spaces.length, // two spaces
+          )
+          ..afterSelection = Selection.collapse(
+            selection.end.path,
+            selection.endIndex - spaces.length,
+          );
+        editorState.apply(transaction);
+      }
+      break;
+    }
+    index += line.length + 1;
+  }
+  return KeyEventResult.handled;
+};

+ 9 - 18
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart

@@ -2,7 +2,7 @@ import 'dart:io';
 import 'dart:ui';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -55,7 +55,7 @@ class CoverColorPicker extends StatefulWidget {
 
   final Color pickerBackgroundColor;
   final Color pickerItemHoverColor;
-  final void Function(String color) onSubmittedbackgroundColorHex;
+  final void Function(String color) onSubmittedBackgroundColorHex;
   final List<ColorOption> backgroundColorOptions;
   const CoverColorPicker({
     super.key,
@@ -63,7 +63,7 @@ class CoverColorPicker extends StatefulWidget {
     required this.pickerBackgroundColor,
     required this.backgroundColorOptions,
     required this.pickerItemHoverColor,
-    required this.onSubmittedbackgroundColorHex,
+    required this.onSubmittedBackgroundColorHex,
   });
 
   @override
@@ -215,21 +215,18 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
   }
 
   Widget _buildColorPickerList() {
+    final theme = Theme.of(context);
     return CoverColorPicker(
-      pickerBackgroundColor:
-          widget.editorState.editorStyle.selectionMenuBackgroundColor ??
-              Colors.white,
-      pickerItemHoverColor:
-          widget.editorState.editorStyle.selectionMenuItemSelectedColor ??
-              Colors.blue.withOpacity(0.3),
+      pickerBackgroundColor: theme.cardColor,
+      pickerItemHoverColor: theme.hoverColor,
       selectedBackgroundColorHex:
           widget.node.attributes[kCoverSelectionTypeAttribute] ==
                   CoverSelectionType.color.toString()
               ? widget.node.attributes[kCoverSelectionAttribute]
-              : "ffffff",
+              : 'ffffff',
       backgroundColorOptions:
           _generateBackgroundColorOptions(widget.editorState),
-      onSubmittedbackgroundColorHex: (color) {
+      onSubmittedBackgroundColorHex: (color) {
         widget.onCoverChanged(CoverSelectionType.color, color);
         setState(() {});
       },
@@ -497,7 +494,7 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
       ),
       hoverColor: widget.pickerItemHoverColor,
       onTap: () {
-        widget.onSubmittedbackgroundColorHex(option.colorHex);
+        widget.onSubmittedBackgroundColorHex(option.colorHex);
       },
       child: Padding(
         padding: const EdgeInsets.only(right: 10.0),
@@ -544,9 +541,3 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
     );
   }
 }
-
-extension on Color {
-  String toHex() {
-    return '0x${value.toRadixString(16)}';
-  }
-}

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover_bloc.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart

@@ -1,8 +1,8 @@
 import 'dart:async';
 import 'dart:io';
 
-import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker.dart

@@ -1,6 +1,6 @@
 import 'dart:io';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart


+ 47 - 30
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart

@@ -1,9 +1,10 @@
 import 'dart:io';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_style.dart';
 import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
 import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
 import 'package:appflowy_popover/appflowy_popover.dart';
@@ -39,7 +40,7 @@ enum CoverSelectionType {
 class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
   @override
   Widget build(NodeWidgetContext<Node> context) {
-    return _CoverImageNodeWidget(
+    return CoverImageNodeWidget(
       key: context.node.key,
       node: context.node,
       editorState: context.editorState,
@@ -52,8 +53,8 @@ class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
       };
 }
 
-class _CoverImageNodeWidget extends StatefulWidget {
-  const _CoverImageNodeWidget({
+class CoverImageNodeWidget extends StatefulWidget {
+  const CoverImageNodeWidget({
     Key? key,
     required this.node,
     required this.editorState,
@@ -63,14 +64,32 @@ class _CoverImageNodeWidget extends StatefulWidget {
   final EditorState editorState;
 
   @override
-  State<_CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
+  State<CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
 }
 
-class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
+class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
   CoverSelectionType get selectionType => CoverSelectionType.fromString(
         widget.node.attributes[kCoverSelectionTypeAttribute],
       );
 
+  @override
+  void initState() {
+    super.initState();
+
+    widget.node.addListener(_reload);
+  }
+
+  @override
+  void dispose() {
+    widget.node.removeListener(_reload);
+
+    super.dispose();
+  }
+
+  void _reload() {
+    setState(() {});
+  }
+
   PopoverController iconPopoverController = PopoverController();
   @override
   Widget build(BuildContext context) {
@@ -141,7 +160,11 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
         height: widget.hasIcon ? 180 : 50.0,
         alignment: Alignment.bottomLeft,
         width: double.infinity,
-        padding: const EdgeInsets.only(top: 20, bottom: 5),
+        padding: EdgeInsets.only(
+          left: EditorStyleCustomizer.horizontalPadding + 30,
+          top: 20,
+          bottom: 5,
+        ),
         child: isHidden
             ? Container()
             : Row(
@@ -304,7 +327,8 @@ class _CoverImageState extends State<_CoverImage> {
         ),
         hasIcon
             ? Positioned(
-                bottom: !hasCover ? 30 : 10,
+                left: EditorStyleCustomizer.horizontalPadding + 30,
+                bottom: !hasCover ? 30 : 40,
                 child: AppFlowyPopover(
                   offset: const Offset(100, 0),
                   controller: iconPopoverController,
@@ -313,9 +337,6 @@ class _CoverImageState extends State<_CoverImage> {
                   margin: EdgeInsets.zero,
                   child: EmojiIconWidget(
                     emoji: widget.node.attributes[kIconSelectionAttribute],
-                    onEmojiTapped: () {
-                      iconPopoverController.show();
-                    },
                   ),
                   popupBuilder: (BuildContext popoverContext) {
                     return EmojiPopover(
@@ -454,7 +475,6 @@ class _CoverImageState extends State<_CoverImage> {
   }
 
   Widget _buildCoverImage(BuildContext context, EditorState editorState) {
-    final screenSize = MediaQuery.of(context).size;
     const height = 250.0;
     final Widget coverImage;
     switch (selectionType) {
@@ -493,7 +513,7 @@ class _CoverImageState extends State<_CoverImage> {
         coverImage = const SizedBox();
         break;
     }
-//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
+// OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
     return MouseRegion(
       onEnter: (event) {
         setOverlayButtonsHidden(false);
@@ -503,21 +523,18 @@ class _CoverImageState extends State<_CoverImage> {
       },
       child: SizedBox(
         height: height,
-        child: OverflowBox(
-          maxWidth: screenSize.width,
-          child: Stack(
-            children: [
-              Container(
-                padding: const EdgeInsets.only(bottom: 10),
-                height: double.infinity,
-                width: double.infinity,
-                child: coverImage,
-              ),
-              hasCover
-                  ? _buildCoverOverlayButtons(context)
-                  : const SizedBox.shrink()
-            ],
-          ),
+        child: Stack(
+          children: [
+            Container(
+              padding: const EdgeInsets.only(bottom: 10),
+              height: double.infinity,
+              width: double.infinity,
+              child: coverImage,
+            ),
+            hasCover
+                ? _buildCoverOverlayButtons(context)
+                : const SizedBox.shrink()
+          ],
         ),
       ),
     );

+ 15 - 23
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/icon_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart

@@ -1,17 +1,18 @@
-import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 
 class EmojiIconWidget extends StatefulWidget {
-  final String? emoji;
-  final void Function() onEmojiTapped;
-
   const EmojiIconWidget({
     super.key,
     required this.emoji,
-    required this.onEmojiTapped,
+    this.size = 80,
+    this.emojiSize = 60,
   });
 
+  final String emoji;
+  final double size;
+  final double emojiSize;
+
   @override
   State<EmojiIconWidget> createState() => _EmojiIconWidgetState();
 }
@@ -22,31 +23,22 @@ class _EmojiIconWidgetState extends State<EmojiIconWidget> {
   @override
   Widget build(BuildContext context) {
     return MouseRegion(
-      onEnter: (event) {
-        setHidden(false);
-      },
-      onExit: (event) {
-        setHidden(true);
-      },
+      onEnter: (_) => setHidden(false),
+      onExit: (_) => setHidden(true),
       child: Container(
-        height: 130,
-        width: 130,
-        margin: const EdgeInsets.only(top: 18),
+        height: widget.size,
+        width: widget.size,
         decoration: BoxDecoration(
           color: !hover
               ? Theme.of(context).colorScheme.inverseSurface
               : Colors.transparent,
-          borderRadius: Corners.s8Border,
+          borderRadius: BorderRadius.circular(8),
         ),
         alignment: Alignment.center,
-        child: Stack(
-          clipBehavior: Clip.none,
-          children: [
-            FlowyText(
-              widget.emoji.toString(),
-              fontSize: 80,
-            ),
-          ],
+        child: FlowyText(
+          widget.emoji,
+          fontSize: widget.emojiSize,
+          textAlign: TextAlign.center,
         ),
       ),
     );

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/emoji_popover.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart


+ 65 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/divider/divider_character_shortcut_event.dart

@@ -0,0 +1,65 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/divider/divider_node_widget.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+/// insert divider into a document by typing three minuses(-).
+///
+/// - support
+///   - desktop
+///   - web
+///   - mobile
+///
+final CharacterShortcutEvent convertMinusesToDivider = CharacterShortcutEvent(
+  key: 'insert a divider',
+  character: '-',
+  handler: _convertMinusesToDividerHandler,
+);
+
+CharacterShortcutEventHandler _convertMinusesToDividerHandler =
+    (editorState) async {
+  final selection = editorState.selection;
+  if (selection == null || !selection.isCollapsed) {
+    return false;
+  }
+  final path = selection.end.path;
+  final node = editorState.getNodeAtPath(path);
+  final delta = node?.delta;
+  if (node == null || delta == null) {
+    return false;
+  }
+  if (delta.toPlainText() != '--') {
+    return false;
+  }
+  final transaction = editorState.transaction
+    ..insertNode(path, dividerNode())
+    ..insertNode(path, paragraphNode())
+    ..deleteNode(node)
+    ..afterSelection = Selection.collapse(path.next, 0);
+  editorState.apply(transaction);
+  return true;
+};
+
+SelectionMenuItem dividerMenuItem = SelectionMenuItem(
+  name: 'Divider',
+  icon: (editorState, onSelected) => const Icon(
+    Icons.horizontal_rule,
+    size: 18.0,
+  ),
+  keywords: ['horizontal rule', 'divider'],
+  handler: (editorState, _, __) {
+    final selection = editorState.selection;
+    if (selection == null || !selection.isCollapsed) {
+      return;
+    }
+    final path = selection.end.path;
+    final node = editorState.getNodeAtPath(path);
+    final delta = node?.delta;
+    if (node == null || delta == null) {
+      return;
+    }
+    final insertedPath = delta.isEmpty ? path : path.next;
+    final transaction = editorState.transaction
+      ..insertNode(insertedPath, dividerNode());
+    editorState.apply(transaction);
+  },
+);

+ 107 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/divider/divider_node_widget.dart

@@ -0,0 +1,107 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+class DividerBlockKeys {
+  const DividerBlockKeys._();
+
+  static const String type = 'divider';
+}
+
+// creating a new callout node
+Node dividerNode() {
+  return Node(
+    type: DividerBlockKeys.type,
+  );
+}
+
+class DividerBlockComponentBuilder extends BlockComponentBuilder {
+  DividerBlockComponentBuilder({
+    this.padding = const EdgeInsets.symmetric(vertical: 8.0),
+    this.lineColor = Colors.grey,
+  });
+
+  final EdgeInsets padding;
+  final Color lineColor;
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return DividerBlockComponentWidget(
+      key: node.key,
+      node: node,
+      padding: padding,
+      lineColor: lineColor,
+    );
+  }
+
+  @override
+  bool validate(Node node) => node.children.isEmpty;
+}
+
+class DividerBlockComponentWidget extends StatefulWidget {
+  const DividerBlockComponentWidget({
+    Key? key,
+    required this.node,
+    this.padding = const EdgeInsets.symmetric(vertical: 8.0),
+    this.lineColor = Colors.grey,
+  }) : super(key: key);
+
+  final Node node;
+  final EdgeInsets padding;
+  final Color lineColor;
+
+  @override
+  State<DividerBlockComponentWidget> createState() =>
+      _DividerBlockComponentWidgetState();
+}
+
+class _DividerBlockComponentWidgetState
+    extends State<DividerBlockComponentWidget> with SelectableMixin {
+  RenderBox get _renderBox => context.findRenderObject() as RenderBox;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: widget.padding,
+      child: Container(
+        height: 1,
+        color: widget.lineColor,
+      ),
+    );
+  }
+
+  @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.cover;
+
+  @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);
+}

+ 120 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart

@@ -0,0 +1,120 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'emoji_picker.dart';
+
+SelectionMenuItem emojiMenuItem = SelectionMenuItem(
+  name: 'Emoji',
+  icon: (editorState, onSelected) => SelectableIconWidget(
+    icon: Icons.emoji_emotions_outlined,
+    isSelected: onSelected,
+  ),
+  keywords: ['emoji'],
+  handler: (editorState, menuService, context) {
+    final container = Overlay.of(context);
+    showEmojiPickerMenu(
+      container,
+      editorState,
+      menuService,
+    );
+  },
+);
+
+void showEmojiPickerMenu(
+  OverlayState container,
+  EditorState editorState,
+  SelectionMenuService menuService,
+) {
+  menuService.dismiss();
+
+  final alignment = menuService.alignment;
+  final offset = menuService.offset;
+  final top = alignment == Alignment.bottomLeft ? offset.dy : null;
+  final bottom = alignment == Alignment.topLeft ? offset.dy : null;
+
+  final emojiPickerMenuEntry = FullScreenOverlayEntry(
+    top: top,
+    bottom: bottom,
+    left: offset.dx,
+    builder: (context) => Material(
+      child: Container(
+        width: 300,
+        height: 250,
+        padding: const EdgeInsets.all(4.0),
+        child: EmojiSelectionMenu(
+          onSubmitted: (emoji) {
+            editorState.insertTextAtCurrentSelection(emoji.emoji);
+          },
+          onExit: () {
+            // close emoji panel
+          },
+        ),
+      ),
+    ),
+  ).build();
+  container.insert(emojiPickerMenuEntry);
+}
+
+class EmojiSelectionMenu extends StatefulWidget {
+  const EmojiSelectionMenu({
+    Key? key,
+    required this.onSubmitted,
+    required this.onExit,
+  }) : super(key: key);
+
+  final void Function(Emoji emoji) onSubmitted;
+  final void Function() onExit;
+
+  @override
+  State<EmojiSelectionMenu> createState() => _EmojiSelectionMenuState();
+}
+
+class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
+  @override
+  void initState() {
+    HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
+    super.initState();
+  }
+
+  bool _handleGlobalKeyEvent(KeyEvent event) {
+    if (event.logicalKey == LogicalKeyboardKey.escape &&
+        event is KeyDownEvent) {
+      //triggers on esc
+      widget.onExit();
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  @override
+  void deactivate() {
+    HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
+    super.deactivate();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return EmojiPicker(
+      onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
+      config: const Config(
+        columns: 7,
+        emojiSizeMax: 28,
+        bgColor: Colors.transparent,
+        iconColor: Colors.grey,
+        iconColorSelected: Color(0xff333333),
+        indicatorColor: Color(0xff333333),
+        progressIndicatorColor: Color(0xff333333),
+        buttonMode: ButtonMode.CUPERTINO,
+        initCategory: Category.RECENT,
+      ),
+    );
+  }
+}

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_picker.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/config.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/config.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/default_emoji_picker_view.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_lists.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_lists.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_picker.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_picker_builder.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker_builder.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_view_state.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_view_state.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/category_models.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/category_models.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/emoji_model.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/emoji_model.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/recent_emoji_model.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/recent_emoji_model.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/extensions/flowy_tint_extension.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart


+ 8 - 12
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_menu_item.dart

@@ -1,28 +1,24 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
 import 'package:flutter/material.dart';
 
 SelectionMenuItem gridMenuItem = SelectionMenuItem(
   name: LocaleKeys.document_plugins_referencedGrid.tr(),
-  icon: (editorState, onSelected) {
-    return svgWidget(
-      'editor/grid',
-      size: const Size.square(18.0),
-      color: onSelected
-          ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-          : editorState.editorStyle.selectionMenuItemIconColor,
-    );
-  },
+  icon: (editorState, onSelected) => SelectableSvgWidget(
+    name: 'editor/board',
+    isSelected: onSelected,
+  ),
   keywords: ['referenced', 'grid'],
   handler: (editorState, menuService, context) {
+    final container = Overlay.of(context);
     showLinkToPageMenu(
+      container,
       editorState,
       menuService,
-      context,
       ViewLayoutPB.Grid,
     );
   },

+ 76 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart

@@ -0,0 +1,76 @@
+import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class GridBlockKeys {
+  const GridBlockKeys._();
+
+  static const String type = 'grid';
+}
+
+class GridBlockComponentBuilder extends BlockComponentBuilder {
+  GridBlockComponentBuilder({
+    this.configuration = const BlockComponentConfiguration(),
+  });
+
+  @override
+  final BlockComponentConfiguration configuration;
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return GridBlockComponentWidget(
+      key: node.key,
+      node: node,
+      configuration: configuration,
+    );
+  }
+
+  @override
+  bool validate(Node node) =>
+      node.children.isEmpty &&
+      node.attributes[DatabaseBlockKeys.kAppID] is String &&
+      node.attributes[DatabaseBlockKeys.kViewID] is String;
+}
+
+class GridBlockComponentWidget extends StatefulWidget {
+  const GridBlockComponentWidget({
+    super.key,
+    required this.configuration,
+    required this.node,
+  });
+
+  final Node node;
+  final BlockComponentConfiguration configuration;
+
+  @override
+  State<GridBlockComponentWidget> createState() =>
+      _GridBlockComponentWidgetState();
+}
+
+class _GridBlockComponentWidgetState extends State<GridBlockComponentWidget>
+    with BlockComponentConfigurable {
+  @override
+  Node get node => widget.node;
+
+  @override
+  BlockComponentConfiguration get configuration => widget.configuration;
+
+  @override
+  Widget build(BuildContext context) {
+    final editorState = Provider.of<EditorState>(context, listen: false);
+    return BuiltInPageWidget(
+      node: widget.node,
+      editorState: editorState,
+      builder: (viewPB) {
+        return GridPage(
+          key: ValueKey(viewPB.id),
+          view: viewPB,
+        );
+      },
+    );
+  }
+}

+ 6 - 12
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_view_menu_item.dart

@@ -1,25 +1,19 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/application/doc_bloc.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
 import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
-import 'package:flutter/material.dart';
 
 SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
     SelectionMenuItem(
       name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
-      icon: (editorState, onSelected) {
-        return svgWidget(
-          'editor/grid',
-          size: const Size.square(18.0),
-          color: onSelected
-              ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-              : editorState.editorStyle.selectionMenuItemIconColor,
-        );
-      },
+      icon: (editorState, onSelected) => SelectableSvgWidget(
+        name: 'editor/grid',
+        isSelected: onSelected,
+      ),
       keywords: ['grid'],
       handler: (editorState, menuService, context) async {
         if (!documentBloc.view.hasParentViewId()) {

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/infra/svg.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart


+ 214 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart

@@ -0,0 +1,214 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
+import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_math_fork/flutter_math.dart';
+import 'package:provider/provider.dart';
+
+class MathEquationBlockKeys {
+  const MathEquationBlockKeys._();
+
+  static const String type = 'math_equation';
+
+  /// The content of a math equation block.
+  ///
+  /// The value is a String.
+  static const String formula = 'formula';
+}
+
+Node mathEquationNode({
+  String formula = '',
+}) {
+  final attributes = {
+    MathEquationBlockKeys.formula: formula,
+  };
+  return Node(
+    type: MathEquationBlockKeys.type,
+    attributes: attributes,
+  );
+}
+
+// defining the callout block menu item for selection
+SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
+  name: 'MathEquation',
+  iconData: Icons.text_fields_rounded,
+  keywords: ['tex, latex, katex', 'math equation', 'formula'],
+  nodeBuilder: (editorState) => mathEquationNode(),
+  replace: (_, node) => node.delta?.isEmpty ?? false,
+  updateSelection: (editorState, path, __, ___) {
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      final mathEquationState =
+          editorState.getNodeAtPath(path)?.key.currentState;
+      if (mathEquationState != null &&
+          mathEquationState is _MathEquationBlockComponentWidgetState) {
+        mathEquationState.showEditingDialog();
+      }
+    });
+    return null;
+  },
+);
+
+class MathEquationBlockComponentBuilder extends BlockComponentBuilder {
+  MathEquationBlockComponentBuilder({
+    this.configuration = const BlockComponentConfiguration(),
+  });
+
+  @override
+  final BlockComponentConfiguration configuration;
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return MathEquationBlockComponentWidget(
+      key: node.key,
+      node: node,
+      configuration: configuration,
+    );
+  }
+
+  @override
+  bool validate(Node node) =>
+      node.children.isEmpty &&
+      node.attributes[MathEquationBlockKeys.formula] is String;
+}
+
+class MathEquationBlockComponentWidget extends StatefulWidget {
+  const MathEquationBlockComponentWidget({
+    Key? key,
+    required this.node,
+    this.configuration = const BlockComponentConfiguration(),
+  }) : super(key: key);
+
+  final Node node;
+  final BlockComponentConfiguration configuration;
+
+  @override
+  State<MathEquationBlockComponentWidget> createState() =>
+      _MathEquationBlockComponentWidgetState();
+}
+
+class _MathEquationBlockComponentWidgetState
+    extends State<MathEquationBlockComponentWidget>
+    with BlockComponentConfigurable {
+  @override
+  BlockComponentConfiguration get configuration => widget.configuration;
+
+  @override
+  Node get node => widget.node;
+
+  bool isHover = false;
+  String get formula =>
+      widget.node.attributes[MathEquationBlockKeys.formula] as String;
+
+  late final editorState = context.read<EditorState>();
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      onHover: (value) => setState(() => isHover = value),
+      onTap: showEditingDialog,
+      child: _buildMathEquation(context),
+    );
+  }
+
+  Widget _buildMathEquation(BuildContext context) {
+    return Container(
+      width: double.infinity,
+      constraints: const BoxConstraints(minHeight: 50),
+      padding: padding,
+      decoration: BoxDecoration(
+        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
+        color: isHover || formula.isEmpty
+            ? Theme.of(context).colorScheme.tertiaryContainer
+            : Colors.transparent,
+      ),
+      child: Center(
+        child: formula.isEmpty
+            ? FlowyText.medium(
+                LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
+                fontSize: 16,
+              )
+            : Math.tex(
+                formula,
+                textStyle: const TextStyle(fontSize: 20),
+                mathStyle: MathStyle.display,
+              ),
+      ),
+    );
+  }
+
+  void showEditingDialog() {
+    showDialog(
+      context: context,
+      builder: (context) {
+        final controller = TextEditingController(text: formula);
+        return AlertDialog(
+          backgroundColor: Theme.of(context).canvasColor,
+          title: Text(
+            LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),
+          ),
+          content: RawKeyboardListener(
+            focusNode: FocusNode(),
+            onKey: (key) {
+              if (key is! RawKeyDownEvent) return;
+              if (key.logicalKey == LogicalKeyboardKey.enter &&
+                  !key.isShiftPressed) {
+                updateMathEquation(controller.text, context);
+              } else if (key.logicalKey == LogicalKeyboardKey.escape) {
+                dismiss(context);
+              }
+            },
+            child: SizedBox(
+              width: MediaQuery.of(context).size.width * 0.3,
+              child: TextField(
+                autofocus: true,
+                controller: controller,
+                maxLines: null,
+                decoration: const InputDecoration(
+                  border: OutlineInputBorder(),
+                  hintText: 'E = MC^2',
+                ),
+              ),
+            ),
+          ),
+          actions: [
+            SecondaryTextButton(
+              LocaleKeys.button_Cancel.tr(),
+              onPressed: () => dismiss(context),
+            ),
+            PrimaryTextButton(
+              LocaleKeys.button_Done.tr(),
+              onPressed: () => updateMathEquation(controller.text, context),
+            ),
+          ],
+          actionsPadding: const EdgeInsets.only(bottom: 20),
+          actionsAlignment: MainAxisAlignment.spaceAround,
+        );
+      },
+    );
+  }
+
+  void updateMathEquation(String mathEquation, BuildContext context) {
+    if (mathEquation == formula) {
+      dismiss(context);
+      return;
+    }
+    final transaction = editorState.transaction
+      ..updateNode(
+        widget.node,
+        {
+          MathEquationBlockKeys.formula: mathEquation,
+        },
+      );
+    editorState.apply(transaction);
+    dismiss(context);
+  }
+
+  void dismiss(BuildContext context) {
+    Navigator.of(context).pop();
+  }
+}

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/error.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart


+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart

@@ -1,6 +1,6 @@
 import 'dart:convert';
 
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
 
 import 'text_completion.dart';
 import 'package:dartz/dartz.dart';

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_completion.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_edit.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart


+ 418 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart

@@ -0,0 +1,418 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
+import 'package:appflowy/user/application/user_service.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
+import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:provider/provider.dart';
+
+class AutoCompletionBlockKeys {
+  const AutoCompletionBlockKeys._();
+
+  static const String type = 'auto_completion';
+  static const String prompt = 'prompt';
+  static const String startSelection = 'start_selection';
+}
+
+Node autoCompletionNode({
+  String prompt = '',
+  required Selection start,
+}) {
+  return Node(
+    type: AutoCompletionBlockKeys.type,
+    attributes: {
+      AutoCompletionBlockKeys.prompt: prompt,
+      AutoCompletionBlockKeys.startSelection: start.toJson(),
+    },
+  );
+}
+
+SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
+  name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
+  iconData: Icons.generating_tokens,
+  keywords: ['ai', 'openai' 'writer', 'autogenerator'],
+  nodeBuilder: (editorState) {
+    final node = autoCompletionNode(start: editorState.selection!);
+    return node;
+  },
+  replace: (_, node) => false,
+);
+
+class AutoCompletionBlockComponentBuilder extends BlockComponentBuilder {
+  AutoCompletionBlockComponentBuilder();
+
+  @override
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return AutoCompletionBlockComponent(
+      key: node.key,
+      node: node,
+    );
+  }
+
+  @override
+  bool validate(Node node) {
+    return node.children.isEmpty &&
+        node.attributes[AutoCompletionBlockKeys.prompt] is String &&
+        node.attributes[AutoCompletionBlockKeys.startSelection] is Map;
+  }
+}
+
+class AutoCompletionBlockComponent extends StatefulWidget {
+  const AutoCompletionBlockComponent({
+    super.key,
+    required this.node,
+  });
+
+  final Node node;
+
+  @override
+  State<AutoCompletionBlockComponent> createState() =>
+      _AutoCompletionBlockComponentState();
+}
+
+class _AutoCompletionBlockComponentState
+    extends State<AutoCompletionBlockComponent> {
+  final controller = TextEditingController();
+  final textFieldFocusNode = FocusNode();
+
+  late final editorState = context.read<EditorState>();
+  late final SelectionGestureInterceptor interceptor;
+
+  String get prompt => widget.node.attributes[AutoCompletionBlockKeys.prompt];
+
+  @override
+  void initState() {
+    super.initState();
+
+    _subscribeSelectionGesture();
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      editorState.selection = null;
+      textFieldFocusNode.requestFocus();
+    });
+  }
+
+  @override
+  void dispose() {
+    _unsubscribeSelectionGesture();
+    controller.dispose();
+
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Card(
+      elevation: 5,
+      color: Theme.of(context).colorScheme.surface,
+      child: Container(
+        margin: const EdgeInsets.all(10),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            const AutoCompletionHeader(),
+            const Space(0, 10),
+            if (prompt.isEmpty) ...[
+              _buildInputWidget(context),
+              const Space(0, 10),
+              AutoCompletionInputFooter(
+                onGenerate: _onGenerate,
+                onExit: _onExit,
+              ),
+            ] else ...[
+              AutoCompletionFooter(
+                onKeep: _onExit,
+                onDiscard: _onDiscard,
+              )
+            ],
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildInputWidget(BuildContext context) {
+    return FlowyTextField(
+      hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
+      controller: controller,
+      maxLines: 3,
+      focusNode: textFieldFocusNode,
+      autoFocus: false,
+    );
+  }
+
+  Future<void> _onExit() async {
+    final transaction = editorState.transaction..deleteNode(widget.node);
+    await editorState.apply(
+      transaction,
+      options: const ApplyOptions(
+        // disable undo/redo
+        recordRedo: false,
+        recordUndo: false,
+      ),
+    );
+  }
+
+  Future<void> _onGenerate() async {
+    final loading = Loading(context);
+    loading.start();
+
+    await _updateEditingText();
+
+    final userProfile = await UserBackendService.getCurrentUserProfile()
+        .then((value) => value.toOption().toNullable());
+    if (userProfile == null) {
+      loading.stop();
+      await _showError(
+        LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
+      );
+      return;
+    }
+
+    final textRobot = TextRobot(editorState: editorState);
+    BarrierDialog? barrierDialog;
+    final openAIRepository = HttpOpenAIRepository(
+      client: http.Client(),
+      apiKey: userProfile.openaiKey,
+    );
+    await openAIRepository.getStreamedCompletions(
+      prompt: controller.text,
+      onStart: () async {
+        loading.stop();
+        barrierDialog = BarrierDialog(context);
+        barrierDialog?.show();
+        await _makeSurePreviousNodeIsEmptyParagraphNode();
+      },
+      onProcess: (response) async {
+        if (response.choices.isNotEmpty) {
+          final text = response.choices.first.text;
+          await textRobot.autoInsertText(
+            text,
+            inputType: TextRobotInputType.word,
+            delay: Duration.zero,
+          );
+        }
+      },
+      onEnd: () async {
+        await barrierDialog?.dismiss();
+      },
+      onError: (error) async {
+        loading.stop();
+        await _showError(error.message);
+      },
+    );
+  }
+
+  Future<void> _onDiscard() async {
+    final selection =
+        widget.node.attributes[AutoCompletionBlockKeys.startSelection];
+    if (selection != null) {
+      final start = Selection.fromJson(selection).start.path;
+      final end = widget.node.previous?.path;
+      if (end != null) {
+        final transaction = editorState.transaction;
+        transaction.deleteNodesAtPath(
+          start,
+          end.last - start.last + 1,
+        );
+        await editorState.apply(transaction);
+      }
+    }
+    _onExit();
+  }
+
+  Future<void> _updateEditingText() async {
+    final transaction = editorState.transaction;
+    transaction.updateNode(
+      widget.node,
+      {
+        AutoCompletionBlockKeys.prompt: controller.text,
+      },
+    );
+    await editorState.apply(transaction);
+  }
+
+  Future<void> _makeSurePreviousNodeIsEmptyParagraphNode() async {
+    // make sure the previous node is a empty paragraph node without any styles.
+    final transaction = editorState.transaction;
+    final previous = widget.node.previous;
+    final Selection selection;
+    if (previous == null ||
+        previous.type != 'paragraph' ||
+        (previous.delta?.toPlainText().isNotEmpty ?? false)) {
+      selection = Selection.single(
+        path: widget.node.path,
+        startOffset: 0,
+      );
+      transaction.insertNode(
+        widget.node.path,
+        paragraphNode(),
+      );
+    } else {
+      selection = Selection.single(
+        path: previous.path,
+        startOffset: 0,
+      );
+    }
+    transaction.updateNode(widget.node, {
+      AutoCompletionBlockKeys.startSelection: selection.toJson(),
+    });
+    transaction.afterSelection = selection;
+    await editorState.apply(transaction);
+  }
+
+  Future<void> _showError(String message) async {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(
+        action: SnackBarAction(
+          label: LocaleKeys.button_Cancel.tr(),
+          onPressed: () {
+            ScaffoldMessenger.of(context).hideCurrentSnackBar();
+          },
+        ),
+        content: FlowyText(message),
+      ),
+    );
+  }
+
+  void _subscribeSelectionGesture() {
+    interceptor = SelectionGestureInterceptor(
+      key: AutoCompletionBlockKeys.type,
+      canTap: (details) {
+        if (!context.isOffsetInside(details.globalPosition)) {
+          if (prompt.isNotEmpty || controller.text.isNotEmpty) {
+            // show dialog
+            showDialog(
+              context: context,
+              builder: (context) {
+                return DiscardDialog(
+                  onConfirm: () => _onDiscard(),
+                  onCancel: () {},
+                );
+              },
+            );
+          } else if (controller.text.isEmpty) {
+            _onExit();
+          }
+        }
+        editorState.service.keyboardService?.disable();
+        return false;
+      },
+    );
+    editorState.service.selectionService.registerGestureInterceptor(
+      interceptor,
+    );
+  }
+
+  void _unsubscribeSelectionGesture() {
+    editorState.service.selectionService.unregisterGestureInterceptor(
+      AutoCompletionBlockKeys.type,
+    );
+  }
+}
+
+class AutoCompletionHeader extends StatelessWidget {
+  const AutoCompletionHeader({
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        FlowyText.medium(
+          LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
+          fontSize: 14,
+        ),
+        const Spacer(),
+        FlowyButton(
+          useIntrinsicWidth: true,
+          text: FlowyText.regular(
+            LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
+          ),
+          onTap: () async {
+            await openLearnMorePage();
+          },
+        )
+      ],
+    );
+  }
+}
+
+class AutoCompletionInputFooter extends StatelessWidget {
+  const AutoCompletionInputFooter({
+    super.key,
+    required this.onGenerate,
+    required this.onExit,
+  });
+
+  final VoidCallback onGenerate;
+  final VoidCallback onExit;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        PrimaryTextButton(
+          LocaleKeys.button_generate.tr(),
+          onPressed: onGenerate,
+        ),
+        const Space(10, 0),
+        SecondaryTextButton(
+          LocaleKeys.button_Cancel.tr(),
+          onPressed: onExit,
+        ),
+        Expanded(
+          child: Container(
+            alignment: Alignment.centerRight,
+            child: FlowyText.regular(
+              LocaleKeys.document_plugins_warning.tr(),
+              color: Theme.of(context).hintColor,
+              overflow: TextOverflow.ellipsis,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class AutoCompletionFooter extends StatelessWidget {
+  const AutoCompletionFooter({
+    super.key,
+    required this.onKeep,
+    required this.onDiscard,
+  });
+
+  final VoidCallback onKeep;
+  final VoidCallback onDiscard;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        PrimaryTextButton(
+          LocaleKeys.button_keep.tr(),
+          onPressed: onKeep,
+        ),
+        const Space(10, 0),
+        SecondaryTextButton(
+          LocaleKeys.button_discard.tr(),
+          onPressed: onDiscard,
+        ),
+      ],
+    );
+  }
+}

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart


+ 47 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart

@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+
+class Loading {
+  Loading(this.context);
+
+  late BuildContext loadingContext;
+  final BuildContext context;
+
+  Future<void> start() async => await showDialog<void>(
+        context: context,
+        barrierDismissible: false,
+        builder: (BuildContext context) {
+          loadingContext = context;
+          return const SimpleDialog(
+            elevation: 0.0,
+            backgroundColor:
+                Colors.transparent, // can change this to your preferred color
+            children: [
+              Center(
+                child: CircularProgressIndicator(),
+              )
+            ],
+          );
+        },
+      );
+
+  Future<void> stop() async => Navigator.of(loadingContext).pop();
+}
+
+class BarrierDialog {
+  BarrierDialog(this.context);
+
+  late BuildContext loadingContext;
+  final BuildContext context;
+
+  Future<void> show() async => await showDialog<void>(
+        context: context,
+        barrierDismissible: false,
+        barrierColor: Colors.transparent,
+        builder: (BuildContext context) {
+          loadingContext = context;
+          return Container();
+        },
+      );
+
+  Future<void> dismiss() async => Navigator.of(loadingContext).pop();
+}

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart


+ 134 - 91
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart

@@ -1,9 +1,9 @@
 import 'dart:async';
 
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
@@ -13,59 +13,98 @@ import 'package:flutter/material.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:http/http.dart' as http;
+import 'package:provider/provider.dart';
 
 const String kSmartEditType = 'smart_edit_input';
 const String kSmartEditInstructionType = 'smart_edit_instruction';
 const String kSmartEditInputType = 'smart_edit_input';
 
-class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return SmartEditAction.values
-                .map((e) => e.index)
-                .contains(node.attributes[kSmartEditInstructionType]) &&
-            node.attributes[kSmartEditInputType] is String;
-      };
+class SmartEditBlockKeys {
+  const SmartEditBlockKeys._();
+
+  static const type = 'smart_edit';
+
+  /// The instruction of the smart edit.
+  ///
+  /// It is a [SmartEditAction] value.
+  static const action = 'action';
+
+  /// The input of the smart edit.
+  static const content = 'content';
+}
+
+Node smartEditNode({
+  required SmartEditAction action,
+  required String content,
+}) {
+  return Node(
+    type: SmartEditBlockKeys.type,
+    attributes: {
+      SmartEditBlockKeys.action: action.index,
+      SmartEditBlockKeys.content: content,
+    },
+  );
+}
+
+class SmartEditBlockComponentBuilder extends BlockComponentBuilder {
+  SmartEditBlockComponentBuilder();
 
   @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _HoverSmartInput(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
+  Widget build(BlockComponentContext blockComponentContext) {
+    final node = blockComponentContext.node;
+    return SmartEditBlockComponentWidget(
+      key: node.key,
+      node: node,
     );
   }
+
+  @override
+  bool validate(Node node) =>
+      node.attributes[SmartEditBlockKeys.action] is int &&
+      node.attributes[SmartEditBlockKeys.content] is String;
 }
 
-class _HoverSmartInput extends StatefulWidget {
-  const _HoverSmartInput({
+class SmartEditBlockComponentWidget extends StatefulWidget {
+  const SmartEditBlockComponentWidget({
     required super.key,
     required this.node,
-    required this.editorState,
   });
 
   final Node node;
-  final EditorState editorState;
 
   @override
-  State<_HoverSmartInput> createState() => _HoverSmartInputState();
+  State<SmartEditBlockComponentWidget> createState() =>
+      _SmartEditBlockComponentWidgetState();
 }
 
-class _HoverSmartInputState extends State<_HoverSmartInput> {
+class _SmartEditBlockComponentWidgetState
+    extends State<SmartEditBlockComponentWidget> {
   final popoverController = PopoverController();
   final key = GlobalKey(debugLabel: 'smart_edit_input');
 
+  late final editorState = context.read<EditorState>();
+
   @override
   void initState() {
     super.initState();
+
+    // todo: don't use a popover to show the content of the smart edit.
     WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
       popoverController.show();
     });
   }
 
+  @override
+  void reassemble() {
+    super.reassemble();
+
+    final transaction = editorState.transaction..deleteNode(widget.node);
+    editorState.apply(transaction);
+  }
+
   @override
   Widget build(BuildContext context) {
-    final width = _maxWidth();
+    final width = _getEditorWidth();
 
     return AppFlowyPopover(
       controller: popoverController,
@@ -82,7 +121,7 @@ class _HoverSmartInputState extends State<_HoverSmartInput> {
       ),
       canClose: () async {
         final completer = Completer<bool>();
-        final state = key.currentState as _SmartEditInputState;
+        final state = key.currentState as _SmartEditInputWidgetState;
         if (state.result.isEmpty) {
           completer.complete(true);
         } else {
@@ -98,20 +137,24 @@ class _HoverSmartInputState extends State<_HoverSmartInput> {
         }
         return completer.future;
       },
+      onClose: () {
+        final transaction = editorState.transaction..deleteNode(widget.node);
+        editorState.apply(transaction);
+      },
       popupBuilder: (BuildContext popoverContext) {
-        return _SmartEditInput(
+        return SmartEditInputWidget(
           key: key,
           node: widget.node,
-          editorState: widget.editorState,
+          editorState: editorState,
         );
       },
     );
   }
 
-  double _maxWidth() {
+  double _getEditorWidth() {
     var width = double.infinity;
-    final editorSize = widget.editorState.renderBox?.size;
-    final padding = widget.editorState.editorStyle.padding;
+    final editorSize = editorState.renderBox?.size;
+    final padding = editorState.editorStyle.padding;
     if (editorSize != null && padding != null) {
       width = editorSize.width - padding.left - padding.right;
     }
@@ -119,8 +162,8 @@ class _HoverSmartInputState extends State<_HoverSmartInput> {
   }
 }
 
-class _SmartEditInput extends StatefulWidget {
-  const _SmartEditInput({
+class SmartEditInputWidget extends StatefulWidget {
+  const SmartEditInputWidget({
     required super.key,
     required this.node,
     required this.editorState,
@@ -130,16 +173,19 @@ class _SmartEditInput extends StatefulWidget {
   final EditorState editorState;
 
   @override
-  State<_SmartEditInput> createState() => _SmartEditInputState();
+  State<SmartEditInputWidget> createState() => _SmartEditInputWidgetState();
 }
 
-class _SmartEditInputState extends State<_SmartEditInput> {
-  SmartEditAction get action =>
-      SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
-  String get input => widget.node.attributes[kSmartEditInputType];
-
+class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
   final focusNode = FocusNode();
   final client = http.Client();
+
+  SmartEditAction get action => SmartEditAction.from(
+        widget.node.attributes[SmartEditBlockKeys.action],
+      );
+  String get content => widget.node.attributes[SmartEditBlockKeys.content];
+  EditorState get editorState => widget.editorState;
+
   bool loading = true;
   String result = '';
 
@@ -147,19 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> {
   void initState() {
     super.initState();
 
-    widget.editorState.service.keyboardService?.disable(showCursor: true);
-    focusNode.requestFocus();
-    focusNode.addListener(() {
-      if (!focusNode.hasFocus) {
-        widget.editorState.service.keyboardService?.enable();
-      }
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      editorState.service.keyboardService?.disable();
+      // editorState.selection = null;
     });
+
+    focusNode.requestFocus();
     _requestCompletions();
   }
 
   @override
   void dispose() {
     client.close();
+    focusNode.dispose();
     super.dispose();
   }
 
@@ -272,12 +318,15 @@ class _SmartEditInputState extends State<_SmartEditInput> {
           ),
           onPressed: () async => await _onExit(),
         ),
-        const Spacer(flex: 2),
+        const Spacer(flex: 1),
         Expanded(
-          child: FlowyText.regular(
-            overflow: TextOverflow.ellipsis,
-            LocaleKeys.document_plugins_warning.tr(),
-            color: Theme.of(context).hintColor,
+          child: Container(
+            alignment: Alignment.centerRight,
+            child: FlowyText.regular(
+              LocaleKeys.document_plugins_warning.tr(),
+              color: Theme.of(context).hintColor,
+              overflow: TextOverflow.ellipsis,
+            ),
           ),
         ),
       ],
@@ -285,72 +334,66 @@ class _SmartEditInputState extends State<_SmartEditInput> {
   }
 
   Future<void> _onReplace() async {
-    final selection = widget.editorState.service.selectionService
-        .currentSelection.value?.normalized;
-    final selectedNodes = widget
-        .editorState.service.selectionService.currentSelectedNodes.normalized
-        .whereType<TextNode>();
-    if (selection == null || result.isEmpty) {
+    final selection = editorState.selection?.normalized;
+    if (selection == null) {
       return;
     }
-
-    final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
-    final transaction = widget.editorState.transaction;
+    final nodes = editorState.getNodesInSelection(selection);
+    if (nodes.isEmpty || !nodes.every((element) => element.delta != null)) {
+      return;
+    }
+    final replaceTexts = result.split('\n')
+      ..removeWhere((element) => element.isEmpty);
+    final transaction = editorState.transaction;
     transaction.replaceTexts(
-      selectedNodes.toList(growable: false),
+      nodes,
       selection,
-      texts,
+      replaceTexts,
     );
-    await widget.editorState.apply(transaction);
+    await editorState.apply(transaction);
 
-    int endOffset = texts.last.length;
-    if (texts.length == 1) {
+    int endOffset = replaceTexts.last.length;
+    if (replaceTexts.length == 1) {
       endOffset += selection.start.offset;
     }
 
-    await widget.editorState.updateCursorSelection(
-      Selection(
-        start: selection.start,
-        end: Position(
-          path: [selection.start.path.first + texts.length - 1],
-          offset: endOffset,
-        ),
+    editorState.selection = Selection(
+      start: selection.start,
+      end: Position(
+        path: [selection.start.path.first + replaceTexts.length - 1],
+        offset: endOffset,
       ),
     );
   }
 
   Future<void> _onInsertBelow() async {
-    final selection = widget.editorState.service.selectionService
-        .currentSelection.value?.normalized;
-    if (selection == null || result.isEmpty) {
+    final selection = editorState.selection?.normalized;
+    if (selection == null) {
       return;
     }
-    final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
-    final transaction = widget.editorState.transaction;
+    final insertedText = result.split('\n')
+      ..removeWhere((element) => element.isEmpty);
+    final transaction = editorState.transaction;
     transaction.insertNodes(
-      selection.normalized.end.path.next,
-      texts.map(
-        (e) => TextNode(
-          delta: Delta()..insert(e),
+      selection.end.path.next,
+      insertedText.map(
+        (e) => paragraphNode(
+          text: e,
         ),
       ),
     );
-    await widget.editorState.apply(transaction);
-
-    await widget.editorState.updateCursorSelection(
-      Selection(
-        start: Position(path: selection.end.path.next, offset: 0),
-        end: Position(
-          path: [selection.end.path.next.first + texts.length],
-        ),
+    transaction.afterSelection = Selection(
+      start: Position(path: selection.end.path.next, offset: 0),
+      end: Position(
+        path: [selection.end.path.next.first + insertedText.length],
       ),
     );
+    await editorState.apply(transaction);
   }
 
   Future<void> _onExit() async {
-    final transaction = widget.editorState.transaction;
-    transaction.deleteNode(widget.node);
-    return widget.editorState.apply(
+    final transaction = editorState.transaction..deleteNode(widget.node);
+    return editorState.apply(
       transaction,
       options: const ApplyOptions(
         recordRedo: false,
@@ -362,7 +405,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
   Future<void> _requestCompletions() async {
     final openAIRepository = await getIt.getAsync<OpenAIRepository>();
 
-    var lines = input.split('\n\n');
+    var lines = content.split('\n\n');
     if (action == SmartEditAction.summarize) {
       lines = [lines.join('\n')];
     }

+ 31 - 39
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -1,5 +1,5 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -10,33 +10,34 @@ import 'package:flutter/material.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
 
-ToolbarItem smartEditItem = ToolbarItem(
-  id: 'appflowy.toolbar.smart_edit',
-  type: 0, // headmost
-  validator: (editorState) {
-    // All selected nodes must be text.
-    final nodes = editorState.service.selectionService.currentSelectedNodes;
-    return nodes.whereType<TextNode>().length == nodes.length;
-  },
-  itemBuilder: (context, editorState) {
-    return _SmartEditWidget(
-      editorState: editorState,
-    );
+final ToolbarItem smartEditItem = ToolbarItem(
+  id: 'appflowy.editor.smart_edit',
+  isActive: (editorState) {
+    final selection = editorState.selection;
+    if (selection == null) {
+      return false;
+    }
+    final nodes = editorState.getNodesInSelection(selection);
+    return nodes.every((element) => element.delta != null);
   },
+  builder: (context, editorState) => SmartEditActionList(
+    editorState: editorState,
+  ),
 );
 
-class _SmartEditWidget extends StatefulWidget {
-  const _SmartEditWidget({
+class SmartEditActionList extends StatefulWidget {
+  const SmartEditActionList({
+    super.key,
     required this.editorState,
   });
 
   final EditorState editorState;
 
   @override
-  State<_SmartEditWidget> createState() => _SmartEditWidgetState();
+  State<SmartEditActionList> createState() => _SmartEditActionListState();
 }
 
-class _SmartEditWidgetState extends State<_SmartEditWidget> {
+class _SmartEditActionListState extends State<SmartEditActionList> {
   bool isOpenAIEnabled = false;
 
   @override
@@ -45,8 +46,10 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
 
     UserBackendService.getCurrentUserProfile().then((value) {
       setState(() {
-        isOpenAIEnabled =
-            value.fold((l) => l.openaiKey.isNotEmpty, (r) => false);
+        isOpenAIEnabled = value.fold(
+          (l) => false,
+          (r) => r.openaiKey.isNotEmpty,
+        );
       });
     });
   }
@@ -67,7 +70,7 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
           preferBelow: false,
           icon: const Icon(
             Icons.lightbulb_outline,
-            size: 13,
+            size: 15,
             color: Colors.white,
           ),
           onPressed: () {
@@ -89,40 +92,29 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
   Future<void> _insertSmartEditNode(
     SmartEditActionWrapper actionWrapper,
   ) async {
-    final selection =
-        widget.editorState.service.selectionService.currentSelection.value;
+    final selection = widget.editorState.selection?.normalized;
     if (selection == null) {
       return;
     }
-    final textNodes = widget
-        .editorState.service.selectionService.currentSelectedNodes
-        .whereType<TextNode>()
-        .toList(growable: false);
-    final input = widget.editorState.getTextInSelection(
-      textNodes.normalized,
-      selection.normalized,
-    );
+    final input = widget.editorState.getTextInSelection(selection);
     while (input.last.isEmpty) {
       input.removeLast();
     }
     final transaction = widget.editorState.transaction;
     transaction.insertNode(
       selection.normalized.end.path.next,
-      Node(
-        type: kSmartEditType,
-        attributes: {
-          kSmartEditInstructionType: actionWrapper.inner.index,
-          kSmartEditInputType: input.join('\n\n'),
-        },
+      smartEditNode(
+        action: actionWrapper.inner,
+        content: input.join('\n\n'),
       ),
     );
-    return widget.editorState.apply(
+    await widget.editorState.apply(
       transaction,
       options: const ApplyOptions(
         recordUndo: false,
         recordRedo: false,
       ),
-      withUpdateCursor: false,
+      withUpdateSelection: false,
     );
   }
 

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/divider_node_parser.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart


+ 2 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart

@@ -1,10 +1,11 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 
 class MathEquationNodeParser extends NodeParser {
   const MathEquationNodeParser();
 
   @override
-  String get id => 'math_equation';
+  String get id => MathEquationBlockKeys.type;
 
   @override
   String transform(Node node) {

+ 4 - 5
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/plugins.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart

@@ -1,21 +1,20 @@
 export 'board/board_node_widget.dart';
 export 'board/board_menu_item.dart';
 export 'board/board_view_menu_item.dart';
-export 'callout/callout_node_widget.dart';
-export 'code_block/code_block_node_widget.dart';
+export 'callout/callout_block_component.dart';
+export 'code_block/code_block_component.dart';
 export 'code_block/code_block_shortcut_event.dart';
 export 'cover/change_cover_popover_bloc.dart';
 export 'cover/cover_node_widget.dart';
 export 'cover/cover_image_picker.dart';
 export 'divider/divider_node_widget.dart';
-export 'divider/divider_shortcut_event.dart';
+export 'divider/divider_character_shortcut_event.dart';
 export 'emoji_picker/emoji_menu_item.dart';
 export 'extensions/flowy_tint_extension.dart';
 export 'grid/grid_menu_item.dart';
 export 'grid/grid_node_widget.dart';
 export 'grid/grid_view_menu_item.dart';
-export 'math_equation/math_equation_node_widget.dart';
+export 'math_equation/math_equation_block_component.dart';
 export 'openai/widgets/auto_completion_node_widget.dart';
-export 'openai/widgets/auto_completion_plugins.dart';
 export 'openai/widgets/smart_edit_node_widget.dart';
 export 'openai/widgets/smart_edit_toolbar_item.dart';

+ 125 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart

@@ -0,0 +1,125 @@
+import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+class EditorStyleCustomizer {
+  EditorStyleCustomizer({
+    required this.context,
+  });
+
+  static double get horizontalPadding =>
+      PlatformExtension.isDesktop ? 100.0 : 10.0;
+
+  final BuildContext context;
+
+  EditorStyle style() {
+    if (PlatformExtension.isDesktopOrWeb) {
+      return desktop();
+    } else if (PlatformExtension.isMobile) {
+      return mobile();
+    }
+    throw UnimplementedError();
+  }
+
+  EditorStyle desktop() {
+    final theme = Theme.of(context);
+    final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
+    return EditorStyle.desktop(
+      padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
+      backgroundColor: theme.colorScheme.surface,
+      cursorColor: theme.colorScheme.primary,
+      textStyleConfiguration: TextStyleConfiguration(
+        text: TextStyle(
+          fontFamily: 'poppins',
+          fontSize: fontSize,
+          color: theme.colorScheme.onBackground,
+          height: 1.5,
+        ),
+        bold: const TextStyle(
+          fontFamily: 'poppins-Bold',
+          fontWeight: FontWeight.w600,
+        ),
+        italic: const TextStyle(fontStyle: FontStyle.italic),
+        underline: const TextStyle(decoration: TextDecoration.underline),
+        strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
+        href: TextStyle(
+          color: theme.colorScheme.primary,
+          decoration: TextDecoration.underline,
+        ),
+        code: GoogleFonts.robotoMono(
+          textStyle: TextStyle(
+            fontSize: fontSize,
+            fontWeight: FontWeight.normal,
+            color: Colors.red,
+            backgroundColor: theme.colorScheme.inverseSurface,
+          ),
+        ),
+      ),
+    );
+  }
+
+  EditorStyle mobile() {
+    final theme = Theme.of(context);
+    final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
+    return EditorStyle.desktop(
+      padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
+      backgroundColor: theme.colorScheme.surface,
+      cursorColor: theme.colorScheme.primary,
+      textStyleConfiguration: TextStyleConfiguration(
+        text: TextStyle(
+          fontFamily: 'poppins',
+          fontSize: fontSize,
+          color: theme.colorScheme.onBackground,
+          height: 1.5,
+        ),
+        bold: const TextStyle(
+          fontFamily: 'poppins-Bold',
+          fontWeight: FontWeight.w600,
+        ),
+        italic: const TextStyle(fontStyle: FontStyle.italic),
+        underline: const TextStyle(decoration: TextDecoration.underline),
+        strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
+        href: TextStyle(
+          color: theme.colorScheme.primary,
+          decoration: TextDecoration.underline,
+        ),
+        code: GoogleFonts.robotoMono(
+          textStyle: TextStyle(
+            fontSize: fontSize,
+            fontWeight: FontWeight.normal,
+            color: Colors.red,
+            backgroundColor: theme.colorScheme.inverseSurface,
+          ),
+        ),
+      ),
+    );
+  }
+
+  TextStyle headingStyleBuilder(int level) {
+    final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
+    final fontSizes = [
+      fontSize + 16,
+      fontSize + 12,
+      fontSize + 8,
+      fontSize + 4,
+      fontSize + 2,
+      fontSize
+    ];
+    return TextStyle(
+      fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize,
+      fontWeight: FontWeight.bold,
+    );
+  }
+
+  TextStyle codeBlockStyleBuilder() {
+    final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
+    return TextStyle(
+      fontFamily: 'poppins',
+      fontSize: fontSize,
+      height: 1.5,
+    );
+  }
+}

+ 41 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart

@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+
+class ExportPageWidget extends StatelessWidget {
+  const ExportPageWidget({
+    super.key,
+    required this.onTap,
+  });
+
+  final VoidCallback onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        const FlowyText.regular(
+          'There are some errors.',
+          fontSize: 16.0,
+        ),
+        const SizedBox(
+          height: 10,
+        ),
+        const FlowyText.regular(
+          'Please try to export the page and contact us.',
+          fontSize: 14.0,
+        ),
+        const SizedBox(
+          height: 5,
+        ),
+        FlowyTextButton(
+          'Export page',
+          constraints: const BoxConstraints(maxWidth: 100),
+          mainAxisAlignment: MainAxisAlignment.center,
+          onPressed: onTap,
+        )
+      ],
+    );
+  }
+}

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart

@@ -20,11 +20,11 @@ class DocumentAppearance {
 }
 
 class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
-  DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 14.0));
+  DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 16.0));
 
   void fetch() async {
     final prefs = await SharedPreferences.getInstance();
-    final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 14.0;
+    final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
     emit(
       state.copyWith(
         fontSize: fontSize,

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart

@@ -18,9 +18,9 @@ class FontSizeSwitcher extends StatefulWidget {
 
 class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
   final List<Tuple3<String, double, bool>> _fontSizes = [
-    Tuple3(LocaleKeys.moreAction_small.tr(), 12.0, false),
-    Tuple3(LocaleKeys.moreAction_medium.tr(), 14.0, true),
-    Tuple3(LocaleKeys.moreAction_large.tr(), 18.0, false),
+    Tuple3(LocaleKeys.moreAction_small.tr(), 14.0, false),
+    Tuple3(LocaleKeys.moreAction_medium.tr(), 18.0, true),
+    Tuple3(LocaleKeys.moreAction_large.tr(), 22.0, false),
   ];
 
   @override

+ 0 - 54
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_node_widget.dart

@@ -1,54 +0,0 @@
-import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-const String kBoardType = 'board';
-
-class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _BoardWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return node.attributes[kViewID] is String &&
-            node.attributes[kAppID] is String;
-      };
-}
-
-class _BoardWidget extends StatefulWidget {
-  const _BoardWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  final Node node;
-  final EditorState editorState;
-
-  @override
-  State<_BoardWidget> createState() => _BoardWidgetState();
-}
-
-class _BoardWidgetState extends State<_BoardWidget> {
-  @override
-  Widget build(BuildContext context) {
-    return BuiltInPageWidget(
-      node: widget.node,
-      editorState: widget.editorState,
-      builder: (viewPB) {
-        return BoardPage(
-          key: ValueKey(viewPB.id),
-          view: viewPB,
-        );
-      },
-    );
-  }
-}

+ 0 - 300
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/callout/callout_node_widget.dart

@@ -1,300 +0,0 @@
-import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:flowy_infra/theme_extension.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-const String kCalloutType = 'callout';
-const String kCalloutAttrColor = 'color';
-const String kCalloutAttrEmoji = 'emoji';
-
-SelectionMenuItem calloutMenuItem = SelectionMenuItem.node(
-  name: 'Callout',
-  iconData: Icons.note,
-  keywords: ['callout'],
-  nodeBuilder: (editorState) {
-    final node = Node(type: kCalloutType);
-    node.insert(TextNode.empty());
-    return node;
-  },
-  replace: (_, textNode) => textNode.toPlainText().isEmpty,
-  updateSelection: (_, path, __, ___) {
-    return Selection.single(path: [...path, 0], startOffset: 0);
-  },
-);
-
-class CalloutNodeWidgetBuilder extends NodeWidgetBuilder<Node>
-    with ActionProvider<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _CalloutWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (node) => node.type == kCalloutType;
-
-  _CalloutWidgetState? _getState(NodeWidgetContext<Node> context) {
-    return context.node.key.currentState as _CalloutWidgetState?;
-  }
-
-  BuildContext? _getBuildContext(NodeWidgetContext<Node> context) {
-    return context.node.key.currentContext;
-  }
-
-  @override
-  List<ActionMenuItem> actions(NodeWidgetContext<Node> context) {
-    return [
-      ActionMenuItem.icon(
-        iconData: Icons.color_lens_outlined,
-        onPressed: () {
-          final state = _getState(context);
-          final ctx = _getBuildContext(context);
-          if (state == null || ctx == null) {
-            return;
-          }
-          final menuState = Provider.of<ActionMenuState>(ctx, listen: false);
-          menuState.isPinned = true;
-          state.colorPopoverController.show();
-        },
-        itemWrapper: (item) {
-          final state = _getState(context);
-          final ctx = _getBuildContext(context);
-          if (state == null || ctx == null) {
-            return item;
-          }
-          return AppFlowyPopover(
-            controller: state.colorPopoverController,
-            popupBuilder: (context) => state._buildColorPicker(),
-            constraints: BoxConstraints.loose(const Size(200, 460)),
-            triggerActions: 0,
-            offset: const Offset(0, 30),
-            child: item,
-            onClose: () {
-              final menuState =
-                  Provider.of<ActionMenuState>(ctx, listen: false);
-              menuState.isPinned = false;
-            },
-          );
-        },
-      ),
-      ActionMenuItem.svg(
-        name: 'delete',
-        onPressed: () {
-          final transaction = context.editorState.transaction
-            ..deleteNode(context.node);
-          context.editorState.apply(transaction);
-        },
-      ),
-    ];
-  }
-}
-
-class _CalloutWidget extends StatefulWidget {
-  const _CalloutWidget({
-    super.key,
-    required this.node,
-    required this.editorState,
-  });
-
-  final Node node;
-  final EditorState editorState;
-
-  @override
-  State<_CalloutWidget> createState() => _CalloutWidgetState();
-}
-
-class _CalloutWidgetState extends State<_CalloutWidget> with SelectableMixin {
-  final PopoverController colorPopoverController = PopoverController();
-  final PopoverController emojiPopoverController = PopoverController();
-  RenderBox get _renderBox => context.findRenderObject() as RenderBox;
-
-  @override
-  void initState() {
-    widget.node.addListener(nodeChanged);
-    super.initState();
-  }
-
-  @override
-  void dispose() {
-    widget.node.removeListener(nodeChanged);
-    super.dispose();
-  }
-
-  void nodeChanged() {
-    if (widget.node.children.isEmpty) {
-      deleteNode();
-    }
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      decoration: BoxDecoration(
-        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
-        color: tint.color(context),
-      ),
-      padding: const EdgeInsets.only(top: 8, bottom: 8, left: 0, right: 15),
-      width: double.infinity,
-      child: Row(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          _buildEmoji(),
-          Expanded(
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: widget.node.children
-                  .map(
-                    (child) => widget.editorState.service.renderPluginService
-                        .buildPluginWidget(
-                      child is TextNode
-                          ? NodeWidgetContext<TextNode>(
-                              context: context,
-                              node: child,
-                              editorState: widget.editorState,
-                            )
-                          : NodeWidgetContext<Node>(
-                              context: context,
-                              node: child,
-                              editorState: widget.editorState,
-                            ),
-                    ),
-                  )
-                  .toList(),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  Widget _popover({
-    required PopoverController controller,
-    required Widget Function(BuildContext context) popupBuilder,
-    required Widget child,
-    Size size = const Size(200, 460),
-  }) {
-    return AppFlowyPopover(
-      controller: controller,
-      constraints: BoxConstraints.loose(size),
-      triggerActions: 0,
-      popupBuilder: popupBuilder,
-      child: child,
-    );
-  }
-
-  Widget _buildColorPicker() {
-    return FlowyColorPicker(
-      colors: FlowyTint.values
-          .map(
-            (t) => ColorOption(
-              color: t.color(context),
-              name: t.tintName(AppFlowyEditorLocalizations.current),
-            ),
-          )
-          .toList(),
-      selected: tint.color(context),
-      onTap: (color, index) {
-        setColor(FlowyTint.values[index]);
-        colorPopoverController.close();
-      },
-    );
-  }
-
-  Widget _buildEmoji() {
-    return _popover(
-      controller: emojiPopoverController,
-      popupBuilder: (context) => _buildEmojiPicker(),
-      size: const Size(300, 200),
-      child: FlowyTextButton(
-        emoji,
-        fontSize: 18,
-        fillColor: Colors.transparent,
-        onPressed: () {
-          emojiPopoverController.show();
-        },
-      ),
-    );
-  }
-
-  Widget _buildEmojiPicker() {
-    return EmojiSelectionMenu(
-      editorState: widget.editorState,
-      onSubmitted: (emoji) {
-        setEmoji(emoji.emoji);
-        emojiPopoverController.close();
-      },
-      onExit: () {},
-    );
-  }
-
-  void setColor(FlowyTint tint) {
-    final transaction = widget.editorState.transaction
-      ..updateNode(widget.node, {
-        kCalloutAttrColor: tint.name,
-      });
-    widget.editorState.apply(transaction);
-  }
-
-  void setEmoji(String emoji) {
-    final transaction = widget.editorState.transaction
-      ..updateNode(widget.node, {
-        kCalloutAttrEmoji: emoji,
-      });
-    widget.editorState.apply(transaction);
-  }
-
-  void deleteNode() {
-    final transaction = widget.editorState.transaction..deleteNode(widget.node);
-    widget.editorState.apply(transaction);
-  }
-
-  FlowyTint get tint {
-    final name = widget.node.attributes[kCalloutAttrColor];
-    return (name is String) ? FlowyTint.fromJson(name) : FlowyTint.tint1;
-  }
-
-  String get emoji {
-    return widget.node.attributes[kCalloutAttrEmoji] ?? "💡";
-  }
-
-  @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);
-}

+ 0 - 224
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_node_widget.dart

@@ -1,224 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
-import 'package:highlight/highlight.dart' as highlight;
-import 'package:highlight/languages/all.dart';
-
-const String kCodeBlockType = 'text/$kCodeBlockSubType';
-const String kCodeBlockSubType = 'code_block';
-const String kCodeBlockAttrTheme = 'theme';
-const String kCodeBlockAttrLanguage = 'language';
-
-class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder<TextNode>
-    with ActionProvider<TextNode> {
-  @override
-  Widget build(NodeWidgetContext<TextNode> context) {
-    return _CodeBlockNodeWidge(
-      key: context.node.key,
-      textNode: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return node is TextNode &&
-            node.attributes[kCodeBlockAttrTheme] is String;
-      };
-
-  @override
-  List<ActionMenuItem> actions(NodeWidgetContext<TextNode> context) {
-    return [
-      ActionMenuItem.svg(
-        name: 'delete',
-        onPressed: () {
-          final transaction = context.editorState.transaction
-            ..deleteNode(context.node);
-          context.editorState.apply(transaction);
-        },
-      ),
-    ];
-  }
-}
-
-class _CodeBlockNodeWidge extends StatefulWidget {
-  const _CodeBlockNodeWidge({
-    Key? key,
-    required this.textNode,
-    required this.editorState,
-  }) : super(key: key);
-
-  final TextNode textNode;
-  final EditorState editorState;
-
-  @override
-  State<_CodeBlockNodeWidge> createState() => __CodeBlockNodeWidgeState();
-}
-
-class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
-    with SelectableMixin, DefaultSelectable {
-  final _richTextKey = GlobalKey(debugLabel: kCodeBlockType);
-  final _padding = const EdgeInsets.only(left: 20, top: 30, bottom: 30);
-  String? get _language =>
-      widget.textNode.attributes[kCodeBlockAttrLanguage] as String?;
-  String? _detectLanguage;
-
-  @override
-  SelectableMixin<StatefulWidget> get forward =>
-      _richTextKey.currentState as SelectableMixin;
-
-  @override
-  GlobalKey<State<StatefulWidget>>? get iconKey => null;
-
-  @override
-  Offset get baseOffset => super.baseOffset + _padding.topLeft;
-
-  @override
-  Widget build(BuildContext context) {
-    return Stack(
-      children: [
-        _buildCodeBlock(context),
-        _buildSwitchCodeButton(context),
-      ],
-    );
-  }
-
-  Widget _buildCodeBlock(BuildContext context) {
-    final result = highlight.highlight.parse(
-      widget.textNode.toPlainText(),
-      language: _language,
-      autoDetection: _language == null,
-    );
-    _detectLanguage = _language ?? result.language;
-    final code = result.nodes;
-    final codeTextSpan = _convert(code!);
-    return Container(
-      decoration: BoxDecoration(
-        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
-        color: Colors.grey.withOpacity(0.1),
-      ),
-      padding: _padding,
-      width: MediaQuery.of(context).size.width,
-      child: FlowyRichText(
-        key: _richTextKey,
-        textNode: widget.textNode,
-        editorState: widget.editorState,
-        lineHeight: 1.0,
-        cursorHeight: 15.0,
-        textSpanDecorator: (textSpan) => TextSpan(
-          style: widget.editorState.editorStyle.textStyle,
-          children: codeTextSpan,
-        ),
-      ),
-    );
-  }
-
-  Widget _buildSwitchCodeButton(BuildContext context) {
-    return Positioned(
-      top: -5,
-      left: 10,
-      child: SizedBox(
-        height: 35,
-        child: DropdownButton<String>(
-          value: _detectLanguage,
-          iconSize: 14.0,
-          onChanged: (value) {
-            final transaction = widget.editorState.transaction
-              ..updateNode(widget.textNode, {
-                kCodeBlockAttrLanguage: value,
-              });
-            widget.editorState.apply(transaction);
-          },
-          items:
-              allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
-            return DropdownMenuItem<String>(
-              value: value,
-              child: FlowyText.medium(
-                value,
-                color: Theme.of(context).colorScheme.tertiary,
-              ),
-            );
-          }).toList(growable: false),
-        ),
-      ),
-    );
-  }
-
-  // Copy from flutter.highlight package.
-  // https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
-  List<TextSpan> _convert(List<highlight.Node> nodes) {
-    List<TextSpan> spans = [];
-    var currentSpans = spans;
-    List<List<TextSpan>> stack = [];
-
-    void traverse(highlight.Node node) {
-      if (node.value != null) {
-        currentSpans.add(node.className == null
-            ? TextSpan(text: node.value)
-            : TextSpan(
-                text: node.value,
-                style: _builtInCodeBlockTheme[node.className!],),);
-      } else if (node.children != null) {
-        List<TextSpan> tmp = [];
-        currentSpans.add(TextSpan(
-            children: tmp, style: _builtInCodeBlockTheme[node.className!],),);
-        stack.add(currentSpans);
-        currentSpans = tmp;
-
-        for (var n in node.children!) {
-          traverse(n);
-          if (n == node.children!.last) {
-            currentSpans = stack.isEmpty ? spans : stack.removeLast();
-          }
-        }
-      }
-    }
-
-    for (var node in nodes) {
-      traverse(node);
-    }
-
-    return spans;
-  }
-}
-
-const _builtInCodeBlockTheme = {
-  'root':
-      TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)),
-  'comment': TextStyle(color: Color(0xff007400)),
-  'quote': TextStyle(color: Color(0xff007400)),
-  'tag': TextStyle(color: Color(0xffaa0d91)),
-  'attribute': TextStyle(color: Color(0xffaa0d91)),
-  'keyword': TextStyle(color: Color(0xffaa0d91)),
-  'selector-tag': TextStyle(color: Color(0xffaa0d91)),
-  'literal': TextStyle(color: Color(0xffaa0d91)),
-  'name': TextStyle(color: Color(0xffaa0d91)),
-  'variable': TextStyle(color: Color(0xff3F6E74)),
-  'template-variable': TextStyle(color: Color(0xff3F6E74)),
-  'code': TextStyle(color: Color(0xffc41a16)),
-  'string': TextStyle(color: Color(0xffc41a16)),
-  'meta-string': TextStyle(color: Color(0xffc41a16)),
-  'regexp': TextStyle(color: Color(0xff0E0EFF)),
-  'link': TextStyle(color: Color(0xff0E0EFF)),
-  'title': TextStyle(color: Color(0xff1c00cf)),
-  'symbol': TextStyle(color: Color(0xff1c00cf)),
-  'bullet': TextStyle(color: Color(0xff1c00cf)),
-  'number': TextStyle(color: Color(0xff1c00cf)),
-  'section': TextStyle(color: Color(0xff643820)),
-  'meta': TextStyle(color: Color(0xff643820)),
-  'type': TextStyle(color: Color(0xff5c2699)),
-  'built_in': TextStyle(color: Color(0xff5c2699)),
-  'builtin-name': TextStyle(color: Color(0xff5c2699)),
-  'params': TextStyle(color: Color(0xff5c2699)),
-  'attr': TextStyle(color: Color(0xff836C28)),
-  'subst': TextStyle(color: Color(0xff000000)),
-  'formula': TextStyle(
-      backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic,),
-  'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
-  'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
-  'selector-id': TextStyle(color: Color(0xff9b703f)),
-  'selector-class': TextStyle(color: Color(0xff9b703f)),
-  'doctag': TextStyle(fontWeight: FontWeight.bold),
-  'strong': TextStyle(fontWeight: FontWeight.bold),
-  'emphasis': TextStyle(fontStyle: FontStyle.italic),
-};

+ 0 - 125
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_shortcut_event.dart

@@ -1,125 +0,0 @@
-import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-
-ShortcutEvent enterInCodeBlock = ShortcutEvent(
-  key: 'Press Enter In Code Block',
-  command: 'enter',
-  handler: _enterInCodeBlockHandler,
-);
-
-ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent(
-  key: 'White space in code block',
-  command: 'space, slash, shift+underscore',
-  handler: _ignorekHandler,
-);
-
-ShortcutEvent pasteInCodeBlock = ShortcutEvent(
-  key: 'Paste in code block',
-  command: 'meta+v',
-  windowsCommand: 'ctrl+v',
-  linuxCommand: 'ctrl+v',
-  handler: _pasteHandler,
-);
-
-ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
-  final selection = editorState.service.selectionService.currentSelection.value;
-  final nodes = editorState.service.selectionService.currentSelectedNodes;
-  final codeBlockNode =
-      nodes.whereType<TextNode>().where((node) => node.id == kCodeBlockType);
-  if (codeBlockNode.length != 1 ||
-      selection == null ||
-      !selection.isCollapsed) {
-    return KeyEventResult.ignored;
-  }
-
-  final transaction = editorState.transaction
-    ..insertText(
-      codeBlockNode.first,
-      selection.end.offset,
-      '\n',
-    );
-  editorState.apply(transaction);
-  return KeyEventResult.handled;
-};
-
-ShortcutEventHandler _ignorekHandler = (editorState, event) {
-  final nodes = editorState.service.selectionService.currentSelectedNodes;
-  final codeBlockNodes =
-      nodes.whereType<TextNode>().where((node) => node.id == kCodeBlockType);
-  if (codeBlockNodes.length == 1) {
-    return KeyEventResult.skipRemainingHandlers;
-  }
-  return KeyEventResult.ignored;
-};
-
-ShortcutEventHandler _pasteHandler = (editorState, event) {
-  final selection = editorState.service.selectionService.currentSelection.value;
-  final nodes = editorState.service.selectionService.currentSelectedNodes;
-  final codeBlockNodes =
-      nodes.whereType<TextNode>().where((node) => node.id == kCodeBlockType);
-  if (selection != null &&
-      selection.isCollapsed &&
-      codeBlockNodes.length == 1) {
-    Clipboard.getData(Clipboard.kTextPlain).then((value) {
-      final text = value?.text;
-      if (text == null) return;
-      final transaction = editorState.transaction;
-      transaction.insertText(
-        codeBlockNodes.first,
-        selection.startIndex,
-        text,
-      );
-      editorState.apply(transaction);
-    });
-    return KeyEventResult.handled;
-  }
-  return KeyEventResult.ignored;
-};
-
-SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
-  name: 'Code Block',
-  icon: (editorState, onSelected) => Icon(
-    Icons.abc,
-    color: onSelected
-        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-        : editorState.editorStyle.selectionMenuItemIconColor,
-    size: 18.0,
-  ),
-  keywords: ['code block', 'code snippet'],
-  handler: (editorState, _, __) {
-    final selection =
-        editorState.service.selectionService.currentSelection.value;
-    final textNodes = editorState.service.selectionService.currentSelectedNodes
-        .whereType<TextNode>();
-    if (selection == null || textNodes.isEmpty) {
-      return;
-    }
-    final transaction = editorState.transaction;
-    final textNode = textNodes.first;
-    if (textNode.toPlainText().isEmpty && textNode.next is TextNode) {
-      transaction.updateNode(textNodes.first, {
-        BuiltInAttributeKey.subtype: kCodeBlockSubType,
-        kCodeBlockAttrTheme: 'vs',
-        kCodeBlockAttrLanguage: null,
-      });
-      transaction.afterSelection = selection;
-      editorState.apply(transaction);
-    } else {
-      transaction.insertNode(
-        selection.end.path,
-        TextNode(
-          attributes: {
-            BuiltInAttributeKey.subtype: kCodeBlockSubType,
-            kCodeBlockAttrTheme: 'vs',
-            kCodeBlockAttrLanguage: null,
-          },
-          delta: Delta()..insert('\n'),
-        ),
-      );
-      transaction.afterSelection = selection;
-    }
-    editorState.apply(transaction);
-  },
-);

+ 0 - 84
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_node_widget.dart

@@ -1,84 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-const String kDividerType = 'divider';
-
-class DividerWidgetBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _DividerWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return true;
-      };
-}
-
-class _DividerWidget extends StatefulWidget {
-  const _DividerWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  final Node node;
-  final EditorState editorState;
-
-  @override
-  State<_DividerWidget> createState() => _DividerWidgetState();
-}
-
-class _DividerWidgetState extends State<_DividerWidget> 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);
-}

+ 0 - 72
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_shortcut_event.dart

@@ -1,72 +0,0 @@
-import 'package:appflowy/plugins/document/presentation/plugins/divider/divider_node_widget.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-// insert divider into a document by typing three minuses.
-// ---
-ShortcutEvent insertDividerEvent = ShortcutEvent(
-  key: 'Divider',
-  command: 'Minus',
-  handler: _insertDividerHandler,
-);
-
-ShortcutEventHandler _insertDividerHandler = (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.toPlainText() != '--') {
-    return KeyEventResult.ignored;
-  }
-  final transaction = editorState.transaction
-    ..deleteText(textNode, 0, 2) // remove the existing minuses.
-    ..insertNode(textNode.path, Node(type: kDividerType)) // insert the divder
-    ..afterSelection = Selection.single(
-      // update selection to the next text node.
-      path: textNode.path.next,
-      startOffset: 0,
-    );
-  editorState.apply(transaction);
-  return KeyEventResult.handled;
-};
-
-SelectionMenuItem dividerMenuItem = SelectionMenuItem(
-  name: 'Divider',
-  icon: (editorState, onSelected) => Icon(
-    Icons.horizontal_rule,
-    color: onSelected
-        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-        : editorState.editorStyle.selectionMenuItemIconColor,
-    size: 18.0,
-  ),
-  keywords: ['horizontal rule', 'divider'],
-  handler: (editorState, _, __) {
-    final selection =
-        editorState.service.selectionService.currentSelection.value;
-    final textNodes = editorState.service.selectionService.currentSelectedNodes
-        .whereType<TextNode>();
-    if (textNodes.length != 1 || selection == null) {
-      return;
-    }
-    final textNode = textNodes.first;
-    // insert the divider at current path if the text node is empty.
-    if (textNode.toPlainText().isEmpty) {
-      final transaction = editorState.transaction
-        ..insertNode(textNode.path, Node(type: kDividerType))
-        ..afterSelection = Selection.single(
-          path: textNode.path.next,
-          startOffset: 0,
-        );
-      editorState.apply(transaction);
-    } else {
-      // insert the divider at the path next to current path if the text node is not empty.
-      final transaction = editorState.transaction
-        ..insertNode(selection.end.path.next, Node(type: kDividerType))
-        ..afterSelection = selection;
-      editorState.apply(transaction);
-    }
-  },
-);

+ 0 - 180
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_menu_item.dart

@@ -1,180 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-
-import 'emoji_picker.dart';
-
-SelectionMenuItem emojiMenuItem = SelectionMenuItem(
-  name: 'Emoji',
-  icon: (editorState, onSelected) => Icon(
-    Icons.emoji_emotions_outlined,
-    color: onSelected
-        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-        : editorState.editorStyle.selectionMenuItemIconColor,
-    size: 18.0,
-  ),
-  keywords: ['emoji'],
-  handler: _showEmojiSelectionMenu,
-);
-
-OverlayEntry? _emojiSelectionMenu;
-EditorState? _editorState;
-void _showEmojiSelectionMenu(
-  EditorState editorState,
-  SelectionMenuService menuService,
-  BuildContext context,
-) {
-  final alignment = menuService.alignment;
-  final offset = menuService.offset;
-  menuService.dismiss();
-
-  _emojiSelectionMenu?.remove();
-  _emojiSelectionMenu = OverlayEntry(
-    builder: (context) {
-      return Positioned(
-        top: alignment == Alignment.bottomLeft ? offset.dy : null,
-        bottom: alignment == Alignment.topLeft ? offset.dy : null,
-        left: offset.dx,
-        child: Material(
-          child: EmojiSelectionMenu(
-            editorState: editorState,
-            onSubmitted: (text) {
-              // insert emoji
-              editorState.insertEmoji(text);
-            },
-            onExit: () {
-              _dismissEmojiSelectionMenu();
-              //close emoji panel
-            },
-          ),
-        ),
-      );
-    },
-  );
-
-  Overlay.of(context).insert(_emojiSelectionMenu!);
-
-  _editorState = editorState;
-  editorState.service.selectionService.currentSelection
-      .addListener(_dismissEmojiSelectionMenu);
-}
-
-void _dismissEmojiSelectionMenu() {
-  _emojiSelectionMenu?.remove();
-  _emojiSelectionMenu = null;
-
-  _editorState?.service.selectionService.currentSelection
-      .removeListener(_dismissEmojiSelectionMenu);
-  _editorState?.service.keyboardService?.enable();
-  _editorState = null;
-}
-
-class EmojiSelectionMenu extends StatefulWidget {
-  const EmojiSelectionMenu({
-    Key? key,
-    required this.onSubmitted,
-    required this.onExit,
-    required this.editorState,
-  }) : super(key: key);
-
-  final void Function(Emoji emoji) onSubmitted;
-  final void Function() onExit;
-  final EditorState editorState;
-
-  @override
-  State<EmojiSelectionMenu> createState() => _EmojiSelectionMenuState();
-}
-
-class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
-  EditorStyle get style => widget.editorState.editorStyle;
-
-  @override
-  void initState() {
-    HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
-    super.initState();
-  }
-
-  bool _handleGlobalKeyEvent(KeyEvent event) {
-    if (event.logicalKey == LogicalKeyboardKey.escape &&
-        event is KeyDownEvent) {
-      //triggers on esc
-      widget.onExit();
-      return true;
-    } else {
-      return false;
-    }
-  }
-
-  @override
-  void deactivate() {
-    HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
-    super.deactivate();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      width: 300,
-      padding: const EdgeInsets.all(8.0),
-      decoration: BoxDecoration(
-        color: style.selectionMenuBackgroundColor,
-        boxShadow: [
-          BoxShadow(
-            blurRadius: 5,
-            spreadRadius: 1,
-            color: Colors.black.withOpacity(0.1),
-          ),
-        ],
-        borderRadius: BorderRadius.circular(6.0),
-      ),
-      child: _buildEmojiBox(context),
-    );
-  }
-
-  Widget _buildEmojiBox(BuildContext context) {
-    return SizedBox(
-      height: 200,
-      child: EmojiPicker(
-        onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji),
-        config: Config(
-          columns: 8,
-          emojiSizeMax: 28,
-          bgColor:
-              style.selectionMenuBackgroundColor ?? const Color(0xffF2F2F2),
-          iconColor: Colors.grey,
-          iconColorSelected: const Color(0xff333333),
-          indicatorColor: const Color(0xff333333),
-          progressIndicatorColor: const Color(0xff333333),
-          buttonMode: ButtonMode.CUPERTINO,
-          initCategory: Category.RECENT,
-        ),
-      ),
-    );
-  }
-}
-
-extension on EditorState {
-  void insertEmoji(Emoji emoji) {
-    final selectionService = service.selectionService;
-    final currentSelection = selectionService.currentSelection.value;
-    final nodes = selectionService.currentSelectedNodes;
-    if (currentSelection == null ||
-        !currentSelection.isCollapsed ||
-        nodes.first is! TextNode) {
-      return;
-    }
-    final textNode = nodes.first as TextNode;
-    final tr = transaction;
-    tr.insertText(
-      textNode,
-      currentSelection.endIndex,
-      emoji.emoji,
-    );
-    apply(tr);
-  }
-}

+ 0 - 54
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_node_widget.dart

@@ -1,54 +0,0 @@
-import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-const String kGridType = 'grid';
-
-class GridNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _GridWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return node.attributes[kAppID] is String &&
-            node.attributes[kViewID] is String;
-      };
-}
-
-class _GridWidget extends StatefulWidget {
-  const _GridWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  final Node node;
-  final EditorState editorState;
-
-  @override
-  State<_GridWidget> createState() => _GridWidgetState();
-}
-
-class _GridWidgetState extends State<_GridWidget> {
-  @override
-  Widget build(BuildContext context) {
-    return BuiltInPageWidget(
-      node: widget.node,
-      editorState: widget.editorState,
-      builder: (viewPB) {
-        return GridPage(
-          key: ValueKey(viewPB.id),
-          view: viewPB,
-        );
-      },
-    );
-  }
-}

+ 0 - 224
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/math_equation/math_equation_node_widget.dart

@@ -1,224 +0,0 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
-import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_math_fork/flutter_math.dart';
-
-const String kMathEquationType = 'math_equation';
-const String kMathEquationAttr = 'math_equation';
-
-SelectionMenuItem mathEquationMenuItem = SelectionMenuItem(
-  name: 'Math Equation',
-  icon: (editorState, onSelected) => Icon(
-    Icons.text_fields_rounded,
-    color: onSelected
-        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-        : editorState.editorStyle.selectionMenuItemIconColor,
-    size: 18.0,
-  ),
-  keywords: ['tex, latex, katex', 'math equation'],
-  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;
-    final Path mathEquationNodePath;
-    if (textNode.toPlainText().isEmpty) {
-      mathEquationNodePath = selection.end.path;
-    } else {
-      mathEquationNodePath = selection.end.path.next;
-    }
-    // insert the math equation node
-    final transaction = editorState.transaction
-      ..insertNode(
-        mathEquationNodePath,
-        Node(type: kMathEquationType, attributes: {kMathEquationAttr: ''}),
-      )
-      ..afterSelection = selection;
-    editorState.apply(transaction);
-
-    // tricy to show the editing dialog.
-    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
-      final mathEquationState = editorState.document
-          .nodeAtPath(mathEquationNodePath)
-          ?.key
-          .currentState;
-      if (mathEquationState != null &&
-          mathEquationState is _MathEquationNodeWidgetState) {
-        mathEquationState.showEditingDialog();
-      }
-    });
-  },
-);
-
-class MathEquationNodeWidgetBuidler extends NodeWidgetBuilder<Node>
-    with ActionProvider<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _MathEquationNodeWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator =>
-      (node) => node.attributes[kMathEquationAttr] is String;
-
-  @override
-  List<ActionMenuItem> actions(NodeWidgetContext<Node> context) {
-    return [
-      ActionMenuItem.svg(
-        name: "delete",
-        onPressed: () {
-          final transaction = context.editorState.transaction
-            ..deleteNode(context.node);
-          context.editorState.apply(transaction);
-        },
-      ),
-    ];
-  }
-}
-
-class _MathEquationNodeWidget extends StatefulWidget {
-  const _MathEquationNodeWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  final Node node;
-  final EditorState editorState;
-
-  @override
-  State<_MathEquationNodeWidget> createState() =>
-      _MathEquationNodeWidgetState();
-}
-
-class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
-  String get _mathEquation =>
-      widget.node.attributes[kMathEquationAttr] as String;
-  bool _isHover = false;
-
-  @override
-  Widget build(BuildContext context) {
-    return InkWell(
-      onHover: (value) {
-        setState(() {
-          _isHover = value;
-        });
-      },
-      onTap: () {
-        showEditingDialog();
-      },
-      child: Stack(
-        children: [
-          _buildMathEquation(context),
-        ],
-      ),
-    );
-  }
-
-  Widget _buildMathEquation(BuildContext context) {
-    return Container(
-      width: MediaQuery.of(context).size.width,
-      constraints: const BoxConstraints(minHeight: 50),
-      padding: const EdgeInsets.symmetric(vertical: 20),
-      decoration: BoxDecoration(
-        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
-        color: _isHover || _mathEquation.isEmpty
-            ? Theme.of(context).colorScheme.tertiaryContainer
-            : Colors.transparent,
-      ),
-      child: Center(
-        child: _mathEquation.isEmpty
-            ? FlowyText.medium(
-                LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
-                fontSize: 16,
-              )
-            : Math.tex(
-                _mathEquation,
-                textStyle: const TextStyle(fontSize: 20),
-                mathStyle: MathStyle.display,
-              ),
-      ),
-    );
-  }
-
-  void showEditingDialog() {
-    showDialog(
-      context: context,
-      builder: (context) {
-        final controller = TextEditingController(text: _mathEquation);
-        return AlertDialog(
-          backgroundColor: Theme.of(context).canvasColor,
-          title: Text(
-            LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),
-          ),
-          content: RawKeyboardListener(
-            focusNode: FocusNode(),
-            onKey: (key) {
-              if (key is! RawKeyDownEvent) return;
-              if (key.logicalKey == LogicalKeyboardKey.enter &&
-                  !key.isShiftPressed) {
-                _updateMathEquation(controller.text, context);
-              } else if (key.logicalKey == LogicalKeyboardKey.escape) {
-                _dismiss(context);
-              }
-            },
-            child: TextField(
-              autofocus: true,
-              controller: controller,
-              maxLines: null,
-              decoration: const InputDecoration(
-                border: OutlineInputBorder(),
-                hintText: 'E = MC^2',
-              ),
-            ),
-          ),
-          actions: [
-            SecondaryTextButton(
-              LocaleKeys.button_Cancel.tr(),
-              onPressed: () => _dismiss(context),
-            ),
-            PrimaryTextButton(
-              LocaleKeys.button_Done.tr(),
-              onPressed: () => _updateMathEquation(controller.text, context),
-            ),
-          ],
-          actionsPadding: const EdgeInsets.only(bottom: 20),
-          actionsAlignment: MainAxisAlignment.spaceAround,
-        );
-      },
-    );
-  }
-
-  void _updateMathEquation(String mathEquation, BuildContext context) {
-    if (mathEquation == _mathEquation) {
-      _dismiss(context);
-      return;
-    }
-    final transaction = widget.editorState.transaction;
-    transaction.updateNode(
-      widget.node,
-      {
-        kMathEquationAttr: mathEquation,
-      },
-    );
-    widget.editorState.apply(transaction);
-    _dismiss(context);
-  }
-
-  void _dismiss(BuildContext context) {
-    Navigator.of(context).pop();
-  }
-}

+ 0 - 366
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart

@@ -1,366 +0,0 @@
-import 'dart:convert';
-
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
-import 'package:appflowy/user/application/user_service.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flowy_infra_ui/style_widget/button.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/style_widget/text_field.dart';
-import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
-import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
-import 'package:http/http.dart' as http;
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:easy_localization/easy_localization.dart';
-import '../util/editor_extension.dart';
-
-const String kAutoCompletionInputType = 'auto_completion_input';
-const String kAutoCompletionInputString = 'auto_completion_input_string';
-const String kAutoCompletionInputStartSelection =
-    'auto_completion_input_start_selection';
-
-class AutoCompletionInputBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return node.attributes[kAutoCompletionInputString] is String;
-      };
-
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _AutoCompletionInput(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-}
-
-class _AutoCompletionInput extends StatefulWidget {
-  final Node node;
-
-  final EditorState editorState;
-  const _AutoCompletionInput({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  });
-
-  @override
-  State<_AutoCompletionInput> createState() => _AutoCompletionInputState();
-}
-
-class _AutoCompletionInputState extends State<_AutoCompletionInput> {
-  String get text => widget.node.attributes[kAutoCompletionInputString];
-
-  final controller = TextEditingController();
-  final focusNode = FocusNode();
-  final textFieldFocusNode = FocusNode();
-  final interceptor = SelectionInterceptor();
-
-  @override
-  void initState() {
-    super.initState();
-
-    textFieldFocusNode.addListener(_onFocusChanged);
-    textFieldFocusNode.requestFocus();
-    widget.editorState.service.selectionService.register(
-      interceptor
-        ..canTap = (details) {
-          final renderBox = context.findRenderObject() as RenderBox?;
-          if (renderBox != null) {
-            if (!isTapDownDetailsInRenderBox(details, renderBox)) {
-              if (text.isNotEmpty || controller.text.isNotEmpty) {
-                showDialog(
-                  context: context,
-                  builder: (context) {
-                    return DiscardDialog(
-                      onConfirm: () => _onDiscard(),
-                      onCancel: () {},
-                    );
-                  },
-                );
-              } else if (controller.text.isEmpty) {
-                _onExit();
-              }
-            }
-          }
-          return false;
-        },
-    );
-  }
-
-  bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
-    var result = BoxHitTestResult();
-    box.hitTest(result, position: box.globalToLocal(details.globalPosition));
-    return result.path.any((entry) => entry.target == box);
-  }
-
-  @override
-  void dispose() {
-    controller.dispose();
-    textFieldFocusNode.removeListener(_onFocusChanged);
-    widget.editorState.service.selectionService.currentSelection
-        .removeListener(_onCancelWhenSelectionChanged);
-    widget.editorState.service.selectionService.unRegister(interceptor);
-
-    super.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Card(
-      elevation: 5,
-      color: Theme.of(context).colorScheme.surface,
-      child: Container(
-        margin: const EdgeInsets.all(10),
-        child: _buildAutoGeneratorPanel(context),
-      ),
-    );
-  }
-
-  Widget _buildAutoGeneratorPanel(BuildContext context) {
-    if (text.isEmpty) {
-      return Column(
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          _buildHeaderWidget(context),
-          const Space(0, 10),
-          _buildInputWidget(context),
-          const Space(0, 10),
-          _buildInputFooterWidget(context),
-        ],
-      );
-    } else {
-      return Column(
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          _buildHeaderWidget(context),
-          const Space(0, 10),
-          _buildFooterWidget(context),
-        ],
-      );
-    }
-  }
-
-  Widget _buildHeaderWidget(BuildContext context) {
-    return Row(
-      children: [
-        FlowyText.medium(
-          LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
-          fontSize: 14,
-        ),
-        const Spacer(),
-        FlowyButton(
-          useIntrinsicWidth: true,
-          text: FlowyText.regular(
-            LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
-          ),
-          onTap: () async {
-            await openLearnMorePage();
-          },
-        )
-      ],
-    );
-  }
-
-  Widget _buildInputWidget(BuildContext context) {
-    return FlowyTextField(
-      hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
-      controller: controller,
-      maxLines: 3,
-      focusNode: textFieldFocusNode,
-      autoFocus: false,
-    );
-  }
-
-  Widget _buildInputFooterWidget(BuildContext context) {
-    return Row(
-      children: [
-        PrimaryTextButton(
-          LocaleKeys.button_generate.tr(),
-          onPressed: () async => await _onGenerate(),
-        ),
-        const Space(10, 0),
-        SecondaryTextButton(
-          LocaleKeys.button_Cancel.tr(),
-          onPressed: () async => await _onExit(),
-        ),
-        Expanded(
-          child: Container(
-            alignment: Alignment.centerRight,
-            child: FlowyText.regular(
-              LocaleKeys.document_plugins_warning.tr(),
-              color: Theme.of(context).hintColor,
-              overflow: TextOverflow.ellipsis,
-            ),
-          ),
-        ),
-      ],
-    );
-  }
-
-  Widget _buildFooterWidget(BuildContext context) {
-    return Row(
-      children: [
-        PrimaryTextButton(
-          LocaleKeys.button_keep.tr(),
-          onPressed: () => _onExit(),
-        ),
-        const Space(10, 0),
-        SecondaryTextButton(
-          LocaleKeys.button_discard.tr(),
-          onPressed: () => _onDiscard(),
-        ),
-      ],
-    );
-  }
-
-  Future<void> _onExit() async {
-    final transaction = widget.editorState.transaction;
-    transaction.deleteNode(widget.node);
-    await widget.editorState.apply(
-      transaction,
-      options: const ApplyOptions(
-        recordRedo: false,
-        recordUndo: false,
-      ),
-    );
-  }
-
-  Future<void> _onGenerate() async {
-    final loading = Loading(context);
-    loading.start();
-    await _updateEditingText();
-    final result = await UserBackendService.getCurrentUserProfile();
-
-    result.fold((userProfile) async {
-      BarrierDialog? barrierDialog;
-      final openAIRepository = HttpOpenAIRepository(
-        client: http.Client(),
-        apiKey: userProfile.openaiKey,
-      );
-      await openAIRepository.getStreamedCompletions(
-        prompt: controller.text,
-        onStart: () async {
-          loading.stop();
-          barrierDialog = BarrierDialog(context);
-          barrierDialog?.show();
-          await _makeSurePreviousNodeIsEmptyTextNode();
-        },
-        onProcess: (response) async {
-          if (response.choices.isNotEmpty) {
-            final text = response.choices.first.text;
-            await widget.editorState.autoInsertText(
-              text,
-              inputType: TextRobotInputType.word,
-              delay: Duration.zero,
-            );
-          }
-        },
-        onEnd: () async {
-          await barrierDialog?.dismiss();
-          widget.editorState.service.selectionService.currentSelection
-              .addListener(_onCancelWhenSelectionChanged);
-        },
-        onError: (error) async {
-          loading.stop();
-          await _showError(error.message);
-        },
-      );
-    }, (error) async {
-      loading.stop();
-      await _showError(
-        LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
-      );
-    });
-  }
-
-  Future<void> _onDiscard() async {
-    final selection =
-        widget.node.attributes[kAutoCompletionInputStartSelection];
-    if (selection != null) {
-      final start = Selection.fromJson(json.decode(selection)).start.path;
-      final end = widget.node.previous?.path;
-      if (end != null) {
-        final transaction = widget.editorState.transaction;
-        transaction.deleteNodesAtPath(
-          start,
-          end.last - start.last + 1,
-        );
-        await widget.editorState.apply(transaction);
-      }
-    }
-    _onExit();
-  }
-
-  Future<void> _updateEditingText() async {
-    final transaction = widget.editorState.transaction;
-    transaction.updateNode(
-      widget.node,
-      {
-        kAutoCompletionInputString: controller.text,
-      },
-    );
-    await widget.editorState.apply(transaction);
-  }
-
-  Future<void> _makeSurePreviousNodeIsEmptyTextNode() async {
-    // make sure the previous node is a empty text node without any styles.
-    final transaction = widget.editorState.transaction;
-    final Selection selection;
-    if (widget.node.previous is! TextNode ||
-        (widget.node.previous as TextNode).toPlainText().isNotEmpty ||
-        (widget.node.previous as TextNode).subtype != null) {
-      transaction.insertNode(
-        widget.node.path,
-        TextNode.empty(),
-      );
-      selection = Selection.single(
-        path: widget.node.path,
-        startOffset: 0,
-      );
-      transaction.afterSelection = selection;
-    } else {
-      selection = Selection.single(
-        path: widget.node.path.previous,
-        startOffset: 0,
-      );
-      transaction.afterSelection = selection;
-    }
-    transaction.updateNode(widget.node, {
-      kAutoCompletionInputStartSelection: jsonEncode(selection.toJson()),
-    });
-    await widget.editorState.apply(transaction);
-  }
-
-  Future<void> _showError(String message) async {
-    ScaffoldMessenger.of(context).showSnackBar(
-      SnackBar(
-        action: SnackBarAction(
-          label: LocaleKeys.button_Cancel.tr(),
-          onPressed: () {
-            ScaffoldMessenger.of(context).hideCurrentSnackBar();
-          },
-        ),
-        content: FlowyText(message),
-      ),
-    );
-  }
-
-  void _onFocusChanged() {
-    if (textFieldFocusNode.hasFocus) {
-      widget.editorState.service.keyboardService?.disable(
-        disposition: UnfocusDisposition.previouslyFocusedChild,
-      );
-    } else {
-      widget.editorState.service.keyboardService?.enable();
-    }
-  }
-
-  void _onCancelWhenSelectionChanged() {}
-}

+ 0 - 23
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart

@@ -1,23 +0,0 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:easy_localization/easy_localization.dart';
-
-SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
-  name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
-  iconData: Icons.generating_tokens,
-  keywords: ['ai', 'openai' 'writer', 'autogenerator'],
-  nodeBuilder: (editorState) {
-    final node = Node(
-      type: kAutoCompletionInputType,
-      attributes: {
-        kAutoCompletionInputString: '',
-      },
-    );
-    return node;
-  },
-  replace: (_, textNode) => textNode.toPlainText().isEmpty,
-  updateSelection: null,
-);

+ 0 - 59
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart

@@ -1,59 +0,0 @@
-import 'package:flutter/material.dart';
-
-class Loading {
-  Loading(
-    this.context,
-  );
-
-  late BuildContext loadingContext;
-  final BuildContext context;
-
-  Future<void> start() async {
-    return showDialog<void>(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        loadingContext = context;
-        return const SimpleDialog(
-          elevation: 0.0,
-          backgroundColor:
-              Colors.transparent, // can change this to your preferred color
-          children: <Widget>[
-            Center(
-              child: CircularProgressIndicator(),
-            )
-          ],
-        );
-      },
-    );
-  }
-
-  Future<void> stop() async {
-    return Navigator.of(loadingContext).pop();
-  }
-}
-
-class BarrierDialog {
-  BarrierDialog(
-    this.context,
-  );
-
-  late BuildContext loadingContext;
-  final BuildContext context;
-
-  Future<void> show() async {
-    return showDialog<void>(
-      context: context,
-      barrierDismissible: false,
-      barrierColor: Colors.transparent,
-      builder: (BuildContext context) {
-        loadingContext = context;
-        return Container();
-      },
-    );
-  }
-
-  Future<void> dismiss() async {
-    return Navigator.of(loadingContext).pop();
-  }
-}

+ 6 - 6
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -4,7 +4,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
 import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
 import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
 import 'package:appflowy/user/application/user_listener.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy/util/file_picker/file_picker_impl.dart';
@@ -52,14 +52,14 @@ void _resolveCommonService(GetIt getIt) async {
       final result = await UserBackendService.getCurrentUserProfile();
       return result.fold(
         (l) {
+          throw Exception('Failed to get user profile: ${l.msg}');
+        },
+        (r) {
           return HttpOpenAIRepository(
             client: http.Client(),
-            apiKey: l.openaiKey,
+            apiKey: r.openaiKey,
           );
         },
-        (r) {
-          throw Exception('Failed to get user profile: ${r.msg}');
-        },
       );
     },
   );
@@ -172,4 +172,4 @@ void _resolveGridDeps(GetIt getIt) {
     (viewId, cache) =>
         DatabasePropertyBloc(viewId: viewId, fieldController: cache),
   );
-}
+}

+ 4 - 2
frontend/appflowy_flutter/lib/user/application/user_service.dart

@@ -14,8 +14,10 @@ class UserBackendService {
 
   final Int64 userId;
 
-  static Future<Either<UserProfilePB, FlowyError>> getCurrentUserProfile() {
-    return UserEventGetUserProfile().send();
+  static Future<Either<FlowyError, UserProfilePB>>
+      getCurrentUserProfile() async {
+    final result = await UserEventGetUserProfile().send();
+    return result.swap();
   }
 
   Future<Either<Unit, FlowyError>> updateUserProfile({

+ 5 - 0
frontend/appflowy_flutter/lib/util/base64_string.dart

@@ -0,0 +1,5 @@
+import 'dart:convert';
+
+extension Base64Encode on String {
+  String get base64 => base64Encode(utf8.encode(this));
+}

+ 8 - 0
frontend/appflowy_flutter/lib/util/json_print.dart

@@ -0,0 +1,8 @@
+import 'dart:convert';
+
+import 'package:appflowy_backend/log.dart';
+
+const JsonEncoder _encoder = JsonEncoder.withIndent('  ');
+void prettyPrintJson(Object? object) {
+  Log.debug(_encoder.convert(object));
+}

+ 6 - 4
frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart

@@ -1,10 +1,10 @@
 import 'dart:async';
 import 'dart:typed_data';
 import 'package:appflowy/core/document_notification.dart';
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart';
 import 'package:appflowy_backend/rust_stream.dart';
 
 class DocumentListener {
@@ -17,10 +17,10 @@ class DocumentListener {
   StreamSubscription<SubscribeObject>? _subscription;
   DocumentNotificationParser? _parser;
 
-  Function()? didReceiveUpdate;
+  Function(DocEventPB docEvent)? didReceiveUpdate;
 
   void start({
-    void Function()? didReceiveUpdate,
+    Function(DocEventPB docEvent)? didReceiveUpdate,
   }) {
     this.didReceiveUpdate = didReceiveUpdate;
 
@@ -39,7 +39,9 @@ class DocumentListener {
   ) {
     switch (ty) {
       case DocumentNotification.DidReceiveUpdate:
-        didReceiveUpdate?.call();
+        result
+            .swap()
+            .map((r) => didReceiveUpdate?.call(DocEventPB.fromBuffer(r)));
         break;
       default:
         break;

+ 92 - 5
frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flutter/material.dart';
 import 'package:styled_widget/styled_widget.dart';
@@ -11,6 +11,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
   final BoxConstraints constraints;
   final PopoverDirection direction;
   final Widget Function(PopoverController) buildChild;
+  final VoidCallback? onPopupBuilder;
   final VoidCallback? onClosed;
   final bool asBarrier;
   final Offset offset;
@@ -21,6 +22,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
     required this.onSelected,
     this.mutex,
     this.onClosed,
+    this.onPopupBuilder,
     this.direction = PopoverDirection.rightWithTopAligned,
     this.asBarrier = false,
     this.offset = Offset.zero,
@@ -60,6 +62,7 @@ class _PopoverActionListState<T extends PopoverAction>
       triggerActions: PopoverTriggerFlags.none,
       onClose: widget.onClosed,
       popupBuilder: (BuildContext popoverContext) {
+        widget.onPopupBuilder?.call();
         final List<Widget> children = widget.actions.map((action) {
           if (action is ActionCell) {
             return ActionCellWidget<T>(
@@ -69,6 +72,13 @@ class _PopoverActionListState<T extends PopoverAction>
                 widget.onSelected(action, popoverController);
               },
             );
+          } else if (action is PopoverActionCell) {
+            return PopoverActionCellWidget<T>(
+              mutex: widget.mutex,
+              // popoverController: popoverController,
+              action: action,
+              itemHeight: ActionListSizes.itemHeight,
+            );
           } else {
             final custom = action as CustomActionCell;
             return custom.buildWithContext(context);
@@ -94,6 +104,15 @@ abstract class ActionCell extends PopoverAction {
   String get name;
 }
 
+abstract class PopoverActionCell extends PopoverAction {
+  Widget? leftIcon(Color iconColor) => null;
+  Widget? rightIcon(Color iconColor) => null;
+  String get name;
+
+  Widget Function(BuildContext context, PopoverController controller)
+      get builder;
+}
+
 abstract class CustomActionCell extends PopoverAction {
   Widget buildWithContext(BuildContext context);
 }
@@ -127,27 +146,95 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
     final rightIcon =
         actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
 
+    return _HoverButton(
+      itemHeight: itemHeight,
+      leftIcon: leftIcon,
+      rightIcon: rightIcon,
+      name: actionCell.name,
+      onTap: () => onSelected(action),
+    );
+  }
+}
+
+class PopoverActionCellWidget<T extends PopoverAction> extends StatelessWidget {
+  PopoverActionCellWidget({
+    Key? key,
+    this.mutex,
+    // required this.popoverController,
+    required this.action,
+    required this.itemHeight,
+  }) : super(key: key);
+
+  final T action;
+  final double itemHeight;
+
+  final PopoverMutex? mutex;
+  final PopoverController popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    final actionCell = action as PopoverActionCell;
+    final leftIcon =
+        actionCell.leftIcon(Theme.of(context).colorScheme.onSurface);
+    final rightIcon =
+        actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
+    return AppFlowyPopover(
+      // mutex: mutex,
+      controller: popoverController,
+      asBarrier: true,
+      popupBuilder: (context) => actionCell.builder(
+        context,
+        popoverController,
+      ),
+      child: _HoverButton(
+        itemHeight: itemHeight,
+        leftIcon: leftIcon,
+        rightIcon: rightIcon,
+        name: actionCell.name,
+        onTap: () => popoverController.show(),
+      ),
+    );
+  }
+}
+
+class _HoverButton extends StatelessWidget {
+  const _HoverButton({
+    required this.onTap,
+    required this.itemHeight,
+    required this.leftIcon,
+    required this.name,
+    required this.rightIcon,
+  });
+
+  final VoidCallback onTap;
+  final double itemHeight;
+  final Widget? leftIcon;
+  final Widget? rightIcon;
+  final String name;
+
+  @override
+  Widget build(BuildContext context) {
     return FlowyHover(
       child: GestureDetector(
         behavior: HitTestBehavior.opaque,
-        onTap: () => onSelected(action),
+        onTap: onTap,
         child: SizedBox(
           height: itemHeight,
           child: Row(
             children: [
               if (leftIcon != null) ...[
-                leftIcon,
+                leftIcon!,
                 HSpace(ActionListSizes.itemHPadding)
               ],
               Expanded(
                 child: FlowyText.medium(
-                  actionCell.name,
+                  name,
                   overflow: TextOverflow.visible,
                 ),
               ),
               if (rightIcon != null) ...[
                 HSpace(ActionListSizes.itemHPadding),
-                rightIcon,
+                rightIcon!,
               ],
             ],
           ),

+ 13 - 4
frontend/appflowy_flutter/pubspec.lock

@@ -44,10 +44,11 @@ packages:
   appflowy_editor:
     dependency: "direct main"
     description:
-      name: appflowy_editor
-      sha256: "6f7d2b0b54ca1049cb396229549d228b5bbd7ea6d09f1f7325a20db2d7586a5f"
-      url: "https://pub.dev"
-    source: hosted
+      path: "."
+      ref: "4f66f7"
+      resolved-ref: "4f66f77debabbc35cf4a56c816f9432a831a40e2"
+      url: "https://github.com/LucasXu0/appflowy-editor.git"
+    source: git
     version: "0.1.12"
   appflowy_popover:
     dependency: "direct main"
@@ -816,6 +817,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.3.0"
+  nanoid:
+    dependency: "direct main"
+    description:
+      name: nanoid
+      sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
   nested:
     dependency: transitive
     description:

+ 7 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -42,7 +42,12 @@ dependencies:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-board.git
       ref: a183c57
-  appflowy_editor: ^0.1.12
+  # appflowy_editor: ^0.1.9
+  appflowy_editor:
+    # path: /Users/lucas.xu/Desktop/appflowy-editor
+    git:
+      url: https://github.com/LucasXu0/appflowy-editor.git
+      ref: 4f66f7
   appflowy_popover:
     path: packages/appflowy_popover
 
@@ -101,6 +106,7 @@ dependencies:
   mocktail: ^0.3.0
   archive: ^3.3.0
   flutter_svg: ^2.0.5
+  nanoid: ^1.0.0
 
 dev_dependencies:
   flutter_lints: ^2.0.1

+ 4 - 4
frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart

@@ -1,8 +1,8 @@
 import 'dart:convert';
 
-import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -12,7 +12,7 @@ void main() {
       const text = '''
 {
     "document":{
-        "type":"editor",
+        "type":"document",
         "children":[
             {
                 "type":"math_equation",

+ 581 - 14
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -96,6 +96,21 @@ version = "1.0.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
 
+[[package]]
+name = "appflowy-integrate"
+version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
+dependencies = [
+ "collab",
+ "collab-database",
+ "collab-document",
+ "collab-folder",
+ "collab-persistence",
+ "collab-plugins",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "appflowy_tauri"
 version = "0.0.0"
@@ -193,6 +208,324 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "aws-config"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc00553f5f3c06ffd4510a9d576f92143618706c45ea6ff81e84ad9be9588abd"
+dependencies = [
+ "aws-credential-types",
+ "aws-http",
+ "aws-sdk-sso",
+ "aws-sdk-sts",
+ "aws-smithy-async",
+ "aws-smithy-client",
+ "aws-smithy-http",
+ "aws-smithy-http-tower",
+ "aws-smithy-json",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand",
+ "hex",
+ "http",
+ "hyper",
+ "ring",
+ "time 0.3.20",
+ "tokio",
+ "tower",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-credential-types"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cb57ac6088805821f78d282c0ba8aec809f11cbee10dda19a97b03ab040ccc2"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-types",
+ "fastrand",
+ "tokio",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-endpoint"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c5f6f84a4f46f95a9bb71d9300b73cd67eb868bc43ae84f66ad34752299f4ac"
+dependencies = [
+ "aws-smithy-http",
+ "aws-smithy-types",
+ "aws-types",
+ "http",
+ "regex",
+ "tracing",
+]
+
+[[package]]
+name = "aws-http"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a754683c322f7dc5167484266489fdebdcd04d26e53c162cad1f3f949f2c5671"
+dependencies = [
+ "aws-credential-types",
+ "aws-smithy-http",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http",
+ "http-body",
+ "lazy_static",
+ "percent-encoding",
+ "pin-project-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-dynamodb"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67fb64867fe098cffee7e34352b01bbfa2beb3aa1b2ff0e0a7bf9ff293557852"
+dependencies = [
+ "aws-credential-types",
+ "aws-endpoint",
+ "aws-http",
+ "aws-sig-auth",
+ "aws-smithy-async",
+ "aws-smithy-client",
+ "aws-smithy-http",
+ "aws-smithy-http-tower",
+ "aws-smithy-json",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand",
+ "http",
+ "regex",
+ "tokio-stream",
+ "tower",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-sso"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "babfd626348836a31785775e3c08a4c345a5ab4c6e06dfd9167f2bee0e6295d6"
+dependencies = [
+ "aws-credential-types",
+ "aws-endpoint",
+ "aws-http",
+ "aws-sig-auth",
+ "aws-smithy-async",
+ "aws-smithy-client",
+ "aws-smithy-http",
+ "aws-smithy-http-tower",
+ "aws-smithy-json",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http",
+ "regex",
+ "tokio-stream",
+ "tower",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-sts"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d0fbe3c2c342bc8dfea4bb43937405a8ec06f99140a0dcb9c7b59e54dfa93a1"
+dependencies = [
+ "aws-credential-types",
+ "aws-endpoint",
+ "aws-http",
+ "aws-sig-auth",
+ "aws-smithy-async",
+ "aws-smithy-client",
+ "aws-smithy-http",
+ "aws-smithy-http-tower",
+ "aws-smithy-json",
+ "aws-smithy-query",
+ "aws-smithy-types",
+ "aws-smithy-xml",
+ "aws-types",
+ "bytes",
+ "http",
+ "regex",
+ "tower",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sig-auth"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84dc92a63ede3c2cbe43529cb87ffa58763520c96c6a46ca1ced80417afba845"
+dependencies = [
+ "aws-credential-types",
+ "aws-sigv4",
+ "aws-smithy-http",
+ "aws-types",
+ "http",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sigv4"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "392fefab9d6fcbd76d518eb3b1c040b84728ab50f58df0c3c53ada4bea9d327e"
+dependencies = [
+ "aws-smithy-http",
+ "form_urlencoded",
+ "hex",
+ "hmac",
+ "http",
+ "once_cell",
+ "percent-encoding",
+ "regex",
+ "sha2",
+ "time 0.3.20",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-async"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae23b9fe7a07d0919000116c4c5c0578303fbce6fc8d32efca1f7759d4c20faf"
+dependencies = [
+ "futures-util",
+ "pin-project-lite",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "aws-smithy-client"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5230d25d244a51339273b8870f0f77874cd4449fb4f8f629b21188ae10cfc0ba"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-http-tower",
+ "aws-smithy-types",
+ "bytes",
+ "fastrand",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-rustls",
+ "lazy_static",
+ "pin-project-lite",
+ "rustls",
+ "tokio",
+ "tower",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-http"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b60e2133beb9fe6ffe0b70deca57aaeff0a35ad24a9c6fab2fd3b4f45b99fdb5"
+dependencies = [
+ "aws-smithy-types",
+ "bytes",
+ "bytes-utils",
+ "futures-core",
+ "http",
+ "http-body",
+ "hyper",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "pin-utils",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-http-tower"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a4d94f556c86a0dd916a5d7c39747157ea8cb909ca469703e20fee33e448b67"
+dependencies = [
+ "aws-smithy-http",
+ "aws-smithy-types",
+ "bytes",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-json"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ce3d6e6ebb00b2cce379f079ad5ec508f9bcc3a9510d9b9c1840ed1d6f8af39"
+dependencies = [
+ "aws-smithy-types",
+]
+
+[[package]]
+name = "aws-smithy-query"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d58edfca32ef9bfbc1ca394599e17ea329cb52d6a07359827be74235b64b3298"
+dependencies = [
+ "aws-smithy-types",
+ "urlencoding",
+]
+
+[[package]]
+name = "aws-smithy-types"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58db46fc1f4f26be01ebdb821751b4e2482cd43aa2b64a0348fb89762defaffa"
+dependencies = [
+ "base64-simd",
+ "itoa 1.0.6",
+ "num-integer",
+ "ryu",
+ "time 0.3.20",
+]
+
+[[package]]
+name = "aws-smithy-xml"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb557fe4995bd9ec87fb244bbb254666a971dc902a783e9da8b7711610e9664c"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
+name = "aws-types"
+version = "0.55.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0869598bfe46ec44ffe17e063ed33336e59df90356ca8ff0e8da6f7c1d994b"
+dependencies = [
+ "aws-credential-types",
+ "aws-smithy-async",
+ "aws-smithy-client",
+ "aws-smithy-http",
+ "aws-smithy-types",
+ "http",
+ "rustc_version",
+ "tracing",
+]
+
 [[package]]
 name = "backtrace"
 version = "0.3.67"
@@ -220,6 +553,16 @@ version = "0.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
 
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
 [[package]]
 name = "bincode"
 version = "1.3.3"
@@ -419,6 +762,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bytes-utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
+dependencies = [
+ "bytes",
+ "either",
+]
+
 [[package]]
 name = "bzip2-sys"
 version = "0.1.11+1.0.8"
@@ -533,6 +886,7 @@ dependencies = [
  "js-sys",
  "num-integer",
  "num-traits",
+ "serde",
  "time 0.1.45",
  "wasm-bindgen",
  "winapi",
@@ -662,7 +1016,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "anyhow",
  "bytes",
@@ -679,7 +1033,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -697,9 +1051,10 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "anyhow",
+ "async-trait",
  "chrono",
  "collab",
  "collab-derive",
@@ -720,7 +1075,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -732,7 +1087,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "anyhow",
  "collab",
@@ -749,7 +1104,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "anyhow",
  "collab",
@@ -767,7 +1122,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "bincode",
  "chrono",
@@ -787,12 +1142,24 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
+ "anyhow",
+ "async-trait",
+ "aws-config",
+ "aws-credential-types",
+ "aws-sdk-dynamodb",
  "collab",
  "collab-client-ws",
  "collab-persistence",
  "collab-sync",
+ "futures-util",
+ "parking_lot 0.12.1",
+ "rand 0.8.5",
+ "rusoto_credential",
+ "thiserror",
+ "tokio",
+ "tokio-retry",
  "tracing",
  "y-sync",
  "yrs",
@@ -801,7 +1168,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f66084#f66084454c143418cf69155dc8cce77df2d57d0c"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d074c9#d074c94471f222e2701fe451f13c51aab39d2bf8"
 dependencies = [
  "bytes",
  "collab",
@@ -1201,6 +1568,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
  "block-buffer 0.10.4",
  "crypto-common",
+ "subtle",
 ]
 
 [[package]]
@@ -1497,8 +1865,8 @@ dependencies = [
 name = "flowy-core"
 version = "0.1.0"
 dependencies = [
+ "appflowy-integrate",
  "bytes",
- "collab-persistence",
  "database-model",
  "flowy-client-ws",
  "flowy-database2",
@@ -1531,13 +1899,14 @@ name = "flowy-database2"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "appflowy-integrate",
  "async-stream",
+ "async-trait",
  "bytes",
  "chrono",
  "chrono-tz 0.8.2",
  "collab",
  "collab-database",
- "collab-plugins",
  "dashmap",
  "database-model",
  "fancy-regex 0.10.0",
@@ -1624,10 +1993,10 @@ dependencies = [
 name = "flowy-document2"
 version = "0.1.0"
 dependencies = [
+ "appflowy-integrate",
  "bytes",
  "collab",
  "collab-document",
- "collab-plugins",
  "flowy-codegen",
  "flowy-derive",
  "flowy-error",
@@ -1651,6 +2020,7 @@ dependencies = [
  "anyhow",
  "bytes",
  "collab-database",
+ "collab-document",
  "flowy-client-sync",
  "flowy-client-ws",
  "flowy-codegen",
@@ -1673,11 +2043,11 @@ dependencies = [
 name = "flowy-folder2"
 version = "0.1.0"
 dependencies = [
+ "appflowy-integrate",
  "bytes",
  "chrono",
  "collab",
  "collab-folder",
- "collab-plugins",
  "flowy-codegen",
  "flowy-derive",
  "flowy-document",
@@ -1856,8 +2226,8 @@ dependencies = [
 name = "flowy-user"
 version = "0.1.0"
 dependencies = [
+ "appflowy-integrate",
  "bytes",
- "collab-persistence",
  "diesel",
  "diesel_derives",
  "flowy-codegen",
@@ -2412,6 +2782,21 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.6",
+]
+
 [[package]]
 name = "html5ever"
 version = "0.25.2"
@@ -2509,6 +2894,21 @@ dependencies = [
  "want",
 ]
 
+[[package]]
+name = "hyper-rustls"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
+dependencies = [
+ "http",
+ "hyper",
+ "log",
+ "rustls",
+ "rustls-native-certs",
+ "tokio",
+ "tokio-rustls",
+]
+
 [[package]]
 name = "hyper-tls"
 version = "0.5.0"
@@ -3446,6 +3846,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "outref"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -4252,6 +4658,21 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
 [[package]]
 name = "rkyv"
 version = "0.7.41"
@@ -4287,6 +4708,24 @@ dependencies = [
  "librocksdb-sys",
 ]
 
+[[package]]
+name = "rusoto_credential"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "dirs-next",
+ "futures",
+ "hyper",
+ "serde",
+ "serde_json",
+ "shlex",
+ "tokio",
+ "zeroize",
+]
+
 [[package]]
 name = "rust_decimal"
 version = "1.29.1"
@@ -4350,6 +4789,39 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
+dependencies = [
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
+dependencies = [
+ "base64 0.21.0",
+]
+
 [[package]]
 name = "rustversion"
 version = "1.0.12"
@@ -4423,6 +4895,16 @@ version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
 
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
 [[package]]
 name = "seahash"
 version = "4.1.0"
@@ -4779,6 +5261,12 @@ dependencies = [
  "system-deps 5.0.0",
 ]
 
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
 [[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
@@ -4850,6 +5338,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
 [[package]]
 name = "syn"
 version = "1.0.109"
@@ -5348,6 +5842,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
+dependencies = [
+ "rustls",
+ "tokio",
+ "webpki",
+]
+
 [[package]]
 name = "tokio-stream"
 version = "0.1.14"
@@ -5442,6 +5947,28 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
 [[package]]
 name = "tower-service"
 version = "0.3.2"
@@ -5728,6 +6255,12 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
 
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
 [[package]]
 name = "url"
 version = "2.3.1"
@@ -5740,6 +6273,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
 [[package]]
 name = "user-model"
 version = "0.1.0"
@@ -5820,6 +6359,12 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
 [[package]]
 name = "walkdir"
 version = "2.3.3"
@@ -5981,6 +6526,16 @@ dependencies = [
  "system-deps 6.0.5",
 ]
 
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
 [[package]]
 name = "webview2-com"
 version = "0.19.1"
@@ -6399,6 +6954,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "xmlparser"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
+
 [[package]]
 name = "y-sync"
 version = "0.3.1"
@@ -6433,6 +6994,12 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "zeroize"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
+
 [[package]]
 name = "zstd-sys"
 version = "2.0.8+zstd.1.5.5"

+ 1 - 0
frontend/rust-lib/Cargo.lock

@@ -1920,6 +1920,7 @@ dependencies = [
  "anyhow",
  "bytes",
  "collab-database",
+ "collab-document",
  "flowy-client-sync",
  "flowy-client-ws",
  "flowy-codegen",

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