Merge pull request #55 from krawieck/account-management-pages

Convert add instance and account dialogs to pages
This commit is contained in:
Marcin Wojnarowski 2020-09-24 16:52:23 +02:00 committed by GitHub
commit 1079431b33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 714 additions and 179 deletions

53
lib/hooks/debounce.dart Normal file
View File

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'ref.dart';
class Debounce {
final bool loading;
final void Function() callback;
void call() => callback();
// void dispose() {}
const Debounce({
@required this.loading,
@required this.callback,
});
}
/// will run `callback()` after debounce hook hasn't been called for the
/// specified `delayDuration`
Debounce useDebounce(
Future<Null> Function() callback, [
Duration delayDuration = const Duration(seconds: 1),
]) {
final loading = useState(false);
final timerHandle = useRef<Timer>(null);
cancel() {
timerHandle.current?.cancel();
loading.value = false;
}
useEffect(() => () => timerHandle.current?.cancel(), []);
start() {
timerHandle.current = Timer(delayDuration, () async {
loading.value = true;
await callback();
cancel();
});
}
return Debounce(
loading: loading.value,
callback: () {
cancel();
start();
},
);
}

203
lib/pages/add_account.dart Normal file
View File

@ -0,0 +1,203 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import 'package:url_launcher/url_launcher.dart' as ul;
import '../hooks/delayed_loading.dart';
import '../hooks/stores.dart';
import '../widgets/bottom_modal.dart';
import '../widgets/fullscreenable_image.dart';
import 'add_instance.dart';
class AddAccountPage extends HookWidget {
final String instanceUrl;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
AddAccountPage({@required this.instanceUrl}) : assert(instanceUrl != null);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final usernameController = useTextEditingController();
final passwordController = useTextEditingController();
useValueListenable(usernameController);
useValueListenable(passwordController);
final accountsStore = useAccountsStore();
final loading = useDelayedLoading(Duration(milliseconds: 500));
final selectedInstance = useState(instanceUrl);
final icon = useState<String>(null);
useEffect(() {
LemmyApi(selectedInstance.value)
.v1
.getSite()
.then((site) => icon.value = site.site.icon);
return null;
}, [selectedInstance.value]);
selectInstance() async {
final val = await showModalBottomSheet<String>(
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) => BottomModal(
title: 'select instance',
child: Column(children: [
for (final i in accountsStore.users.keys)
RadioListTile<String>(
value: i,
groupValue: selectedInstance.value,
onChanged: (val) {
Navigator.of(context).pop(val);
},
title: Text(i),
),
ListTile(
leading: Padding(
padding: const EdgeInsets.all(8),
child: Icon(Icons.add),
),
title: Text('Add instance'),
onTap: () async {
final val = await showCupertinoModalPopup<String>(
context: context,
builder: (context) => AddInstancePage(),
);
Navigator.of(context).pop(val);
},
),
]),
),
);
if (val != null) {
selectedInstance.value = val;
}
}
handleOnAdd() async {
try {
loading.start();
await accountsStore.addAccount(
selectedInstance.value,
usernameController.text,
passwordController.text,
);
Navigator.of(context).pop();
} on Exception catch (err) {
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(err.toString()),
));
}
loading.cancel();
}
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
leading: CloseButton(),
actionsIconTheme: theme.iconTheme,
iconTheme: theme.iconTheme,
textTheme: theme.textTheme,
brightness: theme.brightness,
centerTitle: true,
title: Text('Add account'),
backgroundColor: theme.canvasColor,
shadowColor: Colors.transparent,
),
body: Padding(
padding: const EdgeInsets.all(15),
child: ListView(
children: [
if (icon.value == null)
SizedBox(height: 150)
else
SizedBox(
height: 150,
child: FullscreenableImage(
url: icon.value,
child: CachedNetworkImage(
imageUrl: icon.value,
errorWidget: (_, __, ___) => Container(),
),
),
),
FlatButton(
onPressed: selectInstance,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(selectedInstance.value),
Icon(Icons.arrow_drop_down),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
// TODO: add support for password managers
TextField(
autofocus: true,
controller: usernameController,
decoration: InputDecoration(
isDense: true,
contentPadding:
EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
labelText: 'Username or email',
),
),
const SizedBox(height: 5),
TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
isDense: true,
contentPadding:
EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
labelText: 'Password',
),
),
RaisedButton(
color: theme.accentColor,
padding: EdgeInsets.all(0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: !loading.loading
? Text('Sign in')
: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(theme.canvasColor),
),
),
onPressed: usernameController.text.isEmpty ||
passwordController.text.isEmpty
? null
: loading.pending ? () {} : handleOnAdd,
),
FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Text('Register'),
onPressed: () {
ul.launch('https://${selectedInstance.value}/login');
},
),
],
),
),
);
}
}

161
lib/pages/add_instance.dart Normal file
View File

@ -0,0 +1,161 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import '../hooks/debounce.dart';
import '../hooks/stores.dart';
import '../widgets/fullscreenable_image.dart';
/// A page that let's user add a new instance. Pops a url of the added instance
class AddInstancePage extends HookWidget {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
AddInstancePage();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final instanceController = useTextEditingController();
useValueListenable(instanceController);
final accountsStore = useAccountsStore();
final isSite = useState<bool>(null);
final icon = useState<String>(null);
final prevInput = usePrevious(instanceController.text);
final debounce = useDebounce(() async {
if (prevInput == instanceController.text) return;
final inst = _fixInstanceUrl(instanceController.text);
if (inst.isEmpty) {
isSite.value = null;
return;
}
try {
icon.value = (await LemmyApi(inst).v1.getSite()).site.icon;
isSite.value = true;
// ignore: avoid_catches_without_on_clauses
} catch (e) {
isSite.value = false;
}
});
useEffect(() {
instanceController.addListener(debounce);
return () {
instanceController.removeListener(debounce);
};
}, []);
final inst = _fixInstanceUrl(instanceController.text);
handleOnAdd() async {
try {
await accountsStore.addInstance(inst, assumeValid: true);
Navigator.of(context).pop(inst);
} on Exception catch (err) {
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(err.toString()),
));
}
}
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor,
brightness: theme.brightness,
shadowColor: Colors.transparent,
iconTheme: theme.iconTheme,
centerTitle: true,
leading: CloseButton(),
actionsIconTheme: theme.iconTheme,
textTheme: theme.textTheme,
title: Text('Add instance'),
),
body: ListView(
children: [
if (isSite.value == true)
SizedBox(
height: 150,
child: FullscreenableImage(
child: CachedNetworkImage(
imageUrl: icon.value,
errorWidget: (_, __, ___) => Container(),
),
url: icon.value,
))
else if (isSite.value == false)
SizedBox(
height: 150,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.close, color: Colors.red),
Text('instance not found')
],
),
)
else
SizedBox(height: 150),
SizedBox(height: 15),
SizedBox(
height: 40,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: TextField(
autofocus: true,
controller: instanceController,
autocorrect: false,
decoration: InputDecoration(
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
labelText: 'instance url',
),
),
),
),
SizedBox(height: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: SizedBox(
height: 40,
child: RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: theme.accentColor,
child: !debounce.loading
? Text('Add')
: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(theme.canvasColor),
),
),
onPressed: isSite.value == true ? handleOnAdd : null,
),
),
),
],
),
);
}
}
String _fixInstanceUrl(String inst) {
if (inst.startsWith('https://')) {
inst = inst.substring(8);
}
if (inst.startsWith('http://')) {
inst = inst.substring(7);
}
if (inst.endsWith('/')) inst = inst.substring(0, inst.length - 1);
return inst;
}

View File

@ -2,9 +2,13 @@ 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';
import '../hooks/stores.dart';
import '../util/goto.dart';
import 'add_account.dart';
import 'add_instance.dart';
class SettingsPage extends StatelessWidget {
@override
@ -13,6 +17,7 @@ class SettingsPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(
brightness: theme.brightness,
backgroundColor: theme.scaffoldBackgroundColor,
shadowColor: Colors.transparent,
iconTheme: theme.iconTheme,
@ -51,6 +56,7 @@ class AppearanceConfigPage extends HookWidget {
return Scaffold(
appBar: AppBar(
brightness: theme.brightness,
backgroundColor: theme.scaffoldBackgroundColor,
shadowColor: Colors.transparent,
iconTheme: theme.iconTheme,
@ -91,24 +97,88 @@ class AccountsConfigPage extends HookWidget {
final theme = Theme.of(context);
final accountsStore = useAccountsStore();
removeInstanceDialog(String instanceUrl) async {
if (await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Remove instance?'),
content: Text('Are you sure you want to remove $instanceUrl?'),
actions: [
FlatButton(
child: Text('no'),
onPressed: () => Navigator.of(context).pop(false),
),
FlatButton(
child: Text('yes'),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
) ??
false) {
accountsStore.removeInstance(instanceUrl);
}
}
Future<void> removeUserDialog(String instanceUrl, String username) async {
if (await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Remove user?'),
content: Text(
'Are you sure you want to remove $username@$instanceUrl?'),
actions: [
FlatButton(
child: Text('no'),
onPressed: () => Navigator.of(context).pop(false),
),
FlatButton(
child: Text('yes'),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
) ??
false) {
accountsStore.removeAccount(instanceUrl, username);
}
}
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor,
brightness: theme.brightness,
shadowColor: Colors.transparent,
iconTheme: theme.iconTheme,
title: Text('Accounts', style: theme.textTheme.headline6),
centerTitle: true,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (_) =>
_AccountsConfigAddInstanceDialog(scaffoldKey: _scaffoldKey),
);
},
floatingActionButton: SpeedDial(
animatedIcon: AnimatedIcons.menu_close, // TODO: change to + => x
closeManually: false,
curve: Curves.bounceIn,
tooltip: 'Add account or instance',
child: Icon(Icons.add),
overlayColor: theme.canvasColor,
children: [
SpeedDialChild(
child: Icon(Icons.person_add),
label: 'Add account',
labelBackgroundColor: theme.canvasColor,
onTap: () => showCupertinoModalPopup(
context: context,
builder: (_) =>
AddAccountPage(instanceUrl: accountsStore.users.keys.last)),
),
SpeedDialChild(
child: Icon(Icons.dns),
labelBackgroundColor: theme.canvasColor,
label: 'Add instance',
onTap: () => showCupertinoModalPopup(
context: context, builder: (_) => AddInstancePage()),
),
],
),
body: Observer(
builder: (ctx) {
@ -116,38 +186,91 @@ class AccountsConfigPage extends HookWidget {
return ListView(
children: [
if (accountsStore.users.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.users.entries) ...[
_SectionHeading(entry.key),
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) ...[
ListTile(
trailing:
username == accountsStore.defaultUserFor(entry.key).name
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.defaultUserFor(entry.key).name
? Icon(
Icons.check_circle_outline,
color: theme.accentColor,
)
: null,
title: Text(username),
onLongPress: () {
accountsStore.setDefaultAccountFor(entry.key, username);
},
title: Text(username),
onLongPress: () {
accountsStore.setDefaultAccountFor(
entry.key, username);
},
onTap: () {}, // TODO: go to managing account
onTap: () {}, // TODO: go to managing account
),
),
),
],
ListTile(
leading: Icon(Icons.add),
title: Text('Add account'),
onTap: () {
showDialog(
context: context,
builder: (_) => _AccountsConfigAddAccountDialog(
scaffoldKey: _scaffoldKey,
instanceUrl: entry.key,
),
);
},
),
if (entry.value.keys.isEmpty)
ListTile(
leading: Icon(Icons.add),
title: Text('Add account'),
onTap: () {
showCupertinoModalPopup(
context: context,
builder: (_) =>
AddAccountPage(instanceUrl: entry.key));
},
),
]
],
);
@ -157,135 +280,6 @@ class AccountsConfigPage extends HookWidget {
}
}
class _AccountsConfigAddInstanceDialog extends HookWidget {
final GlobalKey<ScaffoldState> scaffoldKey;
const _AccountsConfigAddInstanceDialog({@required this.scaffoldKey})
: assert(scaffoldKey != null);
@override
Widget build(BuildContext context) {
final instanceController = useTextEditingController();
useValueListenable(instanceController);
final accountsStore = useAccountsStore();
final loading = useState(false);
handleOnAdd() async {
try {
loading.value = true;
await accountsStore.addInstance(instanceController.text);
scaffoldKey.currentState.hideCurrentSnackBar();
} on Exception catch (err) {
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(err.toString()),
));
}
loading.value = false;
Navigator.of(context).pop();
}
return AlertDialog(
title: Text('Add instance'),
content: TextField(
autofocus: true,
controller: instanceController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Instance url',
),
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: !loading.value ? Text('Add') : CircularProgressIndicator(),
onPressed: instanceController.text.isEmpty ? null : handleOnAdd,
),
],
);
}
}
class _AccountsConfigAddAccountDialog extends HookWidget {
final GlobalKey<ScaffoldState> scaffoldKey;
final String instanceUrl;
const _AccountsConfigAddAccountDialog(
{@required this.scaffoldKey, @required this.instanceUrl})
: assert(scaffoldKey != null),
assert(instanceUrl != null);
@override
Widget build(BuildContext context) {
final usernameController = useTextEditingController();
final passwordController = useTextEditingController();
useValueListenable(usernameController);
useValueListenable(passwordController);
final accountsStore = useAccountsStore();
final loading = useState(false);
handleOnAdd() async {
try {
loading.value = true;
await accountsStore.addAccount(
instanceUrl,
usernameController.text,
passwordController.text,
);
} on Exception catch (err) {
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(err.toString()),
));
}
loading.value = false;
Navigator.of(context).pop();
}
return AlertDialog(
title: Text('Add account'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
autofocus: true,
controller: usernameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username or email',
),
),
const SizedBox(height: 5),
TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
),
],
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: !loading.value ? Text('Add') : CircularProgressIndicator(),
onPressed:
usernameController.text.isEmpty || passwordController.text.isEmpty
? null
: handleOnAdd,
),
],
);
}
}
class _SectionHeading extends StatelessWidget {
final String text;
@ -297,7 +291,7 @@ class _SectionHeading extends StatelessWidget {
return Padding(
child: Text(text.toUpperCase(),
style: theme.textTheme.subtitle2.copyWith(color: theme.accentColor)),
padding: EdgeInsets.only(left: 20, top: 40),
padding: EdgeInsets.only(left: 20, top: 0),
);
}
}

View File

@ -10,6 +10,7 @@ class AccountsStore extends _AccountsStore with _$AccountsStore {}
abstract class _AccountsStore with Store {
ReactionDisposer _saveReactionDisposer;
ReactionDisposer _pickDefaultsDisposer;
_AccountsStore() {
// persistently save settings each time they are changed
@ -24,10 +25,67 @@ abstract class _AccountsStore with Store {
],
(_) => save(),
);
// check if there's a default profile and if not, select one
_pickDefaultsDisposer = reaction(
(_) => [
users.forEach((k, submap) =>
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
tokens.forEach((k, submap) =>
MapEntry(k, submap.forEach((k2, v2) => MapEntry(k2, v2)))),
],
(_) => _assignDefaultAccounts(),
);
}
@action
void _assignDefaultAccounts() {
// remove dangling defaults
_defaultAccounts.entries.map((dft) {
final instance = dft.key;
final username = dft.value;
// if instance or username doesn't exist, remove
if (!users.containsKey(instance) ||
!users[instance].containsKey(username)) {
return instance;
}
}).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 (!users.containsKey(instance) ||
!users[instance].containsKey(username)) {
_defaultAccount = null;
}
}
// set local defaults
for (final instanceUrl in users.keys) {
// if this instance is not in defaults
if (!_defaultAccounts.containsKey(instanceUrl)) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceUrl)) {
setDefaultAccountFor(instanceUrl, users[instanceUrl].keys.first);
}
}
}
// set global default
if (_defaultAccount == null) {
// select first account of first instance
for (final instanceUrl in users.keys) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceUrl)) {
setDefaultAccount(instanceUrl, users[instanceUrl].keys.first);
}
}
}
}
void dispose() {
_saveReactionDisposer();
_pickDefaultsDisposer();
}
void load() async {
@ -122,11 +180,13 @@ abstract class _AccountsStore with Store {
return tokens[instanceUrl][_defaultAccounts[instanceUrl]];
}).value;
/// sets globally default account
@action
void setDefaultAccount(String instanceUrl, String username) {
_defaultAccount = '$username@$instanceUrl';
}
/// sets default account for given instance
@action
void setDefaultAccountFor(String instanceUrl, String username) {
_defaultAccounts[instanceUrl] = username;
@ -167,15 +227,6 @@ abstract class _AccountsStore with Store {
final userData =
await lemmy.getSite(auth: token.raw).then((value) => value.myUser);
// first account for this instance
if (users[instanceUrl].isEmpty) {
// first account ever
if (hasNoAccount) {
setDefaultAccount(instanceUrl, userData.name);
}
setDefaultAccountFor(instanceUrl, userData.name);
}
users[instanceUrl][userData.name] = userData;
tokens[instanceUrl][userData.name] = token;
}
@ -183,21 +234,36 @@ abstract class _AccountsStore with Store {
/// adds a new instance with no accounts associated with it.
/// Additionally makes a test GET /site request to check if the instance exists
@action
Future<void> addInstance(String instanceUrl) async {
Future<void> addInstance(
String instanceUrl, {
bool assumeValid = false,
}) async {
if (users.containsKey(instanceUrl)) {
throw Exception('This instance has already been added');
}
try {
await LemmyApi(instanceUrl).v1.getSite();
// ignore: avoid_catches_without_on_clauses
} catch (_) {
throw Exception('This instance seems to not exist');
if (!assumeValid) {
try {
await LemmyApi(instanceUrl).v1.getSite();
// ignore: avoid_catches_without_on_clauses
} catch (_) {
throw Exception('This instance seems to not exist');
}
}
users[instanceUrl] = ObservableMap();
tokens[instanceUrl] = ObservableMap();
}
// TODO: add a way of removing accounts/instances
@action
void removeInstance(String instanceUrl) {
users.remove(instanceUrl);
tokens.remove(instanceUrl);
}
@action
void removeAccount(String instanceUrl, String username) {
users[instanceUrl].remove(username);
tokens[instanceUrl].remove(username);
}
}

View File

@ -23,6 +23,13 @@ mixin _$AccountsStore on _AccountsStore, Store {
(_$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;
final _$usersAtom = Atom(name: '_AccountsStore.users');
@ -96,13 +103,25 @@ mixin _$AccountsStore on _AccountsStore, Store {
final _$addInstanceAsyncAction = AsyncAction('_AccountsStore.addInstance');
@override
Future<void> addInstance(String instanceUrl) {
return _$addInstanceAsyncAction.run(() => super.addInstance(instanceUrl));
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(
@ -125,13 +144,36 @@ mixin _$AccountsStore on _AccountsStore, Store {
}
}
@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 '''
users: ${users},
tokens: ${tokens},
defaultUser: ${defaultUser},
defaultToken: ${defaultToken}
defaultToken: ${defaultToken},
hasNoAccount: ${hasNoAccount}
''';
}
}

View File

@ -265,6 +265,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0+2"
flutter_slidable:
dependency: "direct main"
description:
name: flutter_slidable
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.7"
flutter_speed_dial:
dependency: "direct main"
description:
name: flutter_speed_dial
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.5"
flutter_test:
dependency: "direct dev"
description: flutter

View File

@ -21,6 +21,8 @@ environment:
sdk: '>=2.7.0 <3.0.0'
dependencies:
flutter_speed_dial: ^1.2.5
flutter_slidable: ^0.5.7
photo_view: ^0.10.2
url_launcher: ^5.5.1
markdown: ^2.1.8