mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
[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:
parent
9fb8f221cf
commit
323cb3b60f
@ -186,6 +186,10 @@
|
||||
"open": "Open Settings"
|
||||
},
|
||||
"appearance": {
|
||||
"fontFamily": {
|
||||
"label": "Font Family",
|
||||
"search": "Search"
|
||||
},
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Light Mode",
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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>(
|
||||
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -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];
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user