Merge pull request #22 from krawieck/user-profile-tab

This commit is contained in:
Filip Krawczyk 2020-08-31 23:11:34 +02:00 committed by GitHub
commit 377f0b7c4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 611 additions and 60 deletions

View File

@ -1,19 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
import 'stores/config_store.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
var configStore = ConfigStore();
await configStore.load();
runApp(
MultiProvider(
providers: [
Provider<ConfigStore>(
create: (_) => configStore,
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) => Observer(
builder: (ctx) => MaterialApp(
title: 'Flutter Demo',
themeMode: ctx.watch<ConfigStore>().theme,
darkTheme: ThemeData.dark(),
theme: ThemeData(
primarySwatch: ctx.watch<ConfigStore>().accentColor,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter hello world'),
),
);
}
class MyHomePage extends StatefulWidget {
@ -36,25 +59,25 @@ class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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: <Widget>[
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),
),
);
}

View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
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;
UserProfileTab(this.user);
@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,
),
],
),
onPressed: () {}, // TODO: should open bottomsheet
),
actions: [
IconButton(
icon: Container(
decoration: BoxDecoration(boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black54,
)
]),
child: Icon(
Icons.settings,
color: user.banner == null ? theme.iconTheme.color : null,
),
),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => Settings()));
},
)
],
),
body: UserProfile(user),
);
}
}

82
lib/pages/settings.dart Normal file
View File

@ -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<ThemeMode>(
value: theme,
title: Text(theme.toString().split('.')[1]),
groupValue: ctx.watch<ConfigStore>().theme,
onChanged: (selected) {
ctx.read<ConfigStore>().theme = selected;
},
),
Text(
'Accent color',
style: theme.textTheme.headline6,
),
// TODO: add accent color picking
],
),
),
);
}
}

View File

@ -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') ?? 'system');
}
void save() async {
var prefs = await SharedPreferences.getInstance();
await prefs.setString('theme', describeEnum(theme));
}
@observable
ThemeMode theme;
@observable
MaterialColor accentColor;
}
ThemeMode _themeModeFromString(String theme) =>
ThemeMode.values.firstWhere((e) => describeEnum(e) == theme);

View File

@ -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}
''';
}
}

5
lib/util/intl.dart Normal file
View File

@ -0,0 +1,5 @@
import 'package:intl/intl.dart';
String pluralS(int howMany) => howMany == 1 ? '' : 's';
String compactNumber(int number) => NumberFormat.compact().format(number);

View File

@ -0,0 +1,229 @@
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';
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> _userView;
final String _instanceUrl;
UserProfile(this.user)
: _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]);
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
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: [
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),
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: [
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: user.avatar == null
? const EdgeInsets.only(top: 70)
: 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}@$_instanceUrl',
style: theme.textTheme.caption,
),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
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,
),
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,
),
),
],
),
),
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())
],
),
),
],
),
);
}
}
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),
),
],
),
),
);
}
}

View File

@ -244,6 +244,13 @@ 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_markdown:
dependency: "direct main"
description:
@ -373,6 +380,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:
@ -478,6 +506,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:
@ -506,6 +541,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:
@ -525,6 +595,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:

View File

@ -28,7 +28,13 @@ dependencies:
flutter_hooks: ^0.13.2
cached_network_image: ^2.2.0+1
timeago: ^2.0.27
lemmy_api_client: ^0.2.1
lemmy_api_client: ^0.2.0
mobx: ^1.2.1
flutter_mobx: ^1.1.0
provider: ^4.3.1
shared_preferences: '>=0.5.0 <2.0.0'
flutter:
sdk: flutter
@ -42,6 +48,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

View File

@ -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);
});
}