瀏覽代碼

Merge pull request #1495 from LucasXu0/customize_font_size

Customize Font Size In AppFlowy #1479
Lucas.Xu 2 年之前
父節點
當前提交
fba69767a7

+ 6 - 0
frontend/app_flowy/assets/translations/en.json

@@ -40,6 +40,12 @@
     "markdown": "Markdown",
     "markdown": "Markdown",
     "copyLink": "Copy Link"
     "copyLink": "Copy Link"
   },
   },
+  "moreAction": {
+    "small": "small",
+    "medium": "medium",
+    "large": "large",
+    "fontSize": "Font Size"
+  },
   "disclosureAction": {
   "disclosureAction": {
     "rename": "Rename",
     "rename": "Rename",
     "delete": "Delete",
     "delete": "Delete",

+ 46 - 144
frontend/app_flowy/lib/plugins/document/document.dart

@@ -1,29 +1,19 @@
 library document_plugin;
 library document_plugin;
 
 
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/document/document_page.dart';
+import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:app_flowy/plugins/document/presentation/more/more_button.dart';
+import 'package:app_flowy/plugins/document/presentation/share/share_button.dart';
 import 'package:app_flowy/plugins/util.dart';
 import 'package:app_flowy/plugins/util.dart';
 import 'package:app_flowy/startup/plugin/plugin.dart';
 import 'package:app_flowy/startup/plugin/plugin.dart';
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/plugins/document/application/share_bloc.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
-import 'package:app_flowy/workspace/presentation/home/toast.dart';
 import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
 import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
-import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
-import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
-import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:clipboard/clipboard.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:file_picker/file_picker.dart';
-import 'package:flowy_infra_ui/widget/rounded_button.dart';
-import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 
-import 'document_page.dart';
-
 class DocumentPluginBuilder extends PluginBuilder {
 class DocumentPluginBuilder extends PluginBuilder {
   @override
   @override
   Plugin build(dynamic data) {
   Plugin build(dynamic data) {
@@ -49,6 +39,8 @@ class DocumentPluginBuilder extends PluginBuilder {
 
 
 class DocumentPlugin extends Plugin<int> {
 class DocumentPlugin extends Plugin<int> {
   late PluginType _pluginType;
   late PluginType _pluginType;
+  final DocumentAppearanceCubit _documentAppearanceCubit =
+      DocumentAppearanceCubit();
 
 
   @override
   @override
   final ViewPluginNotifier notifier;
   final ViewPluginNotifier notifier;
@@ -59,10 +51,22 @@ class DocumentPlugin extends Plugin<int> {
     Key? key,
     Key? key,
   }) : notifier = ViewPluginNotifier(view: view) {
   }) : notifier = ViewPluginNotifier(view: view) {
     _pluginType = pluginType;
     _pluginType = pluginType;
+    _documentAppearanceCubit.fetch();
+  }
+
+  @override
+  void dispose() {
+    _documentAppearanceCubit.close();
+    super.dispose();
   }
   }
 
 
   @override
   @override
-  PluginDisplay get display => DocumentPluginDisplay(notifier: notifier);
+  PluginDisplay get display {
+    return DocumentPluginDisplay(
+      notifier: notifier,
+      documentAppearanceCubit: _documentAppearanceCubit,
+    );
+  }
 
 
   @override
   @override
   PluginType get ty => _pluginType;
   PluginType get ty => _pluginType;
@@ -75,8 +79,13 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
   final ViewPluginNotifier notifier;
   final ViewPluginNotifier notifier;
   ViewPB get view => notifier.view;
   ViewPB get view => notifier.view;
   int? deletedViewIndex;
   int? deletedViewIndex;
+  DocumentAppearanceCubit documentAppearanceCubit;
 
 
-  DocumentPluginDisplay({required this.notifier, Key? key});
+  DocumentPluginDisplay({
+    required this.notifier,
+    required this.documentAppearanceCubit,
+    Key? key,
+  });
 
 
   @override
   @override
   Widget buildWidget(PluginContext context) {
   Widget buildWidget(PluginContext context) {
@@ -88,144 +97,37 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
       });
       });
     });
     });
 
 
-    return DocumentPage(
-      view: view,
-      onDeleted: () => context.onDeleted(view, deletedViewIndex),
-      key: ValueKey(view.id),
-    );
-  }
-
-  @override
-  Widget get leftBarItem => ViewLeftBarItem(view: view);
-
-  @override
-  Widget? get rightBarItem => DocumentShareButton(view: view);
-
-  @override
-  List<NavigationItem> get navigationItems => [this];
-}
-
-class DocumentShareButton extends StatelessWidget {
-  final ViewPB view;
-  DocumentShareButton({Key? key, required this.view})
-      : super(key: ValueKey(view.hashCode));
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => getIt<DocShareBloc>(param1: view),
-      child: BlocListener<DocShareBloc, DocShareState>(
-        listener: (context, state) {
-          state.map(
-            initial: (_) {},
-            loading: (_) {},
-            finish: (state) {
-              state.successOrFail.fold(
-                _handleExportData,
-                _handleExportError,
-              );
-            },
+    return BlocProvider.value(
+      value: documentAppearanceCubit,
+      child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
+        builder: (_, state) {
+          return DocumentPage(
+            view: view,
+            onDeleted: () => context.onDeleted(view, deletedViewIndex),
+            key: ValueKey(view.id),
           );
           );
         },
         },
-        child: BlocBuilder<DocShareBloc, DocShareState>(
-          builder: (context, state) => ConstrainedBox(
-            constraints: const BoxConstraints.expand(
-              height: 30,
-              width: 100,
-            ),
-            child: ShareActionList(view: view),
-          ),
-        ),
       ),
       ),
     );
     );
   }
   }
 
 
-  void _handleExportData(ExportDataPB exportData) {
-    switch (exportData.exportType) {
-      case ExportType.Link:
-        break;
-      case ExportType.Markdown:
-        FlutterClipboard.copy(exportData.data)
-            .then((value) => Log.info('copied to clipboard'));
-        break;
-      case ExportType.Text:
-        break;
-    }
-  }
-
-  void _handleExportError(FlowyError error) {}
-}
-
-class ShareActionList extends StatelessWidget {
-  const ShareActionList({
-    Key? key,
-    required this.view,
-  }) : super(key: key);
-
-  final ViewPB view;
+  @override
+  Widget get leftBarItem => ViewLeftBarItem(view: view);
 
 
   @override
   @override
-  Widget build(BuildContext context) {
-    final docShareBloc = context.read<DocShareBloc>();
-    return PopoverActionList<ShareActionWrapper>(
-      direction: PopoverDirection.bottomWithCenterAligned,
-      actions: ShareAction.values
-          .map((action) => ShareActionWrapper(action))
-          .toList(),
-      buildChild: (controller) {
-        return RoundedTextButton(
-          title: LocaleKeys.shareAction_buttonText.tr(),
-          onPressed: () => controller.show(),
-        );
-      },
-      onSelected: (action, controller) async {
-        switch (action.inner) {
-          case ShareAction.markdown:
-            final exportPath = await FilePicker.platform.saveFile(
-              dialogTitle: '',
-              fileName: '${view.name}.md',
-            );
-            if (exportPath != null) {
-              docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
-              showMessageToast('Exported to: $exportPath');
-            }
-            break;
-          case ShareAction.copyLink:
-            NavigatorAlertDialog(
-                    title: LocaleKeys.shareAction_workInProgress.tr())
-                .show(context);
-            break;
-        }
-        controller.close();
-      },
+  Widget? get rightBarItem {
+    return Row(
+      children: [
+        DocumentShareButton(view: view),
+        const SizedBox(width: 10),
+        BlocProvider.value(
+          value: documentAppearanceCubit,
+          child: const DocumentMoreButton(),
+        ),
+      ],
     );
     );
   }
   }
-}
-
-enum ShareAction {
-  markdown,
-  copyLink,
-}
-
-class ShareActionWrapper extends ActionCell {
-  final ShareAction inner;
-
-  ShareActionWrapper(this.inner);
-
-  @override
-  Widget? leftIcon(Color iconColor) => null;
 
 
   @override
   @override
-  String get name => inner.name;
-}
-
-extension QuestionBubbleExtension on ShareAction {
-  String get name {
-    switch (this) {
-      case ShareAction.markdown:
-        return LocaleKeys.shareAction_markdown.tr();
-      case ShareAction.copyLink:
-        return LocaleKeys.shareAction_copyLink.tr();
-    }
-  }
+  List<NavigationItem> get navigationItems => [this];
 }
 }

+ 38 - 14
frontend/app_flowy/lib/plugins/document/editor_styles.dart

@@ -1,9 +1,11 @@
+import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-
-const _baseFontSize = 14.0;
+import 'package:provider/provider.dart';
 
 
 EditorStyle customEditorTheme(BuildContext context) {
 EditorStyle customEditorTheme(BuildContext context) {
+  final documentStyle =
+      context.watch<DocumentAppearanceCubit>().documentAppearance;
   var editorStyle = Theme.of(context).brightness == Brightness.dark
   var editorStyle = Theme.of(context).brightness == Brightness.dark
       ? EditorStyle.dark
       ? EditorStyle.dark
       : EditorStyle.light;
       : EditorStyle.light;
@@ -11,14 +13,15 @@ EditorStyle customEditorTheme(BuildContext context) {
     padding: const EdgeInsets.all(0),
     padding: const EdgeInsets.all(0),
     textStyle: editorStyle.textStyle?.copyWith(
     textStyle: editorStyle.textStyle?.copyWith(
       fontFamily: 'poppins',
       fontFamily: 'poppins',
-      fontSize: _baseFontSize,
+      fontSize: documentStyle.fontSize,
     ),
     ),
     placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
     placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
       fontFamily: 'poppins',
       fontFamily: 'poppins',
-      fontSize: _baseFontSize,
+      fontSize: documentStyle.fontSize,
     ),
     ),
     bold: editorStyle.bold?.copyWith(
     bold: editorStyle.bold?.copyWith(
-      fontWeight: FontWeight.w500,
+      fontWeight: FontWeight.w600,
+      fontFamily: 'poppins-Bold',
     ),
     ),
     backgroundColor: Theme.of(context).colorScheme.surface,
     backgroundColor: Theme.of(context).colorScheme.surface,
   );
   );
@@ -26,6 +29,9 @@ EditorStyle customEditorTheme(BuildContext context) {
 }
 }
 
 
 Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
 Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
+  final documentStyle =
+      context.watch<DocumentAppearanceCubit>().documentAppearance;
+  final baseFontSize = documentStyle.fontSize;
   const basePadding = 12.0;
   const basePadding = 12.0;
   var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
   var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
       ? HeadingPluginStyle.dark
       ? HeadingPluginStyle.dark
@@ -33,15 +39,15 @@ Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
   headingPluginStyle = headingPluginStyle.copyWith(
   headingPluginStyle = headingPluginStyle.copyWith(
     textStyle: (EditorState editorState, Node node) {
     textStyle: (EditorState editorState, Node node) {
       final headingToFontSize = {
       final headingToFontSize = {
-        'h1': _baseFontSize + 12,
-        'h2': _baseFontSize + 8,
-        'h3': _baseFontSize + 4,
-        'h4': _baseFontSize,
-        'h5': _baseFontSize,
-        'h6': _baseFontSize,
+        'h1': baseFontSize + 12,
+        'h2': baseFontSize + 8,
+        'h3': baseFontSize + 4,
+        'h4': baseFontSize,
+        'h5': baseFontSize,
+        'h6': baseFontSize,
       };
       };
       final fontSize =
       final fontSize =
-          headingToFontSize[node.attributes.heading] ?? _baseFontSize;
+          headingToFontSize[node.attributes.heading] ?? baseFontSize;
       return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
       return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
     },
     },
     padding: (EditorState editorState, Node node) {
     padding: (EditorState editorState, Node node) {
@@ -57,10 +63,28 @@ Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
       return EdgeInsets.only(bottom: padding);
       return EdgeInsets.only(bottom: padding);
     },
     },
   );
   );
+  var numberListPluginStyle = Theme.of(context).brightness == Brightness.dark
+      ? NumberListPluginStyle.dark
+      : NumberListPluginStyle.light;
+
+  numberListPluginStyle = numberListPluginStyle.copyWith(
+    icon: (_, textNode) {
+      const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
+      return Container(
+        padding: iconPadding,
+        child: Text(
+          '${textNode.attributes.number.toString()}.',
+          style: customEditorTheme(context).textStyle,
+        ),
+      );
+    },
+  );
   final pluginTheme = Theme.of(context).brightness == Brightness.dark
   final pluginTheme = Theme.of(context).brightness == Brightness.dark
       ? darkPlguinStyleExtension
       ? darkPlguinStyleExtension
       : lightPlguinStyleExtension;
       : lightPlguinStyleExtension;
   return pluginTheme.toList()
   return pluginTheme.toList()
-    ..removeWhere((element) => element is HeadingPluginStyle)
-    ..add(headingPluginStyle);
+    ..removeWhere((element) =>
+        element is HeadingPluginStyle || element is NumberListPluginStyle)
+    ..add(headingPluginStyle)
+    ..add(numberListPluginStyle);
 }
 }

+ 40 - 0
frontend/app_flowy/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart

@@ -0,0 +1,40 @@
+import 'package:bloc/bloc.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+const String _kDocumentAppearenceFontSize = 'kDocumentAppearenceFontSize';
+
+class DocumentAppearance {
+  const DocumentAppearance({
+    required this.fontSize,
+  });
+
+  final double fontSize;
+  // Will be supported...
+  // final String fontName;
+
+  DocumentAppearance copyWith({double? fontSize}) {
+    return DocumentAppearance(
+      fontSize: fontSize ?? this.fontSize,
+    );
+  }
+}
+
+class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
+  DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 14.0));
+
+  late DocumentAppearance documentAppearance;
+
+  void fetch() async {
+    final prefs = await SharedPreferences.getInstance();
+    final fontSize = prefs.getDouble(_kDocumentAppearenceFontSize) ?? 14.0;
+    documentAppearance = DocumentAppearance(fontSize: fontSize);
+    emit(documentAppearance);
+  }
+
+  void sync(DocumentAppearance documentAppearance) async {
+    final prefs = await SharedPreferences.getInstance();
+    prefs.setDouble(_kDocumentAppearenceFontSize, documentAppearance.fontSize);
+    this.documentAppearance = documentAppearance;
+    emit(documentAppearance);
+  }
+}

+ 89 - 0
frontend/app_flowy/lib/plugins/document/presentation/more/font_size_switcher.dart

@@ -0,0 +1,89 @@
+import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:provider/provider.dart';
+import 'package:tuple/tuple.dart';
+import 'package:easy_localization/easy_localization.dart';
+
+class FontSizeSwitcher extends StatefulWidget {
+  const FontSizeSwitcher({
+    super.key,
+  });
+
+  @override
+  State<FontSizeSwitcher> createState() => _FontSizeSwitcherState();
+}
+
+class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
+  final List<bool> _selectedFontSizes = [false, true, false];
+  final List<Tuple2<String, double>> _fontSizes = [
+    Tuple2(LocaleKeys.moreAction_small.tr(), 12.0),
+    Tuple2(LocaleKeys.moreAction_medium.tr(), 14.0),
+    Tuple2(LocaleKeys.moreAction_large.tr(), 18.0),
+  ];
+
+  @override
+  void initState() {
+    super.initState();
+
+    final fontSize =
+        context.read<DocumentAppearanceCubit>().documentAppearance.fontSize;
+    final index = _fontSizes.indexWhere((element) => element.item2 == fontSize);
+    _updateSelectedFontSize(index);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        FlowyText.semibold(
+          LocaleKeys.moreAction_fontSize.tr(),
+          fontSize: 12,
+        ),
+        const SizedBox(
+          height: 5,
+        ),
+        ToggleButtons(
+          isSelected: _selectedFontSizes,
+          onPressed: (int index) {
+            _updateSelectedFontSize(index);
+            _sync(index);
+          },
+          borderRadius: const BorderRadius.all(Radius.circular(5)),
+          selectedBorderColor: Theme.of(context).colorScheme.primaryContainer,
+          selectedColor: Theme.of(context).colorScheme.onSurface,
+          fillColor: Theme.of(context).colorScheme.primaryContainer,
+          color: Theme.of(context).hintColor,
+          constraints: const BoxConstraints(
+            minHeight: 40.0,
+            minWidth: 80.0,
+          ),
+          children: _fontSizes
+              .map((e) => Text(
+                    e.item1,
+                    style: TextStyle(fontSize: e.item2),
+                  ))
+              .toList(),
+        ),
+      ],
+    );
+  }
+
+  void _updateSelectedFontSize(int index) {
+    setState(() {
+      for (int i = 0; i < _selectedFontSizes.length; i++) {
+        _selectedFontSizes[i] = i == index;
+      }
+    });
+  }
+
+  void _sync(int index) {
+    if (index < 0 || index >= _fontSizes.length) return;
+    final fontSize = _fontSizes[index].item2;
+    final cubit = context.read<DocumentAppearanceCubit>();
+    final documentAppearance = cubit.documentAppearance;
+    cubit.sync(documentAppearance.copyWith(fontSize: fontSize));
+  }
+}

+ 35 - 0
frontend/app_flowy/lib/plugins/document/presentation/more/more_button.dart

@@ -0,0 +1,35 @@
+import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:app_flowy/plugins/document/presentation/more/font_size_switcher.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class DocumentMoreButton extends StatelessWidget {
+  const DocumentMoreButton({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return PopupMenuButton<int>(
+      offset: const Offset(0, 30),
+      itemBuilder: (context) {
+        return [
+          PopupMenuItem(
+            value: 1,
+            enabled: false,
+            child: BlocProvider.value(
+              value: context.read<DocumentAppearanceCubit>(),
+              child: const FontSizeSwitcher(),
+            ),
+          )
+        ];
+      },
+      child: svgWidget(
+        'editor/details',
+        size: const Size(18, 18),
+        color: Theme.of(context).colorScheme.onSurface,
+      ),
+    );
+  }
+}

+ 132 - 0
frontend/app_flowy/lib/plugins/document/presentation/share/share_button.dart

@@ -0,0 +1,132 @@
+library document_plugin;
+
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/plugins/document/application/share_bloc.dart';
+import 'package:app_flowy/workspace/presentation/home/toast.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:clipboard/clipboard.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flowy_infra_ui/widget/rounded_button.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class DocumentShareButton extends StatelessWidget {
+  final ViewPB view;
+  DocumentShareButton({Key? key, required this.view})
+      : super(key: ValueKey(view.hashCode));
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => getIt<DocShareBloc>(param1: view),
+      child: BlocListener<DocShareBloc, DocShareState>(
+        listener: (context, state) {
+          state.map(
+            initial: (_) {},
+            loading: (_) {},
+            finish: (state) {
+              state.successOrFail.fold(
+                _handleExportData,
+                _handleExportError,
+              );
+            },
+          );
+        },
+        child: BlocBuilder<DocShareBloc, DocShareState>(
+          builder: (context, state) => ConstrainedBox(
+            constraints: const BoxConstraints.expand(
+              height: 30,
+              width: 100,
+            ),
+            child: ShareActionList(view: view),
+          ),
+        ),
+      ),
+    );
+  }
+
+  void _handleExportData(ExportDataPB exportData) {
+    switch (exportData.exportType) {
+      case ExportType.Link:
+        break;
+      case ExportType.Markdown:
+        FlutterClipboard.copy(exportData.data)
+            .then((value) => Log.info('copied to clipboard'));
+        break;
+      case ExportType.Text:
+        break;
+    }
+  }
+
+  void _handleExportError(FlowyError error) {}
+}
+
+class ShareActionList extends StatelessWidget {
+  const ShareActionList({
+    Key? key,
+    required this.view,
+  }) : super(key: key);
+
+  final ViewPB view;
+
+  @override
+  Widget build(BuildContext context) {
+    final docShareBloc = context.read<DocShareBloc>();
+    return PopoverActionList<ShareActionWrapper>(
+      direction: PopoverDirection.bottomWithCenterAligned,
+      actions: ShareAction.values
+          .map((action) => ShareActionWrapper(action))
+          .toList(),
+      buildChild: (controller) {
+        return RoundedTextButton(
+          title: LocaleKeys.shareAction_buttonText.tr(),
+          onPressed: () => controller.show(),
+        );
+      },
+      onSelected: (action, controller) async {
+        switch (action.inner) {
+          case ShareAction.markdown:
+            final exportPath = await FilePicker.platform.saveFile(
+              dialogTitle: '',
+              fileName: '${view.name}.md',
+            );
+            if (exportPath != null) {
+              docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
+              showMessageToast('Exported to: $exportPath');
+            }
+            break;
+          case ShareAction.copyLink:
+            NavigatorAlertDialog(
+                    title: LocaleKeys.shareAction_workInProgress.tr())
+                .show(context);
+            break;
+        }
+        controller.close();
+      },
+    );
+  }
+}
+
+enum ShareAction {
+  markdown,
+  copyLink,
+}
+
+class ShareActionWrapper extends ActionCell {
+  final ShareAction inner;
+
+  ShareActionWrapper(this.inner);
+
+  Widget? icon(Color iconColor) => null;
+
+  @override
+  String get name => inner.name;
+}

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -84,6 +84,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
           GestureDetector(
           GestureDetector(
             key: iconKey,
             key: iconKey,
             child: icon,
             child: icon,
+            behavior: HitTestBehavior.opaque,
             onTap: () async {
             onTap: () async {
               await widget.editorState.formatTextToCheckbox(
               await widget.editorState.formatTextToCheckbox(
                 widget.editorState,
                 widget.editorState,

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart

@@ -72,7 +72,7 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
       child: FlowyRichText(
       child: FlowyRichText(
         key: _richTextKey,
         key: _richTextKey,
         textNode: widget.textNode,
         textNode: widget.textNode,
-        textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
+        textSpanDecorator: (textSpan) => textSpan,
         placeholderTextSpanDecorator: (textSpan) =>
         placeholderTextSpanDecorator: (textSpan) =>
             textSpan.updateTextStyle(textStyle),
             textSpan.updateTextStyle(textStyle),
         lineHeight: widget.editorState.editorStyle.lineHeight,
         lineHeight: widget.editorState.editorStyle.lineHeight,

+ 3 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -255,7 +255,9 @@ List<ToolbarItem> defaultToolbarItems = [
     highlightCallback: (editorState) => _allSatisfy(
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
       editorState,
       BuiltInAttributeKey.backgroundColor,
       BuiltInAttributeKey.backgroundColor,
-      (value) => value != null,
+      (value) {
+        return value != null && value != '0x00000000'; // transparent color;
+      },
     ),
     ),
     handler: (editorState, context) => formatHighlight(
     handler: (editorState, context) => formatHighlight(
       editorState,
       editorState,

+ 17 - 6
frontend/app_flowy/packages/flowy_infra/lib/image.dart

@@ -1,15 +1,26 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 
 
-Widget svgWidget(String name, {Color? color}) {
-  final Widget svg = SvgPicture.asset('assets/images/$name.svg', color: color);
-
-  return svg;
-}
-
 Widget svgWithSize(String name, Size size) {
 Widget svgWithSize(String name, Size size) {
   return SizedBox.fromSize(
   return SizedBox.fromSize(
     size: size,
     size: size,
     child: svgWidget(name),
     child: svgWidget(name),
   );
   );
 }
 }
+
+Widget svgWidget(String name, {Size? size, Color? color}) {
+  if (size != null) {
+    return SizedBox.fromSize(
+      size: size,
+      child: _svgWidget(name, color: color),
+    );
+  } else {
+    return _svgWidget(name, color: color);
+  }
+}
+
+Widget _svgWidget(String name, {Color? color}) {
+  final Widget svg = SvgPicture.asset('assets/images/$name.svg', color: color);
+
+  return svg;
+}