feat: customize text font (#3467)

* feat: update UI in settings page

* feat: customzing font in document page

* fix: flutter analyze and format issues
This commit is contained in:
Lucas.Xu
2023-09-21 09:12:25 +08:00
committed by GitHub
parent 4b9b723521
commit 9c59e1487e
26 changed files with 334 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -8,26 +8,26 @@ import 'emoji_picker.dart';
/// Config for customizations
class Config {
/// 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
final int columns;

View File

@ -27,14 +27,16 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
@override
void initState() {
var initCategory = widget.state.categoryEmoji.indexWhere(
(element) => element.category == widget.config.initCategory,);
(element) => element.category == widget.config.initCategory,
);
if (initCategory == -1) {
initCategory = 0;
}
_tabController = TabController(
initialIndex: initCategory,
length: widget.state.categoryEmoji.length,
vsync: this,);
initialIndex: initCategory,
length: widget.state.categoryEmoji.length,
vsync: this,
);
_pageController = PageController(initialPage: initCategory);
_emojiFocusNode.requestFocus();
@ -72,14 +74,15 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
return Material(
type: MaterialType.transparency,
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();
@ -160,8 +163,12 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
: widget.state.categoryEmoji
.asMap()
.entries
.map<Widget>((item) => _buildCategory(
item.value.category, emojiSize,),)
.map<Widget>(
(item) => _buildCategory(
item.value.category,
emojiSize,
),
)
.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) {
return InkWell(
onTap: onPressed,
@ -266,29 +275,31 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
Emoji emoji,
) {
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() {
return Center(
child: FlowyText.regular(
child: FlowyText.regular(
widget.config.noRecentsText,
color: Theme.of(context).colorScheme.tertiary.withAlpha(77),
fontSize: widget.config.noRecentsStyle.fontSize,
textAlign: TextAlign.center,
),);
),
);
}
}

View File

@ -190,30 +190,49 @@ class EmojiPickerState extends State<EmojiPicker> {
categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
}
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(
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
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;
// Get Emojis cached locally if available
@ -236,15 +255,18 @@ class EmojiPickerState extends State<EmojiPicker> {
// Check if emoji is available on current platform
Future<Map<String, String>?> _getPlatformAvailableEmoji(
Map<String, String> emoji,) async {
Map<String, String> emoji,
) async {
if (Platform.isAndroid) {
Map<String, String>? filtered = {};
const delimiter = '|';
try {
final entries = emoji.values.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);
for (var i = 0; i < resultKeys.length; 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
Future<void> _cacheFilteredEmojis(
String title, Map<String, String> emojis,) async {
String title,
Map<String, String> emojis,
) async {
final prefs = await SharedPreferences.getInstance();
final emojiJson = jsonEncode(emojis);
prefs.setString(title, emojiJson);
@ -305,7 +329,9 @@ class EmojiPickerState extends State<EmojiPicker> {
recentEmoji.sort((a, b) => b.counter - a.counter);
// Limit entries to recentsLimit
recentEmoji = recentEmoji.sublist(
0, min(widget.config.recentsLimit, recentEmoji.length),);
0,
min(widget.config.recentsLimit, recentEmoji.length),
);
// save locally
prefs.setString('recent', jsonEncode(recentEmoji));
}

View File

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

View File

@ -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,
),
),
),
);
},
);

View File

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

View File

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

View File

@ -14,6 +14,7 @@ export 'database/inline_database_menu_item.dart';
export 'database/referenced_database_menu_item.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.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';

View File

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

View File

@ -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/mention/mention_block.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:collection/collection.dart';
import 'package:flutter/material.dart';
@ -193,6 +194,15 @@ class EditorStyleCustomizer {
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
final mention = attributes[MentionBlockKeys.mention] as Map?;
if (mention != null) {