make stores easily serializable

This commit is contained in:
shilangyu 2021-02-24 21:54:15 +01:00
parent 19b2688316
commit 8cf3859ed8
8 changed files with 419 additions and 102 deletions

View File

@ -19,21 +19,14 @@ import 'util/extensions/brightness.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final configStore = ConfigStore();
await configStore.load();
final accountsStore = AccountsStore();
await accountsStore.load();
final configStore = await ConfigStore.load();
final accountsStore = await AccountsStore.load();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: configStore,
),
ChangeNotifierProvider.value(
value: accountsStore,
),
ChangeNotifierProvider.value(value: configStore),
ChangeNotifierProvider.value(value: accountsStore),
],
child: const MyApp(),
),

View File

@ -2,73 +2,57 @@ import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:lemmy_api_client/v2.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../util/unawaited.dart';
import 'shared_pref_keys.dart';
part 'accounts_store.g.dart';
/// Store that manages all accounts
@JsonSerializable()
class AccountsStore extends ChangeNotifier {
static const prefsKey = 'v1:AccountsStore';
static final _prefs = SharedPreferences.getInstance();
/// Map containing JWT tokens of specific users.
/// If a token is in this map, the user is considered logged in
/// for that account.
/// `tokens['instanceHost']['username']`
HashMap<String, HashMap<String, Jwt>> _tokens;
@protected
@JsonKey(defaultValue: {'lemmy.ml': {}})
Map<String, Map<String, Jwt>> tokens;
/// default account for a given instance
/// map where keys are instanceHosts and values are usernames
HashMap<String, String> _defaultAccounts;
@protected
@JsonKey(defaultValue: {})
Map<String, String> defaultAccounts;
/// default account for the app
/// It is in a form of `username@instanceHost`
String _defaultAccount;
@protected
String defaultAccount;
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
static Future<AccountsStore> load() async {
final prefs = await _prefs;
// I barely understand what I did. Long story short it casts a
// raw json into a nested ObservableMap
nestedMapsCast<T>(T f(String jwt)) => HashMap.of(
(jsonDecode(prefs.getString(SharedPrefKeys.tokens) ??
'{"lemmy.ml":{}}') as Map<String, dynamic>)
?.map(
(k, e) => MapEntry(
k,
HashMap.of(
(e as Map<String, dynamic>)?.map(
(k, e) => MapEntry(k, e == null ? null : f(e as String)),
),
),
),
),
);
// set saved settings or create defaults
_tokens = nestedMapsCast((json) => Jwt(json));
_defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount);
_defaultAccounts = HashMap.of(Map.castFrom(
jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null')
as Map<dynamic, dynamic> ??
{},
));
notifyListeners();
return _$AccountsStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
);
}
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _prefs;
await prefs.setString(SharedPrefKeys.defaultAccount, _defaultAccount);
await prefs.setString(
SharedPrefKeys.defaultAccounts, jsonEncode(_defaultAccounts));
await prefs.setString(SharedPrefKeys.tokens, jsonEncode(_tokens));
await prefs.setString(prefsKey, jsonEncode(_$AccountsStoreToJson(this)));
}
/// automatically sets default accounts
void _assignDefaultAccounts() {
// remove dangling defaults
_defaultAccounts.entries
defaultAccounts.entries
.map((dft) {
final instance = dft.key;
final username = dft.value;
@ -79,21 +63,21 @@ class AccountsStore extends ChangeNotifier {
}
})
.toList()
.forEach(_defaultAccounts.remove);
if (_defaultAccount != null) {
final instance = _defaultAccount.split('@')[1];
final username = _defaultAccount.split('@')[0];
.forEach(defaultAccounts.remove);
if (defaultAccount != null) {
final instance = defaultAccount.split('@')[1];
final username = defaultAccount.split('@')[0];
// if instance or username doesn't exist, remove
if (!instances.contains(instance) ||
!usernamesFor(instance).contains(username)) {
_defaultAccount = null;
defaultAccount = null;
}
}
// set local defaults
for (final instanceHost in instances) {
// if this instance is not in defaults
if (!_defaultAccounts.containsKey(instanceHost)) {
if (!defaultAccounts.containsKey(instanceHost)) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceHost)) {
setDefaultAccountFor(instanceHost, usernamesFor(instanceHost).first);
@ -102,7 +86,7 @@ class AccountsStore extends ChangeNotifier {
}
// set global default
if (_defaultAccount == null) {
if (defaultAccount == null) {
// select first account of first instance
for (final instanceHost in instances) {
// select first account in this instance, if any
@ -114,19 +98,19 @@ class AccountsStore extends ChangeNotifier {
}
String get defaultUsername {
if (_defaultAccount == null) {
if (defaultAccount == null) {
return null;
}
return _defaultAccount.split('@')[0];
return defaultAccount.split('@')[0];
}
String get defaultInstanceHost {
if (_defaultAccount == null) {
if (defaultAccount == null) {
return null;
}
return _defaultAccount.split('@')[1];
return defaultAccount.split('@')[1];
}
String defaultUsernameFor(String instanceHost) {
@ -134,16 +118,16 @@ class AccountsStore extends ChangeNotifier {
return null;
}
return _defaultAccounts[instanceHost];
return defaultAccounts[instanceHost];
}
Jwt get defaultToken {
if (_defaultAccount == null) {
if (defaultAccount == null) {
return null;
}
final userTag = _defaultAccount.split('@');
return _tokens[userTag[1]][userTag[0]];
final userTag = defaultAccount.split('@');
return tokens[userTag[1]][userTag[0]];
}
Jwt defaultTokenFor(String instanceHost) {
@ -151,13 +135,13 @@ class AccountsStore extends ChangeNotifier {
return null;
}
return _tokens[instanceHost][_defaultAccounts[instanceHost]];
return tokens[instanceHost][defaultAccounts[instanceHost]];
}
/// returns token for user of a certain id
Jwt tokenForId(String instanceHost, int userId) =>
_tokens.containsKey(instanceHost)
? _tokens[instanceHost]
tokens.containsKey(instanceHost)
? tokens[instanceHost]
.values
.firstWhere((val) => val.payload.id == userId, orElse: () => null)
: null;
@ -167,12 +151,12 @@ class AccountsStore extends ChangeNotifier {
return null;
}
return _tokens[instanceHost][username];
return tokens[instanceHost][username];
}
/// sets globally default account
void setDefaultAccount(String instanceHost, String username) {
_defaultAccount = '$username@$instanceHost';
defaultAccount = '$username@$instanceHost';
notifyListeners();
save();
@ -180,7 +164,7 @@ class AccountsStore extends ChangeNotifier {
/// sets default account for given instance
void setDefaultAccountFor(String instanceHost, String username) {
_defaultAccounts[instanceHost] = username;
defaultAccounts[instanceHost] = username;
notifyListeners();
save();
@ -193,20 +177,20 @@ class AccountsStore extends ChangeNotifier {
return true;
}
return _tokens[instanceHost].isEmpty;
return tokens[instanceHost].isEmpty;
}
/// `true` if no added instance has an account assigned to it
bool get hasNoAccount => loggedInInstances.isEmpty;
Iterable<String> get instances => _tokens.keys;
Iterable<String> get instances => tokens.keys;
Iterable<String> get loggedInInstances =>
instances.where((e) => !isAnonymousFor(e));
/// Usernames that are assigned to a given instance
Iterable<String> usernamesFor(String instanceHost) =>
_tokens[instanceHost].keys;
tokens[instanceHost].keys;
/// adds a new account
/// if it's the first account ever the account is
@ -230,7 +214,7 @@ class AccountsStore extends ChangeNotifier {
final userData =
await lemmy.run(GetSite(auth: token.raw)).then((value) => value.myUser);
_tokens[instanceHost][userData.name] = token;
tokens[instanceHost][userData.name] = token;
_assignDefaultAccounts();
notifyListeners();
@ -257,7 +241,7 @@ class AccountsStore extends ChangeNotifier {
}
}
_tokens[instanceHost] = HashMap();
tokens[instanceHost] = HashMap();
_assignDefaultAccounts();
notifyListeners();
@ -266,7 +250,7 @@ class AccountsStore extends ChangeNotifier {
/// This also removes all accounts assigned to this instance
void removeInstance(String instanceHost) {
_tokens.remove(instanceHost);
tokens.remove(instanceHost);
_assignDefaultAccounts();
notifyListeners();
@ -274,7 +258,7 @@ class AccountsStore extends ChangeNotifier {
}
void removeAccount(String instanceHost, String username) {
_tokens[instanceHost].remove(username);
tokens[instanceHost].remove(username);
_assignDefaultAccounts();
notifyListeners();

View File

@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'accounts_store.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AccountsStore _$AccountsStoreFromJson(Map<String, dynamic> json) {
return AccountsStore()
..tokens = (json['tokens'] as Map<String, dynamic>)?.map(
(k, e) => MapEntry(
k,
(e as Map<String, dynamic>)?.map(
(k, e) =>
MapEntry(k, e == null ? null : Jwt.fromJson(e as String)),
)),
) ??
{'lemmy.ml': {}}
..defaultAccounts = (json['defaultAccounts'] as Map<String, dynamic>)?.map(
(k, e) => MapEntry(k, e as String),
) ??
{}
..defaultAccount = json['defaultAccount'] as String;
}
Map<String, dynamic> _$AccountsStoreToJson(AccountsStore instance) =>
<String, dynamic>{
'tokens': instance.tokens,
'defaultAccounts': instance.defaultAccounts,
'defaultAccount': instance.defaultAccount,
};

View File

@ -1,12 +1,20 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'shared_pref_keys.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();
ThemeMode _theme;
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode get theme => _theme;
set theme(ThemeMode theme) {
_theme = theme;
@ -15,6 +23,7 @@ class ConfigStore extends ChangeNotifier {
}
bool _amoledDarkMode;
@JsonKey(defaultValue: false)
bool get amoledDarkMode => _amoledDarkMode;
set amoledDarkMode(bool amoledDarkMode) {
_amoledDarkMode = amoledDarkMode;
@ -22,23 +31,17 @@ class ConfigStore extends ChangeNotifier {
save();
}
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
// load saved settings or create defaults
theme =
_themeModeFromString(prefs.getString(SharedPrefKeys.theme) ?? 'system');
amoledDarkMode = prefs.getBool(SharedPrefKeys.amoledDarkMode) ?? false;
notifyListeners();
static Future<ConfigStore> load() async {
final prefs = await _prefs;
return _$ConfigStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
);
}
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _prefs;
await prefs.setString(SharedPrefKeys.theme, describeEnum(theme));
await prefs.setBool(SharedPrefKeys.amoledDarkMode, amoledDarkMode);
await prefs.setString(prefsKey, jsonEncode(_$ConfigStoreToJson(this)));
}
}
/// converts string to ThemeMode
ThemeMode _themeModeFromString(String theme) =>
ThemeMode.values.firstWhere((e) => describeEnum(e) == theme);

View File

@ -0,0 +1,58 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'config_store.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ConfigStore _$ConfigStoreFromJson(Map<String, dynamic> json) {
return ConfigStore()
..theme = _$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ??
ThemeMode.system
..amoledDarkMode = json['amoledDarkMode'] as bool ?? false;
}
Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
<String, dynamic>{
'theme': _$ThemeModeEnumMap[instance.theme],
'amoledDarkMode': instance.amoledDarkMode,
};
T _$enumDecode<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
throw ArgumentError('A value must be provided. Supported values: '
'${enumValues.values.join(', ')}');
}
final value = enumValues.entries
.singleWhere((e) => e.value == source, orElse: () => null)
?.key;
if (value == null && unknownValue == null) {
throw ArgumentError('`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}');
}
return value ?? unknownValue;
}
T _$enumDecodeNullable<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
return null;
}
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
}
const _$ThemeModeEnumMap = {
ThemeMode.system: 'system',
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};

View File

@ -1,8 +0,0 @@
/// Collection of string constants that are keys to SharedPreferences
class SharedPrefKeys {
static const tokens = 'tokens';
static const defaultAccount = 'defaultAccount';
static const defaultAccounts = 'defaultAccounts';
static const theme = 'theme';
static const amoledDarkMode = 'amoledDarkMode';
}

View File

@ -1,6 +1,20 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "14.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.41.2"
archive:
dependency: transitive
description:
@ -29,6 +43,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.5"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.7"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "7.1.0"
cached_network_image:
dependency: "direct main"
description:
@ -50,6 +120,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
@ -57,6 +141,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.0"
collection:
dependency: transitive
description:
@ -85,6 +176,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.12"
effective_dart:
dependency: "direct dev"
description:
@ -120,6 +218,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.11"
flutter:
dependency: "direct main"
description: flutter
@ -205,6 +310,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
graphs:
dependency: transitive
description:
name: graphs
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
http:
dependency: transitive
description:
@ -212,6 +331,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
http_parser:
dependency: transitive
description:
@ -247,13 +373,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
json_annotation:
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.1"
latinize:
dependency: transitive
description:
@ -268,6 +415,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
markdown:
dependency: "direct main"
description:
@ -296,6 +450,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
modal_bottom_sheet:
dependency: "direct main"
description:
@ -310,6 +471,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
octo_image:
dependency: transitive
description:
@ -317,6 +492,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
package_info:
dependency: "direct main"
description:
@ -401,6 +583,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
process:
dependency: transitive
description:
@ -415,6 +604,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.3"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.8"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
rxdart:
dependency: transitive
description:
@ -464,11 +674,32 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+3"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.9"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.4+1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.10+3"
source_span:
dependency: transitive
description:
@ -504,6 +735,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
stream_transform:
dependency: transitive
description:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
string_scanner:
dependency: transitive
description:
@ -539,6 +777,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.29"
timing:
dependency: transitive
description:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+3"
typed_data:
dependency: transitive
description:
@ -602,6 +847,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
web_socket_channel:
dependency: transitive
description:

View File

@ -46,6 +46,7 @@ dependencies:
fuzzy: <1.0.0
lemmy_api_client: ^0.12.0
matrix4_transform: ^1.1.7
json_annotation: ^3.1.1
flutter:
sdk: flutter
@ -59,6 +60,8 @@ dev_dependencies:
sdk: flutter
effective_dart: ^1.0.0
flutter_launcher_icons: ^0.8.1
json_serializable: ^3.5.1
build_runner: ^1.11.1
flutter_icons:
android: true