[feat] Allow user to select any Google Font (#2895)

* chore: add label for font selection drop down

* chore: add method to set font family

* feat: add drop down to setting appearance view

* feat: add fontFamily to document appearance cubit

* feat: add bloc provider to root for document appearance style

* feat: syncFont family from setting appearance dialog

* feat: plumbing for font style in editor

* fix: add blocprovider before pushing overlay

* chore: add kv_keys

* fix: use fontFamily in document appearance cubit

* fix: remove bloc providers because bloc is supplied in ancestor

* fix: remove unecessary bloc provider

* chore: add constraints to popover

* chore: add translation for search box

* feat: add levenshtein for string sort

* feat: add search bar view

* refactor: levenshtein

* chore: add tests for levenshtein algorithm

* feat: add unit tests for appearance cubit

* fix: analyzer warnings

* feat: sort by ascending if query is empty

* chore: add test for the font family setting widget

* feat: make comparison case insensitive

* feat: lazy load with listview.builder

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: fonts loaded on open application

* fix: checkmark doesn't show

* fix: try catch before getFont

* fix: clear text editing value on close

* fix: remove autofocus for search text field

* chore: add tests

* feat: use sliver protocol

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: avoid using intrinsic height

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: extra paren caused build failure

* feat: switch order of font family setting

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
This commit is contained in:
Alex Wallen 2023-07-04 14:30:38 -07:00 committed by GitHub
parent 9fb8f221cf
commit 323cb3b60f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 562 additions and 96 deletions

View File

@ -186,6 +186,10 @@
"open": "Open Settings"
},
"appearance": {
"fontFamily": {
"label": "Font Family",
"search": "Search"
},
"themeMode": {
"label": "Theme Mode",
"light": "Light Mode",

View File

@ -18,4 +18,9 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'height': 600.0, 'width': 800.0}
static const String windowSize = 'windowSize';
static const String kDocumentAppearanceFontSize =
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
}

View File

@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_document
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
@ -86,7 +85,6 @@ class _RowEditorState extends State<RowEditor> {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => DocumentAppearanceCubit()),
BlocProvider.value(value: documentBloc),
],
child: BlocBuilder<DocumentBloc, DocumentState>(

View File

@ -39,8 +39,6 @@ class DocumentPluginBuilder extends PluginBuilder {
class DocumentPlugin extends Plugin<int> {
late PluginType _pluginType;
final DocumentAppearanceCubit _documentAppearanceCubit =
DocumentAppearanceCubit();
@override
final ViewPluginNotifier notifier;
@ -52,20 +50,12 @@ class DocumentPlugin extends Plugin<int> {
Key? key,
}) : notifier = ViewPluginNotifier(view: view) {
_pluginType = pluginType;
_documentAppearanceCubit.fetch();
}
@override
void dispose() {
_documentAppearanceCubit.close();
super.dispose();
}
@override
PluginWidgetBuilder get widgetBuilder {
return DocumentPluginWidgetBuilder(
notifier: notifier,
documentAppearanceCubit: _documentAppearanceCubit,
);
}
@ -81,11 +71,9 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
final ViewPluginNotifier notifier;
ViewPB get view => notifier.view;
int? deletedViewIndex;
DocumentAppearanceCubit documentAppearanceCubit;
DocumentPluginWidgetBuilder({
required this.notifier,
required this.documentAppearanceCubit,
Key? key,
});
@ -102,17 +90,14 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
});
});
return BlocProvider.value(
value: documentAppearanceCubit,
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (_, state) {
return DocumentPage(
view: view,
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
key: ValueKey(view.id),
);
},
),
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (_, state) {
return DocumentPage(
view: view,
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
key: ValueKey(view.id),
);
},
);
}
@ -128,10 +113,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
view: view,
),
const SizedBox(width: 10),
BlocProvider.value(
value: documentAppearanceCubit,
child: const DocumentMoreButton(),
),
const DocumentMoreButton(),
],
);
}

View File

@ -28,30 +28,32 @@ class EditorStyleCustomizer {
EditorStyle desktop() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return EditorStyle.desktop(
padding: padding,
backgroundColor: theme.colorScheme.surface,
cursorColor: theme.colorScheme.primary,
textStyleConfiguration: TextStyleConfiguration(
text: TextStyle(
fontFamily: 'Poppins',
text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
color: theme.colorScheme.onBackground,
height: 1.5,
),
bold: const TextStyle(
fontFamily: 'Poppins-Bold',
bold: baseTextStyle(fontFamily).copyWith(
fontWeight: FontWeight.w600,
),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
href: TextStyle(
italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),
underline: baseTextStyle(fontFamily)
.copyWith(decoration: TextDecoration.underline),
strikethrough:
baseTextStyle(fontFamily)
.copyWith(decoration: TextDecoration.lineThrough),
href: baseTextStyle(fontFamily).copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
textStyle: TextStyle(
textStyle: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
@ -66,30 +68,33 @@ class EditorStyleCustomizer {
EditorStyle mobile() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return EditorStyle.desktop(
padding: padding,
backgroundColor: theme.colorScheme.surface,
cursorColor: theme.colorScheme.primary,
textStyleConfiguration: TextStyleConfiguration(
text: TextStyle(
fontFamily: 'poppins',
text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
color: theme.colorScheme.onBackground,
height: 1.5,
),
bold: const TextStyle(
fontFamily: 'poppins-Bold',
bold: baseTextStyle(fontFamily).copyWith(
fontWeight: FontWeight.w600,
),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
href: TextStyle(
italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),
underline: baseTextStyle(fontFamily)
.copyWith(decoration: TextDecoration.underline),
strikethrough:
baseTextStyle(fontFamily)
.copyWith(decoration: TextDecoration.lineThrough),
href: baseTextStyle(fontFamily).copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
textStyle: TextStyle(
textStyle: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
@ -119,8 +124,8 @@ class EditorStyleCustomizer {
TextStyle codeBlockStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: 'poppins',
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
return baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
height: 1.5,
color: theme.colorScheme.onBackground,
@ -157,6 +162,16 @@ class EditorStyleCustomizer {
);
}
TextStyle baseTextStyle(String fontFamily) {
try {
return GoogleFonts.getFont(
fontFamily,
);
} on Exception {
return GoogleFonts.getFont('Poppins');
}
}
InlineSpan customizeAttributeDecorator(
TextInsert textInsert,
TextSpan textSpan,

View File

@ -1,30 +1,53 @@
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:bloc/bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
const String _kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize';
class DocumentAppearance {
const DocumentAppearance({
required this.fontSize,
required this.fontFamily,
});
final double fontSize;
// Will be supported...
// final String fontName;
final String fontFamily;
DocumentAppearance copyWith({double? fontSize}) {
DocumentAppearance copyWith({
double? fontSize,
String? fontFamily,
}) {
return DocumentAppearance(
fontSize: fontSize ?? this.fontSize,
fontFamily: fontFamily ?? this.fontFamily,
);
}
}
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 16.0));
DocumentAppearanceCubit()
: super(const DocumentAppearance(fontSize: 16.0, fontFamily: 'Poppins'));
void fetch() async {
Future<void> fetch() async {
final prefs = await SharedPreferences.getInstance();
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
final fontSize =
prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;
final fontFamily =
prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? 'Poppins';
if (isClosed) {
return;
}
emit(
state.copyWith(
fontSize: fontSize,
fontFamily: fontFamily,
),
);
}
Future<void> syncFontSize(double fontSize) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(KVKeys.kDocumentAppearanceFontSize, fontSize);
if (isClosed) {
return;
@ -37,9 +60,9 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
);
}
void syncFontSize(double fontSize) async {
Future<void> syncFontFamily(String fontFamily) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
prefs.setString(KVKeys.kDocumentAppearanceFontFamily, fontFamily);
if (isClosed) {
return;
@ -47,7 +70,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
emit(
state.copyWith(
fontSize: fontSize,
fontFamily: fontFamily,
),
);
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -79,8 +80,13 @@ class ApplicationWidget extends StatelessWidget {
final cubit = AppearanceSettingsCubit(appearanceSetting)
..readLocaleWhenAppLaunch(context);
return BlocProvider(
create: (context) => cubit,
return MultiBlocProvider(
providers: [
BlocProvider.value(value: cubit),
BlocProvider<DocumentAppearanceCubit>(
create: (_) => DocumentAppearanceCubit()..fetch(),
),
],
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) => MaterialApp(
builder: overlayManagerBuilder(),

View File

@ -10,6 +10,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_fonts/google_fonts.dart';
part 'appearance.freezed.dart';
@ -49,6 +50,14 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
emit(state.copyWith(themeMode: themeMode));
}
/// Update selected font in the user's settings and emit an updated state
/// with the font name.
void setFontFamily(String fontFamilyName) {
_setting.font = fontFamilyName;
_saveAppearanceSettings();
emit(state.copyWith(font: fontFamilyName));
}
/// Updates the current locale and notify the listeners the locale was
/// changed. Fallback to [en] locale if [newLocale] is not supported.
void setLocale(BuildContext context, Locale newLocale) {
@ -341,14 +350,24 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
}
TextStyle _getFontStyle({
String? fontFamily,
required String fontFamily,
double? fontSize,
FontWeight? fontWeight,
Color? fontColor,
double? letterSpacing,
double? lineHeight,
}) =>
TextStyle(
}) {
try {
return GoogleFonts.getFont(
fontFamily,
fontSize: fontSize ?? FontSizes.s12,
color: fontColor,
fontWeight: fontWeight ?? FontWeight.w500,
letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
height: lineHeight,
);
} catch (e) {
return TextStyle(
fontFamily: fontFamily,
fontSize: fontSize ?? FontSizes.s12,
color: fontColor,
@ -357,6 +376,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
height: lineHeight,
);
}
}
TextTheme _getTextTheme({
required String fontFamily,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
@ -103,7 +104,10 @@ class MenuUser extends StatelessWidget {
showDialog(
context: context,
builder: (context) {
return SettingsDialog(userProfile);
return BlocProvider<DocumentAppearanceCubit>.value(
value: BlocProvider.of<DocumentAppearanceCubit>(context),
child: SettingsDialog(userProfile),
);
},
);
},

View File

@ -0,0 +1,26 @@
import 'dart:math';
int levenshtein(String s, String t, {bool caseSensitive = true}) {
if (!caseSensitive) {
s = s.toLowerCase();
t = t.toLowerCase();
}
if (s == t) return 0;
final v0 = List<int>.generate(t.length + 1, (i) => i);
final v1 = List<int>.filled(t.length + 1, 0);
for (var i = 0; i < s.length; i++) {
v1[0] = i + 1;
for (var j = 0; j < t.length; j++) {
final cost = (s[i] == t[j]) ? 0 : 1;
v1[j + 1] = min(v1[j] + 1, min(v0[j + 1] + 1, v0[j] + cost));
}
v0.setAll(0, v1);
}
return v1[t.length];
}

View File

@ -1,8 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
@ -12,6 +14,9 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import 'levenshtein.dart';
class SettingsAppearanceView extends StatelessWidget {
const SettingsAppearanceView({Key? key}) : super(key: key);
@ -31,6 +36,9 @@ class SettingsAppearanceView extends StatelessWidget {
currentTheme: state.appTheme.themeName,
bloc: context.read<DynamicPluginBloc>(),
),
ThemeFontFamilySetting(
currentFontFamily: state.font,
),
],
);
},
@ -209,36 +217,17 @@ class BrightnessSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys.settings_appearance_themeMode_label.tr(),
overflow: TextOverflow.ellipsis,
),
),
AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
child: FlowyTextButton(
_themeModeLabelText(currentThemeMode),
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
onPressed: () {},
),
popupBuilder: (BuildContext context) {
return IntrinsicWidth(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_themeModeItemButton(context, ThemeMode.light),
_themeModeItemButton(context, ThemeMode.dark),
_themeModeItemButton(context, ThemeMode.system),
],
),
);
},
),
],
return ThemeSettingDropDown(
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
currentValue: _themeModeLabelText(currentThemeMode),
popupBuilder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_themeModeItemButton(context, ThemeMode.light),
_themeModeItemButton(context, ThemeMode.dark),
_themeModeItemButton(context, ThemeMode.system),
],
),
);
}
@ -272,3 +261,161 @@ class BrightnessSetting extends StatelessWidget {
}
}
}
class ThemeFontFamilySetting extends StatefulWidget {
const ThemeFontFamilySetting({
super.key,
required this.currentFontFamily,
});
final String currentFontFamily;
@override
State<ThemeFontFamilySetting> createState() => _ThemeFontFamilySettingState();
}
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
final ValueNotifier<String> query = ValueNotifier('');
@override
Widget build(BuildContext context) {
return ThemeSettingDropDown(
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
currentValue: parseFontFamilyName(widget.currentFontFamily),
onClose: () {
query.value = '';
},
popupBuilder: (_) => CustomScrollView(
shrinkWrap: true,
slivers: [
SliverPadding(
padding: const EdgeInsets.only(right: 8),
sliver: SliverToBoxAdapter(
child: FlowyTextField(
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,
);
},
),
],
),
);
}
String parseFontFamilyName(String fontFamilyName) {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return fontFamilyName
.replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
Widget _fontFamilyItemButton(BuildContext context, TextStyle style) {
final buttonFontFamily = parseFontFamilyName(style.fontFamily!);
return SizedBox(
key: UniqueKey(),
height: 32,
child: FlowyButton(
text: FlowyText.medium(
parseFontFamilyName(style.fontFamily!),
fontFamily: style.fontFamily!,
),
rightIcon:
buttonFontFamily == parseFontFamilyName(widget.currentFontFamily)
? const FlowySvg(name: 'grid/checkmark')
: null,
onTap: () {
if (parseFontFamilyName(widget.currentFontFamily) !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()
.setFontFamily(parseFontFamilyName(style.fontFamily!));
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(parseFontFamilyName(style.fontFamily!));
}
},
),
);
}
}
class ThemeSettingDropDown extends StatefulWidget {
const ThemeSettingDropDown({
super.key,
required this.label,
required this.currentValue,
required this.popupBuilder,
this.onClose,
});
final String label;
final String currentValue;
final Widget Function(BuildContext) popupBuilder;
final void Function()? onClose;
@override
State<ThemeSettingDropDown> createState() => _ThemeSettingDropDownState();
}
class _ThemeSettingDropDownState extends State<ThemeSettingDropDown> {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText.medium(
widget.label,
overflow: TextOverflow.ellipsis,
),
),
AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
popupBuilder: widget.popupBuilder,
constraints: const BoxConstraints(
minWidth: 80,
maxWidth: 160,
maxHeight: 400,
),
onClose: widget.onClose,
child: FlowyTextButton(
widget.currentValue,
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
),
),
],
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
group('DocumentAppearanceCubit', () {
late SharedPreferences preferences;
late DocumentAppearanceCubit cubit;
setUpAll(() async {
SharedPreferences.setMockInitialValues({});
});
setUp(() async {
preferences = await SharedPreferences.getInstance();
cubit = DocumentAppearanceCubit();
});
tearDown(() async {
await preferences.clear();
await cubit.close();
});
test('Initial state', () {
expect(cubit.state.fontSize, 16.0);
expect(cubit.state.fontFamily, 'Poppins');
});
test('Fetch document appearance from SharedPreferences', () async {
await preferences.setDouble(KVKeys.kDocumentAppearanceFontSize, 18.0);
await preferences.setString(
KVKeys.kDocumentAppearanceFontFamily,
'Arial',
);
await cubit.fetch();
expect(cubit.state.fontSize, 18.0);
expect(cubit.state.fontFamily, 'Arial');
});
test('Sync font size to SharedPreferences', () async {
await cubit.syncFontSize(20.0);
final fontSize =
preferences.getDouble(KVKeys.kDocumentAppearanceFontSize);
expect(fontSize, 20.0);
expect(cubit.state.fontSize, 20.0);
});
test('Sync font family to SharedPreferences', () async {
await cubit.syncFontFamily('Helvetica');
final fontFamily =
preferences.getString(KVKeys.kDocumentAppearanceFontFamily);
expect(fontFamily, 'Helvetica');
expect(cubit.state.fontFamily, 'Helvetica');
});
});
}

View File

@ -0,0 +1,34 @@
import 'package:appflowy/workspace/presentation/settings/widgets/levenshtein.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('Levenshtein distance between identical strings', () {
final distance = levenshtein('abc', 'abc');
expect(distance, 0);
});
test('Levenshtein distance between strings of different lengths', () {
final distance = levenshtein('kitten', 'sitting');
expect(distance, 3);
});
test('Levenshtein distance between case-insensitive strings', () {
final distance = levenshtein('Hello', 'hello', caseSensitive: false);
expect(distance, 0);
});
test('Levenshtein distance between strings with substitutions', () {
final distance = levenshtein('kitten', 'smtten');
expect(distance, 2);
});
test('Levenshtein distance between strings with deletions', () {
final distance = levenshtein('kitten', 'kiten');
expect(distance, 1);
});
test('Levenshtein distance between strings with insertions', () {
final distance = levenshtein('kitten', 'kitxten');
expect(distance, 1);
});
}

View File

@ -0,0 +1,46 @@
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mocktail/mocktail.dart';
class MockDocumentAppearanceCubit extends Mock
implements DocumentAppearanceCubit {}
class MockBuildContext extends Mock implements BuildContext {}
void main() {
WidgetsFlutterBinding.ensureInitialized();
group('EditorStyleCustomizer', () {
late EditorStyleCustomizer editorStyleCustomizer;
late MockBuildContext mockBuildContext;
setUp(() {
mockBuildContext = MockBuildContext();
editorStyleCustomizer = EditorStyleCustomizer(
context: mockBuildContext,
padding: EdgeInsets.zero,
);
});
test('baseTextStyle should return the expected TextStyle', () {
const fontFamily = 'Roboto';
final result = editorStyleCustomizer.baseTextStyle(fontFamily);
expect(result, isA<TextStyle>());
expect(result.fontFamily, 'Roboto_regular');
});
test(
'baseTextStyle should return the default TextStyle when an exception occurs',
() {
const garbage = 'Garbage';
final result = editorStyleCustomizer.baseTextStyle(garbage);
expect(result, isA<TextStyle>());
expect(
result.fontFamily,
GoogleFonts.getFont('Poppins').fontFamily,
);
});
});
}

View File

@ -0,0 +1,92 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockAppearanceSettingsCubit extends Mock
implements AppearanceSettingsCubit {}
class MockDocumentAppearanceCubit extends Mock
implements DocumentAppearanceCubit {}
class MockAppearanceSettingsState extends Mock
implements AppearanceSettingsState {}
class MockDocumentAppearance extends Mock implements DocumentAppearance {}
void main() {
late MockAppearanceSettingsCubit appearanceSettingsCubit;
late MockDocumentAppearanceCubit documentAppearanceCubit;
setUp(() {
appearanceSettingsCubit = MockAppearanceSettingsCubit();
when(() => appearanceSettingsCubit.stream).thenAnswer(
(_) => Stream.fromIterable([MockAppearanceSettingsState()]),
);
documentAppearanceCubit = MockDocumentAppearanceCubit();
when(() => documentAppearanceCubit.stream).thenAnswer(
(_) => Stream.fromIterable([MockDocumentAppearance()]),
);
});
testWidgets('ThemeFontFamilySetting updates font family on selection',
(WidgetTester tester) async {
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<AppearanceSettingsCubit>.value(
value: appearanceSettingsCubit,
),
BlocProvider<DocumentAppearanceCubit>.value(
value: documentAppearanceCubit,
),
],
child: MaterialApp(
home: MultiBlocProvider(
providers: [
BlocProvider<AppearanceSettingsCubit>.value(
value: appearanceSettingsCubit,
),
BlocProvider<DocumentAppearanceCubit>.value(
value: documentAppearanceCubit,
),
],
child: const Scaffold(
body: ThemeFontFamilySetting(
currentFontFamily: 'Poppins',
),
),
),
),
),
);
final popover = find.byType(AppFlowyPopover);
await tester.tap(popover);
await tester.pumpAndSettle();
// Verify the initial font family
expect(find.text('Poppins'), findsAtLeastNWidgets(1));
when(() => appearanceSettingsCubit.setFontFamily(any<String>()))
.thenAnswer((_) async {});
verifyNever(() => appearanceSettingsCubit.setFontFamily(any<String>()));
when(() => documentAppearanceCubit.syncFontFamily(any<String>()))
.thenAnswer((_) async {});
verifyNever(() => documentAppearanceCubit.syncFontFamily(any<String>()));
// Tap on a different font family
final abel = find.textContaining('Abel');
await tester.tap(abel);
await tester.pumpAndSettle();
// Verify that the font family is updated
verify(() => appearanceSettingsCubit.setFontFamily(any<String>()))
.called(1);
verify(() => documentAppearanceCubit.syncFontFamily(any<String>()))
.called(1);
});
}