Reset theme buttons (#3137)

* feat: add translation for tooltip

* feat: add defaults for theme settings

* feat: refactor appearance view so that all theme settings can be reset

* chore: add keys for reset button

* chore: add tests for the reset button

* feat: register appearance test in runner

* chore: remove comment

* feat: add default file for appearance

* chore: move around files

* feat: make reset button respond to hover

* fix: incorrect use of resetTheme

* refactor: use maybeWhen

* fix: icon button style

* fix: rebase errors
This commit is contained in:
Alex Wallen 2023-08-15 21:28:58 -07:00 committed by GitHub
parent f994f50ef9
commit a1f1559936
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 575 additions and 416 deletions

View File

@ -1,5 +1,6 @@
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/settings_appearance.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -41,5 +42,47 @@ void main() {
expect(find.textContaining('Abel'), findsOneWidget);
});
testWidgets('reset the font family', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
final dropDown = find.byKey(ThemeFontFamilySetting.popoverKey);
await tester.tap(dropDown);
await tester.pumpAndSettle();
final textField = find.byKey(ThemeFontFamilySetting.textFieldKey);
await tester.tap(textField);
await tester.pumpAndSettle();
await tester.enterText(textField, 'Abel');
await tester.pumpAndSettle();
final fontFamilyButton = find.byKey(const Key('Abel'));
expect(fontFamilyButton, findsOneWidget);
await tester.tap(fontFamilyButton);
await tester.pumpAndSettle();
// just switch the page and verify that the font family was set after that
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
final resetButton = find.byKey(ThemeFontFamilySetting.resetButtonkey);
await tester.tap(resetButton);
await tester.pumpAndSettle();
// just switch the page and verify that the font family was set after that
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
findsOneWidget,
);
});
});
}

View File

@ -18,6 +18,7 @@ import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'board/board_test_runner.dart' as board_test_runner;
import 'tabs_test.dart' as tabs_test;
import 'hotkeys_test.dart' as hotkeys_test;
import 'appearance_settings_test.dart' as appearance_test_runner;
/// The main task runner for all integration tests in AppFlowy.
///
@ -59,6 +60,9 @@ void main() {
// Others
hotkeys_test.main();
// Appearance integration test
appearance_test_runner.main();
// board_test.main();
// empty_document_test.main();
// smart_menu_test.main();

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -43,6 +44,10 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
}
/// Reset the current user selected theme back to the default
Future<void> resetTheme() =>
setTheme(DefaultAppearanceSettings.kDefaultThemeName);
/// Update the theme mode in the user's settings and emit an updated state.
void setThemeMode(ThemeMode themeMode) {
_setting.themeMode = _themeModeToPB(themeMode);
@ -50,6 +55,10 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
emit(state.copyWith(themeMode: themeMode));
}
/// Resets the current brightness setting
void resetThemeMode() =>
setThemeMode(DefaultAppearanceSettings.kDefaultThemeMode);
/// Toggle the theme mode
void toggleThemeMode() {
final currentThemeMode = state.themeMode;
@ -66,6 +75,10 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
emit(state.copyWith(font: fontFamilyName));
}
/// Resets the current font family for the user preferences
void resetFontFamily() =>
setFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
/// 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) {

View File

@ -0,0 +1,10 @@
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
/// A class for the default appearance settings for the app
class DefaultAppearanceSettings {
static const kDefaultFontFamily = 'Poppins';
static const kDefaultThemeMode = ThemeMode.system;
static const kDefaultThemeName = "Default";
static const kDefaultTheme = BuiltInTheme.defaultTheme;
}

View File

@ -0,0 +1,86 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme_setting_entry_template.dart';
class BrightnessSetting extends StatelessWidget {
final ThemeMode currentThemeMode;
const BrightnessSetting({required this.currentThemeMode, super.key});
@override
Widget build(BuildContext context) => Tooltip(
richMessage: themeModeTooltipTextSpan(
context,
LocaleKeys.settings_appearance_themeMode_label.tr(),
),
child: ThemeSettingEntryTemplateWidget(
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
onResetRequested:
context.read<AppearanceSettingsCubit>().resetThemeMode,
trailing: [
ThemeValueDropDown(
currentValue: _themeModeLabelText(currentThemeMode),
popupBuilder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_themeModeItemButton(context, ThemeMode.light),
_themeModeItemButton(context, ThemeMode.dark),
_themeModeItemButton(context, ThemeMode.system),
],
),
),
],
),
);
TextSpan themeModeTooltipTextSpan(BuildContext context, String hintText) =>
TextSpan(
children: [
TextSpan(
text: "$hintText\n",
),
TextSpan(
text: Platform.isMacOS ? "⌘+Shift+L" : "Ctrl+Shift+L",
),
],
);
Widget _themeModeItemButton(BuildContext context, ThemeMode themeMode) {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(_themeModeLabelText(themeMode)),
rightIcon: currentThemeMode == themeMode
? const FlowySvg(
FlowySvgs.check_s,
)
: null,
onTap: () {
if (currentThemeMode != themeMode) {
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);
}
},
),
);
}
String _themeModeLabelText(ThemeMode themeMode) {
switch (themeMode) {
case (ThemeMode.light):
return LocaleKeys.settings_appearance_themeMode_light.tr();
case (ThemeMode.dark):
return LocaleKeys.settings_appearance_themeMode_dark.tr();
case (ThemeMode.system):
return LocaleKeys.settings_appearance_themeMode_system.tr();
default:
return "";
}
}
}

View File

@ -0,0 +1,175 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ColorSchemeSetting extends StatelessWidget {
const ColorSchemeSetting({
super.key,
required this.currentTheme,
required this.bloc,
});
final String currentTheme;
final DynamicPluginBloc bloc;
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
label: LocaleKeys.settings_appearance_theme.tr(),
onResetRequested: context.read<AppearanceSettingsCubit>().resetTheme,
trailing: [
ColorSchemeUploadOverlayButton(bloc: bloc),
const SizedBox(width: 4),
ColorSchemeUploadPopover(currentTheme: currentTheme, bloc: bloc),
],
);
}
}
class ColorSchemeUploadOverlayButton extends StatelessWidget {
const ColorSchemeUploadOverlayButton({super.key, required this.bloc});
final DynamicPluginBloc bloc;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
width: 24,
icon: const FlowySvg(
FlowySvgs.folder_m,
),
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
onPressed: () => Dialogs.show(
context,
child: BlocProvider<DynamicPluginBloc>.value(
value: bloc,
child: const FlowyDialog(
constraints: BoxConstraints(maxHeight: 300),
child: ThemeUploadWidget(),
),
),
).then((value) {
if (value == null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText.medium(
color: Theme.of(context).colorScheme.onPrimary,
LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(),
),
),
);
}),
);
}
}
class ColorSchemeUploadPopover extends StatelessWidget {
const ColorSchemeUploadPopover({
super.key,
required this.currentTheme,
required this.bloc,
});
final String currentTheme;
final DynamicPluginBloc bloc;
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
child: FlowyTextButton(
currentTheme,
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
onPressed: () {},
),
popupBuilder: (BuildContext context) {
return IntrinsicWidth(
child: BlocBuilder<DynamicPluginBloc, DynamicPluginState>(
bloc: bloc..add(DynamicPluginEvent.load()),
buildWhen: (previous, current) => current is Ready,
builder: (context, state) {
return state.maybeWhen(
ready: (plugins) => Column(
mainAxisSize: MainAxisSize.min,
children: [
...AppTheme.builtins
.map(
(theme) => _themeItemButton(context, theme.themeName),
)
.toList(),
if (plugins.isNotEmpty) ...[
const Divider(),
...plugins
.map((plugin) => plugin.theme)
.whereType<AppTheme>()
.map(
(theme) => _themeItemButton(
context,
theme.themeName,
false,
),
)
.toList()
],
],
),
orElse: () => const SizedBox.shrink(),
);
},
),
);
},
);
}
Widget _themeItemButton(
BuildContext context,
String theme, [
bool isBuiltin = true,
]) {
return SizedBox(
height: 32,
child: Row(
children: [
Expanded(
child: FlowyButton(
text: FlowyText.medium(theme),
rightIcon: currentTheme == theme
? const FlowySvg(
FlowySvgs.check_s,
)
: null,
onTap: () {
if (currentTheme != theme) {
context.read<AppearanceSettingsCubit>().setTheme(theme);
}
},
),
),
if (!isBuiltin)
FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.close_s,
),
width: 20,
onPressed: () =>
bloc.add(DynamicPluginEvent.removePlugin(name: theme)),
)
],
),
);
}
}

View File

@ -0,0 +1,144 @@
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/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.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';
import 'theme_setting_entry_template.dart';
class ThemeFontFamilySetting extends StatefulWidget {
const ThemeFontFamilySetting({
super.key,
required this.currentFontFamily,
});
final String currentFontFamily;
static Key textFieldKey = const Key('FontFamilyTextField');
static Key resetButtonkey = const Key('FontFamilyResetButton');
static Key popoverKey = const Key('FontFamilyPopover');
@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 ThemeSettingEntryTemplateWidget(
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
onResetRequested: () {
context.read<AppearanceSettingsCubit>().resetFontFamily();
context
.read<DocumentAppearanceCubit>()
.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;
},
),
),
),
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(
key: Key(buttonFontFamily),
onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium(
parseFontFamilyName(style.fontFamily!),
fontFamily: style.fontFamily!,
),
rightIcon:
buttonFontFamily == parseFontFamilyName(widget.currentFontFamily)
? const FlowySvg(
FlowySvgs.check_s,
)
: null,
onTap: () {
if (parseFontFamilyName(widget.currentFontFamily) !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()
.setFontFamily(parseFontFamilyName(style.fontFamily!));
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(parseFontFamilyName(style.fontFamily!));
}
},
),
);
}
}

View File

@ -0,0 +1,3 @@
export 'brightness_setting.dart';
export 'font_family_setting.dart';
export 'color_scheme.dart';

View File

@ -0,0 +1,89 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class ThemeSettingEntryTemplateWidget extends StatelessWidget {
const ThemeSettingEntryTemplateWidget({
super.key,
this.resetButtonKey,
required this.label,
this.trailing,
this.onResetRequested,
});
final String label;
final Key? resetButtonKey;
final List<Widget>? trailing;
final void Function()? onResetRequested;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText.medium(
label,
overflow: TextOverflow.ellipsis,
),
),
if (trailing != null) ...trailing!,
if (onResetRequested != null)
FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
key: resetButtonKey,
width: 24,
icon: FlowySvg(
FlowySvgs.reload_s,
color: Theme.of(context).iconTheme.color,
),
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
tooltipText: LocaleKeys.settings_appearance_resetSetting.tr(),
onPressed: onResetRequested,
),
],
);
}
}
class ThemeValueDropDown extends StatefulWidget {
const ThemeValueDropDown({
super.key,
required this.currentValue,
required this.popupBuilder,
this.popoverKey,
this.onClose,
});
final String currentValue;
final Key? popoverKey;
final Widget Function(BuildContext) popupBuilder;
final void Function()? onClose;
@override
State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
}
class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
key: widget.popoverKey,
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

@ -1,23 +1,9 @@
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/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/plugins/bloc/dynamic_plugin_bloc.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
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';
import 'dart:io';
import 'settings_appearance/settings_appearance.dart';
class SettingsAppearanceView extends StatelessWidget {
const SettingsAppearanceView({Key? key}) : super(key: key);
@ -32,7 +18,9 @@ class SettingsAppearanceView extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BrightnessSetting(currentThemeMode: state.themeMode),
BrightnessSetting(
currentThemeMode: state.themeMode,
),
ColorSchemeSetting(
currentTheme: state.appTheme.themeName,
bloc: context.read<DynamicPluginBloc>(),
@ -48,400 +36,3 @@ class SettingsAppearanceView extends StatelessWidget {
);
}
}
class ColorSchemeSetting extends StatelessWidget {
const ColorSchemeSetting({
super.key,
required this.currentTheme,
required this.bloc,
});
final String currentTheme;
final DynamicPluginBloc bloc;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys.settings_appearance_theme.tr(),
overflow: TextOverflow.ellipsis,
),
),
ThemeUploadOverlayButton(bloc: bloc),
const SizedBox(width: 4),
ThemeSelectionPopover(currentTheme: currentTheme, bloc: bloc),
],
);
}
}
class ThemeUploadOverlayButton extends StatelessWidget {
const ThemeUploadOverlayButton({super.key, required this.bloc});
final DynamicPluginBloc bloc;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
width: 24,
icon: const FlowySvg(FlowySvgs.folder_m),
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
onPressed: () => Dialogs.show(
context,
child: BlocProvider<DynamicPluginBloc>.value(
value: bloc,
child: const FlowyDialog(
constraints: BoxConstraints(maxHeight: 300),
child: ThemeUploadWidget(),
),
),
).then((value) {
if (value == null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: FlowyText.medium(
color: Theme.of(context).colorScheme.onPrimary,
LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(),
),
),
);
}),
);
}
}
class ThemeSelectionPopover extends StatelessWidget {
const ThemeSelectionPopover({
super.key,
required this.currentTheme,
required this.bloc,
});
final String currentTheme;
final DynamicPluginBloc bloc;
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
child: FlowyTextButton(
currentTheme,
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
onPressed: () {},
),
popupBuilder: (BuildContext context) {
return IntrinsicWidth(
child: BlocBuilder<DynamicPluginBloc, DynamicPluginState>(
bloc: bloc..add(DynamicPluginEvent.load()),
buildWhen: (previous, current) => current is Ready,
builder: (context, state) {
return state.when(
uninitialized: () => const SizedBox.shrink(),
processing: () => const SizedBox.shrink(),
compilationFailure: (message) => const SizedBox.shrink(),
deletionFailure: (message) => const SizedBox.shrink(),
deletionSuccess: () => const SizedBox.shrink(),
compilationSuccess: () => const SizedBox.shrink(),
ready: (plugins) => Column(
mainAxisSize: MainAxisSize.min,
children: [
...AppTheme.builtins
.map(
(theme) => _themeItemButton(context, theme.themeName),
)
.toList(),
if (plugins.isNotEmpty) ...[
const Divider(),
...plugins
.map((plugin) => plugin.theme)
.whereType<AppTheme>()
.map(
(theme) => _themeItemButton(
context,
theme.themeName,
false,
),
)
.toList()
],
],
),
);
},
),
);
},
);
}
Widget _themeItemButton(
BuildContext context,
String theme, [
bool isBuiltin = true,
]) {
return SizedBox(
height: 32,
child: Row(
children: [
Expanded(
child: FlowyButton(
text: FlowyText.medium(theme),
rightIcon: currentTheme == theme
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (currentTheme != theme) {
context.read<AppearanceSettingsCubit>().setTheme(theme);
}
},
),
),
if (!isBuiltin)
FlowyIconButton(
icon: const FlowySvg(FlowySvgs.close_m),
width: 20,
onPressed: () =>
bloc.add(DynamicPluginEvent.removePlugin(name: theme)),
)
],
),
);
}
}
class BrightnessSetting extends StatelessWidget {
final ThemeMode currentThemeMode;
const BrightnessSetting({required this.currentThemeMode, super.key});
@override
Widget build(BuildContext context) => Tooltip(
richMessage: themeModeTooltipTextSpan(
context,
LocaleKeys.settings_appearance_themeMode_label.tr(),
),
child: 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),
],
),
),
);
TextSpan themeModeTooltipTextSpan(BuildContext context, String hintText) =>
TextSpan(
children: [
TextSpan(
text: "$hintText\n",
),
TextSpan(
text: Platform.isMacOS ? "⌘+Shift+L" : "Ctrl+Shift+L",
),
],
);
Widget _themeModeItemButton(BuildContext context, ThemeMode themeMode) {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(_themeModeLabelText(themeMode)),
rightIcon: currentThemeMode == themeMode
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (currentThemeMode != themeMode) {
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);
}
},
),
);
}
String _themeModeLabelText(ThemeMode themeMode) {
switch (themeMode) {
case (ThemeMode.light):
return LocaleKeys.settings_appearance_themeMode_light.tr();
case (ThemeMode.dark):
return LocaleKeys.settings_appearance_themeMode_dark.tr();
case (ThemeMode.system):
return LocaleKeys.settings_appearance_themeMode_system.tr();
default:
return "";
}
}
}
class ThemeFontFamilySetting extends StatefulWidget {
const ThemeFontFamilySetting({
super.key,
required this.currentFontFamily,
});
final String currentFontFamily;
static Key textFieldKey = const Key('FontFamilyTextField');
static Key popoverKey = const Key('FontFamilyPopover');
@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(
popoverKey: ThemeFontFamilySetting.popoverKey,
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(
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,
);
},
),
],
),
);
}
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(
key: Key(buttonFontFamily),
onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium(
parseFontFamilyName(style.fontFamily!),
fontFamily: style.fontFamily!,
),
rightIcon:
buttonFontFamily == parseFontFamilyName(widget.currentFontFamily)
? const FlowySvg(FlowySvgs.check_s)
: 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.popoverKey,
this.onClose,
});
final String label;
final String currentValue;
final Key? popoverKey;
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(
key: widget.popoverKey,
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

@ -1,4 +1,4 @@
import 'package:appflowy/workspace/presentation/settings/widgets/levenshtein.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/levenshtein.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {

View File

@ -1,6 +1,6 @@
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:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

View File

@ -233,6 +233,7 @@
"openHistoricalUser": "Click to open the anonymous account"
},
"appearance": {
"resetSetting": "Reset this setting",
"fontFamily": {
"label": "Font Family",
"search": "Search"