mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
commit
01230704ea
@ -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);
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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(),
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user