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:
parent
8f34591111
commit
760565384f
|
@ -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:
|
||||
|
|
28
lib/app.dart
28
lib/app.dart
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../comment_tree.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('blank', () {
|
||||
expect(true, true);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue