mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
4b9b723521
commit
9c59e1487e
@ -80,7 +80,8 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
|
||||
expect(
|
||||
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
@ -12,8 +12,11 @@ class MyMockClient extends Mock implements http.Client {
|
||||
final requestType = request.method;
|
||||
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([
|
||||
utf8.encode(
|
||||
'{ "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 = '';
|
||||
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();
|
||||
final data = chunk.trim().split('data: ');
|
||||
if (data[0] != '[DONE]') {
|
||||
|
@ -382,8 +382,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
|
||||
case FieldType.Checkbox:
|
||||
final group = customData.asCheckboxGroup()!;
|
||||
if (group.isCheck) {
|
||||
widget =
|
||||
const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
|
||||
widget = const FlowySvg(
|
||||
FlowySvgs.check_filled_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
} else {
|
||||
widget = const FlowySvg(FlowySvgs.uncheck_s);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
),);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)}');
|
||||
}
|
||||
}
|
@ -300,10 +300,6 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
||||
ThemeData get lightTheme => _getThemeData(Brightness.light);
|
||||
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) {
|
||||
// Poppins and SF Mono are not well supported in some languages, so use the
|
||||
// built-in font for the following languages.
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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/util/google_font_family_extension.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy/workspace/application/appearance_defaults.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';
|
||||
@ -29,9 +31,6 @@ class ThemeFontFamilySetting extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
|
||||
final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
|
||||
final ValueNotifier<String> query = ValueNotifier('');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ThemeSettingEntryTemplateWidget(
|
||||
@ -44,61 +43,101 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
|
||||
.syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
|
||||
},
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -31,6 +31,7 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
label,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (hint != null)
|
||||
@ -71,12 +72,16 @@ class ThemeValueDropDown extends StatefulWidget {
|
||||
required this.popupBuilder,
|
||||
this.popoverKey,
|
||||
this.onClose,
|
||||
this.child,
|
||||
this.popoverController,
|
||||
});
|
||||
|
||||
final String currentValue;
|
||||
final Key? popoverKey;
|
||||
final Widget Function(BuildContext) popupBuilder;
|
||||
final void Function()? onClose;
|
||||
final Widget? child;
|
||||
final PopoverController? popoverController;
|
||||
|
||||
@override
|
||||
State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
|
||||
@ -87,6 +92,7 @@ class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
key: widget.popoverKey,
|
||||
controller: widget.popoverController,
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
popupBuilder: widget.popupBuilder,
|
||||
constraints: const BoxConstraints(
|
||||
@ -95,11 +101,12 @@ class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
|
||||
maxHeight: 400,
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,23 +19,25 @@ class SettingsAppearanceView extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
BrightnessSetting(
|
||||
currentThemeMode: state.themeMode,
|
||||
),
|
||||
ColorSchemeSetting(
|
||||
currentTheme: state.appTheme.themeName,
|
||||
bloc: context.read<DynamicPluginBloc>(),
|
||||
),
|
||||
BrightnessSetting(
|
||||
currentThemeMode: state.themeMode,
|
||||
),
|
||||
const Divider(),
|
||||
ThemeFontFamilySetting(
|
||||
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(),
|
||||
],
|
||||
);
|
||||
|
@ -54,11 +54,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "4a87ec4"
|
||||
resolved-ref: "4a87ec4bd440344b8f51dd61ab84e2c68d4196d2"
|
||||
ref: a0ff609
|
||||
resolved-ref: a0ff609cb1ac53e5d167489f43452074860dd80e
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -47,7 +47,7 @@ dependencies:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: 4a87ec4
|
||||
ref: a0ff609
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
|
3
frontend/resources/flowy_icons/16x/font_family.svg
Normal file
3
frontend/resources/flowy_icons/16x/font_family.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 167 B |
@ -265,13 +265,13 @@
|
||||
},
|
||||
"layoutDirection": {
|
||||
"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",
|
||||
"rtl": "RTL"
|
||||
},
|
||||
"textDirection": {
|
||||
"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",
|
||||
"rtl": "RTL",
|
||||
"auto": "AUTO",
|
||||
|
Loading…
Reference in New Issue
Block a user