diff --git a/lib/main.dart b/lib/main.dart index 89cfe17..cb6b231 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,21 +19,14 @@ import 'util/extensions/brightness.dart'; Future 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(), ), diff --git a/lib/stores/accounts_store.dart b/lib/stores/accounts_store.dart index 61c7c3d..44d59d7 100644 --- a/lib/stores/accounts_store.dart +++ b/lib/stores/accounts_store.dart @@ -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> _tokens; + @protected + @JsonKey(defaultValue: {'lemmy.ml': {}}) + Map> tokens; /// default account for a given instance /// map where keys are instanceHosts and values are usernames - HashMap _defaultAccounts; + @protected + @JsonKey(defaultValue: {}) + Map defaultAccounts; /// default account for the app /// It is in a form of `username@instanceHost` - String _defaultAccount; + @protected + String defaultAccount; - Future load() async { - final prefs = await SharedPreferences.getInstance(); + static Future 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 f(String jwt)) => HashMap.of( - (jsonDecode(prefs.getString(SharedPrefKeys.tokens) ?? - '{"lemmy.ml":{}}') as Map) - ?.map( - (k, e) => MapEntry( - k, - HashMap.of( - (e as Map)?.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 ?? - {}, - )); - - notifyListeners(); + return _$AccountsStoreFromJson( + jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map, + ); } Future 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 get instances => _tokens.keys; + Iterable get instances => tokens.keys; Iterable get loggedInInstances => instances.where((e) => !isAnonymousFor(e)); /// Usernames that are assigned to a given instance Iterable 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(); diff --git a/lib/stores/accounts_store.g.dart b/lib/stores/accounts_store.g.dart new file mode 100644 index 0000000..2b909ae --- /dev/null +++ b/lib/stores/accounts_store.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'accounts_store.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AccountsStore _$AccountsStoreFromJson(Map json) { + return AccountsStore() + ..tokens = (json['tokens'] as Map)?.map( + (k, e) => MapEntry( + k, + (e as Map)?.map( + (k, e) => + MapEntry(k, e == null ? null : Jwt.fromJson(e as String)), + )), + ) ?? + {'lemmy.ml': {}} + ..defaultAccounts = (json['defaultAccounts'] as Map)?.map( + (k, e) => MapEntry(k, e as String), + ) ?? + {} + ..defaultAccount = json['defaultAccount'] as String; +} + +Map _$AccountsStoreToJson(AccountsStore instance) => + { + 'tokens': instance.tokens, + 'defaultAccounts': instance.defaultAccounts, + 'defaultAccount': instance.defaultAccount, + }; diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 27f49f4..e1ee670 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -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 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 load() async { + final prefs = await _prefs; + + return _$ConfigStoreFromJson( + jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map, + ); } Future 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); diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart new file mode 100644 index 0000000..1257853 --- /dev/null +++ b/lib/stores/config_store.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'config_store.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ConfigStore _$ConfigStoreFromJson(Map json) { + return ConfigStore() + ..theme = _$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ?? + ThemeMode.system + ..amoledDarkMode = json['amoledDarkMode'] as bool ?? false; +} + +Map _$ConfigStoreToJson(ConfigStore instance) => + { + 'theme': _$ThemeModeEnumMap[instance.theme], + 'amoledDarkMode': instance.amoledDarkMode, + }; + +T _$enumDecode( + Map 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( + Map enumValues, + dynamic source, { + T unknownValue, +}) { + if (source == null) { + return null; + } + return _$enumDecode(enumValues, source, unknownValue: unknownValue); +} + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; diff --git a/lib/stores/shared_pref_keys.dart b/lib/stores/shared_pref_keys.dart deleted file mode 100644 index 4c2497e..0000000 --- a/lib/stores/shared_pref_keys.dart +++ /dev/null @@ -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'; -} diff --git a/pubspec.lock b/pubspec.lock index 9080205..c63d8b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 2e3c420..97c9248 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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