diff --git a/frontend/appflowy_flutter/integration_test/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/document/document_alignment_test.dart new file mode 100644 index 0000000000..1a4752ead8 --- /dev/null +++ b/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'); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart index 30da907992..7f517678a9 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart +++ b/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(); } diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 3e91ea7b61..193d7f88a0 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/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 tapButtonWithFlowySvgData(FlowySvgData svg) async { + final button = find.byWidgetPredicate( + (widget) => widget is FlowySvg && widget.svg.path == svg.path, + ); + await tapButton(button); + } } extension ViewLayoutPBTest on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 1db398d1e7..5b921181d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -102,7 +102,8 @@ class _DocumentPageState extends State { 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), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index a0eb718458..cd13d0e8e8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -66,6 +66,7 @@ class _AppFlowyEditorPageState extends State { numberedListItem, inlineMathEquationItem, linkItem, + alignToolbarItem, buildTextColorItem(), buildHighlightColorItem(), ]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart new file mode 100644 index 0000000000..6d69602851 --- /dev/null +++ b/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, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart index 71e341d460..3c37ff6a89 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index f1218e3f95..465d4b2a20 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index 7ccf822641..ff037f44c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/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( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 24d6076f66..0eefdb01b9 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/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" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index f45a70893b..64a1f97144 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/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 diff --git a/frontend/resources/flowy_icons/16x/toolbar_align_center.svg b/frontend/resources/flowy_icons/16x/toolbar_align_center.svg new file mode 100644 index 0000000000..ae9c2cfd44 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/toolbar_align_center.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/toolbar_align_left.svg b/frontend/resources/flowy_icons/16x/toolbar_align_left.svg new file mode 100644 index 0000000000..b4f2d0101e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/toolbar_align_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/toolbar_align_right.svg b/frontend/resources/flowy_icons/16x/toolbar_align_right.svg new file mode 100644 index 0000000000..86a1facaac --- /dev/null +++ b/frontend/resources/flowy_icons/16x/toolbar_align_right.svg @@ -0,0 +1,5 @@ + + + + +