Merge pull request #1189 from AppFlowy-IO/refactor/setting_documentation

refactor: add setting documentation and support save key/value in set…
This commit is contained in:
Nathan.fooo 2022-09-28 13:26:21 +08:00 committed by GitHub
commit 01230704ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 127 additions and 72 deletions

View File

@ -104,8 +104,8 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildBoard(BuildContext context) { Widget _buildBoard(BuildContext context) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, AppTheme>( child: Selector<AppearanceSetting, AppTheme>(
selector: (ctx, notifier) => notifier.theme, selector: (ctx, notifier) => notifier.theme,
builder: (ctx, theme, child) => Expanded( builder: (ctx, theme, child) => Expanded(
child: AppFlowyBoard( child: AppFlowyBoard(
@ -331,8 +331,8 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
); );
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, AppTheme>( child: Selector<AppearanceSetting, AppTheme>(
selector: (ctx, notifier) => notifier.theme, selector: (ctx, notifier) => notifier.theme,
builder: (ctx, theme, child) { builder: (ctx, theme, child) {
return BoardToolbar(toolbarContext: toolbarContext); return BoardToolbar(toolbarContext: toolbarContext);

View File

@ -131,8 +131,8 @@ class DocumentShareButton extends StatelessWidget {
child: BlocBuilder<DocShareBloc, DocShareState>( child: BlocBuilder<DocShareBloc, DocShareState>(
builder: (context, state) { builder: (context, state) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, Locale>( child: Selector<AppearanceSetting, Locale>(
selector: (ctx, notifier) => notifier.locale, selector: (ctx, notifier) => notifier.locale,
builder: (ctx, _, child) => ConstrainedBox( builder: (ctx, _, child) => ConstrainedBox(
constraints: const BoxConstraints.expand( constraints: const BoxConstraints.expand(

View File

@ -134,7 +134,7 @@ class _DocumentPageState extends State<DocumentPage> {
Widget _renderToolbar(quill.QuillController controller) { Widget _renderToolbar(quill.QuillController controller) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: EditorToolbar.basic( child: EditorToolbar.basic(
controller: controller, controller: controller,
), ),

View File

@ -33,8 +33,8 @@ class MenuTrash extends StatelessWidget {
Widget _render(BuildContext context) { Widget _render(BuildContext context) {
return Row(children: [ return Row(children: [
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, AppTheme>( child: Selector<AppearanceSetting, AppTheme>(
selector: (ctx, notifier) => notifier.theme, selector: (ctx, notifier) => notifier.theme,
builder: (ctx, theme, child) => SizedBox( builder: (ctx, theme, child) => SizedBox(
width: 16, width: 16,
@ -44,8 +44,8 @@ class MenuTrash extends StatelessWidget {
), ),
const HSpace(6), const HSpace(6),
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, Locale>( child: Selector<AppearanceSetting, Locale>(
selector: (ctx, notifier) => notifier.locale, selector: (ctx, notifier) => notifier.locale,
builder: (ctx, _, child) => builder: (ctx, _, child) =>
FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12), FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12),

View File

@ -17,8 +17,8 @@ class InitAppWidgetTask extends LaunchTask {
@override @override
Future<void> initialize(LaunchContext context) async { Future<void> initialize(LaunchContext context) async {
final widget = context.getIt<EntryPoint>().create(); final widget = context.getIt<EntryPoint>().create();
final setting = await UserSettingsService().getAppearanceSettings(); final setting = await SettingsFFIService().getAppearanceSetting();
final settingModel = AppearanceSettingModel(setting); final settingModel = AppearanceSetting(setting);
final app = ApplicationWidget( final app = ApplicationWidget(
settingModel: settingModel, settingModel: settingModel,
child: widget, child: widget,
@ -58,7 +58,7 @@ class InitAppWidgetTask extends LaunchTask {
class ApplicationWidget extends StatelessWidget { class ApplicationWidget extends StatelessWidget {
final Widget child; final Widget child;
final AppearanceSettingModel settingModel; final AppearanceSetting settingModel;
const ApplicationWidget({ const ApplicationWidget({
Key? key, Key? key,
@ -75,10 +75,10 @@ class ApplicationWidget extends StatelessWidget {
const minWidth = 600.0; const minWidth = 600.0;
setWindowMinSize(const Size(minWidth, minWidth / ratio)); setWindowMinSize(const Size(minWidth, minWidth / ratio));
settingModel.readLocaleWhenAppLaunch(context); settingModel.readLocaleWhenAppLaunch(context);
AppTheme theme = context.select<AppearanceSettingModel, AppTheme>( AppTheme theme = context.select<AppearanceSetting, AppTheme>(
(value) => value.theme, (value) => value.theme,
); );
Locale locale = context.select<AppearanceSettingModel, Locale>( Locale locale = context.select<AppearanceSetting, Locale>(
(value) => value.locale, (value) => value.locale,
); );

View File

@ -4,8 +4,8 @@ import 'package:flowy_sdk/flowy_sdk.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart';
class UserSettingsService { class SettingsFFIService {
Future<AppearanceSettingsPB> getAppearanceSettings() async { Future<AppearanceSettingsPB> getAppearanceSetting() async {
final result = await UserEventGetAppearanceSetting().send(); final result = await UserEventGetAppearanceSetting().send();
return result.fold( return result.fold(
@ -18,7 +18,8 @@ class UserSettingsService {
); );
} }
Future<Either<Unit, FlowyError>> setAppearanceSettings(AppearanceSettingsPB settings) { Future<Either<Unit, FlowyError>> setAppearanceSetting(
return UserEventSetAppearanceSetting(settings).send(); AppearanceSettingsPB setting) {
return UserEventSetAppearanceSetting(setting).send();
} }
} }

View File

@ -8,72 +8,114 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
class AppearanceSettingModel extends ChangeNotifier with EquatableMixin { /// [AppearanceSetting] is used to modify the appear setting of AppFlowy application. Including the [Locale], [AppTheme], etc.
AppearanceSettingsPB setting; class AppearanceSetting extends ChangeNotifier with EquatableMixin {
final AppearanceSettingsPB _setting;
AppTheme _theme; AppTheme _theme;
Locale _locale; Locale _locale;
Timer? _saveOperation; Timer? _debounceSaveOperation;
AppearanceSettingModel(this.setting) AppearanceSetting(AppearanceSettingsPB setting)
: _theme = AppTheme.fromName(name: setting.theme), : _setting = setting,
_locale = _theme = AppTheme.fromName(name: setting.theme),
Locale(setting.locale.languageCode, setting.locale.countryCode); _locale = Locale(
setting.locale.languageCode,
setting.locale.countryCode,
);
/// Returns the current [AppTheme]
AppTheme get theme => _theme; AppTheme get theme => _theme;
/// Returns the current [Locale]
Locale get locale => _locale; Locale get locale => _locale;
Future<void> save() async { /// Updates the current theme and notify the listeners the theme was changed.
_saveOperation?.cancel(); /// Do nothing if the passed in themeType equal to the current theme type.
_saveOperation = Timer(const Duration(seconds: 2), () async { ///
await UserSettingsService().setAppearanceSettings(setting); void setTheme(ThemeType themeType) {
}); if (_theme.ty == themeType) {
return;
} }
@override
List<Object> get props {
return [setting.hashCode];
}
void swapTheme() {
final themeType =
(_theme.ty == ThemeType.light ? ThemeType.dark : ThemeType.light);
if (_theme.ty != themeType) {
_theme = AppTheme.fromType(themeType); _theme = AppTheme.fromType(themeType);
setting.theme = themeTypeToString(themeType); _setting.theme = themeTypeToString(themeType);
_saveAppearSetting();
notifyListeners(); notifyListeners();
save();
}
} }
/// Updates the current locale and notify the listeners the locale was changed
/// Fallback to [en] locale If the newLocale is not supported.
///
void setLocale(BuildContext context, Locale newLocale) { void setLocale(BuildContext context, Locale newLocale) {
if (!context.supportedLocales.contains(newLocale)) { if (!context.supportedLocales.contains(newLocale)) {
Log.warn("Unsupported locale: $newLocale"); Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
newLocale = const Locale('en'); newLocale = const Locale('en');
Log.debug("Fallback to locale: $newLocale");
} }
context.setLocale(newLocale); context.setLocale(newLocale);
if (_locale != newLocale) { if (_locale != newLocale) {
_locale = newLocale; _locale = newLocale;
setting.locale.languageCode = _locale.languageCode; _setting.locale.languageCode = _locale.languageCode;
setting.locale.countryCode = _locale.countryCode ?? ""; _setting.locale.countryCode = _locale.countryCode ?? "";
_saveAppearSetting();
notifyListeners(); notifyListeners();
save();
} }
} }
/// Saves key/value setting to disk.
/// Removes the key if the passed in value is null
void setKeyValue(String key, String? value) {
if (key.isEmpty) {
Log.warn("The key should not be empty");
return;
}
if (value == null) {
_setting.settingKeyValue.remove(key);
}
if (_setting.settingKeyValue[key] != value) {
if (value == null) {
_setting.settingKeyValue.remove(key);
} else {
_setting.settingKeyValue[key] = value;
}
_saveAppearSetting();
notifyListeners();
}
}
/// Called when the application launch.
/// Uses the device locale when open the application for the first time
void readLocaleWhenAppLaunch(BuildContext context) { void readLocaleWhenAppLaunch(BuildContext context) {
if (setting.resetAsDefault) { if (_setting.resetToDefault) {
setting.resetAsDefault = false; _setting.resetToDefault = false;
save(); _saveAppearSetting();
setLocale(context, context.deviceLocale); setLocale(context, context.deviceLocale);
return; return;
} }
// when opening app the first time
setLocale(context, _locale); setLocale(context, _locale);
} }
Future<void> _saveAppearSetting() async {
_debounceSaveOperation?.cancel();
_debounceSaveOperation = Timer(
const Duration(seconds: 1),
() {
SettingsFFIService().setAppearanceSetting(_setting).then((result) {
result.fold((l) => null, (error) => Log.error(error));
});
},
);
}
@override
List<Object> get props {
return [_setting.hashCode];
}
} }

View File

@ -89,8 +89,7 @@ class _MenuAppState extends State<MenuApp> {
hasIcon: false, hasIcon: false,
), ),
header: ChangeNotifierProvider.value( header: ChangeNotifierProvider.value(
value: value: Provider.of<AppearanceSetting>(context, listen: true),
Provider.of<AppearanceSettingModel>(context, listen: true),
child: MenuAppHeader(widget.app), child: MenuAppHeader(widget.app),
), ),
expanded: ViewSection(appViewData: viewDataContext), expanded: ViewSection(appViewData: viewDataContext),

View File

@ -33,8 +33,7 @@ class SettingsDialog extends StatelessWidget {
..add(const SettingsDialogEvent.initial()), ..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>( child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
builder: (context, state) => ChangeNotifierProvider.value( builder: (context, state) => ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, value: Provider.of<AppearanceSetting>(context, listen: true),
listen: true),
child: FlowyDialog( child: FlowyDialog(
title: Text( title: Text(
LocaleKeys.settings_title.tr(), LocaleKeys.settings_title.tr(),

View File

@ -13,7 +13,7 @@ class SettingsAppearanceView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.read<AppTheme>();
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
@ -30,9 +30,7 @@ class SettingsAppearanceView extends StatelessWidget {
), ),
Toggle( Toggle(
value: theme.isDark, value: theme.isDark,
onChanged: (val) { onChanged: (_) => setTheme(context),
context.read<AppearanceSettingModel>().swapTheme();
},
style: ToggleStyle.big(theme), style: ToggleStyle.big(theme),
), ),
Text( Text(
@ -48,4 +46,13 @@ class SettingsAppearanceView extends StatelessWidget {
), ),
); );
} }
void setTheme(BuildContext context) {
final theme = context.read<AppTheme>();
if (theme.isDark) {
context.read<AppearanceSetting>().setTheme(ThemeType.light);
} else {
context.read<AppearanceSetting>().setTheme(ThemeType.dark);
}
}
} }

View File

@ -13,7 +13,7 @@ class SettingsLanguageView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
context.watch<AppTheme>(); context.watch<AppTheme>();
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -43,7 +43,8 @@ class LanguageSelectorDropdown extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<LanguageSelectorDropdown> createState() => _LanguageSelectorDropdownState(); State<LanguageSelectorDropdown> createState() =>
_LanguageSelectorDropdownState();
} }
class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> { class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
@ -77,10 +78,10 @@ class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
), ),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<Locale>( child: DropdownButton<Locale>(
value: context.read<AppearanceSettingModel>().locale, value: context.read<AppearanceSetting>().locale,
onChanged: (val) { onChanged: (val) {
setState(() { setState(() {
context.read<AppearanceSettingModel>().setLocale(context, val!); context.read<AppearanceSetting>().setLocale(context, val!);
}); });
}, },
icon: const Visibility( icon: const Visibility(

View File

@ -1,5 +1,6 @@
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(ProtoBuf, Default, Debug, Clone)] #[derive(ProtoBuf, Default, Debug, Clone)]
pub struct UserPreferencesPB { pub struct UserPreferencesPB {
@ -21,7 +22,11 @@ pub struct AppearanceSettingsPB {
#[pb(index = 3)] #[pb(index = 3)]
#[serde(default = "DEFAULT_RESET_VALUE")] #[serde(default = "DEFAULT_RESET_VALUE")]
pub reset_as_default: bool, pub reset_to_default: bool,
#[pb(index = 4)]
#[serde(default)]
pub setting_key_value: HashMap<String, String>,
} }
const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT; const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT;
@ -52,7 +57,8 @@ impl std::default::Default for AppearanceSettingsPB {
AppearanceSettingsPB { AppearanceSettingsPB {
theme: APPEARANCE_DEFAULT_THEME.to_owned(), theme: APPEARANCE_DEFAULT_THEME.to_owned(),
locale: LocaleSettingsPB::default(), locale: LocaleSettingsPB::default(),
reset_as_default: APPEARANCE_RESET_AS_DEFAULT, reset_to_default: APPEARANCE_RESET_AS_DEFAULT,
setting_key_value: HashMap::default(),
} }
} }
} }