Pārlūkot izejas kodu

feat: add toggle list and integrate add block button (#2547)

Lucas.Xu 2 gadi atpakaļ
vecāks
revīzija
623f182bba
17 mainītis faili ar 507 papildinājumiem un 27 dzēšanām
  1. 1 4
      frontend/appflowy_flutter/assets/images/editor/add.svg
  2. 1 1
      frontend/appflowy_flutter/assets/images/editor/option.svg
  3. 4 0
      frontend/appflowy_flutter/assets/translations/en.json
  4. 1 1
      frontend/appflowy_flutter/lib/plugins/document/document_page.dart
  5. 24 16
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  6. 52 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart
  7. 41 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart
  8. 45 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart
  9. 131 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart
  10. 6 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart
  11. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart
  12. 2 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart
  13. 165 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart
  14. 24 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart
  15. 6 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart
  16. 2 2
      frontend/appflowy_flutter/pubspec.lock
  17. 1 1
      frontend/appflowy_flutter/pubspec.yaml

+ 1 - 4
frontend/appflowy_flutter/assets/images/editor/add.svg

@@ -1,4 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="7.5" y="4" width="1" height="8" rx="0.5" fill="#333333"/>
-<rect x="12" y="7.5" width="1" height="8" rx="0.5" transform="rotate(90 12 7.5)" fill="#333333"/>
-</svg>
+<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="plus" style="width: 16px; height: 100%; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="16"  height="16" ><path d="M7.977 14.963c.407 0 .747-.324.747-.723V8.72h5.362c.399 0 .74-.34.74-.747a.746.746 0 00-.74-.738H8.724V1.706c0-.398-.34-.722-.747-.722a.732.732 0 00-.739.722v5.529h-5.37a.746.746 0 00-.74.738c0 .407.341.747.74.747h5.37v5.52c0 .399.332.723.739.723z" fill-opacity="0.35" fill="#37352F"></path></svg>

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

@@ -1 +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>
+<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" class="dragHandle" style="width: 14px; height: 14px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="14"  height="14" ><path d="M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z" fill-opacity="0.35" fill="#37352F"></path></svg>

+ 4 - 0
frontend/appflowy_flutter/assets/translations/en.json

@@ -395,6 +395,10 @@
       "mathEquation": {
         "addMathEquation": "Add Math Equation",
         "editMathEquation": "Edit Math Equation"
+      },
+      "optionAction": {
+        "click": "Click",
+        "toOpenMenu": " to open menu"
       }
     }
   },

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -89,11 +89,11 @@ class _DocumentPageState extends State<DocumentPage> {
   Widget _buildEditorPage(BuildContext context, DocumentState state) {
     final appflowyEditorPage = AppFlowyEditorPage(
       editorState: editorState!,
+      header: _buildCoverAndIcon(context),
     );
     return Column(
       children: [
         if (state.isDeleted) _buildBanner(context),
-        _buildCoverAndIcon(context),
         Expanded(
           child: appflowyEditorPage,
         ),

+ 24 - 16
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -1,6 +1,6 @@
 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/actions/block_action_list.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';
@@ -13,9 +13,11 @@ class AppFlowyEditorPage extends StatefulWidget {
   const AppFlowyEditorPage({
     super.key,
     required this.editorState,
+    this.header,
   });
 
   final EditorState editorState;
+  final Widget? header;
 
   @override
   State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
@@ -41,16 +43,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
 
   final List<ToolbarItem> toolbarItems = [
     smartEditItem,
-    placeholderItem,
     paragraphItem,
     ...headingItems,
-    placeholderItem,
     ...markdownFormatItems,
-    placeholderItem,
     quoteItem,
     bulletedListItem,
     numberedListItem,
-    placeholderItem,
     linkItem,
     textColorItem,
     highlightColorItem,
@@ -70,8 +68,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
         (element) => element == slashCommand,
       ), // remove the default slash command.
     customSlashCommand(slashMenuItems),
+
+    // formatGreaterToToggleList,
   ];
 
+  late final showSlashMenu = customSlashCommand(
+    slashMenuItems,
+    shouldInsertSlash: false,
+  ).handler;
+
   late final styleCustomizer = EditorStyleCustomizer(context: context);
   DocumentBloc get documentBloc => context.read<DocumentBloc>();
 
@@ -92,6 +97,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
       // customize the shortcuts
       characterShortcutEvents: characterShortcutEvents,
       commandShortcutEvents: commandShortcutEvents,
+      header: widget.header,
     );
 
     return Center(
@@ -183,6 +189,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
       ),
       AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
       SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
+      // ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(),
     };
 
     final builders = {
@@ -207,24 +214,25 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
         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(
+
+      final List<OptionAction> actions = [
+        ...standardActions,
+        if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
+      ];
+
+      builder.actionBuilder = (context, state) => BlockActionList(
             blockComponentContext: context,
             blockComponentState: state,
             editorState: widget.editorState,
-            actions: standardActions + colorAction,
+            actions: actions,
+            showSlashMenu: () => showSlashMenu(
+              widget.editorState,
+            ),
           );
     }
 

+ 52 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart

@@ -0,0 +1,52 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+class BlockAddButton extends StatelessWidget {
+  const BlockAddButton({
+    Key? key,
+    required this.blockComponentContext,
+    required this.blockComponentState,
+    required this.editorState,
+    required this.showSlashMenu,
+  }) : super(key: key);
+
+  final BlockComponentContext blockComponentContext;
+  final BlockComponentState blockComponentState;
+
+  final EditorState editorState;
+  final VoidCallback showSlashMenu;
+
+  @override
+  Widget build(BuildContext context) {
+    return BlockActionButton(
+      svgName: 'editor/add',
+      richMessage: const TextSpan(
+        children: [
+          TextSpan(
+            // todo: l10n.
+            text: 'Click to add below',
+          ),
+        ],
+      ),
+      onTap: () {
+        final transaction = editorState.transaction;
+        // if the current block is not a empty paragraph block, then insert a new block below the current block.
+        final node = blockComponentContext.node;
+        if (node.type != ParagraphBlockKeys.type ||
+            (node.delta?.isNotEmpty ?? true)) {
+          transaction.insertNode(node.path.next, paragraphNode());
+          transaction.afterSelection = Selection.collapse(node.path.next, 0);
+        } else {
+          transaction.afterSelection = Selection.collapse(node.path, 0);
+        }
+        // show the slash menu.
+        editorState.apply(transaction).then(
+              (_) => WidgetsBinding.instance.addPostFrameCallback(
+                (_) => showSlashMenu(),
+              ),
+            );
+      },
+    );
+  }
+}

+ 41 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart

@@ -0,0 +1,41 @@
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
+import 'package:flutter/material.dart';
+
+class BlockActionButton extends StatelessWidget {
+  const BlockActionButton({
+    super.key,
+    required this.svgName,
+    required this.richMessage,
+    required this.onTap,
+  });
+
+  final String svgName;
+  final InlineSpan richMessage;
+  final VoidCallback onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.center,
+      child: Tooltip(
+        preferBelow: false,
+        richMessage: richMessage,
+        child: MouseRegion(
+          cursor: SystemMouseCursors.grab,
+          child: IgnoreParentGestureWidget(
+            child: GestureDetector(
+              onTap: onTap,
+              behavior: HitTestBehavior.deferToChild,
+              child: svgWidget(
+                svgName,
+                size: const Size.square(14.0),
+                color: Theme.of(context).iconTheme.color,
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 45 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart

@@ -0,0 +1,45 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
+import 'package:flutter/material.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+class BlockActionList extends StatelessWidget {
+  const BlockActionList({
+    super.key,
+    required this.blockComponentContext,
+    required this.blockComponentState,
+    required this.editorState,
+    required this.actions,
+    required this.showSlashMenu,
+  });
+
+  final BlockComponentContext blockComponentContext;
+  final BlockComponentState blockComponentState;
+  final List<OptionAction> actions;
+  final VoidCallback showSlashMenu;
+  final EditorState editorState;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.end,
+      children: [
+        BlockAddButton(
+          blockComponentContext: blockComponentContext,
+          blockComponentState: blockComponentState,
+          editorState: editorState,
+          showSlashMenu: showSlashMenu,
+        ),
+        const SizedBox(width: 8.0),
+        BlockOptionButton(
+          blockComponentContext: blockComponentContext,
+          blockComponentState: blockComponentState,
+          actions: actions,
+          editorState: editorState,
+        ),
+        const SizedBox(width: 6.0),
+      ],
+    );
+  }
+}

+ 131 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart

@@ -0,0 +1,131 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
+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:flutter/material.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+
+class BlockOptionButton extends StatelessWidget {
+  const BlockOptionButton({
+    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) => _buildOptionButton(controller),
+    );
+  }
+
+  Widget _buildOptionButton(PopoverController controller) {
+    return BlockActionButton(
+      svgName: 'editor/option',
+      richMessage: TextSpan(
+        children: [
+          TextSpan(
+            // todo: customize the color to highlight the text.
+            text: LocaleKeys.document_plugins_optionAction_click.tr(),
+          ),
+          TextSpan(
+            text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
+          )
+        ],
+      ),
+      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);
+  }
+}

+ 6 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart

@@ -206,6 +206,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
         node: widget.node,
         editorState: editorState,
         placeholderText: placeholderText,
+        lineHeight: 1.5,
         textSpanDecorator: (textSpan) => TextSpan(
           style: textStyle,
           children: codeTextSpans,
@@ -218,10 +219,12 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
   }
 
   Widget _buildSwitchLanguageButton(BuildContext context) {
+    const maxWidth = 100.0;
     return AppFlowyPopover(
       controller: popoverController,
       child: Container(
-        width: 100,
+        width: maxWidth,
+        alignment: Alignment.centerLeft,
         padding: const EdgeInsets.symmetric(horizontal: 4),
         child: FlowyTextButton(
           '${language?.capitalize() ?? 'auto'} ',
@@ -229,8 +232,10 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
             horizontal: 12.0,
             vertical: 4.0,
           ),
+          constraints: const BoxConstraints(maxWidth: maxWidth),
           fontColor: Theme.of(context).colorScheme.onBackground,
           fillColor: Colors.transparent,
+          mainAxisAlignment: MainAxisAlignment.start,
           onPressed: () {},
         ),
       ),

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -12,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart';
 
 final ToolbarItem smartEditItem = ToolbarItem(
   id: 'appflowy.editor.smart_edit',
+  group: 0,
   isActive: (editorState) {
     final selection = editorState.selection;
     if (selection == null) {

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

@@ -18,3 +18,5 @@ export 'math_equation/math_equation_block_component.dart';
 export 'openai/widgets/auto_completion_node_widget.dart';
 export 'openai/widgets/smart_edit_node_widget.dart';
 export 'openai/widgets/smart_edit_toolbar_item.dart';
+export 'toggle/toggle_block_component.dart';
+export 'toggle/toggle_block_shortcut_event.dart';

+ 165 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart

@@ -0,0 +1,165 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class ToggleListBlockKeys {
+  const ToggleListBlockKeys._();
+
+  static const String type = 'toggle_list';
+
+  /// The content of a code block.
+  ///
+  /// The value is a String.
+  static const String delta = 'delta';
+
+  /// The value is a bool.
+  static const String collapsed = 'collapsed';
+}
+
+Node toggleListBlockNode({
+  Delta? delta,
+  bool collapsed = false,
+}) {
+  final attributes = {
+    ToggleListBlockKeys.delta: (delta ?? Delta()).toJson(),
+    ToggleListBlockKeys.collapsed: collapsed,
+  };
+  return Node(
+    type: ToggleListBlockKeys.type,
+    attributes: attributes,
+    children: [paragraphNode()],
+  );
+}
+
+class ToggleListBlockComponentBuilder extends BlockComponentBuilder {
+  ToggleListBlockComponentBuilder({
+    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 ToggleListBlockComponentWidget(
+      key: node.key,
+      node: node,
+      configuration: configuration,
+      padding: padding,
+    );
+  }
+
+  @override
+  bool validate(Node node) => node.delta != null;
+}
+
+class ToggleListBlockComponentWidget extends StatefulWidget {
+  const ToggleListBlockComponentWidget({
+    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<ToggleListBlockComponentWidget> createState() =>
+      _ToggleListBlockComponentWidgetState();
+}
+
+class _ToggleListBlockComponentWidgetState
+    extends State<ToggleListBlockComponentWidget>
+    with
+        SelectableMixin,
+        DefaultSelectable,
+        BlockComponentConfigurable,
+        BackgroundColorMixin {
+  // 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;
+
+  bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false;
+
+  late final editorState = context.read<EditorState>();
+
+  @override
+  Widget build(BuildContext context) {
+    return collapsed
+        ? buildToggleListBlockComponent(context)
+        : buildToggleListBlockComponentWithChildren(context);
+  }
+
+  Widget buildToggleListBlockComponentWithChildren(BuildContext context) {
+    return Container(
+      color: backgroundColor,
+      child: NestedListWidget(
+        children: editorState.renderer.buildList(
+          context,
+          widget.node.children,
+        ),
+        child: buildToggleListBlockComponent(context),
+      ),
+    );
+  }
+
+  // build the richtext child
+  Widget buildToggleListBlockComponent(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        // the emoji picker button for the note
+        FlowyIconButton(
+          width: 24.0,
+          icon: Icon(
+            collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
+          ),
+          onPressed: onCollapsed,
+        ),
+        const SizedBox(
+          width: 4.0,
+        ),
+        Expanded(
+          child: FlowyRichText(
+            key: forwardKey,
+            node: widget.node,
+            editorState: editorState,
+            placeholderText: placeholderText,
+            lineHeight: 1.5,
+            textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
+              textStyle,
+            ),
+            placeholderTextSpanDecorator: (textSpan) =>
+                textSpan.updateTextStyle(
+              placeholderTextStyle,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Future<void> onCollapsed() async {
+    final transaction = editorState.transaction
+      ..updateNode(node, {
+        ToggleListBlockKeys.collapsed: !collapsed,
+      });
+    await editorState.apply(transaction);
+  }
+}

+ 24 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart

@@ -0,0 +1,24 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+const _greater = '>';
+
+/// Convert '> ' to toggle list
+///
+/// - support
+///   - desktop
+///   - mobile
+///   - web
+///
+CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent(
+  key: 'format greater to quote',
+  character: ' ',
+  handler: (editorState) async => await formatMarkdownSymbol(
+    editorState,
+    (node) => node.type != ToggleListBlockKeys.type,
+    (text, _) => text == _greater,
+    (_, node, delta) => toggleListBlockNode(
+      delta: delta.compose(Delta()..delete(_greater.length)),
+    ),
+  ),
+);

+ 6 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart

@@ -28,7 +28,10 @@ class EditorStyleCustomizer {
     final theme = Theme.of(context);
     final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
     return EditorStyle.desktop(
-      padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
+      padding: EdgeInsets.only(
+        left: horizontalPadding / 2.0,
+        right: horizontalPadding,
+      ),
       backgroundColor: theme.colorScheme.surface,
       cursorColor: theme.colorScheme.primary,
       textStyleConfiguration: TextStyleConfiguration(
@@ -115,11 +118,13 @@ class EditorStyleCustomizer {
   }
 
   TextStyle codeBlockStyleBuilder() {
+    final theme = Theme.of(context);
     final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
     return TextStyle(
       fontFamily: 'poppins',
       fontSize: fontSize,
       height: 1.5,
+      color: theme.colorScheme.onBackground,
     );
   }
 }

+ 2 - 2
frontend/appflowy_flutter/pubspec.lock

@@ -45,8 +45,8 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: "4f66f7"
-      resolved-ref: "4f66f77debabbc35cf4a56c816f9432a831a40e2"
+      ref: b1a1b14
+      resolved-ref: b1a1b14f35114a7becdb3e2de909d546d7328a59
       url: "https://github.com/LucasXu0/appflowy-editor.git"
     source: git
     version: "0.1.12"

+ 1 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -47,7 +47,7 @@ dependencies:
     # path: /Users/lucas.xu/Desktop/appflowy-editor
     git:
       url: https://github.com/LucasXu0/appflowy-editor.git
-      ref: 4f66f7
+      ref: b1a1b14
   appflowy_popover:
     path: packages/appflowy_popover