Merge pull request #23 from krawieck/accounts-store
This commit is contained in:
commit
de7d059e9e
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'stores/accounts_store.dart';
|
||||
import 'stores/config_store.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
|
@ -10,6 +11,9 @@ Future<void> main() async {
|
|||
var configStore = ConfigStore();
|
||||
await configStore.load();
|
||||
|
||||
var accountsStore = AccountsStore();
|
||||
await accountsStore.load();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
|
@ -17,6 +21,10 @@ Future<void> main() async {
|
|||
create: (_) => configStore,
|
||||
dispose: (_, store) => store.dispose(),
|
||||
),
|
||||
Provider<AccountsStore>(
|
||||
create: (_) => accountsStore,
|
||||
dispose: (_, store) => store.dispose(),
|
||||
),
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
|
@ -31,7 +39,6 @@ class MyApp extends StatelessWidget {
|
|||
themeMode: ctx.watch<ConfigStore>().theme,
|
||||
darkTheme: ThemeData.dark(),
|
||||
theme: ThemeData(
|
||||
primarySwatch: ctx.watch<ConfigStore>().accentColor,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
home: MyHomePage(title: 'Flutter hello world'),
|
||||
|
|
|
@ -1,64 +1,118 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../stores/accounts_store.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/user_profile.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
class UserProfileTab extends HookWidget {
|
||||
final User user;
|
||||
|
||||
UserProfileTab(this.user);
|
||||
UserProfileTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
centerTitle: true,
|
||||
title: FlatButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@${user.name}',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Icon(
|
||||
Icons.expand_more,
|
||||
color: theme.primaryIconTheme.color,
|
||||
return Observer(
|
||||
builder: (ctx) {
|
||||
var user = ctx.watch<AccountsStore>().defaultUser;
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
centerTitle: true,
|
||||
title: FlatButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@${user.name}',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Icon(
|
||||
Icons.expand_more,
|
||||
color: theme.primaryIconTheme.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) {
|
||||
var userTags = <String>[];
|
||||
|
||||
ctx
|
||||
.read<AccountsStore>()
|
||||
.users
|
||||
.forEach((instanceUrl, value) {
|
||||
value.forEach((username, _) {
|
||||
userTags.add('$username@$instanceUrl');
|
||||
});
|
||||
});
|
||||
|
||||
return Observer(
|
||||
builder: (ctx) {
|
||||
var user = ctx.watch<AccountsStore>().defaultUser;
|
||||
var instanceUrl = user.actorId.split('/')[2];
|
||||
|
||||
return BottomModal(
|
||||
title: 'account',
|
||||
child: Column(
|
||||
children: [
|
||||
for (final tag in userTags)
|
||||
RadioListTile<String>(
|
||||
value: tag,
|
||||
title: Text(tag),
|
||||
groupValue: '${user.name}@$instanceUrl',
|
||||
onChanged: (selected) {
|
||||
var userTag = selected.split('@');
|
||||
ctx.read<AccountsStore>().setDefaultAccount(
|
||||
userTag[1], userTag[0]);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Container(
|
||||
decoration: BoxDecoration(boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 10,
|
||||
color: Colors.black54,
|
||||
)
|
||||
]),
|
||||
child: Icon(
|
||||
Icons.settings,
|
||||
color: user.banner == null ? theme.iconTheme.color : null,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (_) => Settings()));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () {}, // TODO: should open bottomsheet
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Container(
|
||||
decoration: BoxDecoration(boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 10,
|
||||
color: Colors.black54,
|
||||
)
|
||||
]),
|
||||
child: Icon(
|
||||
Icons.settings,
|
||||
color: user.banner == null ? theme.iconTheme.color : null,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (_) => Settings()));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: UserProfile(user),
|
||||
body: UserProfile(
|
||||
userId: user.id,
|
||||
instanceUrl: user.actorId.split('/')[2],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../stores/accounts_store.dart';
|
||||
import '../stores/config_store.dart';
|
||||
|
||||
class Settings extends StatelessWidget {
|
||||
|
@ -23,7 +24,10 @@ class Settings extends StatelessWidget {
|
|||
ListTile(
|
||||
leading: Icon(Icons.person),
|
||||
title: Text('Accounts'),
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (_) => _AccountsConfig()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.color_lens),
|
||||
|
@ -69,14 +73,60 @@ class _AppearanceConfig extends StatelessWidget {
|
|||
ctx.read<ConfigStore>().theme = selected;
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'Accent color',
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
// TODO: add accent color picking
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountsConfig extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
shadowColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text('Accounts', style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Observer(
|
||||
builder: (ctx) {
|
||||
var accountsStore = ctx.watch<AccountsStore>();
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var entry in accountsStore.users.entries) ...[
|
||||
Text(
|
||||
entry.key,
|
||||
style: theme.textTheme.subtitle2,
|
||||
),
|
||||
for (var username in entry.value.keys) ...[
|
||||
ListTile(
|
||||
trailing:
|
||||
username == accountsStore.defaultUserFor(entry.key).name
|
||||
? Icon(Icons.check_circle_outline)
|
||||
: null,
|
||||
selected: username ==
|
||||
accountsStore.defaultUserFor(entry.key).name,
|
||||
title: Text(username),
|
||||
onLongPress: () {
|
||||
accountsStore.setDefaultAccountFor(entry.key, username);
|
||||
},
|
||||
onTap: () {}, // TODO: go to managing account
|
||||
),
|
||||
],
|
||||
Divider(),
|
||||
]
|
||||
]..removeLast(), // removes trailing Divider
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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';
|
||||
|
||||
class AccountsStore extends _AccountsStore with _$AccountsStore {}
|
||||
|
||||
abstract class _AccountsStore with Store {
|
||||
ReactionDisposer _saveReactionDisposer;
|
||||
|
||||
_AccountsStore() {
|
||||
// persistently save settings each time they are changed
|
||||
_saveReactionDisposer = reaction(
|
||||
// TODO: does not react to deep changes in users and tokens
|
||||
(_) => [
|
||||
users.asObservable(),
|
||||
tokens.asObservable(),
|
||||
_defaultAccount,
|
||||
_defaultAccounts.asObservable(),
|
||||
],
|
||||
(_) {
|
||||
save();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_saveReactionDisposer();
|
||||
}
|
||||
|
||||
void load() async {
|
||||
var prefs = await SharedPreferences.getInstance();
|
||||
// set saved settings or create defaults
|
||||
// TODO: load saved users and tokens
|
||||
users = ObservableMap();
|
||||
tokens = ObservableMap();
|
||||
_defaultAccount = prefs.getString('defaultAccount');
|
||||
_defaultAccounts = ObservableMap.of(Map.castFrom(
|
||||
jsonDecode(prefs.getString('defaultAccounts') ?? 'null') ?? {}));
|
||||
}
|
||||
|
||||
void save() async {
|
||||
var prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setString('defaultAccount', _defaultAccount);
|
||||
await prefs.setString('defaultAccounts', jsonEncode(_defaultAccounts));
|
||||
await prefs.setString('users', jsonEncode(users));
|
||||
await prefs.setString('tokens', jsonEncode(tokens));
|
||||
}
|
||||
|
||||
/// if path to tokens map exists, it exists for users as well
|
||||
/// `users['instanceUrl']['username']`
|
||||
@observable
|
||||
ObservableMap<String, ObservableMap<String, User>> users;
|
||||
|
||||
/// if path to users map exists, it exists for tokens as well
|
||||
/// `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
|
||||
/// username@instanceUrl
|
||||
@observable
|
||||
String _defaultAccount;
|
||||
|
||||
@computed
|
||||
User get defaultUser {
|
||||
var userTag = _defaultAccount.split('@');
|
||||
return users[userTag[1]][userTag[0]];
|
||||
}
|
||||
|
||||
@computed
|
||||
Jwt get defaultToken {
|
||||
var userTag = _defaultAccount.split('@');
|
||||
return tokens[userTag[1]][userTag[0]];
|
||||
}
|
||||
|
||||
User defaultUserFor(String instanceUrl) =>
|
||||
Computed(() => users[instanceUrl][_defaultAccounts[instanceUrl]]).value;
|
||||
|
||||
Jwt defaultTokenFor(String instanceUrl) =>
|
||||
Computed(() => tokens[instanceUrl][_defaultAccounts[instanceUrl]]).value;
|
||||
|
||||
@action
|
||||
void setDefaultAccount(String instanceUrl, String username) {
|
||||
_defaultAccount = '$username@$instanceUrl';
|
||||
}
|
||||
|
||||
@action
|
||||
void setDefaultAccountFor(String instanceUrl, String username) {
|
||||
_defaultAccounts[instanceUrl] = username;
|
||||
}
|
||||
|
||||
/// adds a new account
|
||||
/// if it's the first account ever the account is
|
||||
/// 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,
|
||||
String password,
|
||||
) async {
|
||||
var lemmy = LemmyApi(instanceUrl).v1;
|
||||
|
||||
var token = await lemmy.login(
|
||||
usernameOrEmail: usernameOrEmail,
|
||||
password: password,
|
||||
);
|
||||
var userData =
|
||||
await lemmy.getSite(auth: token.raw).then((value) => value.myUser);
|
||||
|
||||
if (!users.containsKey(instanceUrl)) {
|
||||
if (users.isEmpty) {
|
||||
setDefaultAccount(instanceUrl, userData.name);
|
||||
}
|
||||
|
||||
users[instanceUrl] = ObservableMap();
|
||||
tokens[instanceUrl] = ObservableMap();
|
||||
setDefaultAccountFor(instanceUrl, userData.name);
|
||||
}
|
||||
|
||||
users[instanceUrl][userData.name] = userData;
|
||||
tokens[instanceUrl][userData.name] = token;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// 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<User> _$defaultUserComputed;
|
||||
|
||||
@override
|
||||
User get defaultUser =>
|
||||
(_$defaultUserComputed ??= Computed<User>(() => super.defaultUser,
|
||||
name: '_AccountsStore.defaultUser'))
|
||||
.value;
|
||||
Computed<Jwt> _$defaultTokenComputed;
|
||||
|
||||
@override
|
||||
Jwt get defaultToken =>
|
||||
(_$defaultTokenComputed ??= Computed<Jwt>(() => super.defaultToken,
|
||||
name: '_AccountsStore.defaultToken'))
|
||||
.value;
|
||||
|
||||
final _$usersAtom = Atom(name: '_AccountsStore.users');
|
||||
|
||||
@override
|
||||
ObservableMap<String, ObservableMap<String, User>> get users {
|
||||
_$usersAtom.reportRead();
|
||||
return super.users;
|
||||
}
|
||||
|
||||
@override
|
||||
set users(ObservableMap<String, ObservableMap<String, User>> value) {
|
||||
_$usersAtom.reportWrite(value, super.users, () {
|
||||
super.users = 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 _$_AccountsStoreActionController =
|
||||
ActionController(name: '_AccountsStore');
|
||||
|
||||
@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
|
||||
String toString() {
|
||||
return '''
|
||||
users: ${users},
|
||||
tokens: ${tokens},
|
||||
defaultUser: ${defaultUser},
|
||||
defaultToken: ${defaultToken}
|
||||
''';
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ abstract class _ConfigStore with Store {
|
|||
|
||||
void load() async {
|
||||
var prefs = await SharedPreferences.getInstance();
|
||||
// set saved settings or create defaults
|
||||
// load saved settings or create defaults
|
||||
theme = _themeModeFromString(prefs.getString('theme') ?? 'system');
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,7 @@ abstract class _ConfigStore with Store {
|
|||
@observable
|
||||
ThemeMode theme;
|
||||
|
||||
@observable
|
||||
MaterialColor accentColor;
|
||||
// TODO: add amoledDarkMode switch
|
||||
}
|
||||
|
||||
ThemeMode _themeModeFromString(String theme) =>
|
||||
|
|
|
@ -24,26 +24,10 @@ mixin _$ConfigStore on _ConfigStore, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$accentColorAtom = Atom(name: '_ConfigStore.accentColor');
|
||||
|
||||
@override
|
||||
MaterialColor get accentColor {
|
||||
_$accentColorAtom.reportRead();
|
||||
return super.accentColor;
|
||||
}
|
||||
|
||||
@override
|
||||
set accentColor(MaterialColor value) {
|
||||
_$accentColorAtom.reportWrite(value, super.accentColor, () {
|
||||
super.accentColor = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
theme: ${theme},
|
||||
accentColor: ${accentColor}
|
||||
theme: ${theme}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class Badge extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
Badge({@required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.accentColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomModal extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String title;
|
||||
|
||||
BottomModal({@required this.child, this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(const Radius.circular(10.0)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 70),
|
||||
child: Text(
|
||||
'account',
|
||||
style: theme.textTheme.subtitle2,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
Divider()
|
||||
],
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,26 +6,44 @@ import 'package:lemmy_api_client/lemmy_api_client.dart';
|
|||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
import '../util/intl.dart';
|
||||
import 'badge.dart';
|
||||
|
||||
class UserProfile extends HookWidget {
|
||||
final User user;
|
||||
final int userId;
|
||||
final Future<UserView> _userView;
|
||||
final String _instanceUrl;
|
||||
final String instanceUrl;
|
||||
|
||||
UserProfile(this.user)
|
||||
: _instanceUrl = user.actorId.split('/')[2],
|
||||
_userView = LemmyApi(user.actorId.split('/')[2])
|
||||
UserProfile({@required this.userId, @required this.instanceUrl})
|
||||
: _userView = LemmyApi(instanceUrl)
|
||||
.v1
|
||||
.search(q: user.name, type: SearchType.users, sort: SortType.active)
|
||||
.then((res) => res.users[0]);
|
||||
.getUserDetails(
|
||||
userId: userId, savedOnly: true, sort: SortType.active)
|
||||
.then((res) => res.user);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
var userViewSnap = useFuture(_userView);
|
||||
var userViewSnap = useFuture(_userView, preserveState: false);
|
||||
|
||||
Widget _tabs() => DefaultTabController(
|
||||
Widget bio;
|
||||
|
||||
if (userViewSnap.hasData) {
|
||||
if (userViewSnap.data.bio != null) {
|
||||
bio = Text(userViewSnap.data.bio);
|
||||
} else {
|
||||
bio = Center(
|
||||
child: Text(
|
||||
'no bio',
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
bio = Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
Widget tabs() => DefaultTabController(
|
||||
length: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -41,24 +59,18 @@ class UserProfile extends HookWidget {
|
|||
child: TabBarView(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'Posts',
|
||||
style: const TextStyle(fontSize: 36),
|
||||
)),
|
||||
child: Text(
|
||||
'Posts',
|
||||
style: const TextStyle(fontSize: 36),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'Comments',
|
||||
style: const TextStyle(fontSize: 36),
|
||||
)),
|
||||
if (user.bio == null)
|
||||
Center(
|
||||
child: Text(
|
||||
'no bio',
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(user.bio),
|
||||
child: Text(
|
||||
'Comments',
|
||||
style: const TextStyle(fontSize: 36),
|
||||
),
|
||||
),
|
||||
bio,
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -69,9 +81,9 @@ class UserProfile extends HookWidget {
|
|||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (user.banner != null)
|
||||
if (userViewSnap.data?.banner != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: user.banner,
|
||||
imageUrl: userViewSnap.data.banner,
|
||||
)
|
||||
else
|
||||
Container(
|
||||
|
@ -87,7 +99,10 @@ class UserProfile extends HookWidget {
|
|||
height: double.infinity,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(40)),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(40),
|
||||
topLeft: Radius.circular(40),
|
||||
),
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
|
@ -97,7 +112,7 @@ class UserProfile extends HookWidget {
|
|||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
if (user.avatar != null)
|
||||
if (userViewSnap.data?.avatar != null)
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
|
@ -113,24 +128,26 @@ class UserProfile extends HookWidget {
|
|||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: user.avatar,
|
||||
imageUrl: userViewSnap.data.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: user.avatar == null
|
||||
? const EdgeInsets.only(top: 70)
|
||||
: const EdgeInsets.only(top: 8.0),
|
||||
padding: userViewSnap.data?.avatar != null
|
||||
? const EdgeInsets.only(top: 8.0)
|
||||
: const EdgeInsets.only(top: 70),
|
||||
child: Text(
|
||||
user.preferredUsername ?? user.name,
|
||||
userViewSnap.data?.preferredUsername ??
|
||||
userViewSnap.data?.name ??
|
||||
'',
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
'@${user.name}@$_instanceUrl',
|
||||
'@${userViewSnap.data?.name ?? ''}@$instanceUrl',
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
),
|
||||
|
@ -139,19 +156,39 @@ class UserProfile extends HookWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_Badge(
|
||||
icon: Icons.comment, // TODO: should be article icon
|
||||
text: '''
|
||||
${compactNumber(userViewSnap.data?.numberOfPosts ?? 0)} Post${pluralS(userViewSnap.data?.numberOfPosts ?? 0)}''',
|
||||
isLoading: !userViewSnap.hasData,
|
||||
Badge(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.comment, // TODO: should be article icon
|
||||
size: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text('''
|
||||
${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfPosts) : '-'} Post${pluralS(userViewSnap.data?.numberOfPosts ?? 0)}'''),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: _Badge(
|
||||
icon: Icons.comment,
|
||||
text: '''
|
||||
${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(userViewSnap.data?.numberOfComments ?? 1)}''',
|
||||
isLoading: !userViewSnap.hasData,
|
||||
child: Badge(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.comment,
|
||||
size: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text('''
|
||||
${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfComments) : '-'} Comment${pluralS(userViewSnap.data?.numberOfComments ?? 0)}'''),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -160,7 +197,8 @@ ${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(user
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'Joined ${timeago.format(user.published)}',
|
||||
'''
|
||||
Joined ${userViewSnap.hasData ? timeago.format(userViewSnap.data.published) : ''}''',
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
|
@ -174,13 +212,16 @@ ${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(user
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
DateFormat('MMM dd, yyyy').format(user.published),
|
||||
userViewSnap.hasData
|
||||
? DateFormat('MMM dd, yyyy')
|
||||
.format(userViewSnap.data.published)
|
||||
: '',
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(child: _tabs())
|
||||
Expanded(child: tabs())
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -189,41 +230,3 @@ ${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(user
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Badge extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final bool isLoading;
|
||||
|
||||
_Badge({
|
||||
@required this.icon,
|
||||
@required this.isLoading,
|
||||
@required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.accentColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: isLoading
|
||||
? CircularProgressIndicator()
|
||||
: Row(
|
||||
children: [
|
||||
Icon(icon, size: 15, color: Colors.white),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(text),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -344,7 +344,7 @@ packages:
|
|||
name: lemmy_api_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
version: "0.3.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -28,7 +28,7 @@ dependencies:
|
|||
flutter_hooks: ^0.13.2
|
||||
cached_network_image: ^2.2.0+1
|
||||
timeago: ^2.0.27
|
||||
lemmy_api_client: ^0.2.0
|
||||
lemmy_api_client: ^0.3.0
|
||||
|
||||
mobx: ^1.2.1
|
||||
flutter_mobx: ^1.1.0
|
||||
|
|
Loading…
Reference in New Issue