Browse Source

feat: customize text font (#3467)

* feat: update UI in settings page

* feat: customzing font in document page

* fix: flutter analyze and format issues
Lucas.Xu 1 year ago
parent
commit
9c59e1487e
26 changed files with 334 additions and 197 deletions
  1. 2 1
      frontend/appflowy_flutter/integration_test/appearance_settings_test.dart
  2. 8 3
      frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart
  3. 4 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  4. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  5. 1 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart
  6. 1 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart
  7. 1 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart
  8. 20 20
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/config.dart
  9. 42 31
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart
  10. 50 24
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart
  11. 0 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart
  12. 43 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart
  13. 1 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart
  14. 1 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart
  15. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart
  16. 1 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart
  17. 10 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart
  18. 7 0
      frontend/appflowy_flutter/lib/util/google_font_family_extension.dart
  19. 0 4
      frontend/appflowy_flutter/lib/workspace/application/appearance.dart
  20. 106 64
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart
  21. 12 5
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart
  22. 12 10
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart
  23. 3 3
      frontend/appflowy_flutter/pubspec.lock
  24. 1 1
      frontend/appflowy_flutter/pubspec.yaml
  25. 3 0
      frontend/resources/flowy_icons/16x/font_family.svg
  26. 2 2
      frontend/resources/translations/en.json

+ 2 - 1
frontend/appflowy_flutter/integration_test/appearance_settings_test.dart

@@ -80,7 +80,8 @@ void main() {
       await tester.openSettingsPage(SettingsPage.files);
       await tester.openSettingsPage(SettingsPage.files);
       await tester.openSettingsPage(SettingsPage.appearance);
       await tester.openSettingsPage(SettingsPage.appearance);
 
 
-      expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
+      expect(
+        find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
         findsOneWidget,
         findsOneWidget,
       );
       );
     });
     });

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

@@ -12,8 +12,11 @@ class MyMockClient extends Mock implements http.Client {
     final requestType = request.method;
     final requestType = request.method;
     final requestUri = request.url;
     final requestUri = request.url;
 
 
-    if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
-      final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
+    if (requestType == 'POST' &&
+        requestUri == OpenAIRequestType.textCompletion.uri) {
+      final responseHeaders = <String, String>{
+        'content-type': 'text/event-stream'
+      };
       final responseBody = Stream.fromIterable([
       final responseBody = Stream.fromIterable([
         utf8.encode(
         utf8.encode(
           '{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
           '{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
@@ -51,7 +54,9 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
 
 
     var previousSyntax = '';
     var previousSyntax = '';
     if (response.statusCode == 200) {
     if (response.statusCode == 200) {
-      await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
+      await for (final chunk in response.stream
+          .transform(const Utf8Decoder())
+          .transform(const LineSplitter())) {
         await onStart();
         await onStart();
         final data = chunk.trim().split('data: ');
         final data = chunk.trim().split('data: ');
         if (data[0] != '[DONE]') {
         if (data[0] != '[DONE]') {

+ 4 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -382,8 +382,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
     case FieldType.Checkbox:
     case FieldType.Checkbox:
       final group = customData.asCheckboxGroup()!;
       final group = customData.asCheckboxGroup()!;
       if (group.isCheck) {
       if (group.isCheck) {
-        widget =
-            const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
+        widget = const FlowySvg(
+          FlowySvgs.check_filled_s,
+          blendMode: BlendMode.dst,
+        );
       } else {
       } else {
         widget = const FlowySvg(FlowySvgs.uncheck_s);
         widget = const FlowySvg(FlowySvgs.uncheck_s);
       }
       }

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

@@ -75,8 +75,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
     alignToolbarItem,
     alignToolbarItem,
     buildTextColorItem(),
     buildTextColorItem(),
     buildHighlightColorItem(),
     buildHighlightColorItem(),
-    // TODO: enable it in version 0.3.3
-    // ...textDirectionItems,
+    customizeFontToolbarItem,
+    ...textDirectionItems,
   ];
   ];
 
 
   late final List<SelectionMenuItem> slashMenuItems;
   late final List<SelectionMenuItem> slashMenuItems;

+ 1 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart

@@ -61,13 +61,10 @@ SelectionMenuItem calloutItem = SelectionMenuItem.node(
 // building the callout block widget
 // building the callout block widget
 class CalloutBlockComponentBuilder extends BlockComponentBuilder {
 class CalloutBlockComponentBuilder extends BlockComponentBuilder {
   CalloutBlockComponentBuilder({
   CalloutBlockComponentBuilder({
-    this.configuration = const BlockComponentConfiguration(),
+    super.configuration,
     required this.defaultColor,
     required this.defaultColor,
   });
   });
 
 
-  @override
-  final BlockComponentConfiguration configuration;
-
   final Color defaultColor;
   final Color defaultColor;
 
 
   @override
   @override

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

@@ -51,13 +51,10 @@ SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
 
 
 class CodeBlockComponentBuilder extends BlockComponentBuilder {
 class CodeBlockComponentBuilder extends BlockComponentBuilder {
   CodeBlockComponentBuilder({
   CodeBlockComponentBuilder({
-    this.configuration = const BlockComponentConfiguration(),
+    super.configuration,
     this.padding = const EdgeInsets.all(0),
     this.padding = const EdgeInsets.all(0),
   });
   });
 
 
-  @override
-  final BlockComponentConfiguration configuration;
-
   final EdgeInsets padding;
   final EdgeInsets padding;
 
 
   @override
   @override

+ 1 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart

@@ -17,12 +17,9 @@ class DatabaseBlockKeys {
 
 
 class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder {
 class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder {
   DatabaseViewBlockComponentBuilder({
   DatabaseViewBlockComponentBuilder({
-    this.configuration = const BlockComponentConfiguration(),
+    super.configuration,
   });
   });
 
 
-  @override
-  final BlockComponentConfiguration configuration;
-
   @override
   @override
   BlockComponentWidget build(BlockComponentContext blockComponentContext) {
   BlockComponentWidget build(BlockComponentContext blockComponentContext) {
     final node = blockComponentContext.node;
     final node = blockComponentContext.node;

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

@@ -8,26 +8,26 @@ import 'emoji_picker.dart';
 /// Config for customizations
 /// Config for customizations
 class Config {
 class Config {
   /// Constructor
   /// Constructor
-  const Config(
-      {this.columns = 7,
-      this.emojiSizeMax = 32.0,
-      this.verticalSpacing = 0,
-      this.horizontalSpacing = 0,
-      this.initCategory = Category.RECENT,
-      this.bgColor = const Color(0xFFEBEFF2),
-      this.indicatorColor = Colors.blue,
-      this.iconColor = Colors.grey,
-      this.iconColorSelected = Colors.blue,
-      this.progressIndicatorColor = Colors.blue,
-      this.backspaceColor = Colors.blue,
-      this.showRecentsTab = true,
-      this.recentsLimit = 28,
-      this.noRecentsText = 'No Recents',
-      this.noRecentsStyle =
-          const TextStyle(fontSize: 20, color: Colors.black26),
-      this.tabIndicatorAnimDuration = kTabScrollDuration,
-      this.categoryIcons = const CategoryIcons(),
-      this.buttonMode = ButtonMode.MATERIAL,});
+  const Config({
+    this.columns = 7,
+    this.emojiSizeMax = 32.0,
+    this.verticalSpacing = 0,
+    this.horizontalSpacing = 0,
+    this.initCategory = Category.RECENT,
+    this.bgColor = const Color(0xFFEBEFF2),
+    this.indicatorColor = Colors.blue,
+    this.iconColor = Colors.grey,
+    this.iconColorSelected = Colors.blue,
+    this.progressIndicatorColor = Colors.blue,
+    this.backspaceColor = Colors.blue,
+    this.showRecentsTab = true,
+    this.recentsLimit = 28,
+    this.noRecentsText = 'No Recents',
+    this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26),
+    this.tabIndicatorAnimDuration = kTabScrollDuration,
+    this.categoryIcons = const CategoryIcons(),
+    this.buttonMode = ButtonMode.MATERIAL,
+  });
 
 
   /// Number of emojis per row
   /// Number of emojis per row
   final int columns;
   final int columns;

+ 42 - 31
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart

@@ -27,14 +27,16 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
   @override
   @override
   void initState() {
   void initState() {
     var initCategory = widget.state.categoryEmoji.indexWhere(
     var initCategory = widget.state.categoryEmoji.indexWhere(
-        (element) => element.category == widget.config.initCategory,);
+      (element) => element.category == widget.config.initCategory,
+    );
     if (initCategory == -1) {
     if (initCategory == -1) {
       initCategory = 0;
       initCategory = 0;
     }
     }
     _tabController = TabController(
     _tabController = TabController(
-        initialIndex: initCategory,
-        length: widget.state.categoryEmoji.length,
-        vsync: this,);
+      initialIndex: initCategory,
+      length: widget.state.categoryEmoji.length,
+      vsync: this,
+    );
     _pageController = PageController(initialPage: initCategory);
     _pageController = PageController(initialPage: initCategory);
     _emojiFocusNode.requestFocus();
     _emojiFocusNode.requestFocus();
 
 
@@ -72,14 +74,15 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
       return Material(
       return Material(
         type: MaterialType.transparency,
         type: MaterialType.transparency,
         child: IconButton(
         child: IconButton(
-            padding: const EdgeInsets.only(bottom: 2),
-            icon: Icon(
-              Icons.backspace,
-              color: widget.config.backspaceColor,
-            ),
-            onPressed: () {
-              widget.state.onBackspacePressed!();
-            },),
+          padding: const EdgeInsets.only(bottom: 2),
+          icon: Icon(
+            Icons.backspace,
+            color: widget.config.backspaceColor,
+          ),
+          onPressed: () {
+            widget.state.onBackspacePressed!();
+          },
+        ),
       );
       );
     }
     }
     return Container();
     return Container();
@@ -160,8 +163,12 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
                           : widget.state.categoryEmoji
                           : widget.state.categoryEmoji
                               .asMap()
                               .asMap()
                               .entries
                               .entries
-                              .map<Widget>((item) => _buildCategory(
-                                  item.value.category, emojiSize,),)
+                              .map<Widget>(
+                                (item) => _buildCategory(
+                                  item.value.category,
+                                  emojiSize,
+                                ),
+                              )
                               .toList(),
                               .toList(),
                     ),
                     ),
                   ),
                   ),
@@ -206,8 +213,10 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
     );
     );
   }
   }
 
 
-  Widget _buildButtonWidget(
-      {required VoidCallback onPressed, required Widget child,}) {
+  Widget _buildButtonWidget({
+    required VoidCallback onPressed,
+    required Widget child,
+  }) {
     if (widget.config.buttonMode == ButtonMode.MATERIAL) {
     if (widget.config.buttonMode == ButtonMode.MATERIAL) {
       return InkWell(
       return InkWell(
         onTap: onPressed,
         onTap: onPressed,
@@ -266,29 +275,31 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
     Emoji emoji,
     Emoji emoji,
   ) {
   ) {
     return _buildButtonWidget(
     return _buildButtonWidget(
-        onPressed: () {
-          widget.state.onEmojiSelected(categoryEmoji.category, emoji);
-        },
-        child: FittedBox(
-          fit: BoxFit.scaleDown,
-          child: Text(
-            emoji.emoji,
-            textScaleFactor: 1.0,
-            style: TextStyle(
-              fontSize: emojiSize,
-              backgroundColor: Colors.transparent,
-            ),
+      onPressed: () {
+        widget.state.onEmojiSelected(categoryEmoji.category, emoji);
+      },
+      child: FittedBox(
+        fit: BoxFit.scaleDown,
+        child: Text(
+          emoji.emoji,
+          textScaleFactor: 1.0,
+          style: TextStyle(
+            fontSize: emojiSize,
+            backgroundColor: Colors.transparent,
           ),
           ),
-        ),);
+        ),
+      ),
+    );
   }
   }
 
 
   Widget _buildNoRecent() {
   Widget _buildNoRecent() {
     return Center(
     return Center(
-        child: FlowyText.regular(
+      child: FlowyText.regular(
         widget.config.noRecentsText,
         widget.config.noRecentsText,
         color: Theme.of(context).colorScheme.tertiary.withAlpha(77),
         color: Theme.of(context).colorScheme.tertiary.withAlpha(77),
         fontSize: widget.config.noRecentsStyle.fontSize,
         fontSize: widget.config.noRecentsStyle.fontSize,
         textAlign: TextAlign.center,
         textAlign: TextAlign.center,
-      ),);
+      ),
+    );
   }
   }
 }
 }

+ 50 - 24
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart

@@ -190,30 +190,49 @@ class EmojiPickerState extends State<EmojiPicker> {
       categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
       categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
     }
     }
     categoryEmoji.addAll([
     categoryEmoji.addAll([
-      CategoryEmoji(Category.SMILEYS,
-          await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),),
-      CategoryEmoji(Category.ANIMALS,
-          await _getAvailableEmojis(emoji_list.animals, title: 'animals'),),
-      CategoryEmoji(Category.FOODS,
-          await _getAvailableEmojis(emoji_list.foods, title: 'foods'),),
       CategoryEmoji(
       CategoryEmoji(
-          Category.ACTIVITIES,
-          await _getAvailableEmojis(emoji_list.activities,
-              title: 'activities',),),
-      CategoryEmoji(Category.TRAVEL,
-          await _getAvailableEmojis(emoji_list.travel, title: 'travel'),),
-      CategoryEmoji(Category.OBJECTS,
-          await _getAvailableEmojis(emoji_list.objects, title: 'objects'),),
-      CategoryEmoji(Category.SYMBOLS,
-          await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),),
-      CategoryEmoji(Category.FLAGS,
-          await _getAvailableEmojis(emoji_list.flags, title: 'flags'),)
+        Category.SMILEYS,
+        await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),
+      ),
+      CategoryEmoji(
+        Category.ANIMALS,
+        await _getAvailableEmojis(emoji_list.animals, title: 'animals'),
+      ),
+      CategoryEmoji(
+        Category.FOODS,
+        await _getAvailableEmojis(emoji_list.foods, title: 'foods'),
+      ),
+      CategoryEmoji(
+        Category.ACTIVITIES,
+        await _getAvailableEmojis(
+          emoji_list.activities,
+          title: 'activities',
+        ),
+      ),
+      CategoryEmoji(
+        Category.TRAVEL,
+        await _getAvailableEmojis(emoji_list.travel, title: 'travel'),
+      ),
+      CategoryEmoji(
+        Category.OBJECTS,
+        await _getAvailableEmojis(emoji_list.objects, title: 'objects'),
+      ),
+      CategoryEmoji(
+        Category.SYMBOLS,
+        await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),
+      ),
+      CategoryEmoji(
+        Category.FLAGS,
+        await _getAvailableEmojis(emoji_list.flags, title: 'flags'),
+      )
     ]);
     ]);
   }
   }
 
 
   // Get available emoji for given category title
   // Get available emoji for given category title
-  Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map,
-      {required String title,}) async {
+  Future<List<Emoji>> _getAvailableEmojis(
+    Map<String, String> map, {
+    required String title,
+  }) async {
     Map<String, String>? newMap;
     Map<String, String>? newMap;
 
 
     // Get Emojis cached locally if available
     // Get Emojis cached locally if available
@@ -236,15 +255,18 @@ class EmojiPickerState extends State<EmojiPicker> {
 
 
   // Check if emoji is available on current platform
   // Check if emoji is available on current platform
   Future<Map<String, String>?> _getPlatformAvailableEmoji(
   Future<Map<String, String>?> _getPlatformAvailableEmoji(
-      Map<String, String> emoji,) async {
+    Map<String, String> emoji,
+  ) async {
     if (Platform.isAndroid) {
     if (Platform.isAndroid) {
       Map<String, String>? filtered = {};
       Map<String, String>? filtered = {};
       const delimiter = '|';
       const delimiter = '|';
       try {
       try {
         final entries = emoji.values.join(delimiter);
         final entries = emoji.values.join(delimiter);
         final keys = emoji.keys.join(delimiter);
         final keys = emoji.keys.join(delimiter);
-        final result = (await platform.invokeMethod<String>('checkAvailability',
-            {'emojiKeys': keys, 'emojiEntries': entries},)) as String;
+        final result = (await platform.invokeMethod<String>(
+          'checkAvailability',
+          {'emojiKeys': keys, 'emojiEntries': entries},
+        )) as String;
         final resultKeys = result.split(delimiter);
         final resultKeys = result.split(delimiter);
         for (var i = 0; i < resultKeys.length; i++) {
         for (var i = 0; i < resultKeys.length; i++) {
           filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
           filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
@@ -272,7 +294,9 @@ class EmojiPickerState extends State<EmojiPicker> {
 
 
   // Stores filtered emoji locally for faster access next time
   // Stores filtered emoji locally for faster access next time
   Future<void> _cacheFilteredEmojis(
   Future<void> _cacheFilteredEmojis(
-      String title, Map<String, String> emojis,) async {
+    String title,
+    Map<String, String> emojis,
+  ) async {
     final prefs = await SharedPreferences.getInstance();
     final prefs = await SharedPreferences.getInstance();
     final emojiJson = jsonEncode(emojis);
     final emojiJson = jsonEncode(emojis);
     prefs.setString(title, emojiJson);
     prefs.setString(title, emojiJson);
@@ -305,7 +329,9 @@ class EmojiPickerState extends State<EmojiPicker> {
     recentEmoji.sort((a, b) => b.counter - a.counter);
     recentEmoji.sort((a, b) => b.counter - a.counter);
     // Limit entries to recentsLimit
     // Limit entries to recentsLimit
     recentEmoji = recentEmoji.sublist(
     recentEmoji = recentEmoji.sublist(
-        0, min(widget.config.recentsLimit, recentEmoji.length),);
+      0,
+      min(widget.config.recentsLimit, recentEmoji.length),
+    );
     // save locally
     // save locally
     prefs.setString('recent', jsonEncode(recentEmoji));
     prefs.setString('recent', jsonEncode(recentEmoji));
   }
   }

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

@@ -1,4 +1,3 @@
-
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';

+ 43 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart

@@ -0,0 +1,43 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flutter/material.dart';
+
+final customizeFontToolbarItem = ToolbarItem(
+  id: 'editor.font',
+  group: 4,
+  isActive: onlyShowInTextType,
+  builder: (context, editorState, highlightColor) {
+    final selection = editorState.selection!;
+    final popoverController = PopoverController();
+    return MouseRegion(
+      cursor: SystemMouseCursors.click,
+      child: FontFamilyDropDown(
+        currentFontFamily: '',
+        popoverController: popoverController,
+        onOpen: () => keepEditorFocusNotifier.value += 1,
+        onClose: () => keepEditorFocusNotifier.value -= 1,
+        onFontFamilyChanged: (fontFamily) async {
+          await popoverController.close();
+          try {
+            await editorState.formatDelta(selection, {
+              AppFlowyRichTextKeys.fontFamily: fontFamily,
+            });
+          } catch (e) {
+            Log.error('Failed to set font family: $e');
+          }
+        },
+        child: const Padding(
+          padding: EdgeInsets.symmetric(horizontal: 4.0),
+          child: FlowySvg(
+            FlowySvgs.font_family_s,
+            size: Size.square(16.0),
+            color: Colors.white,
+          ),
+        ),
+      ),
+    );
+  },
+);

+ 1 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart

@@ -54,12 +54,9 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node(
 
 
 class MathEquationBlockComponentBuilder extends BlockComponentBuilder {
 class MathEquationBlockComponentBuilder extends BlockComponentBuilder {
   MathEquationBlockComponentBuilder({
   MathEquationBlockComponentBuilder({
-    this.configuration = const BlockComponentConfiguration(),
+    super.configuration,
   });
   });
 
 
-  @override
-  final BlockComponentConfiguration configuration;
-
   @override
   @override
   BlockComponentWidget build(BlockComponentContext blockComponentContext) {
   BlockComponentWidget build(BlockComponentContext blockComponentContext) {
     final node = blockComponentContext.node;
     final node = blockComponentContext.node;

+ 1 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart

@@ -32,12 +32,9 @@ Node outlineBlockNode() {
 
 
 class OutlineBlockComponentBuilder extends BlockComponentBuilder {
 class OutlineBlockComponentBuilder extends BlockComponentBuilder {
   OutlineBlockComponentBuilder({
   OutlineBlockComponentBuilder({
-    this.configuration = const BlockComponentConfiguration(),
+    super.configuration,
   });
   });
 
 
-  @override
-  final BlockComponentConfiguration configuration;
-
   @override
   @override
   BlockComponentWidget build(BlockComponentContext blockComponentContext) {
   BlockComponentWidget build(BlockComponentContext blockComponentContext) {
     final node = blockComponentContext.node;
     final node = blockComponentContext.node;

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

@@ -14,6 +14,7 @@ export 'database/inline_database_menu_item.dart';
 export 'database/referenced_database_menu_item.dart';
 export 'database/referenced_database_menu_item.dart';
 export 'emoji_picker/emoji_menu_item.dart';
 export 'emoji_picker/emoji_menu_item.dart';
 export 'extensions/flowy_tint_extension.dart';
 export 'extensions/flowy_tint_extension.dart';
+export 'font/customize_font_toolbar_item.dart';
 export 'header/cover_editor_bloc.dart';
 export 'header/cover_editor_bloc.dart';
 export 'header/custom_cover_picker.dart';
 export 'header/custom_cover_picker.dart';
 export 'header/document_header_node_widget.dart';
 export 'header/document_header_node_widget.dart';

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

@@ -55,13 +55,10 @@ SelectionMenuItem toggleListBlockItem = SelectionMenuItem.node(
 
 
 class ToggleListBlockComponentBuilder extends BlockComponentBuilder {
 class ToggleListBlockComponentBuilder extends BlockComponentBuilder {
   ToggleListBlockComponentBuilder({
   ToggleListBlockComponentBuilder({
-    this.configuration = const BlockComponentConfiguration(),
+    super.configuration,
     this.padding = const EdgeInsets.all(0),
     this.padding = const EdgeInsets.all(0),
   });
   });
 
 
-  @override
-  final BlockComponentConfiguration configuration;
-
   final EdgeInsets padding;
   final EdgeInsets padding;
 
 
   @override
   @override

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

@@ -2,6 +2,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_mat
 import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy/util/google_font_family_extension.dart';
 import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -193,6 +194,15 @@ class EditorStyleCustomizer {
       return textSpan;
       return textSpan;
     }
     }
 
 
+    // try to refresh font here.
+    if (attributes.fontFamily != null) {
+      try {
+        GoogleFonts.getFont(attributes.fontFamily!.parseFontFamilyName());
+      } catch (e) {
+        // ignore
+      }
+    }
+
     // customize the inline mention block, like inline page
     // customize the inline mention block, like inline page
     final mention = attributes[MentionBlockKeys.mention] as Map?;
     final mention = attributes[MentionBlockKeys.mention] as Map?;
     if (mention != null) {
     if (mention != null) {

+ 7 - 0
frontend/appflowy_flutter/lib/util/google_font_family_extension.dart

@@ -0,0 +1,7 @@
+extension GoogleFontsParser on String {
+  String parseFontFamilyName() {
+    final camelCase = RegExp('(?<=[a-z])[A-Z]');
+    return replaceAll('_regular', '')
+        .replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
+  }
+}

+ 0 - 4
frontend/appflowy_flutter/lib/workspace/application/appearance.dart

@@ -300,10 +300,6 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
   ThemeData get lightTheme => _getThemeData(Brightness.light);
   ThemeData get lightTheme => _getThemeData(Brightness.light);
   ThemeData get darkTheme => _getThemeData(Brightness.dark);
   ThemeData get darkTheme => _getThemeData(Brightness.dark);
 
 
-  // only support LTR layout in version 0.3.2, enable it in version 0.3.3
-  LayoutDirectionPB get layoutDirectionPB => LayoutDirectionPB.LTRLayout;
-  TextDirectionPB get textDirectionPB => TextDirectionPB.LTR;
-
   ThemeData _getThemeData(Brightness brightness) {
   ThemeData _getThemeData(Brightness brightness) {
     // Poppins and SF Mono are not well supported in some languages, so use the
     // Poppins and SF Mono are not well supported in some languages, so use the
     // built-in font for the following languages.
     // built-in font for the following languages.

+ 106 - 64
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart

@@ -1,8 +1,10 @@
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy/util/google_font_family_extension.dart';
 import 'package:appflowy/workspace/application/appearance.dart';
 import 'package:appflowy/workspace/application/appearance.dart';
 import 'package:appflowy/workspace/application/appearance_defaults.dart';
 import 'package:appflowy/workspace/application/appearance_defaults.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -29,9 +31,6 @@ class ThemeFontFamilySetting extends StatefulWidget {
 }
 }
 
 
 class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
 class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
-  final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
-  final ValueNotifier<String> query = ValueNotifier('');
-
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return ThemeSettingEntryTemplateWidget(
     return ThemeSettingEntryTemplateWidget(
@@ -44,61 +43,101 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
             .syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
             .syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
       },
       },
       trailing: [
       trailing: [
-        ThemeValueDropDown(
-          popoverKey: ThemeFontFamilySetting.popoverKey,
-          currentValue: parseFontFamilyName(widget.currentFontFamily),
-          onClose: () {
-            query.value = '';
-          },
-          popupBuilder: (_) => CustomScrollView(
-            shrinkWrap: true,
-            slivers: [
-              SliverPadding(
-                padding: const EdgeInsets.only(right: 8),
-                sliver: SliverToBoxAdapter(
-                  child: FlowyTextField(
-                    key: ThemeFontFamilySetting.textFieldKey,
-                    hintText:
-                        LocaleKeys.settings_appearance_fontFamily_search.tr(),
-                    autoFocus: false,
-                    debounceDuration: const Duration(milliseconds: 300),
-                    onChanged: (value) {
-                      query.value = value;
-                    },
-                  ),
+        FontFamilyDropDown(
+          currentFontFamily: widget.currentFontFamily,
+        )
+      ],
+    );
+  }
+}
+
+class FontFamilyDropDown extends StatefulWidget {
+  const FontFamilyDropDown({
+    super.key,
+    required this.currentFontFamily,
+    this.onOpen,
+    this.onClose,
+    this.onFontFamilyChanged,
+    this.child,
+    this.popoverController,
+  });
+
+  final String currentFontFamily;
+  final VoidCallback? onOpen;
+  final VoidCallback? onClose;
+  final void Function(String fontFamily)? onFontFamilyChanged;
+  final Widget? child;
+  final PopoverController? popoverController;
+
+  @override
+  State<FontFamilyDropDown> createState() => _FontFamilyDropDownState();
+}
+
+class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
+  final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
+  final ValueNotifier<String> query = ValueNotifier('');
+
+  @override
+  Widget build(BuildContext context) {
+    return ThemeValueDropDown(
+      popoverKey: ThemeFontFamilySetting.popoverKey,
+      popoverController: widget.popoverController,
+      currentValue: parseFontFamilyName(widget.currentFontFamily),
+      onClose: () {
+        query.value = '';
+        widget.onClose?.call();
+      },
+      child: widget.child,
+      popupBuilder: (_) {
+        widget.onOpen?.call();
+        return CustomScrollView(
+          shrinkWrap: true,
+          slivers: [
+            SliverPadding(
+              padding: const EdgeInsets.only(right: 8),
+              sliver: SliverToBoxAdapter(
+                child: FlowyTextField(
+                  key: ThemeFontFamilySetting.textFieldKey,
+                  hintText:
+                      LocaleKeys.settings_appearance_fontFamily_search.tr(),
+                  autoFocus: false,
+                  debounceDuration: const Duration(milliseconds: 300),
+                  onChanged: (value) {
+                    query.value = value;
+                  },
                 ),
                 ),
               ),
               ),
-              const SliverToBoxAdapter(
-                child: SizedBox(height: 4),
-              ),
-              ValueListenableBuilder(
-                valueListenable: query,
-                builder: (context, value, child) {
-                  var displayed = availableFonts;
-                  if (value.isNotEmpty) {
-                    displayed = availableFonts
-                        .where(
-                          (font) => font
-                              .toLowerCase()
-                              .contains(value.toLowerCase().toString()),
-                        )
-                        .sorted((a, b) => levenshtein(a, b))
-                        .toList();
-                  }
-                  return SliverFixedExtentList.builder(
-                    itemBuilder: (context, index) => _fontFamilyItemButton(
-                      context,
-                      GoogleFonts.getFont(displayed[index]),
-                    ),
-                    itemCount: displayed.length,
-                    itemExtent: 32,
-                  );
-                },
-              ),
-            ],
-          ),
-        ),
-      ],
+            ),
+            const SliverToBoxAdapter(
+              child: SizedBox(height: 4),
+            ),
+            ValueListenableBuilder(
+              valueListenable: query,
+              builder: (context, value, child) {
+                var displayed = availableFonts;
+                if (value.isNotEmpty) {
+                  displayed = availableFonts
+                      .where(
+                        (font) => font
+                            .toLowerCase()
+                            .contains(value.toLowerCase().toString()),
+                      )
+                      .sorted((a, b) => levenshtein(a, b))
+                      .toList();
+                }
+                return SliverFixedExtentList.builder(
+                  itemBuilder: (context, index) => _fontFamilyItemButton(
+                    context,
+                    GoogleFonts.getFont(displayed[index]),
+                  ),
+                  itemCount: displayed.length,
+                  itemExtent: 32,
+                );
+              },
+            ),
+          ],
+        );
+      },
     );
     );
   }
   }
 
 
@@ -128,14 +167,17 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
                   )
                   )
                 : null,
                 : null,
         onTap: () {
         onTap: () {
-          if (parseFontFamilyName(widget.currentFontFamily) !=
-              buttonFontFamily) {
-            context
-                .read<AppearanceSettingsCubit>()
-                .setFontFamily(parseFontFamilyName(style.fontFamily!));
-            context
-                .read<DocumentAppearanceCubit>()
-                .syncFontFamily(parseFontFamilyName(style.fontFamily!));
+          if (widget.onFontFamilyChanged != null) {
+            widget.onFontFamilyChanged!(style.fontFamily!);
+          } else {
+            final fontFamily = style.fontFamily!.parseFontFamilyName();
+            if (parseFontFamilyName(widget.currentFontFamily) !=
+                buttonFontFamily) {
+              context.read<AppearanceSettingsCubit>().setFontFamily(fontFamily);
+              context
+                  .read<DocumentAppearanceCubit>()
+                  .syncFontFamily(fontFamily);
+            }
           }
           }
         },
         },
       ),
       ),

+ 12 - 5
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart

@@ -31,6 +31,7 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
             children: [
             children: [
               FlowyText.medium(
               FlowyText.medium(
                 label,
                 label,
+                fontSize: 14,
                 overflow: TextOverflow.ellipsis,
                 overflow: TextOverflow.ellipsis,
               ),
               ),
               if (hint != null)
               if (hint != null)
@@ -71,12 +72,16 @@ class ThemeValueDropDown extends StatefulWidget {
     required this.popupBuilder,
     required this.popupBuilder,
     this.popoverKey,
     this.popoverKey,
     this.onClose,
     this.onClose,
+    this.child,
+    this.popoverController,
   });
   });
 
 
   final String currentValue;
   final String currentValue;
   final Key? popoverKey;
   final Key? popoverKey;
   final Widget Function(BuildContext) popupBuilder;
   final Widget Function(BuildContext) popupBuilder;
   final void Function()? onClose;
   final void Function()? onClose;
+  final Widget? child;
+  final PopoverController? popoverController;
 
 
   @override
   @override
   State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
   State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
@@ -87,6 +92,7 @@ class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return AppFlowyPopover(
     return AppFlowyPopover(
       key: widget.popoverKey,
       key: widget.popoverKey,
+      controller: widget.popoverController,
       direction: PopoverDirection.bottomWithRightAligned,
       direction: PopoverDirection.bottomWithRightAligned,
       popupBuilder: widget.popupBuilder,
       popupBuilder: widget.popupBuilder,
       constraints: const BoxConstraints(
       constraints: const BoxConstraints(
@@ -95,11 +101,12 @@ class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
         maxHeight: 400,
         maxHeight: 400,
       ),
       ),
       onClose: widget.onClose,
       onClose: widget.onClose,
-      child: FlowyTextButton(
-        widget.currentValue,
-        fontColor: Theme.of(context).colorScheme.onBackground,
-        fillColor: Colors.transparent,
-      ),
+      child: widget.child ??
+          FlowyTextButton(
+            widget.currentValue,
+            fontColor: Theme.of(context).colorScheme.onBackground,
+            fillColor: Colors.transparent,
+          ),
     );
     );
   }
   }
 }
 }

+ 12 - 10
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart

@@ -19,23 +19,25 @@ class SettingsAppearanceView extends StatelessWidget {
             return Column(
             return Column(
               crossAxisAlignment: CrossAxisAlignment.center,
               crossAxisAlignment: CrossAxisAlignment.center,
               children: [
               children: [
-                BrightnessSetting(
-                  currentThemeMode: state.themeMode,
-                ),
                 ColorSchemeSetting(
                 ColorSchemeSetting(
                   currentTheme: state.appTheme.themeName,
                   currentTheme: state.appTheme.themeName,
                   bloc: context.read<DynamicPluginBloc>(),
                   bloc: context.read<DynamicPluginBloc>(),
                 ),
                 ),
+                BrightnessSetting(
+                  currentThemeMode: state.themeMode,
+                ),
+                const Divider(),
                 ThemeFontFamilySetting(
                 ThemeFontFamilySetting(
                   currentFontFamily: state.font,
                   currentFontFamily: state.font,
                 ),
                 ),
-                // TODO: enablt them in version 0.3.3
-                // LayoutDirectionSetting(
-                //   currentLayoutDirection: state.layoutDirection,
-                // ),
-                // TextDirectionSetting(
-                //   currentTextDirection: state.textDirection,
-                // ),
+                const Divider(),
+                LayoutDirectionSetting(
+                  currentLayoutDirection: state.layoutDirection,
+                ),
+                TextDirectionSetting(
+                  currentTextDirection: state.textDirection,
+                ),
+                const Divider(),
                 CreateFileSettings(),
                 CreateFileSettings(),
               ],
               ],
             );
             );

+ 3 - 3
frontend/appflowy_flutter/pubspec.lock

@@ -54,11 +54,11 @@ packages:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
       path: "."
       path: "."
-      ref: "4a87ec4"
-      resolved-ref: "4a87ec4bd440344b8f51dd61ab84e2c68d4196d2"
+      ref: a0ff609
+      resolved-ref: a0ff609cb1ac53e5d167489f43452074860dd80e
       url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
       url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
     source: git
     source: git
-    version: "1.3.0"
+    version: "1.4.0"
   appflowy_popover:
   appflowy_popover:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:

+ 1 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -47,7 +47,7 @@ dependencies:
   appflowy_editor:
   appflowy_editor:
     git:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-editor.git
       url: https://github.com/AppFlowy-IO/appflowy-editor.git
-      ref: 4a87ec4
+      ref: a0ff609
   appflowy_popover:
   appflowy_popover:
     path: packages/appflowy_popover
     path: packages/appflowy_popover
 
 

+ 3 - 0
frontend/resources/flowy_icons/16x/font_family.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
+	<path d="M420-160v-520H200v-120h560v120H540v520H420Z" fill="white" />
+</svg>

+ 2 - 2
frontend/resources/translations/en.json

@@ -265,13 +265,13 @@
       },
       },
       "layoutDirection": {
       "layoutDirection": {
         "label": "Layout Direction",
         "label": "Layout Direction",
-        "hint": "To start aligning elements from left or right of the screen.",
+        "hint": "Control the flow of content on your screen, from left to right or right to left.",
         "ltr": "LTR",
         "ltr": "LTR",
         "rtl": "RTL"
         "rtl": "RTL"
       },
       },
       "textDirection": {
       "textDirection": {
         "label": "Default text direction",
         "label": "Default text direction",
-        "hint": "Default text direction when the text direction is not set on the element.",
+        "hint": "Specify whether text should start from left or right as the default.",
         "ltr": "LTR",
         "ltr": "LTR",
         "rtl": "RTL",
         "rtl": "RTL",
         "auto": "AUTO",
         "auto": "AUTO",