From 80dd04ea1ba2be9c8e39d99445d29be31ac5d60a Mon Sep 17 00:00:00 2001 From: shilangyu Date: Sun, 30 Aug 2020 22:43:16 +0200 Subject: [PATCH 01/12] created mobx ConfigStore --- lib/main.dart | 40 +++++++++++++---- lib/stores/config_store.dart | 44 ++++++++++++++++++ lib/stores/config_store.g.dart | 49 ++++++++++++++++++++ pubspec.lock | 82 ++++++++++++++++++++++++++++++++++ pubspec.yaml | 7 +++ 5 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 lib/stores/config_store.dart create mode 100644 lib/stores/config_store.g.dart diff --git a/lib/main.dart b/lib/main.dart index e94c8cd..dcef430 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,19 +1,41 @@ import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:provider/provider.dart'; + +import 'stores/config_store.dart'; void main() { - runApp(MyApp()); + runApp( + MultiProvider( + providers: [ + Provider( + create: (_) => ConfigStore()..load(), + dispose: (_, store) => store.dispose(), + ), + ], + child: MyApp(), + ), + ); } class MyApp extends StatelessWidget { @override - Widget build(BuildContext context) => MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); + Widget build(BuildContext context) { + return Observer( + builder: (ctx) { + return MaterialApp( + title: 'Flutter Demo', + themeMode: ctx.watch().theme, + darkTheme: ThemeData.dark(), + theme: ThemeData( + primarySwatch: ctx.watch().accentColor, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: MyHomePage(title: 'Flutter Demo Home Page'), + ); + }, + ); + } } class MyHomePage extends StatefulWidget { diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart new file mode 100644 index 0000000..8153973 --- /dev/null +++ b/lib/stores/config_store.dart @@ -0,0 +1,44 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'config_store.g.dart'; + +class ConfigStore extends _ConfigStore with _$ConfigStore {} + +abstract class _ConfigStore with Store { + ReactionDisposer _saveReactionDisposer; + + _ConfigStore() { + // persitently save settings each time they are changed + _saveReactionDisposer = reaction((_) => theme, (_) { + save(); + }); + } + + void dispose() { + _saveReactionDisposer(); + } + + void load() async { + var prefs = await SharedPreferences.getInstance(); + // set saved settings or create defaults + theme = _themeModeFromString(prefs.getString('theme') ?? 'dark'); + } + + void save() async { + var prefs = await SharedPreferences.getInstance(); + + await prefs.setString('theme', describeEnum(theme)); + } + + @observable + ThemeMode theme = ThemeMode.dark; + + @observable + MaterialColor accentColor = Colors.green; +} + +ThemeMode _themeModeFromString(String theme) => + ThemeMode.values.firstWhere((e) => describeEnum(e) == theme); diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart new file mode 100644 index 0000000..ca7a423 --- /dev/null +++ b/lib/stores/config_store.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'config_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic + +mixin _$ConfigStore on _ConfigStore, Store { + final _$themeAtom = Atom(name: '_ConfigStore.theme'); + + @override + ThemeMode get theme { + _$themeAtom.reportRead(); + return super.theme; + } + + @override + set theme(ThemeMode value) { + _$themeAtom.reportWrite(value, super.theme, () { + super.theme = value; + }); + } + + final _$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} + '''; + } +} diff --git a/pubspec.lock b/pubspec.lock index 5da9ea7..228e882 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -244,11 +244,23 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.2" + flutter_mobx: + dependency: "direct main" + description: + name: flutter_mobx + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0+2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -354,6 +366,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.6+3" + mobx: + dependency: "direct main" + description: + name: mobx + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1+2" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0+1" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" node_interop: dependency: transitive description: @@ -452,6 +485,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.13" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2+1" pub_semver: dependency: transitive description: @@ -480,6 +520,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.24.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.10" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+2" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+10" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+7" shelf: dependency: transitive description: @@ -499,6 +574,13 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6" source_span: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 690eed9..e4a03a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,12 @@ dependencies: timeago: ^2.0.27 lemmy_api_client: ^0.1.3 + mobx: ^1.2.1 + flutter_mobx: ^1.1.0 + provider: ^4.3.1 + + shared_preferences: '>=0.5.0 <2.0.0' + flutter: sdk: flutter @@ -39,6 +45,7 @@ dev_dependencies: sdk: flutter effective_dart: ^1.0.0 build_runner: ^1.10.0 + mobx_codegen: ^1.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From b7b5661f457659154cdb52f2d2de3ca8561191a5 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 01:04:08 +0200 Subject: [PATCH 02/12] basic profile tab --- lib/pages/profile_tab.dart | 123 +++++++++++++++++++++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 lib/pages/profile_tab.dart diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart new file mode 100644 index 0000000..abd8e9e --- /dev/null +++ b/lib/pages/profile_tab.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:intl/intl.dart'; +import 'package:lemmy_api_client/lemmy_api_client.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class UserProfileTab extends HookWidget { + final User user; + final Future _userView; + + UserProfileTab(this.user) + : _userView = LemmyApi('dev.lemmy.ml') + .v1 + .search(q: user.name, type: SearchType.users, sort: SortType.active) + .then((res) => res.users[0]); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var userViewSnap = useFuture(_userView); + + return Scaffold( + appBar: AppBar( + title: Text('hello'), + ), + body: Center( + child: Column( + children: [ + SizedBox( + width: 65, + height: 65, + child: Container( + decoration: BoxDecoration( + boxShadow: [BoxShadow(blurRadius: 6, color: Colors.black54)], + color: theme.backgroundColor, // TODO: add avatar, not color + borderRadius: BorderRadius.all(Radius.circular(15)), + border: Border.all(color: Colors.white, width: 3), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + user.preferredUsername ?? user.name, + style: theme.textTheme.headline6, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '@${user.name}@', // TODO: add instance host uri + style: theme.textTheme.caption, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _blueBadge( + icon: Icons.comment, + text: ''' +${NumberFormat.compact().format(userViewSnap.data.numberOfPosts ?? 0)} Posts''', + loading: !userViewSnap.hasData, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: _blueBadge( + icon: Icons.comment, + text: ''' +${NumberFormat.compact().format(userViewSnap.data.numberOfPosts ?? 0)} Comments''', + loading: !userViewSnap.hasData, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text('''Joined ${timeago.format(user.published)}'''), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cake, + size: 13, + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: + Text(DateFormat('MMM dd, yyyy').format(user.published)), + ), + ], + ) + ], + ), + ), + ); + } + + Widget _blueBadge({IconData icon, String text, bool loading}) => Container( + decoration: BoxDecoration( + color: Colors.blue[300], + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: loading + ? CircularProgressIndicator() + : Row( + children: [ + Icon(icon, size: 15, color: Colors.white), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text(text, style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 228e882..672430a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,7 +337,7 @@ packages: name: lemmy_api_client url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.2.1" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e4a03a1..1f950e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: flutter_hooks: ^0.13.2 cached_network_image: ^2.2.0+1 timeago: ^2.0.27 - lemmy_api_client: ^0.1.3 + lemmy_api_client: ^0.2.0 mobx: ^1.2.1 flutter_mobx: ^1.1.0 From dd06b79efaf5f1a9957fe4fe1c60baab3610b8a0 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 03:54:24 +0200 Subject: [PATCH 03/12] implemented rest of the main screen mockup --- lib/pages/profile_tab.dart | 229 +++++++++++++++++++++++++++---------- 1 file changed, 167 insertions(+), 62 deletions(-) diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index abd8e9e..b0a4557 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -21,88 +21,152 @@ class UserProfileTab extends HookWidget { var userViewSnap = useFuture(_userView); return Scaffold( + extendBodyBehindAppBar: true, appBar: AppBar( - title: Text('hello'), + 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: theme.accentTextTheme.bodyText1.color), + ), + Icon( + Icons.expand_more, + color: theme.primaryIconTheme.color, + ), + ], + ), + onPressed: () {}, // TODO: should open bottomsheet + ), + actions: [ + IconButton( + icon: Icon( + Icons.settings, + ), + onPressed: () {}, // TODO: go to settings + ) + ], ), body: Center( - child: Column( + child: Stack( children: [ - SizedBox( - width: 65, - height: 65, - child: Container( - decoration: BoxDecoration( - boxShadow: [BoxShadow(blurRadius: 6, color: Colors.black54)], - color: theme.backgroundColor, // TODO: add avatar, not color - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Colors.white, width: 3), + Image.network( + 'https://c4.wallpaperflare.com/wallpaper/500/442/354/outrun-vaporwave-hd-wallpaper-preview.jpg'), // TODO: should be the banner + SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(40)), + color: theme.scaffoldBackgroundColor, + ), + ), ), ), ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - user.preferredUsername ?? user.name, - style: theme.textTheme.headline6, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '@${user.name}@', // TODO: add instance host uri - style: theme.textTheme.caption, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + SafeArea( + child: Column( children: [ - _blueBadge( - icon: Icons.comment, - text: ''' -${NumberFormat.compact().format(userViewSnap.data.numberOfPosts ?? 0)} Posts''', - loading: !userViewSnap.hasData, - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: _blueBadge( - icon: Icons.comment, - text: ''' -${NumberFormat.compact().format(userViewSnap.data.numberOfPosts ?? 0)} Comments''', - loading: !userViewSnap.hasData, + SizedBox( + width: 80, + height: 80, + child: Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow(blurRadius: 6, color: Colors.black54) + ], + color: theme + .backgroundColor, // TODO: add avatar, not color + borderRadius: BorderRadius.all(Radius.circular(15)), + border: Border.all(color: Colors.white, width: 3), + ), ), ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + user.preferredUsername ?? user.name, + style: theme.textTheme.headline6, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '@${user.name}@', // TODO: add instance host uri + style: theme.textTheme.caption, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _badge( + context: context, + icon: Icons.comment, + text: ''' +${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Posts''', + loading: !userViewSnap.hasData, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: _badge( + context: context, + icon: Icons.comment, + text: ''' +${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Comments''', + loading: !userViewSnap.hasData, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Joined ${timeago.format(user.published)}', + style: theme.textTheme.bodyText1, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cake, + size: 13, + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + DateFormat('MMM dd, yyyy').format(user.published), + style: theme.textTheme.bodyText1, + ), + ), + ], + ), + Expanded(child: _tabs()) ], ), ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text('''Joined ${timeago.format(user.published)}'''), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.cake, - size: 13, - ), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: - Text(DateFormat('MMM dd, yyyy').format(user.published)), - ), - ], - ) ], ), ), ); } - Widget _blueBadge({IconData icon, String text, bool loading}) => Container( + Widget _badge( + {IconData icon, String text, bool loading, BuildContext context}) => + Container( decoration: BoxDecoration( - color: Colors.blue[300], + color: Theme.of(context).accentColor, borderRadius: BorderRadius.all(Radius.circular(5)), ), child: Padding( @@ -120,4 +184,45 @@ ${NumberFormat.compact().format(userViewSnap.data.numberOfPosts ?? 0)} Comments' ), ), ); + + Widget _tabs() => DefaultTabController( + length: 3, + child: Column( + children: [ + TabBar( + labelColor: Colors.black, + tabs: [ + Tab(text: 'Posts'), + Tab(text: 'Comments'), + Tab(text: 'About'), + ], + ), + Expanded( + child: TabBarView( + children: [ + Center( + 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), + ], + ), + ) + ], + ), + ); } From 8b738bf5e50424e03cf134a1c6f53d9fee91134d Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 12:05:45 +0200 Subject: [PATCH 04/12] moved user profile to a widget --- lib/main.dart | 47 ++++--- lib/pages/profile_tab.dart | 185 +----------------------- lib/{components => widgets}/post.dart | 0 lib/widgets/user_profile.dart | 195 ++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 202 deletions(-) rename lib/{components => widgets}/post.dart (100%) create mode 100644 lib/widgets/user_profile.dart diff --git a/lib/main.dart b/lib/main.dart index dcef430..936dac7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,11 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:provider/provider.dart'; +import 'pages/profile_tab.dart'; import 'stores/config_store.dart'; void main() { @@ -31,7 +35,8 @@ class MyApp extends StatelessWidget { primarySwatch: ctx.watch().accentColor, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: MyHomePage(title: 'Flutter Demo Home Page'), + home: UserProfileTab(User.fromJson(jsonDecode( + '''{"id":13917,"name":"shilangyu","preferred_username":null,"password_encrypted":"","email":"xmarcinmarcin@gmail.com","avatar":null,"admin":false,"banned":false,"published":"2020-08-23T07:13:23.229279","updated":"2020-08-29T21:11:11.508707","show_nsfw":true,"theme":"minty","default_sort_type":0,"default_listing_type":1,"lang":"browser","show_avatars":true,"send_notifications_to_email":false,"matrix_user_id":null,"actor_id":"https://dev.lemmy.ml/u/shilangyu","bio":null,"local":true,"private_key":null,"public_key":null,"last_refreshed_at":"2020-08-23T07:13:23.229279","banner":null}'''))), ); }, ); @@ -58,25 +63,25 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('You have pushed the button this many times:'), + Text( + '$_counter', + style: Theme.of(context).textTheme.headline4, + ), + ], ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), - ); + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), + ); } diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index b0a4557..0a4601a 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -1,25 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:intl/intl.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; -import 'package:timeago/timeago.dart' as timeago; + +import '../widgets/user_profile.dart'; class UserProfileTab extends HookWidget { final User user; - final Future _userView; - UserProfileTab(this.user) - : _userView = LemmyApi('dev.lemmy.ml') - .v1 - .search(q: user.name, type: SearchType.users, sort: SortType.active) - .then((res) => res.users[0]); + UserProfileTab(this.user); @override Widget build(BuildContext context) { var theme = Theme.of(context); - var userViewSnap = useFuture(_userView); - return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( @@ -52,177 +45,7 @@ class UserProfileTab extends HookWidget { ) ], ), - body: Center( - child: Stack( - children: [ - Image.network( - 'https://c4.wallpaperflare.com/wallpaper/500/442/354/outrun-vaporwave-hd-wallpaper-preview.jpg'), // TODO: should be the banner - SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 60), - child: SizedBox( - width: double.infinity, - height: double.infinity, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(40)), - color: theme.scaffoldBackgroundColor, - ), - ), - ), - ), - ), - SafeArea( - child: Column( - children: [ - SizedBox( - width: 80, - height: 80, - child: Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow(blurRadius: 6, color: Colors.black54) - ], - color: theme - .backgroundColor, // TODO: add avatar, not color - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Colors.white, width: 3), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - user.preferredUsername ?? user.name, - style: theme.textTheme.headline6, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - '@${user.name}@', // TODO: add instance host uri - style: theme.textTheme.caption, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _badge( - context: context, - icon: Icons.comment, - text: ''' -${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Posts''', - loading: !userViewSnap.hasData, - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: _badge( - context: context, - icon: Icons.comment, - text: ''' -${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Comments''', - loading: !userViewSnap.hasData, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'Joined ${timeago.format(user.published)}', - style: theme.textTheme.bodyText1, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.cake, - size: 13, - ), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - DateFormat('MMM dd, yyyy').format(user.published), - style: theme.textTheme.bodyText1, - ), - ), - ], - ), - Expanded(child: _tabs()) - ], - ), - ), - ], - ), - ), + body: UserProfile(user), ); } - - Widget _badge( - {IconData icon, String text, bool loading, BuildContext context}) => - Container( - decoration: BoxDecoration( - color: Theme.of(context).accentColor, - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: loading - ? CircularProgressIndicator() - : Row( - children: [ - Icon(icon, size: 15, color: Colors.white), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(text, style: TextStyle(color: Colors.white)), - ), - ], - ), - ), - ); - - Widget _tabs() => DefaultTabController( - length: 3, - child: Column( - children: [ - TabBar( - labelColor: Colors.black, - tabs: [ - Tab(text: 'Posts'), - Tab(text: 'Comments'), - Tab(text: 'About'), - ], - ), - Expanded( - child: TabBarView( - children: [ - Center( - 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), - ], - ), - ) - ], - ), - ); } diff --git a/lib/components/post.dart b/lib/widgets/post.dart similarity index 100% rename from lib/components/post.dart rename to lib/widgets/post.dart diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart new file mode 100644 index 0000000..99c09ee --- /dev/null +++ b/lib/widgets/user_profile.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:intl/intl.dart'; +import 'package:lemmy_api_client/lemmy_api_client.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class UserProfile extends HookWidget { + final User user; + final Future _userView; + + UserProfile(this.user) + : _userView = LemmyApi('dev.lemmy.ml') + .v1 + .search(q: user.name, type: SearchType.users, sort: SortType.active) + .then((res) => res.users[0]); + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + var userViewSnap = useFuture(_userView); + + return Center( + child: Stack( + children: [ + Image.network( + 'https://c4.wallpaperflare.com/wallpaper/500/442/354/outrun-vaporwave-hd-wallpaper-preview.jpg'), // TODO: should be the banner + SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(40)), + color: theme.scaffoldBackgroundColor, + ), + ), + ), + ), + ), + SafeArea( + child: Column( + children: [ + SizedBox( + width: 80, + height: 80, + child: Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow(blurRadius: 6, color: Colors.black54) + ], + color: + theme.backgroundColor, // TODO: add avatar, not color + borderRadius: BorderRadius.all(Radius.circular(15)), + border: Border.all(color: Colors.white, width: 3), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + user.preferredUsername ?? user.name, + style: theme.textTheme.headline6, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '@${user.name}@', // TODO: add instance host uri + style: theme.textTheme.caption, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _badge( + context: context, + icon: Icons.comment, + text: ''' +${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Posts''', + loading: !userViewSnap.hasData, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: _badge( + context: context, + icon: Icons.comment, + text: ''' +${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Comments''', + loading: !userViewSnap.hasData, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'Joined ${timeago.format(user.published)}', + style: theme.textTheme.bodyText1, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cake, + size: 13, + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + DateFormat('MMM dd, yyyy').format(user.published), + style: theme.textTheme.bodyText1, + ), + ), + ], + ), + Expanded(child: _tabs()) + ], + ), + ), + ], + ), + ); + } + + Widget _badge( + {IconData icon, String text, bool loading, BuildContext context}) => + Container( + decoration: BoxDecoration( + color: Theme.of(context).accentColor, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: loading + ? CircularProgressIndicator() + : Row( + children: [ + Icon(icon, size: 15, color: Colors.white), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text(text, style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ); + + Widget _tabs() => DefaultTabController( + length: 3, + child: Column( + children: [ + TabBar( + labelColor: Colors.black, + tabs: [ + Tab(text: 'Posts'), + Tab(text: 'Comments'), + Tab(text: 'About'), + ], + ), + Expanded( + child: TabBarView( + children: [ + Center( + 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), + ], + ), + ) + ], + ), + ); +} From a0850836dd3bc0778ec2c5abf044a38d295e3171 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 15:43:09 +0200 Subject: [PATCH 05/12] added utility functions --- lib/util/intl.dart | 5 +++++ lib/widgets/user_profile.dart | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 lib/util/intl.dart diff --git a/lib/util/intl.dart b/lib/util/intl.dart new file mode 100644 index 0000000..f793d00 --- /dev/null +++ b/lib/util/intl.dart @@ -0,0 +1,5 @@ +import 'package:intl/intl.dart'; + +String pluralS(int howMany) => howMany == 1 ? '' : 's'; + +String compactNumber(int number) => NumberFormat.compact().format(number); diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index 99c09ee..add994f 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -4,6 +4,8 @@ import 'package:intl/intl.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:timeago/timeago.dart' as timeago; +import '../util/intl.dart'; + class UserProfile extends HookWidget { final User user; final Future _userView; @@ -79,9 +81,9 @@ class UserProfile extends HookWidget { children: [ _badge( context: context, - icon: Icons.comment, + icon: Icons.comment, // TODO: should be article icon text: ''' -${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Posts''', +${compactNumber(userViewSnap.data?.numberOfPosts ?? 0)} Post${pluralS(userViewSnap.data?.numberOfPosts ?? 0)}''', loading: !userViewSnap.hasData, ), Padding( @@ -90,7 +92,7 @@ ${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Posts''' context: context, icon: Icons.comment, text: ''' -${NumberFormat.compact().format(userViewSnap.data?.numberOfPosts ?? 0)} Comments''', +${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(userViewSnap.data?.numberOfComments ?? 1)}''', loading: !userViewSnap.hasData, ), ), From f4bd1eb65b8cf825e59f6d8c6d91d8e804cd05fc Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 16:17:39 +0200 Subject: [PATCH 06/12] added settings screen --- lib/pages/profile_tab.dart | 6 ++- lib/pages/settings.dart | 82 ++++++++++++++++++++++++++++++++++++ lib/stores/config_store.dart | 2 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 lib/pages/settings.dart diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index 0a4601a..f98fb5e 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; import '../widgets/user_profile.dart'; +import 'settings.dart'; class UserProfileTab extends HookWidget { final User user; @@ -41,7 +42,10 @@ class UserProfileTab extends HookWidget { icon: Icon( Icons.settings, ), - onPressed: () {}, // TODO: go to settings + onPressed: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => Settings())); + }, // TODO: go to settings ) ], ), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart new file mode 100644 index 0000000..89a161f --- /dev/null +++ b/lib/pages/settings.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:provider/provider.dart'; + +import '../stores/config_store.dart'; + +class Settings 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('Settings', style: theme.textTheme.headline6), + centerTitle: true, + ), + body: Container( + child: ListView( + children: [ + ListTile( + leading: Icon(Icons.person), + title: Text('Accounts'), + onTap: () {}, + ), + ListTile( + leading: Icon(Icons.color_lens), + title: Text('Appearance'), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => _appearanceConfig())); + }, + ) + ], + ), + ), + ); + } +} + +class _appearanceConfig 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('Appearance', style: theme.textTheme.headline6), + centerTitle: true, + ), + body: Observer( + builder: (ctx) => Column( + children: [ + Text( + 'Theme', + style: theme.textTheme.headline6, + ), + for (final theme in ThemeMode.values) + RadioListTile( + value: theme, + title: Text(theme.toString().split('.')[1]), + groupValue: ctx.watch().theme, + onChanged: (selected) { + 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 8153973..870f4f3 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -12,7 +12,7 @@ abstract class _ConfigStore with Store { _ConfigStore() { // persitently save settings each time they are changed - _saveReactionDisposer = reaction((_) => theme, (_) { + _saveReactionDisposer = reaction((_) => [theme], (_) { save(); }); } From 9133ef94cd6b48ac96ae4f6e42364257e6f6be28 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 16:19:44 +0200 Subject: [PATCH 07/12] fix lint errors --- lib/main.dart | 12 +- lib/widgets/post.dart | 316 +++++++++++++++++++++--------------------- 2 files changed, 160 insertions(+), 168 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 936dac7..9634dfa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,10 +24,8 @@ void main() { class MyApp extends StatelessWidget { @override - Widget build(BuildContext context) { - return Observer( - builder: (ctx) { - return MaterialApp( + Widget build(BuildContext context) => Observer( + builder: (ctx) => MaterialApp( title: 'Flutter Demo', themeMode: ctx.watch().theme, darkTheme: ThemeData.dark(), @@ -37,10 +35,8 @@ class MyApp extends StatelessWidget { ), home: UserProfileTab(User.fromJson(jsonDecode( '''{"id":13917,"name":"shilangyu","preferred_username":null,"password_encrypted":"","email":"xmarcinmarcin@gmail.com","avatar":null,"admin":false,"banned":false,"published":"2020-08-23T07:13:23.229279","updated":"2020-08-29T21:11:11.508707","show_nsfw":true,"theme":"minty","default_sort_type":0,"default_listing_type":1,"lang":"browser","show_avatars":true,"send_notifications_to_email":false,"matrix_user_id":null,"actor_id":"https://dev.lemmy.ml/u/shilangyu","bio":null,"local":true,"private_key":null,"public_key":null,"last_refreshed_at":"2020-08-23T07:13:23.229279","banner":null}'''))), - ); - }, - ); - } + ), + ); } class MyHomePage extends StatefulWidget { diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart index 2001b26..dd78bf1 100644 --- a/lib/widgets/post.dart +++ b/lib/widgets/post.dart @@ -43,155 +43,152 @@ class PostWidget extends StatelessWidget { ); } - Widget _info() { - return Column(children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Row(children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (post.communityIcon != null) - Padding( - padding: const EdgeInsets.only(right: 10), - child: InkWell( - onTap: () => print('GO TO COMMUNITY'), - child: SizedBox( - height: 40, - width: 40, - child: CachedNetworkImage( - imageBuilder: (context, imageProvider) => Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - fit: BoxFit.cover, - image: imageProvider, + Widget _info() => Column(children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Row(children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (post.communityIcon != null) + Padding( + padding: const EdgeInsets.only(right: 10), + child: InkWell( + onTap: () => print('GO TO COMMUNITY'), + child: SizedBox( + height: 40, + width: 40, + child: CachedNetworkImage( + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: imageProvider, + ), ), ), + imageUrl: post.communityIcon, + errorWidget: (context, url, error) => + Text(error.toString()), ), - imageUrl: post.communityIcon, - errorWidget: (context, url, error) => - Text(error.toString()), ), ), ), - ), - ], - ), - Column( - children: [ - Row(children: [ - RichText( - overflow: TextOverflow.ellipsis, // @TODO: fix overflowing - text: TextSpan( - style: TextStyle( - fontSize: 15, color: _theme.textTheme.bodyText1.color), - children: [ - TextSpan( - text: '!', - style: TextStyle(fontWeight: FontWeight.w300)), - TextSpan( - text: post.communityName, - style: TextStyle(fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () => print('GO TO COMMUNITY')), - TextSpan( - text: '@', - style: TextStyle(fontWeight: FontWeight.w300)), - TextSpan( - text: hostUrl, - style: TextStyle(fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () => print('GO TO INSTANCE')), - ], - ), - ) - ]), - Row(children: [ - RichText( - overflow: TextOverflow.ellipsis, + ], + ), + Column( + children: [ + Row(children: [ + RichText( + overflow: TextOverflow.ellipsis, // @TODO: fix overflowing text: TextSpan( style: TextStyle( - fontSize: 13, + fontSize: 15, color: _theme.textTheme.bodyText1.color), children: [ TextSpan( - text: 'by', + text: '!', style: TextStyle(fontWeight: FontWeight.w300)), TextSpan( - text: - ''' ${post.creatorPreferredUsername ?? post.creatorName}''', - style: TextStyle(fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () => print('GO TO USER'), - ), + text: post.communityName, + style: TextStyle(fontWeight: FontWeight.w600), + recognizer: TapGestureRecognizer() + ..onTap = () => print('GO TO COMMUNITY')), TextSpan( - text: - ''' 路 ${timeago.format(post.published, locale: 'en_short')}'''), - if (linkPostDomain != null) - TextSpan(text: ' 路 $linkPostDomain'), - if (post.locked) TextSpan(text: ' 路 馃敀'), + text: '@', + style: TextStyle(fontWeight: FontWeight.w300)), + TextSpan( + text: hostUrl, + style: TextStyle(fontWeight: FontWeight.w600), + recognizer: TapGestureRecognizer() + ..onTap = () => print('GO TO INSTANCE')), ], - )) - ]), - ], - crossAxisAlignment: CrossAxisAlignment.start, - ), - Spacer(), - Column( - children: [ - IconButton( - onPressed: () => print('POPUP MENU'), - icon: Icon(Icons.more_vert), - ) - ], - ) - ]), - ), - ]); - } - - Widget _title() { - return Padding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), - child: Row( - children: [ - Flexible( - child: Text( - '${post.name}', - textAlign: TextAlign.left, - softWrap: true, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + ) + ]), + Row(children: [ + RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: TextStyle( + fontSize: 13, + color: _theme.textTheme.bodyText1.color), + children: [ + TextSpan( + text: 'by', + style: TextStyle(fontWeight: FontWeight.w300)), + TextSpan( + text: + ''' ${post.creatorPreferredUsername ?? post.creatorName}''', + style: TextStyle(fontWeight: FontWeight.w600), + recognizer: TapGestureRecognizer() + ..onTap = () => print('GO TO USER'), + ), + TextSpan( + text: + ''' 路 ${timeago.format(post.published, locale: 'en_short')}'''), + if (linkPostDomain != null) + TextSpan(text: ' 路 $linkPostDomain'), + if (post.locked) TextSpan(text: ' 路 馃敀'), + ], + )) + ]), + ], + crossAxisAlignment: CrossAxisAlignment.start, ), - ), - if (post.thumbnailUrl != null) - InkWell( - onTap: () => print('OPEN LINK'), - child: Stack(children: [ - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: CachedNetworkImage( - imageUrl: post.thumbnailUrl, - width: 70, - height: 70, - fit: BoxFit.cover, - errorWidget: (context, url, error) => - Text(error.toString()), - )), - Positioned( - top: 8, - right: 8, - child: Icon( - Icons.launch, - size: 20, - ), + Spacer(), + Column( + children: [ + IconButton( + onPressed: () => print('POPUP MENU'), + icon: Icon(Icons.more_vert), ) - ]), + ], ) - ], - ), - ); - } + ]), + ), + ]); + + Widget _title() => Padding( + padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + child: Row( + children: [ + Flexible( + child: Text( + '${post.name}', + textAlign: TextAlign.left, + softWrap: true, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + ), + if (post.thumbnailUrl != null) + InkWell( + onTap: () => print('OPEN LINK'), + child: Stack(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + imageUrl: post.thumbnailUrl, + width: 70, + height: 70, + fit: BoxFit.cover, + errorWidget: (context, url, error) => + Text(error.toString()), + )), + Positioned( + top: 8, + right: 8, + child: Icon( + Icons.launch, + size: 20, + ), + ) + ]), + ) + ], + ), + ); Widget _content() { if (post.url == null) return Container(); @@ -252,33 +249,32 @@ class PostWidget extends StatelessWidget { ); } - Widget _actions() { - return Padding( - padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), - child: Row( - children: [ - Icon(Icons.comment), - post.numberOfComments == 1 - ? Text(' 1 comment') - : Text(' ${post.numberOfComments} comments'), - Spacer(), - IconButton( - icon: Icon(Icons.share), - onPressed: () => Share.text('Share post url', post.apId, - 'text/plain')), // @TODO: find a way to mark it as url - IconButton( - icon: post.saved == true - ? Icon(Icons.bookmark) - : Icon(Icons.bookmark_border), - onPressed: () => print('SAVE')), - IconButton( - icon: Icon(Icons.arrow_upward), onPressed: () => print('UPVOTE')), - Text(post.score.toString()), - IconButton( - icon: Icon(Icons.arrow_downward), - onPressed: () => print('DOWNVOTE')), - ], - ), - ); - } + Widget _actions() => Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Row( + children: [ + Icon(Icons.comment), + post.numberOfComments == 1 + ? Text(' 1 comment') + : Text(' ${post.numberOfComments} comments'), + Spacer(), + IconButton( + icon: Icon(Icons.share), + onPressed: () => Share.text('Share post url', post.apId, + 'text/plain')), // @TODO: find a way to mark it as url + IconButton( + icon: post.saved == true + ? Icon(Icons.bookmark) + : Icon(Icons.bookmark_border), + onPressed: () => print('SAVE')), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: () => print('UPVOTE')), + Text(post.score.toString()), + IconButton( + icon: Icon(Icons.arrow_downward), + onPressed: () => print('DOWNVOTE')), + ], + ), + ); } From 8c52ec1109cb8bbf2b7bdf3bad3ee15eb59943bb Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 17:52:16 +0200 Subject: [PATCH 08/12] making sure stores are initialiazed before rendering --- lib/main.dart | 11 ++++++++--- lib/stores/config_store.dart | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 9634dfa..c952204 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,12 +8,17 @@ import 'package:provider/provider.dart'; import 'pages/profile_tab.dart'; import 'stores/config_store.dart'; -void main() { +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + var configStore = ConfigStore(); + await configStore.load(); + runApp( MultiProvider( providers: [ Provider( - create: (_) => ConfigStore()..load(), + create: (_) => configStore, dispose: (_, store) => store.dispose(), ), ], @@ -34,7 +39,7 @@ class MyApp extends StatelessWidget { visualDensity: VisualDensity.adaptivePlatformDensity, ), home: UserProfileTab(User.fromJson(jsonDecode( - '''{"id":13917,"name":"shilangyu","preferred_username":null,"password_encrypted":"","email":"xmarcinmarcin@gmail.com","avatar":null,"admin":false,"banned":false,"published":"2020-08-23T07:13:23.229279","updated":"2020-08-29T21:11:11.508707","show_nsfw":true,"theme":"minty","default_sort_type":0,"default_listing_type":1,"lang":"browser","show_avatars":true,"send_notifications_to_email":false,"matrix_user_id":null,"actor_id":"https://dev.lemmy.ml/u/shilangyu","bio":null,"local":true,"private_key":null,"public_key":null,"last_refreshed_at":"2020-08-23T07:13:23.229279","banner":null}'''))), + '''{"id":13917,"name":"shilangyu","preferred_username":null,"password_encrypted":"","email":"xmarcinmarcin@gmail.com","avatar":null,"admin":false,"banned":false,"published":"2020-08-23T07:13:23.229279","updated":"2020-08-31T14:24:42.495740","show_nsfw":true,"theme":"minty","default_sort_type":0,"default_listing_type":1,"lang":"browser","show_avatars":true,"send_notifications_to_email":false,"matrix_user_id":null,"actor_id":"https://dev.lemmy.ml/u/shilangyu","bio":null,"local":true,"private_key":null,"public_key":null,"last_refreshed_at":"2020-08-23T07:13:23.229279","banner":"https://dev.lemmy.ml/pictrs/image/Cdx3TfNb8g.jpg"}'''))), ), ); } diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 870f4f3..62ec6ee 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -24,7 +24,7 @@ abstract class _ConfigStore with Store { void load() async { var prefs = await SharedPreferences.getInstance(); // set saved settings or create defaults - theme = _themeModeFromString(prefs.getString('theme') ?? 'dark'); + theme = _themeModeFromString(prefs.getString('theme') ?? 'system'); } void save() async { @@ -34,10 +34,10 @@ abstract class _ConfigStore with Store { } @observable - ThemeMode theme = ThemeMode.dark; + ThemeMode theme; @observable - MaterialColor accentColor = Colors.green; + MaterialColor accentColor; } ThemeMode _themeModeFromString(String theme) => From 3be7d518ea77e1cfa2ed28e004a4dd251bee4b8c Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 21:04:17 +0200 Subject: [PATCH 09/12] clean up some TODOs --- lib/pages/profile_tab.dart | 17 ++- lib/pages/settings.dart | 4 +- lib/widgets/user_profile.dart | 192 ++++++++++++++++++++-------------- 3 files changed, 126 insertions(+), 87 deletions(-) diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index f98fb5e..9a6e7d4 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -27,7 +27,7 @@ class UserProfileTab extends HookWidget { children: [ Text( '@${user.name}', - style: TextStyle(color: theme.accentTextTheme.bodyText1.color), + style: TextStyle(color: Colors.white), ), Icon( Icons.expand_more, @@ -39,13 +39,22 @@ class UserProfileTab extends HookWidget { ), actions: [ IconButton( - icon: Icon( - Icons.settings, + 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())); - }, // TODO: go to settings + }, ) ], ), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 89a161f..adb62a1 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -30,7 +30,7 @@ class Settings extends StatelessWidget { title: Text('Appearance'), onTap: () { Navigator.of(context).push( - MaterialPageRoute(builder: (_) => _appearanceConfig())); + MaterialPageRoute(builder: (_) => _AppearanceConfig())); }, ) ], @@ -40,7 +40,7 @@ class Settings extends StatelessWidget { } } -class _appearanceConfig extends StatelessWidget { +class _AppearanceConfig extends StatelessWidget { @override Widget build(BuildContext context) { var theme = Theme.of(context); diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index add994f..320234a 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:intl/intl.dart'; @@ -22,11 +23,60 @@ class UserProfile extends HookWidget { var userViewSnap = useFuture(_userView); + Widget _tabs() => DefaultTabController( + length: 3, + child: Column( + children: [ + TabBar( + labelColor: theme.textTheme.bodyText1.color, + tabs: [ + Tab(text: 'Posts'), + Tab(text: 'Comments'), + Tab(text: 'About'), + ], + ), + Expanded( + child: TabBarView( + children: [ + Center( + 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), + ], + ), + ) + ], + ), + ); + return Center( child: Stack( children: [ - Image.network( - 'https://c4.wallpaperflare.com/wallpaper/500/442/354/outrun-vaporwave-hd-wallpaper-preview.jpg'), // TODO: should be the banner + if (user.banner != null) + CachedNetworkImage( + imageUrl: user.banner, + ) + else + Container( + width: double.infinity, + height: double.infinity, + color: theme.primaryColor, + ), SafeArea( child: Padding( padding: const EdgeInsets.only(top: 60), @@ -45,23 +95,31 @@ class UserProfile extends HookWidget { SafeArea( child: Column( children: [ - SizedBox( - width: 80, - height: 80, - child: Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow(blurRadius: 6, color: Colors.black54) - ], - color: - theme.backgroundColor, // TODO: add avatar, not color - borderRadius: BorderRadius.all(Radius.circular(15)), - border: Border.all(color: Colors.white, width: 3), + if (user.avatar != null) + SizedBox( + width: 80, + height: 80, + child: Container( + // clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow(blurRadius: 6, color: Colors.black54) + ], + borderRadius: BorderRadius.all(Radius.circular(15)), + border: Border.all(color: Colors.white, width: 3), + ), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: user.avatar, + ), + ), ), ), - ), Padding( - padding: const EdgeInsets.only(top: 8.0), + padding: user.avatar == null + ? const EdgeInsets.only(top: 70) + : const EdgeInsets.only(top: 8.0), child: Text( user.preferredUsername ?? user.name, style: theme.textTheme.headline6, @@ -79,21 +137,19 @@ class UserProfile extends HookWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _badge( - context: context, + _Badge( icon: Icons.comment, // TODO: should be article icon text: ''' ${compactNumber(userViewSnap.data?.numberOfPosts ?? 0)} Post${pluralS(userViewSnap.data?.numberOfPosts ?? 0)}''', - loading: !userViewSnap.hasData, + isLoading: !userViewSnap.hasData, ), Padding( padding: const EdgeInsets.only(left: 16.0), - child: _badge( - context: context, + child: _Badge( icon: Icons.comment, text: ''' ${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(userViewSnap.data?.numberOfComments ?? 1)}''', - loading: !userViewSnap.hasData, + isLoading: !userViewSnap.hasData, ), ), ], @@ -130,68 +186,42 @@ ${compactNumber(userViewSnap.data?.numberOfComments ?? 0)} Comment${pluralS(user ), ); } +} - Widget _badge( - {IconData icon, String text, bool loading, BuildContext context}) => - Container( - decoration: BoxDecoration( - color: Theme.of(context).accentColor, - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: loading - ? CircularProgressIndicator() - : Row( - children: [ - Icon(icon, size: 15, color: Colors.white), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(text, style: TextStyle(color: Colors.white)), - ), - ], - ), - ), - ); +class _Badge extends StatelessWidget { + final IconData icon; + final String text; + final bool isLoading; - Widget _tabs() => DefaultTabController( - length: 3, - child: Column( - children: [ - TabBar( - labelColor: Colors.black, - tabs: [ - Tab(text: 'Posts'), - Tab(text: 'Comments'), - Tab(text: 'About'), - ], - ), - Expanded( - child: TabBarView( + _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: [ - Center( - 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), + Icon(icon, size: 15, color: Colors.white), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text(text), + ), ], ), - ) - ], - ), - ); + ), + ); + } } From 45d164e23e461b06aa480b92d7211bf2cdef2084 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 22:30:11 +0200 Subject: [PATCH 10/12] reverted main.dart --- lib/main.dart | 7 +------ test/widget_test.dart | 29 ----------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/lib/main.dart b/lib/main.dart index c952204..e1ecae5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,7 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:provider/provider.dart'; -import 'pages/profile_tab.dart'; import 'stores/config_store.dart'; Future main() async { @@ -38,8 +34,7 @@ class MyApp extends StatelessWidget { primarySwatch: ctx.watch().accentColor, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: UserProfileTab(User.fromJson(jsonDecode( - '''{"id":13917,"name":"shilangyu","preferred_username":null,"password_encrypted":"","email":"xmarcinmarcin@gmail.com","avatar":null,"admin":false,"banned":false,"published":"2020-08-23T07:13:23.229279","updated":"2020-08-31T14:24:42.495740","show_nsfw":true,"theme":"minty","default_sort_type":0,"default_listing_type":1,"lang":"browser","show_avatars":true,"send_notifications_to_email":false,"matrix_user_id":null,"actor_id":"https://dev.lemmy.ml/u/shilangyu","bio":null,"local":true,"private_key":null,"public_key":null,"last_refreshed_at":"2020-08-23T07:13:23.229279","banner":"https://dev.lemmy.ml/pictrs/image/Cdx3TfNb8g.jpg"}'''))), + home: MyHomePage(title: 'Flutter hello world'), ), ); } diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index fe479c8..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:lemmur/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From 4539a5c4cad8cdff4d2ec2a16ed32e12a56d7b4d Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 22:49:23 +0200 Subject: [PATCH 11/12] extracting instance url --- lib/widgets/user_profile.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index 320234a..53beb06 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -10,9 +10,11 @@ import '../util/intl.dart'; class UserProfile extends HookWidget { final User user; final Future _userView; + final String _instanceUrl; UserProfile(this.user) - : _userView = LemmyApi('dev.lemmy.ml') + : _instanceUrl = user.actorId.split('/')[2], + _userView = LemmyApi(user.actorId.split('/')[2]) .v1 .search(q: user.name, type: SearchType.users, sort: SortType.active) .then((res) => res.users[0]); @@ -128,7 +130,7 @@ class UserProfile extends HookWidget { Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( - '@${user.name}@', // TODO: add instance host uri + '@${user.name}@$_instanceUrl', style: theme.textTheme.caption, ), ), From 0824e67b3fc8a321355663bdae5911d3f6475007 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 31 Aug 2020 23:05:04 +0200 Subject: [PATCH 12/12] better 'no bio' text --- lib/widgets/user_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index 53beb06..1a6e22d 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -53,7 +53,7 @@ class UserProfile extends HookWidget { if (user.bio == null) Center( child: Text( - 'No bio.', + 'no bio', style: const TextStyle(fontStyle: FontStyle.italic), ), )