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 54b45d0397..822fe4d354 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -130,8 +130,8 @@ class _AppFlowyEditorPageState extends State { final List toolbarItems = [ smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, - ...headingItems - ..forEach((e) => e.isActive = onlyShowInSingleSelectionAndTextType), + headingsToolbarItem + ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, ...markdownFormatItems..forEach((e) => e.isActive = showInAnyTextType), quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, bulletedListItem 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 index 4fd81395e9..8d15c0e6be 100644 --- 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 @@ -94,9 +94,9 @@ class _AlignmentButtonsState extends State<_AlignmentButtons> { windowPadding: const EdgeInsets.all(0), margin: const EdgeInsets.symmetric(vertical: 2.0), direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 12), + offset: const Offset(0, 10), decorationColor: Theme.of(context).colorScheme.onTertiary, - borderRadius: const BorderRadius.all(Radius.circular(4)), + borderRadius: BorderRadius.circular(6.0), popupBuilder: (_) { keepEditorFocusNotifier.increase(); return _AlignButtons(onAlignChanged: widget.onAlignChanged); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart new file mode 100644 index 0000000000..b21955f357 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart @@ -0,0 +1,207 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +final _headingData = [ + (FlowySvgs.h1_s, LocaleKeys.editor_heading1.tr()), + (FlowySvgs.h2_s, LocaleKeys.editor_heading2.tr()), + (FlowySvgs.h3_s, LocaleKeys.editor_heading3.tr()), +]; + +final headingsToolbarItem = ToolbarItem( + id: 'editor.headings', + group: 1, + isActive: onlyShowInTextType, + builder: (context, editorState, highlightColor, _, __) { + final selection = editorState.selection!; + final node = editorState.getNodeAtPath(selection.start.path)!; + final delta = (node.delta ?? Delta()).toJson(); + int level = node.attributes[HeadingBlockKeys.level] ?? 1; + final isHighlight = + node.type == HeadingBlockKeys.type && (level >= 1 && level <= 3); + // only supports the level 1 - 3 in the toolbar, ignore the other levels + level = level.clamp(1, 3); + + final svg = _headingData[level - 1].$1; + final message = _headingData[level - 1].$2; + + final child = FlowyTooltip( + message: message, + preferBelow: false, + child: Row( + children: [ + FlowySvg( + svg, + size: const Size.square(18), + color: isHighlight ? highlightColor : Colors.white, + ), + const HSpace(2.0), + const FlowySvg( + FlowySvgs.arrow_down_s, + size: Size.square(12), + color: Colors.grey, + ), + ], + ), + ); + return _HeadingPopup( + currentLevel: isHighlight ? level : -1, + highlightColor: highlightColor, + child: child, + onLevelChanged: (level) async { + await editorState.formatNode( + selection, + (node) => node.copyWith( + type: isHighlight ? ParagraphBlockKeys.type : HeadingBlockKeys.type, + attributes: { + HeadingBlockKeys.level: level, + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + blockComponentTextDirection: + node.attributes[blockComponentTextDirection], + blockComponentDelta: delta, + }, + ), + ); + }, + ); + }, +); + +class _HeadingPopup extends StatelessWidget { + const _HeadingPopup({ + required this.currentLevel, + required this.highlightColor, + required this.onLevelChanged, + required this.child, + }); + + final int currentLevel; + final Color highlightColor; + final Function(int level) onLevelChanged; + final Widget child; + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + windowPadding: const EdgeInsets.all(0), + margin: const EdgeInsets.symmetric(vertical: 2.0), + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 10), + decorationColor: Theme.of(context).colorScheme.onTertiary, + borderRadius: BorderRadius.circular(6.0), + popupBuilder: (_) { + keepEditorFocusNotifier.increase(); + return _HeadingButtons( + currentLevel: currentLevel, + highlightColor: highlightColor, + onLevelChanged: onLevelChanged, + ); + }, + onClose: () { + keepEditorFocusNotifier.decrease(); + }, + child: FlowyButton( + useIntrinsicWidth: true, + hoverColor: Colors.grey.withOpacity(0.3), + text: child, + ), + ); + } +} + +class _HeadingButtons extends StatelessWidget { + const _HeadingButtons({ + required this.highlightColor, + required this.currentLevel, + required this.onLevelChanged, + }); + + final int currentLevel; + final Color highlightColor; + final Function(int level) onLevelChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(4), + ..._headingData.mapIndexed((index, data) { + final svg = data.$1; + final message = data.$2; + return [ + _HeadingButton( + icon: svg, + tooltip: message, + onTap: () => onLevelChanged(index + 1), + isHighlight: index + 1 == currentLevel, + highlightColor: highlightColor, + ), + index != _headingData.length - 1 + ? const _Divider() + : const SizedBox.shrink(), + ]; + }).flattened, + const HSpace(4), + ], + ), + ); + } +} + +class _HeadingButton extends StatelessWidget { + const _HeadingButton({ + required this.icon, + required this.tooltip, + required this.onTap, + required this.highlightColor, + required this.isHighlight, + }); + + final Color highlightColor; + final FlowySvgData icon; + final String tooltip; + final VoidCallback onTap; + final bool isHighlight; + + @override + Widget build(BuildContext context) { + return FlowyButton( + useIntrinsicWidth: true, + hoverColor: Colors.grey.withOpacity(0.3), + onTap: onTap, + text: FlowyTooltip( + message: tooltip, + preferBelow: true, + child: FlowySvg( + icon, + size: const Size.square(18), + color: isHighlight ? highlightColor : Colors.white, + ), + ), + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4), + child: Container( + width: 1, + color: Colors.grey, + ), + ); + } +} 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 a4ba3e64aa..ef877d61ab 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 @@ -14,15 +14,17 @@ export 'database/inline_database_menu_item.dart'; export 'database/referenced_database_menu_item.dart'; export 'error/error_block_component_builder.dart'; export 'extensions/flowy_tint_extension.dart'; +export 'file/file_block.dart'; export 'find_and_replace/find_and_replace_menu.dart'; export 'font/customize_font_toolbar_item.dart'; export 'header/cover_editor_bloc.dart'; export 'header/custom_cover_picker.dart'; export 'header/document_header_node_widget.dart'; +export 'heading/heading_toolbar_item.dart'; export 'image/custom_image_block_component/image_menu.dart'; -export 'image/multi_image_block_component/multi_image_menu.dart'; export 'image/image_selection_menu.dart'; export 'image/mobile_image_toolbar_item.dart'; +export 'image/multi_image_block_component/multi_image_menu.dart'; export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'link_preview/custom_link_preview.dart'; @@ -53,4 +55,3 @@ export 'table/table_option_action.dart'; export 'todo_list/todo_list_icon.dart'; export 'toggle/toggle_block_component.dart'; export 'toggle/toggle_block_shortcut_event.dart'; -export 'file/file_block.dart'; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 84efac4f50..fc7a0156d1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1479,7 +1479,7 @@ "autoGeneratorRewrite": "Rewrite", "smartEdit": "AI Assistants", "aI": "AI", - "smartEditFixSpelling": "Fix spelling", + "smartEditFixSpelling": "Fix spelling & grammar", "warning": "⚠️ AI responses can be inaccurate or misleading.", "smartEditSummarize": "Summarize", "smartEditImproveWriting": "Improve writing", @@ -2376,4 +2376,4 @@ "commentAddedSuccessfully": "Comment added successfully.", "commentAddedSuccessTip": "You've just added or replied to a comment. Would you like to jump to the top to see the latest comments?" } -} \ No newline at end of file +}