Migrate ConfigStore to mobx (#270)

* Migrate ConfigStore to mobx

* Add tests

* Remove provider imports

* Mock shared_preferences

* Reorganize saving in ConfigStore
This commit is contained in:
Marcin Wojnarowski 2021-10-31 12:52:58 +01:00 committed by GitHub
parent 8f34591111
commit 760565384f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 497 additions and 245 deletions

View File

@ -19,7 +19,6 @@ linter:
- avoid_setters_without_getters
- avoid_single_cascade_in_expression_statements
- avoid_type_to_string
- avoid_types_on_closure_parameters
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- avoid_void_async
@ -105,13 +104,13 @@ linter:
- unrelated_type_equality_checks
- use_full_hex_values_for_flutter_colors
- use_is_even_rather_than_modulo
- use_test_throws_matchers
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_test_throws_matchers
- use_to_and_as_if_applicable
- void_checks
- use_named_constants
analyzer:
exclude:

View File

@ -1,29 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'hooks/stores.dart';
import 'l10n/l10n.dart';
import 'pages/home_page.dart';
import 'stores/config_store.dart';
import 'theme.dart';
import 'util/observer_consumers.dart';
class MyApp extends HookWidget {
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
final configStore = useConfigStore();
return KeyboardDismisser(
child: MaterialApp(
title: 'lemmur',
supportedLocales: L10n.supportedLocales,
localizationsDelegates: L10n.localizationsDelegates,
themeMode: configStore.theme,
darkTheme: configStore.amoledDarkMode ? amoledTheme : darkTheme,
locale: configStore.locale,
theme: lightTheme,
home: const HomePage(),
child: ObserverBuilder<ConfigStore>(
builder: (context, store) => MaterialApp(
title: 'lemmur',
supportedLocales: L10n.supportedLocales,
localizationsDelegates: L10n.localizationsDelegates,
themeMode: store.theme,
darkTheme: store.amoledDarkMode ? amoledTheme : darkTheme,
locale: store.locale,
theme: lightTheme,
home: const HomePage(),
),
),
);
}

View File

@ -1,13 +1,23 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import '../stores/accounts_store.dart';
import '../stores/config_store.dart';
AccountsStore useAccountsStore() => useContext().watch<AccountsStore>();
T useAccountsStoreSelect<T>(T selector(AccountsStore store)) =>
useContext().select<AccountsStore, T>(selector);
ConfigStore useConfigStore() => useContext().watch<ConfigStore>();
T useConfigStoreSelect<T>(T selector(ConfigStore store)) =>
useContext().select<ConfigStore, T>(selector);
V useStore<S extends Store, V>(V Function(S value) selector) {
final context = useContext();
final store = context.read<S>();
final state = useState(selector(store));
useEffect(() {
return autorun((_) {
state.value = selector(store);
});
}, []);
return state.value;
}

View File

@ -1,12 +1,16 @@
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
export 'package:flutter_gen/gen_l10n/l10n.dart';
export 'l10n_api.dart';
export 'l10n_from_string.dart';
abstract class LocaleSerde {
static Locale fromJson(String? json) {
class LocaleConverter implements JsonConverter<Locale, String?> {
const LocaleConverter();
@override
Locale fromJson(String? json) {
if (json == null) return const Locale('en');
final lang = json.split('-');
@ -14,7 +18,8 @@ abstract class LocaleSerde {
return Locale(lang[0], lang.length > 1 ? lang[1] : null);
}
static String toJson(Locale locale) => locale.toLanguageTag();
@override
String? toJson(Locale locale) => locale.toLanguageTag();
}
const _languageNames = {

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart';
import 'app_config.dart';
@ -14,16 +15,17 @@ Future<void> mainCommon(AppConfig appConfig) async {
WidgetsFlutterBinding.ensureInitialized();
final logConsoleStore = LogConsolePageStore();
final sharedPrefs = await SharedPreferences.getInstance();
_setupLogger(appConfig, logConsoleStore);
final configStore = await ConfigStore.load();
final configStore = ConfigStore.load(sharedPrefs);
final accountsStore = await AccountsStore.load();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: configStore),
Provider.value(value: configStore),
ChangeNotifierProvider.value(value: accountsStore),
Provider.value(value: logConsoleStore),
],

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../comment_tree.dart';
import '../../l10n/l10n.dart';

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:provider/provider.dart';
import '../../hooks/logged_in_action.dart';
import '../../stores/accounts_store.dart';

View File

@ -8,6 +8,7 @@ import '../hooks/infinite_scroll.dart';
import '../hooks/memo_future.dart';
import '../hooks/stores.dart';
import '../l10n/l10n.dart';
import '../stores/config_store.dart';
import '../util/goto.dart';
import '../widgets/bottom_modal.dart';
import '../widgets/cached_network_image.dart';
@ -25,7 +26,7 @@ class HomeTab extends HookWidget {
Widget build(BuildContext context) {
final accStore = useAccountsStore();
final defaultListingType =
useConfigStoreSelect((configStore) => configStore.defaultListingType);
useStore((ConfigStore store) => store.defaultListingType);
final selectedList = useState(_SelectedList(
listingType: accStore.hasNoAccount &&
defaultListingType == PostListingType.subscribed

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import '../../util/observer_consumers.dart';
import '../../widgets/bottom_safe.dart';

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
import '../../../util/async_store_listener.dart';
import '../../../util/extensions/api.dart';

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
import '../../../hooks/stores.dart';
import '../../../l10n/l10n_from_string.dart';

View File

@ -6,7 +6,10 @@ import 'package:lemmy_api_client/v3.dart';
import '../../hooks/stores.dart';
import '../../l10n/l10n.dart';
import '../../stores/config_store.dart';
import '../../util/async_store_listener.dart';
import '../../util/goto.dart';
import '../../util/observer_consumers.dart';
import '../../widgets/about_tile.dart';
import '../../widgets/bottom_modal.dart';
import '../../widgets/radio_picker.dart';
@ -66,112 +69,112 @@ class SettingsPage extends HookWidget {
}
/// Settings for theme color, AMOLED switch
class AppearanceConfigPage extends HookWidget {
class AppearanceConfigPage extends StatelessWidget {
const AppearanceConfigPage();
@override
Widget build(BuildContext context) {
final configStore = useConfigStore();
return Scaffold(
appBar: AppBar(title: const Text('Appearance')),
body: ListView(
children: [
const _SectionHeading('Theme'),
for (final theme in ThemeMode.values)
RadioListTile<ThemeMode>(
value: theme,
title: Text(describeEnum(theme)),
groupValue: configStore.theme,
onChanged: (selected) {
if (selected != null) configStore.theme = selected;
body: ObserverBuilder<ConfigStore>(
builder: (context, store) => ListView(
children: [
const _SectionHeading('Theme'),
for (final theme in ThemeMode.values)
RadioListTile<ThemeMode>(
value: theme,
title: Text(describeEnum(theme)),
groupValue: store.theme,
onChanged: (selected) {
if (selected != null) store.theme = selected;
},
),
SwitchListTile.adaptive(
title: const Text('AMOLED dark mode'),
value: store.amoledDarkMode,
onChanged: (checked) {
store.amoledDarkMode = checked;
},
),
SwitchListTile.adaptive(
title: const Text('AMOLED dark mode'),
value: configStore.amoledDarkMode,
onChanged: (checked) {
configStore.amoledDarkMode = checked;
},
),
const SizedBox(height: 12),
const _SectionHeading('Other'),
SwitchListTile.adaptive(
title: Text(L10n.of(context)!.show_avatars),
value: configStore.showAvatars,
onChanged: (checked) {
configStore.showAvatars = checked;
},
),
SwitchListTile.adaptive(
title: const Text('Show scores'),
value: configStore.showScores,
onChanged: (checked) {
configStore.showScores = checked;
},
),
],
const SizedBox(height: 12),
const _SectionHeading('Other'),
SwitchListTile.adaptive(
title: Text(L10n.of(context)!.show_avatars),
value: store.showAvatars,
onChanged: (checked) {
store.showAvatars = checked;
},
),
SwitchListTile.adaptive(
title: const Text('Show scores'),
value: store.showScores,
onChanged: (checked) {
store.showScores = checked;
},
),
],
),
),
);
}
}
/// General settings
class GeneralConfigPage extends HookWidget {
class GeneralConfigPage extends StatelessWidget {
const GeneralConfigPage();
@override
Widget build(BuildContext context) {
final configStore = useConfigStore();
return Scaffold(
appBar: AppBar(title: const Text('General')),
body: ListView(
children: [
ListTile(
title: Text(L10n.of(context)!.sort_type),
trailing: SizedBox(
width: 120,
child: RadioPicker<SortType>(
values: SortType.values,
groupValue: configStore.defaultSortType,
onChanged: (value) => configStore.defaultSortType = value,
mapValueToString: (value) => value.value,
body: ObserverBuilder<ConfigStore>(
builder: (context, store) => ListView(
children: [
ListTile(
title: Text(L10n.of(context)!.sort_type),
trailing: SizedBox(
width: 120,
child: RadioPicker<SortType>(
values: SortType.values,
groupValue: store.defaultSortType,
onChanged: (value) => store.defaultSortType = value,
mapValueToString: (value) => value.value,
),
),
),
),
ListTile(
title: Text(L10n.of(context)!.type),
trailing: SizedBox(
width: 120,
child: RadioPicker<PostListingType>(
values: const [
PostListingType.all,
PostListingType.local,
PostListingType.subscribed,
],
groupValue: configStore.defaultListingType,
onChanged: (value) => configStore.defaultListingType = value,
mapValueToString: (value) => value.value,
ListTile(
title: Text(L10n.of(context)!.type),
trailing: SizedBox(
width: 120,
child: RadioPicker<PostListingType>(
values: const [
PostListingType.all,
PostListingType.local,
PostListingType.subscribed,
],
groupValue: store.defaultListingType,
onChanged: (value) => store.defaultListingType = value,
mapValueToString: (value) => value.value,
),
),
),
),
ListTile(
title: Text(L10n.of(context)!.language),
trailing: SizedBox(
width: 120,
child: RadioPicker<Locale>(
title: 'Choose language',
groupValue: configStore.locale,
values: L10n.supportedLocales,
mapValueToString: (locale) => locale.languageName,
onChanged: (selected) {
configStore.locale = selected;
},
ListTile(
title: Text(L10n.of(context)!.language),
trailing: SizedBox(
width: 120,
child: RadioPicker<Locale>(
title: 'Choose language',
groupValue: store.locale,
values: L10n.supportedLocales,
mapValueToString: (locale) => locale.languageName,
onChanged: (selected) {
store.locale = selected;
},
),
),
),
),
],
],
),
),
);
}
@ -191,8 +194,6 @@ class _AccountOptions extends HookWidget {
@override
Widget build(BuildContext context) {
final accountsStore = useAccountsStore();
final configStore = useConfigStore();
final importLoading = useState(false);
Future<void> removeUserDialog(String instanceHost, String username) async {
if (await showDialog<bool>(
@ -235,34 +236,28 @@ class _AccountOptions extends HookWidget {
title: const Text('Remove account'),
onTap: () => removeUserDialog(instanceHost, username),
),
ListTile(
leading: importLoading.value
? const SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator.adaptive(),
)
: const Icon(Icons.cloud_download),
title: const Text('Import settings to lemmur'),
onTap: () async {
importLoading.value = true;
try {
await configStore.importLemmyUserSettings(
accountsStore.userDataFor(instanceHost, username)!.jwt,
);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Import successful'),
));
} on Exception catch (err) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(err.toString()),
));
} finally {
AsyncStoreListener(
asyncStore: context.read<ConfigStore>().lemmyImportState,
successMessageBuilder: (context, data) => 'Import successful',
child: ObserverBuilder<ConfigStore>(
builder: (context, store) => ListTile(
leading: store.lemmyImportState.isLoading
? const SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator.adaptive(),
)
: const Icon(Icons.cloud_download),
title: const Text('Import settings to lemmur'),
onTap: () async {
await context.read<ConfigStore>().importLemmyUserSettings(
accountsStore.userDataFor(instanceHost, username)!.jwt,
);
Navigator.of(context).pop();
importLoading.value = false;
}
}),
},
),
),
),
],
);
}

View File

@ -1,89 +1,84 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../l10n/l10n.dart';
import '../util/async_store.dart';
part 'config_store.g.dart';
/// Store managing user-level configuration such as theme or language
@JsonSerializable()
class ConfigStore extends ChangeNotifier {
static const prefsKey = 'v1:ConfigStore';
static final _prefs = SharedPreferences.getInstance();
@LocaleConverter()
class ConfigStore extends _ConfigStore with _$ConfigStore {
static const _prefsKey = 'v1:ConfigStore';
late final SharedPreferences _sharedPrefs;
late final ReactionDisposer _saveDisposer;
late ThemeMode _theme;
@visibleForTesting
ConfigStore();
factory ConfigStore.load(SharedPreferences sharedPrefs) {
final store = _$ConfigStoreFromJson(
jsonDecode(sharedPrefs.getString(_prefsKey) ?? '{}')
as Map<String, dynamic>,
).._sharedPrefs = sharedPrefs;
store._saveDisposer = autorun((_) => store.save());
return store;
}
Future<void> save() async {
final serialized = jsonEncode(_$ConfigStoreToJson(this));
await _sharedPrefs.setString(_prefsKey, serialized);
}
void dispose() {
_saveDisposer();
}
}
abstract class _ConfigStore with Store {
@observable
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode get theme => _theme;
set theme(ThemeMode theme) {
_theme = theme;
notifyListeners();
save();
}
ThemeMode theme = ThemeMode.system;
late bool _amoledDarkMode;
@observable
@JsonKey(defaultValue: false)
bool get amoledDarkMode => _amoledDarkMode;
set amoledDarkMode(bool amoledDarkMode) {
_amoledDarkMode = amoledDarkMode;
notifyListeners();
save();
}
bool amoledDarkMode = false;
late Locale _locale;
// default value is set in the `LocaleSerde.fromJson` method because json_serializable does
// not accept non-literals as defaultValue
@JsonKey(fromJson: LocaleSerde.fromJson, toJson: LocaleSerde.toJson)
Locale get locale => _locale;
set locale(Locale locale) {
_locale = locale;
notifyListeners();
save();
}
// default value is set in the `LocaleConverter.fromJson`
@observable
Locale locale = const Locale('en');
late bool _showAvatars;
@observable
@JsonKey(defaultValue: true)
bool get showAvatars => _showAvatars;
set showAvatars(bool showAvatars) {
_showAvatars = showAvatars;
notifyListeners();
save();
}
bool showAvatars = true;
late bool _showScores;
@observable
@JsonKey(defaultValue: true)
bool get showScores => _showScores;
set showScores(bool showScores) {
_showScores = showScores;
notifyListeners();
save();
}
bool showScores = true;
late SortType _defaultSortType;
// default is set in fromJson
@observable
@JsonKey(fromJson: _sortTypeFromJson)
SortType get defaultSortType => _defaultSortType;
set defaultSortType(SortType defaultSortType) {
_defaultSortType = defaultSortType;
notifyListeners();
save();
}
SortType defaultSortType = SortType.hot;
late PostListingType _defaultListingType;
// default is set in fromJson
@observable
@JsonKey(fromJson: _postListingTypeFromJson)
PostListingType get defaultListingType => _defaultListingType;
set defaultListingType(PostListingType defaultListingType) {
_defaultListingType = defaultListingType;
notifyListeners();
save();
}
PostListingType defaultListingType = PostListingType.all;
final lemmyImportState = AsyncStore<FullSiteView>();
/// Copies over settings from lemmy to [ConfigStore]
@action
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
// themes from lemmy-ui that are dark mode
const darkModeLemmyUiThemes = {
@ -94,8 +89,8 @@ class ConfigStore extends ChangeNotifier {
'i386',
};
_showAvatars = localUserSettings.showAvatars;
_theme = () {
showAvatars = localUserSettings.showAvatars;
theme = () {
if (localUserSettings.theme == 'browser') return ThemeMode.system;
if (darkModeLemmyUiThemes.contains(localUserSettings.theme)) {
@ -104,36 +99,27 @@ class ConfigStore extends ChangeNotifier {
return ThemeMode.light;
}();
_locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang))
? Locale(localUserSettings.lang)
: _locale;
_showScores = localUserSettings.showScores;
_defaultSortType = localUserSettings.defaultSortType;
_defaultListingType = localUserSettings.defaultListingType;
notifyListeners();
save();
if (L10n.supportedLocales.contains(Locale(localUserSettings.lang))) {
locale = Locale(localUserSettings.lang);
}
showScores = localUserSettings.showScores;
defaultSortType = localUserSettings.defaultSortType;
defaultListingType = localUserSettings.defaultListingType;
}
/// Fetches [LocalUserSettings] and imports them with [.copyLemmyUserSettings]
@action
Future<void> importLemmyUserSettings(Jwt token) async {
final site =
await LemmyApiV3(token.payload.iss).run(GetSite(auth: token.raw));
copyLemmyUserSettings(site.myUser!.localUserView.localUser);
}
static Future<ConfigStore> load() async {
final prefs = await _prefs;
return _$ConfigStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
final site = await lemmyImportState.runLemmy(
token.payload.iss,
GetSite(auth: token.raw),
);
}
Future<void> save() async {
final prefs = await _prefs;
await prefs.setString(prefsKey, jsonEncode(_$ConfigStoreToJson(this)));
if (site != null) {
copyLemmyUserSettings(site.myUser!.localUserView.localUser);
}
}
}

View File

@ -10,7 +10,7 @@ ConfigStore _$ConfigStoreFromJson(Map<String, dynamic> json) => ConfigStore()
..theme =
$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ?? ThemeMode.system
..amoledDarkMode = json['amoledDarkMode'] as bool? ?? false
..locale = LocaleSerde.fromJson(json['locale'] as String?)
..locale = const LocaleConverter().fromJson(json['locale'] as String?)
..showAvatars = json['showAvatars'] as bool? ?? true
..showScores = json['showScores'] as bool? ?? true
..defaultSortType = _sortTypeFromJson(json['defaultSortType'] as String?)
@ -21,7 +21,7 @@ Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
<String, dynamic>{
'theme': _$ThemeModeEnumMap[instance.theme],
'amoledDarkMode': instance.amoledDarkMode,
'locale': LocaleSerde.toJson(instance.locale),
'locale': const LocaleConverter().toJson(instance.locale),
'showAvatars': instance.showAvatars,
'showScores': instance.showScores,
'defaultSortType': instance.defaultSortType,
@ -33,3 +33,152 @@ const _$ThemeModeEnumMap = {
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};
// **************************************************************************
// StoreGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$ConfigStore on _ConfigStore, Store {
final _$themeAtom = Atom(name: '_ConfigStore.theme');
@override
ThemeMode get theme {
_$themeAtom.reportRead();
return super.theme;
}
@override
set theme(ThemeMode value) {
_$themeAtom.reportWrite(value, super.theme, () {
super.theme = value;
});
}
final _$amoledDarkModeAtom = Atom(name: '_ConfigStore.amoledDarkMode');
@override
bool get amoledDarkMode {
_$amoledDarkModeAtom.reportRead();
return super.amoledDarkMode;
}
@override
set amoledDarkMode(bool value) {
_$amoledDarkModeAtom.reportWrite(value, super.amoledDarkMode, () {
super.amoledDarkMode = value;
});
}
final _$localeAtom = Atom(name: '_ConfigStore.locale');
@override
Locale get locale {
_$localeAtom.reportRead();
return super.locale;
}
@override
set locale(Locale value) {
_$localeAtom.reportWrite(value, super.locale, () {
super.locale = value;
});
}
final _$showAvatarsAtom = Atom(name: '_ConfigStore.showAvatars');
@override
bool get showAvatars {
_$showAvatarsAtom.reportRead();
return super.showAvatars;
}
@override
set showAvatars(bool value) {
_$showAvatarsAtom.reportWrite(value, super.showAvatars, () {
super.showAvatars = value;
});
}
final _$showScoresAtom = Atom(name: '_ConfigStore.showScores');
@override
bool get showScores {
_$showScoresAtom.reportRead();
return super.showScores;
}
@override
set showScores(bool value) {
_$showScoresAtom.reportWrite(value, super.showScores, () {
super.showScores = value;
});
}
final _$defaultSortTypeAtom = Atom(name: '_ConfigStore.defaultSortType');
@override
SortType get defaultSortType {
_$defaultSortTypeAtom.reportRead();
return super.defaultSortType;
}
@override
set defaultSortType(SortType value) {
_$defaultSortTypeAtom.reportWrite(value, super.defaultSortType, () {
super.defaultSortType = value;
});
}
final _$defaultListingTypeAtom =
Atom(name: '_ConfigStore.defaultListingType');
@override
PostListingType get defaultListingType {
_$defaultListingTypeAtom.reportRead();
return super.defaultListingType;
}
@override
set defaultListingType(PostListingType value) {
_$defaultListingTypeAtom.reportWrite(value, super.defaultListingType, () {
super.defaultListingType = value;
});
}
final _$importLemmyUserSettingsAsyncAction =
AsyncAction('_ConfigStore.importLemmyUserSettings');
@override
Future<void> importLemmyUserSettings(Jwt token) {
return _$importLemmyUserSettingsAsyncAction
.run(() => super.importLemmyUserSettings(token));
}
final _$_ConfigStoreActionController = ActionController(name: '_ConfigStore');
@override
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
final _$actionInfo = _$_ConfigStoreActionController.startAction(
name: '_ConfigStore.copyLemmyUserSettings');
try {
return super.copyLemmyUserSettings(localUserSettings);
} finally {
_$_ConfigStoreActionController.endAction(_$actionInfo);
}
}
@override
String toString() {
return '''
theme: ${theme},
amoledDarkMode: ${amoledDarkMode},
locale: ${locale},
showAvatars: ${showAvatars},
showScores: ${showScores},
defaultSortType: ${defaultSortType},
defaultListingType: ${defaultListingType}
''';
}
}

View File

@ -4,6 +4,8 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
export 'package:provider/provider.dart';
typedef MobxBuilder<T extends Store> = Widget Function(BuildContext, T);
typedef MobxListener<T extends Store> = void Function(BuildContext, T);

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../hooks/stores.dart';
import '../stores/config_store.dart';
import 'cached_network_image.dart';
/// User's avatar. Respects the `showAvatars` setting from configStore
@ -26,8 +27,7 @@ class Avatar extends HookWidget {
@override
Widget build(BuildContext context) {
final showAvatars =
useConfigStoreSelect((configStore) => configStore.showAvatars) ||
alwaysShow;
useStore((ConfigStore store) => store.showAvatars) || alwaysShow;
final blankWidget = () {
if (noBlank) return const SizedBox.shrink();

View File

@ -2,7 +2,6 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:provider/provider.dart';
import '../../comment_tree.dart';
import '../../l10n/l10n.dart';

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:provider/provider.dart';
import '../../hooks/logged_in_action.dart';
import '../../l10n/l10n.dart';

View File

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
import '../../l10n/l10n.dart';
import '../../util/icons.dart';

View File

@ -1,6 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../util/observer_consumers.dart';
import '../markdown_text.dart';

View File

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../l10n/l10n.dart';
import '../../util/extensions/api.dart';

View File

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart' as ul;
import '../../hooks/logged_in_action.dart';

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:provider/provider.dart';
import '../../hooks/logged_in_action.dart';
import '../../hooks/stores.dart';
import '../../stores/config_store.dart';
import '../../util/intl.dart';
import '../../util/observer_consumers.dart';
import 'post_store.dart';
@ -15,8 +15,7 @@ class PostVoting extends HookWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final showScores =
useConfigStoreSelect((configStore) => configStore.showScores);
final showScores = useStore((ConfigStore store) => store.showScores);
final loggedInAction = useLoggedInAction(context
.select<PostStore, String>((store) => store.postView.instanceHost));

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
import '../../hooks/logged_in_action.dart';
import '../../util/observer_consumers.dart';

View File

@ -5,6 +5,7 @@ import 'package:lemmy_api_client/v3.dart';
import '../comment_tree.dart';
import '../hooks/infinite_scroll.dart';
import '../hooks/stores.dart';
import '../stores/config_store.dart';
import 'comment/comment.dart';
import 'infinite_scroll.dart';
import 'post/post.dart';
@ -39,7 +40,7 @@ class SortableInfiniteList<T> extends HookWidget {
@override
Widget build(BuildContext context) {
final defaultSortType =
useConfigStoreSelect((store) => store.defaultSortType);
useStore((ConfigStore store) => store.defaultSortType);
final defaultController = useInfiniteScrollController();
final isc = controller ?? defaultController;

View File

@ -1,7 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
test('blank', () {
expect(true, true);
});
}

View File

@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lemmur/stores/config_store.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _lemmyUserSettings = LocalUserSettings(
id: 1,
personId: 1,
showNsfw: true,
theme: 'browser',
defaultSortType: SortType.active,
defaultListingType: PostListingType.local,
lang: 'en',
showAvatars: true,
showScores: true,
sendNotificationsToEmail: true,
showReadPosts: true,
showBotAccounts: true,
showNewPostNotifs: true,
instanceHost: '',
);
void main() {
group('ConfigStore', () {
late SharedPreferences prefs;
late ConfigStore store;
setUpAll(() async {
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
});
setUp(() async {
store = ConfigStore.load(prefs);
await prefs.clear();
});
test('Store defaults match json defaults', () {
final store = ConfigStore();
final loaded = ConfigStore.load(prefs);
expect(store.theme, loaded.theme);
expect(store.amoledDarkMode, loaded.amoledDarkMode);
expect(store.locale, loaded.locale);
expect(store.showAvatars, loaded.showAvatars);
expect(store.showScores, loaded.showScores);
expect(store.defaultSortType, loaded.defaultSortType);
expect(store.defaultListingType, loaded.defaultListingType);
});
test('Changes are saved', () {
store.amoledDarkMode = false;
var loaded = ConfigStore.load(prefs);
expect(loaded.amoledDarkMode, false);
store.amoledDarkMode = true;
loaded = ConfigStore.load(prefs);
expect(loaded.amoledDarkMode, true);
});
test('Changes are not saved after disposing', () {
store.amoledDarkMode = false;
var loaded = ConfigStore.load(prefs);
expect(loaded.amoledDarkMode, false);
store
..dispose()
..amoledDarkMode = true;
loaded = ConfigStore.load(prefs);
expect(loaded.amoledDarkMode, false);
});
group('Copying LemmyUserSettings', () {
test('works', () {
store
..theme = ThemeMode.dark
..amoledDarkMode = false
..locale = const Locale('pl')
..showAvatars = false
..showScores = false
..defaultSortType = SortType.topYear
..defaultListingType = PostListingType.all
..copyLemmyUserSettings(_lemmyUserSettings);
expect(store.theme, ThemeMode.system);
expect(store.amoledDarkMode, false);
expect(store.locale, const Locale('en'));
expect(store.showAvatars, true);
expect(store.showScores, true);
expect(store.defaultSortType, SortType.active);
expect(store.defaultListingType, PostListingType.local);
});
test('detects dark theme', () {
store
..theme = ThemeMode.light
..copyLemmyUserSettings(_lemmyUserSettings.copyWith(theme: 'darkly'));
expect(store.theme, ThemeMode.dark);
});
test('lang ignores unrecognized', () {
store
..locale = const Locale('en')
..copyLemmyUserSettings(
_lemmyUserSettings.copyWith(lang: 'qweqweqwe'));
expect(store.locale, const Locale('en'));
});
test('detects browser theme', () {
store
..theme = ThemeMode.light
..copyLemmyUserSettings(
_lemmyUserSettings.copyWith(theme: 'browser'));
expect(store.theme, ThemeMode.system);
});
});
});
}