diff --git a/lib/main.dart b/lib/main.dart index e1ecae5..ccbdd9c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main() async { @@ -10,6 +11,9 @@ Future main() async { var configStore = ConfigStore(); await configStore.load(); + var accountsStore = AccountsStore(); + await accountsStore.load(); + runApp( MultiProvider( providers: [ @@ -17,6 +21,10 @@ Future main() async { create: (_) => configStore, dispose: (_, store) => store.dispose(), ), + Provider( + create: (_) => accountsStore, + dispose: (_, store) => store.dispose(), + ), ], child: MyApp(), ), @@ -31,7 +39,6 @@ class MyApp extends StatelessWidget { themeMode: ctx.watch().theme, darkTheme: ThemeData.dark(), theme: ThemeData( - primarySwatch: ctx.watch().accentColor, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(title: 'Flutter hello world'), diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index 9a6e7d4..a7bd4ba 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -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().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 = []; + + ctx + .read() + .users + .forEach((instanceUrl, value) { + value.forEach((username, _) { + userTags.add('$username@$instanceUrl'); + }); + }); + + return Observer( + builder: (ctx) { + var user = ctx.watch().defaultUser; + var instanceUrl = user.actorId.split('/')[2]; + + return BottomModal( + title: 'account', + child: Column( + children: [ + for (final tag in userTags) + RadioListTile( + value: tag, + title: Text(tag), + groupValue: '${user.name}@$instanceUrl', + onChanged: (selected) { + var userTag = selected.split('@'); + ctx.read().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], + ), + ); + }, ); } } diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index adb62a1..3f83f7b 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -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().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(); + 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 + ); + }, + ), + ); + } +} diff --git a/lib/stores/accounts_store.dart b/lib/stores/accounts_store.dart new file mode 100644 index 0000000..4db49c2 --- /dev/null +++ b/lib/stores/accounts_store.dart @@ -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> users; + + /// if path to users map exists, it exists for tokens as well + /// `tokens['instanceUrl']['username']` + @observable + ObservableMap> tokens; + + /// default account for a given instance + /// map where keys are instanceUrls and values are usernames + @observable + ObservableMap _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 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; + } +} diff --git a/lib/stores/accounts_store.g.dart b/lib/stores/accounts_store.g.dart new file mode 100644 index 0000000..e91514f --- /dev/null +++ b/lib/stores/accounts_store.g.dart @@ -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 _$defaultUserComputed; + + @override + User get defaultUser => + (_$defaultUserComputed ??= Computed(() => super.defaultUser, + name: '_AccountsStore.defaultUser')) + .value; + Computed _$defaultTokenComputed; + + @override + Jwt get defaultToken => + (_$defaultTokenComputed ??= Computed(() => super.defaultToken, + name: '_AccountsStore.defaultToken')) + .value; + + final _$usersAtom = Atom(name: '_AccountsStore.users'); + + @override + ObservableMap> get users { + _$usersAtom.reportRead(); + return super.users; + } + + @override + set users(ObservableMap> value) { + _$usersAtom.reportWrite(value, super.users, () { + super.users = value; + }); + } + + final _$tokensAtom = Atom(name: '_AccountsStore.tokens'); + + @override + ObservableMap> get tokens { + _$tokensAtom.reportRead(); + return super.tokens; + } + + @override + set tokens(ObservableMap> value) { + _$tokensAtom.reportWrite(value, super.tokens, () { + super.tokens = value; + }); + } + + final _$_defaultAccountsAtom = Atom(name: '_AccountsStore._defaultAccounts'); + + @override + ObservableMap get _defaultAccounts { + _$_defaultAccountsAtom.reportRead(); + return super._defaultAccounts; + } + + @override + set _defaultAccounts(ObservableMap 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 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} + '''; + } +} diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 62ec6ee..5a94031 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -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) => diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart index ca7a423..5e62d96 100644 --- a/lib/stores/config_store.g.dart +++ b/lib/stores/config_store.g.dart @@ -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} '''; } } diff --git a/lib/widgets/badge.dart b/lib/widgets/badge.dart new file mode 100644 index 0000000..bfb7f7c --- /dev/null +++ b/lib/widgets/badge.dart @@ -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, + ), + ); + } +} diff --git a/lib/widgets/bottom_modal.dart b/lib/widgets/bottom_modal.dart new file mode 100644 index 0000000..c505484 --- /dev/null +++ b/lib/widgets/bottom_modal.dart @@ -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, + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index 1a6e22d..a70763f 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -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; - 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), - ), - ], - ), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 0f5edce..932cc74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 96adf44..2e4ffd0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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