Ver código fonte

feat: support text align and block align (#3292)

* feat: support text align and block align

* test: add test
Lucas.Xu 1 ano atrás
pai
commit
f73d59fb57

+ 46 - 0
frontend/appflowy_flutter/integration_test/document/document_alignment_test.dart

@@ -0,0 +1,46 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import '../util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('document alignment', () {
+    testWidgets('edit alignment in toolbar', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      final selection = Selection.single(
+        path: [0],
+        startOffset: 0,
+        endOffset: 1,
+      );
+      // click the first line of the readme
+      await tester.editor.tapLineOfEditorAt(0);
+      await tester.editor.updateSelection(selection);
+      await tester.pumpAndSettle();
+
+      // click the align center
+      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
+      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
+
+      // expect to see the align center
+      final editorState = tester.editor.getCurrentEditorState();
+      final first = editorState.getNodeAtPath([0])!;
+      expect(first.attributes[blockComponentAlign], 'center');
+
+      // click the align right
+      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
+      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
+      expect(first.attributes[blockComponentAlign], 'right');
+
+      // click the align left
+      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
+      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
+      expect(first.attributes[blockComponentAlign], 'left');
+    });
+  });
+}

+ 5 - 3
frontend/appflowy_flutter/integration_test/document/document_test_runner.dart

@@ -1,5 +1,8 @@
 import 'package:integration_test/integration_test.dart';
 
+import 'document_alignment_test.dart' as document_alignment_test;
+import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
+import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
 import 'document_create_and_delete_test.dart'
     as document_create_and_delete_test;
 import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
@@ -7,11 +10,9 @@ import 'document_with_database_test.dart' as document_with_database_test;
 import 'document_with_inline_math_equation_test.dart'
     as document_with_inline_math_equation_test;
 import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
+import 'document_with_outline_block_test.dart' as document_with_outline_block;
 import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
 import 'edit_document_test.dart' as document_edit_test;
-import 'document_with_outline_block_test.dart' as document_with_outline_block;
-import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
-import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
 
 void startTesting() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -27,4 +28,5 @@ void startTesting() {
   document_with_toggle_list_test.main();
   document_copy_and_paste_test.main();
   document_codeblock_paste_test.main();
+  document_alignment_test.main();
 }

+ 13 - 5
frontend/appflowy_flutter/integration_test/util/common_operations.dart

@@ -1,14 +1,14 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
+import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
 import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
 import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
 import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
 import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
 import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
-import 'package:appflowy_backend/log.dart';
-import 'package:appflowy/generated/locale_keys.g.dart';
-
-import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
-import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
@@ -409,6 +409,14 @@ extension CommonOperations on WidgetTester {
     await gesture.up();
     await pumpAndSettle();
   }
+
+  // tap the button with [FlowySvgData]
+  Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
+    final button = find.byWidgetPredicate(
+      (widget) => widget is FlowySvg && widget.svg.path == svg.path,
+    );
+    await tapButton(button);
+  }
 }
 
 extension ViewLayoutPBTest on ViewLayoutPB {

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

@@ -102,7 +102,8 @@ class _DocumentPageState extends State<DocumentPage> {
       editorState: editorState!,
       styleCustomizer: EditorStyleCustomizer(
         context: context,
-        padding: const EdgeInsets.symmetric(horizontal: 50),
+        // the 44 is the width of the left action list
+        padding: const EdgeInsets.only(left: 40, right: 40 + 44),
       ),
       header: _buildCoverAndIcon(context),
     );

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

@@ -66,6 +66,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
     numberedListItem,
     inlineMathEquationItem,
     linkItem,
+    alignToolbarItem,
     buildTextColorItem(),
     buildHighlightColorItem(),
   ];

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

@@ -0,0 +1,153 @@
+import 'package:appflowy/generated/flowy_svgs.g.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';
+
+final alignToolbarItem = ToolbarItem(
+  id: 'editor.align',
+  group: 4,
+  isActive: onlyShowInTextType,
+  builder: (context, editorState, highlightColor) {
+    final selection = editorState.selection!;
+    final nodes = editorState.getNodesInSelection(selection);
+
+    bool isSatisfyCondition(bool Function(Object? value) test) {
+      return nodes.every(
+        (n) => test(n.attributes[blockComponentAlign]),
+      );
+    }
+
+    bool isHighlight = false;
+    FlowySvgData data = FlowySvgs.toolbar_align_left_s;
+    if (isSatisfyCondition((value) => value == 'left')) {
+      isHighlight = true;
+      data = FlowySvgs.toolbar_align_left_s;
+    } else if (isSatisfyCondition((value) => value == 'center')) {
+      isHighlight = true;
+      data = FlowySvgs.toolbar_align_center_s;
+    } else if (isSatisfyCondition((value) => value == 'right')) {
+      isHighlight = true;
+      data = FlowySvgs.toolbar_align_right_s;
+    }
+
+    final child = FlowySvg(
+      data,
+      size: const Size.square(16),
+      color: isHighlight ? highlightColor : Colors.white,
+    );
+    return _AlignmentButtons(
+      child: child,
+      onAlignChanged: (align) async {
+        await editorState.updateNode(
+          selection,
+          (node) => node.copyWith(
+            attributes: {
+              ...node.attributes,
+              blockComponentAlign: align,
+            },
+          ),
+        );
+      },
+    );
+  },
+);
+
+class _AlignmentButtons extends StatefulWidget {
+  const _AlignmentButtons({
+    required this.child,
+    required this.onAlignChanged,
+  });
+
+  final Widget child;
+  final Function(String align) onAlignChanged;
+
+  @override
+  State<_AlignmentButtons> createState() => _AlignmentButtonsState();
+}
+
+class _AlignmentButtonsState extends State<_AlignmentButtons> {
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      windowPadding: const EdgeInsets.all(0),
+      margin: const EdgeInsets.all(0),
+      direction: PopoverDirection.bottomWithCenterAligned,
+      offset: const Offset(0, 10),
+      child: widget.child,
+      popupBuilder: (_) => _AlignButtons(onAlignChanged: widget.onAlignChanged),
+    );
+  }
+}
+
+class _AlignButtons extends StatelessWidget {
+  const _AlignButtons({
+    required this.onAlignChanged,
+  });
+
+  final Function(String align) onAlignChanged;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 32,
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          const HSpace(4),
+          _AlignButton(
+            icon: FlowySvgs.toolbar_align_left_s,
+            onTap: () => onAlignChanged('left'),
+          ),
+          const _Divider(),
+          _AlignButton(
+            icon: FlowySvgs.toolbar_align_center_s,
+            onTap: () => onAlignChanged('center'),
+          ),
+          const _Divider(),
+          _AlignButton(
+            icon: FlowySvgs.toolbar_align_right_s,
+            onTap: () => onAlignChanged('right'),
+          ),
+          const HSpace(4),
+        ],
+      ),
+    );
+  }
+}
+
+class _AlignButton extends StatelessWidget {
+  const _AlignButton({
+    required this.icon,
+    required this.onTap,
+  });
+
+  final FlowySvgData icon;
+  final VoidCallback onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: onTap,
+      child: FlowySvg(
+        icon,
+        size: const Size.square(16),
+      ),
+    );
+  }
+}
+
+class _Divider extends StatelessWidget {
+  const _Divider();
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(8),
+      child: Container(
+        width: 1,
+        color: Colors.grey,
+      ),
+    );
+  }
+}

+ 0 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart

@@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
-
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
 import 'package:flutter/material.dart';

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

@@ -19,6 +19,7 @@ export 'image/image_menu.dart';
 export 'image/image_selection_menu.dart';
 export 'inline_math_equation/inline_math_equation.dart';
 export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
+export 'align_toolbar_item/align_toolbar_item.dart';
 export 'math_equation/math_equation_block_component.dart';
 export 'openai/widgets/auto_completion_node_widget.dart';
 export 'openai/widgets/smart_edit_node_widget.dart';

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

@@ -132,7 +132,7 @@ class _ToggleListBlockComponentWidgetState
   EdgeInsets get indentPadding => configuration.indentPadding(
         node,
         calculateTextDirection(
-          defaultTextDirection: Directionality.maybeOf(context),
+          layoutDirection: Directionality.maybeOf(context),
         ),
       );
 
@@ -148,7 +148,7 @@ class _ToggleListBlockComponentWidgetState
   @override
   Widget buildComponent(BuildContext context) {
     final textDirection = calculateTextDirection(
-      defaultTextDirection: Directionality.maybeOf(context),
+      layoutDirection: Directionality.maybeOf(context),
     );
 
     Widget child = Container(

+ 2 - 2
frontend/appflowy_flutter/pubspec.lock

@@ -54,8 +54,8 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: a9af2bb
-      resolved-ref: a9af2bbd373a6a478f1bd63d6037817e81d23de2
+      ref: a912c1c
+      resolved-ref: a912c1c96532ec561ea68d5138aee415fdecede2
       url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
     source: git
     version: "1.2.4"

+ 1 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -48,7 +48,7 @@ dependencies:
   appflowy_editor:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-editor.git
-      ref: a9af2bb
+      ref: a912c1c
   appflowy_popover:
     path: packages/appflowy_popover
 

+ 5 - 0
frontend/resources/flowy_icons/16x/toolbar_align_center.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 4L12 4" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 8H11" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 12L12 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
frontend/resources/flowy_icons/16x/toolbar_align_left.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 4L12 4" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 8H10" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 12L12 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
frontend/resources/flowy_icons/16x/toolbar_align_right.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 4L12 4" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8H12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 12L12 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>