Merge pull request #79 from krawieck/experiment-change-notifier

This commit is contained in:
Filip Krawczyk 2020-10-26 22:38:28 +01:00 committed by GitHub
commit 7febc4b3bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 339 additions and 916 deletions

View File

@ -1,20 +0,0 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:mobx/mobx.dart';
/// Observes MobX observables in [fn] and returns the built value.
/// When observable inside have changed, the hook rebuilds the value.
/// The returned value can be ignored for a `useEffect(() { autorun(fn); }, [])`
/// effect.
T useObserved<T>(T Function() fn) {
final returnValue = useState(useMemoized(fn));
useEffect(() {
final disposer = autorun((_) {
returnValue.value = fn();
});
return disposer;
}, []);
return returnValue.value;
}

View File

@ -5,4 +5,9 @@ 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);

View File

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import 'hooks/stores.dart';
@ -26,13 +25,11 @@ Future<void> main() async {
runApp(
MultiProvider(
providers: [
Provider<ConfigStore>(
create: (_) => configStore,
dispose: (_, store) => store.dispose(),
ChangeNotifierProvider.value(
value: configStore,
),
Provider<AccountsStore>(
create: (_) => accountsStore,
dispose: (_, store) => store.dispose(),
ChangeNotifierProvider.value(
value: accountsStore,
),
],
child: MyApp(),
@ -44,28 +41,22 @@ class MyApp extends HookWidget {
@override
Widget build(BuildContext context) {
final configStore = useConfigStore();
final maybeAmoledColor = configStore.amoledDarkMode ? Colors.black : null;
return Observer(
builder: (ctx) {
final maybeAmoledColor =
configStore.amoledDarkMode ? Colors.black : null;
return MaterialApp(
title: 'Lemmur',
themeMode: configStore.theme,
darkTheme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: maybeAmoledColor,
backgroundColor: maybeAmoledColor,
canvasColor: maybeAmoledColor,
cardColor: maybeAmoledColor,
splashColor: maybeAmoledColor,
),
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
},
return MaterialApp(
title: 'Lemmur',
themeMode: configStore.theme,
darkTheme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: maybeAmoledColor,
backgroundColor: maybeAmoledColor,
canvasColor: maybeAmoledColor,
cardColor: maybeAmoledColor,
splashColor: maybeAmoledColor,
),
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}

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:flutter_mobx/flutter_mobx.dart';
import '../hooks/stores.dart';
import '../util/goto.dart';
@ -19,107 +18,101 @@ class UserProfileTab extends HookWidget {
final theme = Theme.of(context);
final accountsStore = useAccountsStore();
return Observer(
builder: (ctx) {
if (accountsStore.hasNoAccount) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('No account was added.'),
FlatButton.icon(
onPressed: () {
goTo(context, (_) => AccountsConfigPage());
},
icon: Icon(Icons.add),
label: Text('Add account'),
)
],
),
),
);
}
return Scaffold(
extendBodyBehindAppBar: true,
// TODO: this is not visible in light mode when the sliver app bar
// in UserProfile is folded
appBar: AppBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
centerTitle: true,
title: FlatButton(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
// TODO: fix overflow issues
'@${accountsStore.defaultUsername}',
style: theme.primaryTextTheme.headline6,
overflow: TextOverflow.fade,
),
Icon(
Icons.expand_more,
color: theme.primaryIconTheme.color,
),
],
),
onPressed: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) {
final userTags = <String>[];
accountsStore.tokens.forEach((instanceUrl, value) {
value.forEach((username, _) {
userTags.add('$username@$instanceUrl');
});
});
return Observer(
builder: (ctx) => BottomModal(
title: 'account',
child: Column(
children: [
for (final tag in userTags)
RadioListTile<String>(
value: tag,
title: Text(tag),
groupValue: '${accountsStore.defaultUsername}'
'@${accountsStore.defaultInstanceUrl}',
onChanged: (selected) {
final userTag = selected.split('@');
accountsStore.setDefaultAccount(
userTag[1], userTag[0]);
Navigator.of(ctx).pop();
},
)
],
),
),
);
},
);
},
),
actions: [
IconButton(
icon: Icon(Icons.settings),
if (accountsStore.hasNoAccount) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('No account was added.'),
FlatButton.icon(
onPressed: () {
goTo(context, (_) => SettingsPage());
goTo(context, (_) => AccountsConfigPage());
},
icon: Icon(Icons.add),
label: Text('Add account'),
)
],
),
body: UserProfile(
userId: accountsStore.defaultToken.payload.id,
instanceUrl: accountsStore.defaultInstanceUrl,
),
);
}
return Scaffold(
extendBodyBehindAppBar: true,
// TODO: this is not visible in light mode when the sliver app bar
// in UserProfile is folded
appBar: AppBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
centerTitle: true,
title: FlatButton(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
// TODO: fix overflow issues
'@${accountsStore.defaultUsername}',
style: theme.primaryTextTheme.headline6,
overflow: TextOverflow.fade,
),
Icon(
Icons.expand_more,
color: theme.primaryIconTheme.color,
),
],
),
);
},
onPressed: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) {
final userTags = <String>[];
accountsStore.tokens.forEach((instanceUrl, value) {
value.forEach((username, _) {
userTags.add('$username@$instanceUrl');
});
});
return BottomModal(
title: 'account',
child: Column(
children: [
for (final tag in userTags)
RadioListTile<String>(
value: tag,
title: Text(tag),
groupValue: '${accountsStore.defaultUsername}'
'@${accountsStore.defaultInstanceUrl}',
onChanged: (selected) {
final userTag = selected.split('@');
accountsStore.setDefaultAccount(
userTag[1], userTag[0]);
Navigator.of(ctx).pop();
},
)
],
),
);
},
);
},
),
actions: [
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
goTo(context, (_) => SettingsPage());
},
)
],
),
body: UserProfile(
userId: accountsStore.defaultToken.payload.id,
instanceUrl: accountsStore.defaultInstanceUrl,
),
);
}
}

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:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
@ -67,27 +66,25 @@ class AppearanceConfigPage extends HookWidget {
title: Text('Appearance', style: theme.textTheme.headline6),
centerTitle: true,
),
body: Observer(
builder: (ctx) => ListView(
children: [
_SectionHeading('Theme'),
for (final theme in ThemeMode.values)
RadioListTile<ThemeMode>(
value: theme,
title: Text(theme.toString().split('.')[1]),
groupValue: configStore.theme,
onChanged: (selected) {
configStore.theme = selected;
},
),
SwitchListTile(
title: Text('AMOLED dark mode'),
value: configStore.amoledDarkMode,
onChanged: (checked) {
configStore.amoledDarkMode = checked;
})
],
),
body: ListView(
children: [
_SectionHeading('Theme'),
for (final theme in ThemeMode.values)
RadioListTile<ThemeMode>(
value: theme,
title: Text(theme.toString().split('.')[1]),
groupValue: configStore.theme,
onChanged: (selected) {
configStore.theme = selected;
},
),
SwitchListTile(
title: Text('AMOLED dark mode'),
value: configStore.amoledDarkMode,
onChanged: (checked) {
configStore.amoledDarkMode = checked;
})
],
),
);
}
@ -185,100 +182,92 @@ class AccountsConfigPage extends HookWidget {
),
],
),
body: Observer(
builder: (ctx) {
final theme = Theme.of(context);
return ListView(
children: [
if (accountsStore.tokens.isEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 100),
child: FlatButton.icon(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onPressed: () => showCupertinoModalPopup(
context: context,
builder: (_) => AddInstancePage(),
),
icon: Icon(Icons.add),
label: Text('Add instance')),
),
],
),
for (final entry in accountsStore.tokens.entries) ...[
SizedBox(height: 40),
Slidable(
actionPane: SlidableBehindActionPane(),
secondaryActions: [
IconSlideAction(
closeOnTap: true,
onTap: () => removeInstanceDialog(entry.key),
icon: Icons.delete_sweep,
color: Colors.red,
),
],
key: Key(entry.key),
child: Container(
color: theme.canvasColor,
child: ListTile(
dense: true,
contentPadding: EdgeInsets.only(left: 0, top: 0),
title: _SectionHeading(entry.key),
),
),
),
for (final username in entry.value.keys) ...[
Slidable(
actionPane: SlidableBehindActionPane(),
key: Key('$username@${entry.key}'),
secondaryActions: [
IconSlideAction(
closeOnTap: true,
onTap: () => removeUserDialog(entry.key, username),
icon: Icons.delete_sweep,
color: Colors.red,
body: ListView(
children: [
if (accountsStore.tokens.isEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 100),
child: FlatButton.icon(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
],
child: Container(
decoration: BoxDecoration(color: theme.canvasColor),
child: ListTile(
trailing: username ==
accountsStore.defaultUsernameFor(entry.key)
onPressed: () => showCupertinoModalPopup(
context: context,
builder: (_) => AddInstancePage(),
),
icon: Icon(Icons.add),
label: Text('Add instance')),
),
],
),
for (final entry in accountsStore.tokens.entries) ...[
SizedBox(height: 40),
Slidable(
actionPane: SlidableBehindActionPane(),
secondaryActions: [
IconSlideAction(
closeOnTap: true,
onTap: () => removeInstanceDialog(entry.key),
icon: Icons.delete_sweep,
color: Colors.red,
),
],
key: Key(entry.key),
child: Container(
color: theme.canvasColor,
child: ListTile(
dense: true,
contentPadding: EdgeInsets.only(left: 0, top: 0),
title: _SectionHeading(entry.key),
),
),
),
for (final username in entry.value.keys) ...[
Slidable(
actionPane: SlidableBehindActionPane(),
key: Key('$username@${entry.key}'),
secondaryActions: [
IconSlideAction(
closeOnTap: true,
onTap: () => removeUserDialog(entry.key, username),
icon: Icons.delete_sweep,
color: Colors.red,
),
],
child: Container(
decoration: BoxDecoration(color: theme.canvasColor),
child: ListTile(
trailing:
username == accountsStore.defaultUsernameFor(entry.key)
? Icon(
Icons.check_circle_outline,
color: theme.accentColor,
)
: null,
title: Text(username),
onLongPress: () {
accountsStore.setDefaultAccountFor(
entry.key, username);
},
onTap: () {}, // TODO: go to managing account
),
),
),
],
if (entry.value.keys.isEmpty)
ListTile(
leading: Icon(Icons.add),
title: Text('Add account'),
onTap: () {
showCupertinoModalPopup(
context: context,
builder: (_) =>
AddAccountPage(instanceUrl: entry.key));
title: Text(username),
onLongPress: () {
accountsStore.setDefaultAccountFor(entry.key, username);
},
onTap: () {}, // TODO: go to managing account
),
]
),
),
],
);
},
if (entry.value.keys.isEmpty)
ListTile(
leading: Icon(Icons.add),
title: Text('Add account'),
onTap: () {
showCupertinoModalPopup(
context: context,
builder: (_) => AddAccountPage(instanceUrl: entry.key));
},
),
]
],
),
);
}

View File

@ -1,41 +1,70 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'accounts_store.g.dart';
import 'shared_pref_keys.dart';
/// Store that manages all accounts
class AccountsStore extends _AccountsStore with _$AccountsStore {}
class AccountsStore extends ChangeNotifier {
/// Map containing JWT tokens of specific users.
/// If a token is in this map, the user is considered logged in
/// for that account.
/// `tokens['instanceUrl']['username']`
HashMap<String, HashMap<String, Jwt>> get tokens => _tokens;
HashMap<String, HashMap<String, Jwt>> _tokens;
abstract class _AccountsStore with Store {
ReactionDisposer _saveReactionDisposer;
ReactionDisposer _pickDefaultsDisposer;
/// default account for a given instance
/// map where keys are instanceUrls and values are usernames
HashMap<String, String> _defaultAccounts;
_AccountsStore() {
// persistently save settings each time they are changed
_saveReactionDisposer = reaction(
(_) => [
tokens.forEach((k, submap) =>
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
_defaultAccount,
_defaultAccounts.asObservable(),
],
(_) => save(),
);
/// default account for the app
/// It is in a form of `username@instanceUrl`
String _defaultAccount;
// automatically set new default accounts when accounts are added/removed
_pickDefaultsDisposer = reaction(
(_) => [
tokens.forEach((k, submap) =>
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
],
(_) => _assignDefaultAccounts(),
);
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
// I barely understand what I did. Long story short it casts a
// raw json into a nested ObservableMap
nestedMapsCast<T>(T f(Map<String, dynamic> json)) => HashMap.of(
(jsonDecode(prefs.getString(SharedPrefKeys.tokens) ?? '{}')
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 Map<String, dynamic>)),
),
),
),
),
);
// set saved settings or create defaults
_tokens = nestedMapsCast((json) => Jwt(json['raw']));
_defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount);
_defaultAccounts = HashMap.of(Map.castFrom(
jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null') ??
{}));
notifyListeners();
}
@action
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(SharedPrefKeys.defaultAccount, _defaultAccount);
await prefs.setString(
SharedPrefKeys.defaultAccounts, jsonEncode(_defaultAccounts));
await prefs.setString(SharedPrefKeys.tokens, jsonEncode(tokens));
}
/// automatically sets default accounts
void _assignDefaultAccounts() {
// remove dangling defaults
_defaultAccounts.entries.map((dft) {
@ -80,65 +109,6 @@ abstract class _AccountsStore with Store {
}
}
void dispose() {
_saveReactionDisposer();
_pickDefaultsDisposer();
}
void load() async {
final prefs = await SharedPreferences.getInstance();
// I barely understand what I did. Long story short it casts a
// raw json into a nested ObservableMap
nestedMapsCast<T>(String key, T f(Map<String, dynamic> json)) =>
ObservableMap.of(
(jsonDecode(prefs.getString(key) ?? '{}') as Map<String, dynamic>)
?.map(
(k, e) => MapEntry(
k,
ObservableMap.of(
(e as Map<String, dynamic>)?.map(
(k, e) => MapEntry(
k, e == null ? null : f(e as Map<String, dynamic>)),
),
),
),
),
);
// set saved settings or create defaults
tokens = nestedMapsCast('tokens', (json) => Jwt(json['raw']));
_defaultAccount = prefs.getString('defaultAccount');
_defaultAccounts = ObservableMap.of(Map.castFrom(
jsonDecode(prefs.getString('defaultAccounts') ?? 'null') ?? {}));
}
void save() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('defaultAccount', _defaultAccount);
await prefs.setString('defaultAccounts', jsonEncode(_defaultAccounts));
await prefs.setString('tokens', jsonEncode(tokens));
}
/// Map containing JWT tokens of specific users.
/// If a token is in this map, the user is considered logged in
/// for that account.
/// `tokens['instanceUrl']['username']`
@observable
ObservableMap<String, ObservableMap<String, Jwt>> tokens;
/// default account for a given instance
/// map where keys are instanceUrls and values are usernames
@observable
ObservableMap<String, String> _defaultAccounts;
/// default account for the app
/// It is in a form of `username@instanceUrl`
@observable
String _defaultAccount;
@computed
String get defaultUsername {
if (_defaultAccount == null) {
return null;
@ -147,7 +117,6 @@ abstract class _AccountsStore with Store {
return _defaultAccount.split('@')[0];
}
@computed
String get defaultInstanceUrl {
if (_defaultAccount == null) {
return null;
@ -156,15 +125,14 @@ abstract class _AccountsStore with Store {
return _defaultAccount.split('@')[1];
}
String defaultUsernameFor(String instanceUrl) => Computed(() {
if (isAnonymousFor(instanceUrl)) {
return null;
}
String defaultUsernameFor(String instanceUrl) {
if (isAnonymousFor(instanceUrl)) {
return null;
}
return _defaultAccounts[instanceUrl];
}).value;
return _defaultAccounts[instanceUrl];
}
@computed
Jwt get defaultToken {
if (_defaultAccount == null) {
return null;
@ -174,44 +142,45 @@ abstract class _AccountsStore with Store {
return tokens[userTag[1]][userTag[0]];
}
Jwt defaultTokenFor(String instanceUrl) => Computed(() {
if (isAnonymousFor(instanceUrl)) {
return null;
}
Jwt defaultTokenFor(String instanceUrl) {
if (isAnonymousFor(instanceUrl)) {
return null;
}
return tokens[instanceUrl][_defaultAccounts[instanceUrl]];
}).value;
return tokens[instanceUrl][_defaultAccounts[instanceUrl]];
}
/// sets globally default account
@action
void setDefaultAccount(String instanceUrl, String username) {
_defaultAccount = '$username@$instanceUrl';
notifyListeners();
save();
}
/// sets default account for given instance
@action
void setDefaultAccountFor(String instanceUrl, String username) {
_defaultAccounts[instanceUrl] = username;
notifyListeners();
save();
}
/// An instance is considered anonymous if it was not
/// added or there are no accounts assigned to it.
bool isAnonymousFor(String instanceUrl) => Computed(() {
if (!instances.contains(instanceUrl)) {
return true;
}
bool isAnonymousFor(String instanceUrl) {
if (!instances.contains(instanceUrl)) {
return true;
}
return tokens[instanceUrl].isEmpty;
}).value;
return tokens[instanceUrl].isEmpty;
}
/// `true` if no added instance has an account assigned to it
@computed
bool get hasNoAccount => loggedInInstances.isEmpty;
@computed
Iterable<String> get instances => tokens.keys;
@computed
Iterable<String> get loggedInInstances =>
instances.where((e) => !isAnonymousFor(e));
@ -220,7 +189,6 @@ abstract class _AccountsStore with Store {
/// set as default for the app
/// if it's the first account for an instance the account is
/// set as default for that instance
@action
Future<void> addAccount(
String instanceUrl,
String usernameOrEmail,
@ -240,12 +208,15 @@ abstract class _AccountsStore with Store {
await lemmy.getSite(auth: token.raw).then((value) => value.myUser);
tokens[instanceUrl][userData.name] = token;
_assignDefaultAccounts();
notifyListeners();
save();
}
/// adds a new instance with no accounts associated with it.
/// Additionally makes a test `GET /site` request to check if the instance exists.
/// Check is skipped when [assumeValid] is `true`
@action
Future<void> addInstance(
String instanceUrl, {
bool assumeValid = false,
@ -263,17 +234,27 @@ abstract class _AccountsStore with Store {
}
}
tokens[instanceUrl] = ObservableMap();
tokens[instanceUrl] = HashMap();
_assignDefaultAccounts();
notifyListeners();
save();
}
/// This also removes all accounts assigned to this instance
@action
void removeInstance(String instanceUrl) {
tokens.remove(instanceUrl);
_assignDefaultAccounts();
notifyListeners();
save();
}
@action
void removeAccount(String instanceUrl, String username) {
tokens[instanceUrl].remove(username);
_assignDefaultAccounts();
notifyListeners();
save();
}
}

View File

@ -1,187 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'accounts_store.dart';
// **************************************************************************
// 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 _$AccountsStore on _AccountsStore, Store {
Computed<String> _$defaultUsernameComputed;
@override
String get defaultUsername => (_$defaultUsernameComputed ??= Computed<String>(
() => super.defaultUsername,
name: '_AccountsStore.defaultUsername'))
.value;
Computed<String> _$defaultInstanceUrlComputed;
@override
String get defaultInstanceUrl => (_$defaultInstanceUrlComputed ??=
Computed<String>(() => super.defaultInstanceUrl,
name: '_AccountsStore.defaultInstanceUrl'))
.value;
Computed<Jwt> _$defaultTokenComputed;
@override
Jwt get defaultToken =>
(_$defaultTokenComputed ??= Computed<Jwt>(() => super.defaultToken,
name: '_AccountsStore.defaultToken'))
.value;
Computed<bool> _$hasNoAccountComputed;
@override
bool get hasNoAccount =>
(_$hasNoAccountComputed ??= Computed<bool>(() => super.hasNoAccount,
name: '_AccountsStore.hasNoAccount'))
.value;
Computed<Iterable<String>> _$instancesComputed;
@override
Iterable<String> get instances =>
(_$instancesComputed ??= Computed<Iterable<String>>(() => super.instances,
name: '_AccountsStore.instances'))
.value;
Computed<Iterable<String>> _$loggedInInstancesComputed;
@override
Iterable<String> get loggedInInstances => (_$loggedInInstancesComputed ??=
Computed<Iterable<String>>(() => super.loggedInInstances,
name: '_AccountsStore.loggedInInstances'))
.value;
final _$tokensAtom = Atom(name: '_AccountsStore.tokens');
@override
ObservableMap<String, ObservableMap<String, Jwt>> get tokens {
_$tokensAtom.reportRead();
return super.tokens;
}
@override
set tokens(ObservableMap<String, ObservableMap<String, Jwt>> value) {
_$tokensAtom.reportWrite(value, super.tokens, () {
super.tokens = value;
});
}
final _$_defaultAccountsAtom = Atom(name: '_AccountsStore._defaultAccounts');
@override
ObservableMap<String, String> get _defaultAccounts {
_$_defaultAccountsAtom.reportRead();
return super._defaultAccounts;
}
@override
set _defaultAccounts(ObservableMap<String, String> value) {
_$_defaultAccountsAtom.reportWrite(value, super._defaultAccounts, () {
super._defaultAccounts = value;
});
}
final _$_defaultAccountAtom = Atom(name: '_AccountsStore._defaultAccount');
@override
String get _defaultAccount {
_$_defaultAccountAtom.reportRead();
return super._defaultAccount;
}
@override
set _defaultAccount(String value) {
_$_defaultAccountAtom.reportWrite(value, super._defaultAccount, () {
super._defaultAccount = value;
});
}
final _$addAccountAsyncAction = AsyncAction('_AccountsStore.addAccount');
@override
Future<void> addAccount(
String instanceUrl, String usernameOrEmail, String password) {
return _$addAccountAsyncAction
.run(() => super.addAccount(instanceUrl, usernameOrEmail, password));
}
final _$addInstanceAsyncAction = AsyncAction('_AccountsStore.addInstance');
@override
Future<void> addInstance(String instanceUrl, {bool assumeValid = false}) {
return _$addInstanceAsyncAction
.run(() => super.addInstance(instanceUrl, assumeValid: assumeValid));
}
final _$_AccountsStoreActionController =
ActionController(name: '_AccountsStore');
@override
void _assignDefaultAccounts() {
final _$actionInfo = _$_AccountsStoreActionController.startAction(
name: '_AccountsStore._assignDefaultAccounts');
try {
return super._assignDefaultAccounts();
} finally {
_$_AccountsStoreActionController.endAction(_$actionInfo);
}
}
@override
void setDefaultAccount(String instanceUrl, String username) {
final _$actionInfo = _$_AccountsStoreActionController.startAction(
name: '_AccountsStore.setDefaultAccount');
try {
return super.setDefaultAccount(instanceUrl, username);
} finally {
_$_AccountsStoreActionController.endAction(_$actionInfo);
}
}
@override
void setDefaultAccountFor(String instanceUrl, String username) {
final _$actionInfo = _$_AccountsStoreActionController.startAction(
name: '_AccountsStore.setDefaultAccountFor');
try {
return super.setDefaultAccountFor(instanceUrl, username);
} finally {
_$_AccountsStoreActionController.endAction(_$actionInfo);
}
}
@override
void removeInstance(String instanceUrl) {
final _$actionInfo = _$_AccountsStoreActionController.startAction(
name: '_AccountsStore.removeInstance');
try {
return super.removeInstance(instanceUrl);
} finally {
_$_AccountsStoreActionController.endAction(_$actionInfo);
}
}
@override
void removeAccount(String instanceUrl, String username) {
final _$actionInfo = _$_AccountsStoreActionController.startAction(
name: '_AccountsStore.removeAccount');
try {
return super.removeAccount(instanceUrl, username);
} finally {
_$_AccountsStoreActionController.endAction(_$actionInfo);
}
}
@override
String toString() {
return '''
tokens: ${tokens},
defaultUsername: ${defaultUsername},
defaultInstanceUrl: ${defaultInstanceUrl},
defaultToken: ${defaultToken},
hasNoAccount: ${hasNoAccount},
instances: ${instances},
loggedInInstances: ${loggedInInstances}
''';
}
}

View File

@ -1,46 +1,42 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'config_store.g.dart';
import 'shared_pref_keys.dart';
/// Store managing user-level configuration such as theme or language
class ConfigStore extends _ConfigStore with _$ConfigStore {}
abstract class _ConfigStore with Store {
ReactionDisposer _saveReactionDisposer;
_ConfigStore() {
// persitently save settings each time they are changed
_saveReactionDisposer = reaction((_) => [theme, amoledDarkMode], (_) {
save();
});
class ConfigStore extends ChangeNotifier {
ThemeMode _theme;
ThemeMode get theme => _theme;
set theme(ThemeMode theme) {
_theme = theme;
notifyListeners();
save();
}
void dispose() {
_saveReactionDisposer();
bool _amoledDarkMode;
bool get amoledDarkMode => _amoledDarkMode;
set amoledDarkMode(bool amoledDarkMode) {
_amoledDarkMode = amoledDarkMode;
notifyListeners();
save();
}
void load() async {
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
// load saved settings or create defaults
theme = _themeModeFromString(prefs.getString('theme') ?? 'system');
amoledDarkMode = prefs.getBool('amoledDarkMode') ?? false;
theme =
_themeModeFromString(prefs.getString(SharedPrefKeys.theme) ?? 'system');
amoledDarkMode = prefs.getBool(SharedPrefKeys.amoledDarkMode) ?? false;
notifyListeners();
}
void save() async {
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme', describeEnum(theme));
await prefs.setBool('amoledDarkMode', amoledDarkMode);
await prefs.setString(SharedPrefKeys.theme, describeEnum(theme));
await prefs.setBool(SharedPrefKeys.amoledDarkMode, amoledDarkMode);
}
@observable
ThemeMode theme;
@observable
bool amoledDarkMode;
}
/// converts string to ThemeMode

View File

@ -1,49 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'config_store.dart';
// **************************************************************************
// 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;
});
}
@override
String toString() {
return '''
theme: ${theme},
amoledDarkMode: ${amoledDarkMode}
''';
}
}

View File

@ -0,0 +1,8 @@
/// 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,20 +1,6 @@
# 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: "6.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.14"
args:
dependency: transitive
description:
@ -36,62 +22,6 @@ 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.3.0"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.11"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
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:
@ -113,20 +43,6 @@ 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.2"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
clock:
dependency: transitive
description:
@ -134,13 +50,6 @@ 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.4.1"
collection:
dependency: transitive
description:
@ -162,13 +71,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.2"
cupertino_icons:
dependency: "direct main"
description:
@ -176,13 +78,6 @@ 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.6"
effective_dart:
dependency: "direct dev"
description:
@ -218,13 +113,6 @@ 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
@ -258,13 +146,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.4"
flutter_mobx:
dependency: "direct main"
description:
name: flutter_mobx
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0+2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -303,27 +184,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
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"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
http:
dependency: transitive
description:
@ -331,13 +191,6 @@ 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:
@ -366,20 +219,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
version: "0.6.3-nullsafety.1"
json_annotation:
dependency: transitive
description:
@ -401,13 +247,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.3"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
markdown:
dependency: "direct main"
description:
@ -428,28 +267,7 @@ packages:
name: meta
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"
mobx:
dependency: "direct main"
description:
name: mobx
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1+2"
mobx_codegen:
dependency: "direct dev"
description:
name: mobx_codegen
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0+1"
version: "1.3.0-nullsafety.4"
nested:
dependency: transitive
description:
@ -457,20 +275,6 @@ 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.1.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
octo_image:
dependency: transitive
description:
@ -478,13 +282,6 @@ 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:
@ -569,13 +366,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
process:
dependency: transitive
description:
@ -597,20 +387,6 @@ packages:
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.5"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
rxdart:
dependency: transitive
description:
@ -653,32 +429,11 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2+7"
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.3"
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.6"
source_span:
dependency: transitive
description:
@ -706,7 +461,7 @@ packages:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.10.0-nullsafety.4"
stream_channel:
dependency: transitive
description:
@ -714,13 +469,6 @@ 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:
@ -756,13 +504,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.27"
timing:
dependency: transitive
description:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+2"
typed_data:
dependency: transitive
description:
@ -826,20 +567,6 @@ 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:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
win32:
dependency: transitive
description:
@ -854,13 +581,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.10.0-110 <2.11.0"
dart: ">=2.10.0-110 <=2.11.0-242.0.dev"
flutter: ">=1.20.0 <2.0.0"

View File

@ -38,8 +38,6 @@ dependencies:
# state management
flutter_hooks: ^0.13.2
mobx: ^1.2.1
flutter_mobx: ^1.1.0
provider: ^4.3.1
# utils
@ -58,8 +56,6 @@ dev_dependencies:
flutter_test:
sdk: flutter
effective_dart: ^1.0.0
build_runner: ^1.10.0
mobx_codegen: ^1.1.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.