mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
f994f50ef9
commit
a1f1559936
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
@ -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)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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!));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export 'brightness_setting.dart';
|
||||
export 'font_family_setting.dart';
|
||||
export 'color_scheme.dart';
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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';
|
||||
|
@ -233,6 +233,7 @@
|
||||
"openHistoricalUser": "Click to open the anonymous account"
|
||||
},
|
||||
"appearance": {
|
||||
"resetSetting": "Reset this setting",
|
||||
"fontFamily": {
|
||||
"label": "Font Family",
|
||||
"search": "Search"
|
||||
|
Loading…
Reference in New Issue
Block a user