From 5808ab17a35ab347b2301f915536c1663db50352 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 1 Sep 2020 11:55:22 +0200 Subject: [PATCH 01/10] restructure user profile to work in both contexts: - profile tab - some user profile --- lib/pages/profile_tab.dart | 5 +- lib/widgets/user_profile.dart | 96 +++++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index 9a6e7d4..b33aeb1 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -58,7 +58,10 @@ class UserProfileTab extends HookWidget { ) ], ), - body: UserProfile(user), + body: UserProfile( + userId: user.id, + instanceUrl: user.actorId.split('/')[2], + ), ); } } diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index 1a6e22d..1662dd3 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -8,16 +8,16 @@ import 'package:timeago/timeago.dart' as timeago; import '../util/intl.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) { @@ -25,7 +25,24 @@ class UserProfile extends HookWidget { var userViewSnap = useFuture(_userView); - 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 +58,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 +80,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 +98,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 +111,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 +127,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, ), ), @@ -160,7 +176,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 +191,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()) ], ), ), From 2b3fc87f88d2f0235460a46a70e2074ee06679b8 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 1 Sep 2020 13:22:37 +0200 Subject: [PATCH 02/10] created accounts store --- lib/main.dart | 8 ++ lib/pages/profile_tab.dart | 104 +++++++++++++------------ lib/stores/accounts_store.dart | 122 +++++++++++++++++++++++++++++ lib/stores/accounts_store.g.dart | 130 +++++++++++++++++++++++++++++++ lib/stores/config_store.dart | 3 +- 5 files changed, 317 insertions(+), 50 deletions(-) create mode 100644 lib/stores/accounts_store.dart create mode 100644 lib/stores/accounts_store.g.dart diff --git a/lib/main.dart b/lib/main.dart index e1ecae5..0be35a8 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(), ), diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index b33aeb1..5dfba77 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -1,67 +1,73 @@ 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/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: () {}, // 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())); + }, + ) ], ), - 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( - userId: user.id, - instanceUrl: user.actorId.split('/')[2], - ), + body: UserProfile( + userId: user.id, + instanceUrl: user.actorId.split('/')[2], + ), + ); + }, ); } } diff --git a/lib/stores/accounts_store.dart b/lib/stores/accounts_store.dart new file mode 100644 index 0000000..d62f9a6 --- /dev/null +++ b/lib/stores/accounts_store.dart @@ -0,0 +1,122 @@ +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() { + // persitently save settings each time they are changed + _saveReactionDisposer = reaction((_) => [users, tokens], (_) { + save(); + }); + } + + void dispose() { + _saveReactionDisposer(); + } + + void load() async { + var prefs = await SharedPreferences.getInstance(); + // set saved settings or create defaults + // TODO: load saved + users = ObservableMap(); + tokens = ObservableMap(); + _defaultAccounts = ObservableMap(); + } + + void save() async { + var prefs = await SharedPreferences.getInstance(); + // TODO: save + } + + /// 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(() { + var userTag = _defaultAccounts[instanceUrl].split('@'); + return users[userTag[1]][userTag[0]]; + }).value; + + Jwt defaultTokenFor(String instanceUrl) => Computed(() { + var userTag = _defaultAccounts[instanceUrl].split('@'); + return tokens[userTag[1]][userTag[0]]; + }).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..30ff35f 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -23,8 +23,9 @@ 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'); + // TODO: persistently save and load accent color } void save() async { From 708a680ddc5abda3647f495573c69de68b708624 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 1 Sep 2020 21:36:58 +0200 Subject: [PATCH 03/10] showing modal for choosing default account --- lib/pages/profile_tab.dart | 47 ++++++++++++++++++++++++++++++++++- lib/widgets/bottom_modal.dart | 42 +++++++++++++++++++++++++++++++ lib/widgets/user_profile.dart | 2 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 lib/widgets/bottom_modal.dart diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index 5dfba77..a7bd4ba 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -4,6 +4,7 @@ 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'; @@ -39,7 +40,51 @@ class UserProfileTab extends HookWidget { ), ], ), - onPressed: () {}, // TODO: should open bottomsheet + 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( diff --git a/lib/widgets/bottom_modal.dart b/lib/widgets/bottom_modal.dart new file mode 100644 index 0000000..4d7e058 --- /dev/null +++ b/lib/widgets/bottom_modal.dart @@ -0,0 +1,42 @@ +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 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 1662dd3..9ce8e6d 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -23,7 +23,7 @@ class UserProfile extends HookWidget { Widget build(BuildContext context) { var theme = Theme.of(context); - var userViewSnap = useFuture(_userView); + var userViewSnap = useFuture(_userView, preserveState: false); Widget bio; From 596e8f4e53445736358da3b9e816ae3500d1a76a Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 1 Sep 2020 21:59:00 +0200 Subject: [PATCH 04/10] moved badge to seperate widget --- lib/widgets/badge.dart | 24 +++++++++++ lib/widgets/user_profile.dart | 79 ++++++++++++++--------------------- 2 files changed, 55 insertions(+), 48 deletions(-) create mode 100644 lib/widgets/badge.dart 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/user_profile.dart b/lib/widgets/user_profile.dart index 9ce8e6d..a70763f 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -6,6 +6,7 @@ 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 int userId; @@ -155,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)}'''), + ), + ], + ), ), ), ], @@ -209,41 +230,3 @@ Joined ${userViewSnap.hasData ? timeago.format(userViewSnap.data.published) : '' ); } } - -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), - ), - ], - ), - ), - ); - } -} From da107a7ad7bdb279e0bf39117002754d2e51e549 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 1 Sep 2020 22:05:33 +0200 Subject: [PATCH 05/10] remove accentColor config --- lib/main.dart | 1 - lib/pages/settings.dart | 5 ----- lib/stores/config_store.dart | 4 +--- lib/stores/config_store.g.dart | 18 +----------------- 4 files changed, 2 insertions(+), 26 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 0be35a8..ccbdd9c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,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/settings.dart b/lib/pages/settings.dart index adb62a1..fde3829 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -69,11 +69,6 @@ class _AppearanceConfig extends StatelessWidget { ctx.read().theme = selected; }, ), - Text( - 'Accent color', - style: theme.textTheme.headline6, - ), - // TODO: add accent color picking ], ), ), diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 30ff35f..5a94031 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -25,7 +25,6 @@ abstract class _ConfigStore with Store { var prefs = await SharedPreferences.getInstance(); // load saved settings or create defaults theme = _themeModeFromString(prefs.getString('theme') ?? 'system'); - // TODO: persistently save and load accent color } void save() async { @@ -37,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} '''; } } From 706bf945026efeff2a9a8fc338c00ae604671c70 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 2 Sep 2020 01:35:30 +0200 Subject: [PATCH 06/10] accounts screen in settings: - you can see your accounts - you can choose the default one for the instance with a long press --- lib/pages/settings.dart | 57 +++++++++++++++++++++++++++++++++- lib/stores/accounts_store.dart | 12 +++---- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index fde3829..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), @@ -75,3 +79,54 @@ class _AppearanceConfig extends StatelessWidget { ); } } + +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 index d62f9a6..cc51bfc 100644 --- a/lib/stores/accounts_store.dart +++ b/lib/stores/accounts_store.dart @@ -66,15 +66,11 @@ abstract class _AccountsStore with Store { return tokens[userTag[1]][userTag[0]]; } - User defaultUserFor(String instanceUrl) => Computed(() { - var userTag = _defaultAccounts[instanceUrl].split('@'); - return users[userTag[1]][userTag[0]]; - }).value; + User defaultUserFor(String instanceUrl) => + Computed(() => users[instanceUrl][_defaultAccounts[instanceUrl]]).value; - Jwt defaultTokenFor(String instanceUrl) => Computed(() { - var userTag = _defaultAccounts[instanceUrl].split('@'); - return tokens[userTag[1]][userTag[0]]; - }).value; + Jwt defaultTokenFor(String instanceUrl) => + Computed(() => tokens[instanceUrl][_defaultAccounts[instanceUrl]]).value; @action void setDefaultAccount(String instanceUrl, String username) { From 624f0e995a9112fea7fc5cfe9f6d96f86ff65ab6 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 2 Sep 2020 10:00:08 +0200 Subject: [PATCH 07/10] persistently save default accounts --- lib/stores/accounts_store.dart | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/stores/accounts_store.dart b/lib/stores/accounts_store.dart index cc51bfc..cf4838e 100644 --- a/lib/stores/accounts_store.dart +++ b/lib/stores/accounts_store.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -11,9 +13,17 @@ abstract class _AccountsStore with Store { _AccountsStore() { // persitently save settings each time they are changed - _saveReactionDisposer = reaction((_) => [users, tokens], (_) { - save(); - }); + _saveReactionDisposer = reaction( + (_) => [ + users.asObservable(), + tokens.asObservable(), + _defaultAccount, + _defaultAccounts.asObservable(), + ], + (_) { + save(); + }, + ); } void dispose() { @@ -23,15 +33,19 @@ abstract class _AccountsStore with Store { void load() async { var prefs = await SharedPreferences.getInstance(); // set saved settings or create defaults - // TODO: load saved + // TODO: load saved users and tokens users = ObservableMap(); tokens = ObservableMap(); - _defaultAccounts = ObservableMap(); + _defaultAccount = prefs.getString('defaultAccount'); + _defaultAccounts = ObservableMap.of(Map.castFrom( + jsonDecode(prefs.getString('defaultAccounts') ?? 'null') ?? {})); } void save() async { var prefs = await SharedPreferences.getInstance(); - // TODO: save + // TODO: save users and tokens + await prefs.setString('defaultAccount', _defaultAccount); + await prefs.setString('defaultAccounts', jsonEncode(_defaultAccounts)); } /// if path to tokens map exists, it exists for users as well From 301e7185d069a5b2a158ad7a4c9301e76c7015a6 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 2 Sep 2020 11:19:50 +0200 Subject: [PATCH 08/10] bump lemmy_api_client --- pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From dda7571c19c4ec99ec61167f6eaa4a4b852d28d9 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 2 Sep 2020 15:16:33 +0200 Subject: [PATCH 09/10] save tokens and users in AccountsStore --- lib/stores/accounts_store.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/stores/accounts_store.dart b/lib/stores/accounts_store.dart index cf4838e..4db49c2 100644 --- a/lib/stores/accounts_store.dart +++ b/lib/stores/accounts_store.dart @@ -12,8 +12,9 @@ abstract class _AccountsStore with Store { ReactionDisposer _saveReactionDisposer; _AccountsStore() { - // persitently save settings each time they are changed + // 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(), @@ -43,9 +44,11 @@ abstract class _AccountsStore with Store { void save() async { var prefs = await SharedPreferences.getInstance(); - // TODO: save users and tokens + 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 From c16f0f412c7ce0a55b50a9666d7b609706ddc6a4 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 2 Sep 2020 16:54:29 +0200 Subject: [PATCH 10/10] wrap modal in safearea --- lib/widgets/bottom_modal.dart | 48 ++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/widgets/bottom_modal.dart b/lib/widgets/bottom_modal.dart index 4d7e058..c505484 100644 --- a/lib/widgets/bottom_modal.dart +++ b/lib/widgets/bottom_modal.dart @@ -10,31 +10,33 @@ class BottomModal extends StatelessWidget { Widget build(BuildContext context) { var theme = Theme.of(context); - return 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, + 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() + Divider() + ], + child, ], - child, - ], + ), ), ), );