feat: login screen style, add settings screen, extract simple scaffold

This commit is contained in:
Rongjian Zhang 2019-02-08 19:34:07 +08:00
parent ef03caa135
commit 434781a27e
12 changed files with 242 additions and 66 deletions

View File

@ -5,7 +5,7 @@ import 'providers/settings.dart';
import 'screens/news.dart';
import 'screens/notifications.dart';
import 'screens/search.dart';
import 'screens/profile.dart';
import 'screens/me.dart';
import 'screens/login.dart';
import 'screens/pull_request.dart';
import 'screens/issue.dart';
@ -21,14 +21,14 @@ class _HomeState extends State<Home> {
Widget _buildNotificationIcon(BuildContext context) {
int count = NotificationProvider.of(context).count;
if (count == 0) {
return Icon(Icons.notifications);
return Icon(Icons.notifications_none);
}
// String text = count > 99 ? '99+' : count.toString();
// https://stackoverflow.com/a/45434404
return new Stack(children: <Widget>[
new Icon(Icons.notifications),
new Icon(Icons.notifications_none),
new Positioned(
// draw a red marble
top: 0.0,
@ -53,7 +53,7 @@ class _HomeState extends State<Home> {
title: Text('Search'),
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
icon: Icon(Icons.person_outline),
title: Text('Me'),
),
];
@ -69,7 +69,7 @@ class _HomeState extends State<Home> {
case 2:
return SearchScreen();
case 3:
return ProfileScreen();
return MeScreen();
}
}
@ -82,7 +82,7 @@ class _HomeState extends State<Home> {
}
if (settings.activeLogin == null) {
return LoginScreen();
return MaterialApp(home: LoginScreen());
}
switch (settings.theme) {

View File

@ -21,18 +21,27 @@ class Account {
String avatarUrl;
String token;
Account({this.avatarUrl, this.token});
/// for github enterprise
String domain;
Account({
@required this.avatarUrl,
@required this.token,
this.domain,
});
Account.fromJson(input) {
avatarUrl = input['avatarUrl'];
token = input['token'];
domain = input['domain'];
}
Map<String, dynamic> toJson() {
return {
'avatarUrl': avatarUrl,
'token': token,
};
var data = {'avatarUrl': avatarUrl, 'token': token};
if (domain != null) {
data['domain'] = domain;
}
return data;
}
}

View File

@ -7,8 +7,7 @@ import '../widgets/loading.dart';
typedef RefreshCallback = Future<void> Function();
typedef WidgetBuilder = Widget Function();
// This is a scaffold for normal screens
// Users can pull to refresh
// This is a scaffold for pull to refresh
class RefreshScaffold extends StatelessWidget {
final Widget title;
final WidgetBuilder bodyBuilder;

46
lib/scaffolds/simple.dart Normal file
View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import '../providers/settings.dart';
typedef RefreshCallback = Future<void> Function();
typedef WidgetBuilder = Widget Function();
class SimpleScaffold extends StatelessWidget {
final Widget title;
final WidgetBuilder bodyBuilder;
final Widget trailing;
final List<Widget> actions;
final PreferredSizeWidget bottom;
SimpleScaffold({
@required this.title,
@required this.bodyBuilder,
this.trailing,
this.actions,
this.bottom,
});
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoPageScaffold(
navigationBar:
CupertinoNavigationBar(middle: title, trailing: trailing),
child: SafeArea(
child: SingleChildScrollView(child: bodyBuilder()),
),
);
default:
return Scaffold(
appBar: AppBar(
title: title,
actions: actions,
bottom: bottom,
),
body: SingleChildScrollView(child: bodyBuilder()),
);
}
}
}

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../providers/settings.dart';
import '../scaffolds/simple.dart';
import '../utils/utils.dart';
import '../widgets/link.dart';
class LoginScreen extends StatefulWidget {
@override
@ -13,23 +15,58 @@ class _LoginScreenState extends State<LoginScreen> {
Widget build(BuildContext context) {
var settings = SettingsProvider.of(context);
return MaterialApp(
home: Scaffold(
body: Container(
padding: EdgeInsets.only(top: 200),
return SimpleScaffold(
title: Text('Accounts'),
bodyBuilder: () {
return Container(
child: Column(
children: settings.githubAccountMap.entries.map<Widget>((entry) {
return RaisedButton(
child: Text(entry.key),
onPressed: () {
return Link(
beforeRedirect: () {
settings.setActiveAccount(entry.key);
},
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.black12)),
),
child: Row(children: <Widget>[
CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(entry.value.avatarUrl),
radius: 24,
),
Padding(padding: EdgeInsets.only(left: 10)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(entry.key, style: TextStyle(fontSize: 20)),
Padding(padding: EdgeInsets.only(top: 6)),
Text(entry.value.domain ?? 'https://github.com')
],
),
),
settings.activeLogin == entry.key
? Icon(Icons.check)
: Container(),
]),
),
);
}).toList()
..add(
RaisedButton(
child: Text('Login'),
onPressed: () {
Link(
child: Container(
padding: EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.add),
Text('Add account', style: TextStyle(fontSize: 16)),
],
),
),
beforeRedirect: () {
var state = settings.generateRandomString();
launch(
'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=user%20repo&state=$state',
@ -39,8 +76,8 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
),
),
),
);
},
);
}
}

View File

@ -3,9 +3,12 @@ import 'package:flutter/cupertino.dart';
import '../providers/settings.dart';
import '../screens/user.dart';
class ProfileScreen extends StatelessWidget {
class MeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UserScreen(SettingsProvider.of(context).activeLogin);
return UserScreen(
SettingsProvider.of(context).activeLogin,
showSettings: true,
);
}
}

View File

@ -125,6 +125,7 @@ $key: pullRequest(number: ${item.number}) {
style: TextStyle(color: Colors.black, fontSize: 15),
),
Link(
material: false,
beforeRedirect: () async {
await SettingsProvider.of(context)
.putWithCredentials('/repos/$repo/notifications');

View File

@ -43,10 +43,8 @@ class _SearchScreenState extends State<SearchScreen> {
var user = users[index];
return Row(
children: <Widget>[
Image.network(
user['avatarUrl'],
),
Text(user['login'])
Image.network(user['avatarUrl']),
Text(user['login']),
],
);
},

19
lib/screens/settings.dart Normal file
View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import '../scaffolds/simple.dart';
class SettingsScreen extends StatefulWidget {
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
return SimpleScaffold(
title: Text('Settings'),
bodyBuilder: () {
return Text('body');
},
);
}
}

View File

@ -1,13 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart';
import '../providers/settings.dart';
import '../scaffolds/refresh.dart';
import '../widgets/avatar.dart';
import '../widgets/entry_item.dart';
import '../widgets/list_group.dart';
import '../widgets/repo_item.dart';
import '../widgets/link.dart';
import '../screens/repos.dart';
import '../screens/users.dart';
import '../screens/settings.dart';
import '../utils/utils.dart';
var repoChunk = '''
@ -30,13 +33,11 @@ primaryLanguage {
}
''';
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
class UserScreen extends StatefulWidget {
final String login;
final bool showSettings;
UserScreen(this.login);
UserScreen(this.login, {this.showSettings = false});
_UserScreenState createState() => _UserScreenState();
}
@ -60,6 +61,8 @@ class _UserScreenState extends State<UserScreen> {
avatarUrl
bio
email
company
location
starredRepositories {
totalCount
}
@ -117,11 +120,66 @@ class _UserScreenState extends State<UserScreen> {
}
}
Widget _buildEmail() {
// TODO: redesign the UI to show all information
String email = payload['email'] ?? '';
if (email.isNotEmpty) {
return Link(
child: Row(children: <Widget>[
Icon(
Octicons.mail,
color: Colors.black54,
size: 16,
),
Padding(padding: EdgeInsets.only(left: 4)),
Text(email, style: TextStyle(color: Colors.black54, fontSize: 16))
]),
beforeRedirect: () {
launch('mailto:' + email);
},
);
}
String company = payload['company'] ?? '';
if (company.isNotEmpty) {
return Row(children: <Widget>[
Icon(
Octicons.organization,
color: Colors.black54,
size: 16,
),
Padding(padding: EdgeInsets.only(left: 4)),
Text(company, style: TextStyle(color: Colors.black54, fontSize: 16))
]);
}
String location = payload['location'] ?? '';
if (location.isNotEmpty) {
return Row(children: <Widget>[
Icon(
Octicons.location,
color: Colors.black54,
size: 16,
),
Padding(padding: EdgeInsets.only(left: 4)),
Text(location, style: TextStyle(color: Colors.black54, fontSize: 16))
]);
}
return Container();
}
@override
Widget build(BuildContext context) {
return RefreshScaffold(
onRefresh: _refresh,
title: Text(widget.login),
trailing: Link(
child: Icon(Icons.settings, size: 24),
screenBuilder: (_) => SettingsScreen(),
material: false,
fullscreenDialog: true,
),
loading: loading,
bodyBuilder: () {
return Column(
@ -144,19 +202,7 @@ class _UserScreenState extends State<UserScreen> {
Text(payload['name'] ?? widget.login,
style: TextStyle(height: 1.2)),
Padding(padding: EdgeInsets.only(top: 10)),
Row(children: <Widget>[
Icon(
Octicons.mail,
color: Colors.black54,
size: 16,
),
Padding(padding: EdgeInsets.only(left: 4)),
Text(
payload['email'],
style:
TextStyle(color: Colors.black54, fontSize: 16),
)
])
_buildEmail(),
],
),
)

View File

@ -7,40 +7,58 @@ class Link extends StatelessWidget {
final WidgetBuilder screenBuilder;
final Function beforeRedirect;
final Color bgColor;
final bool material;
final bool fullscreenDialog;
Link({
@required this.child,
this.screenBuilder,
this.beforeRedirect,
this.bgColor,
this.material = true,
this.fullscreenDialog = false,
});
void _onTap(BuildContext context, int theme) {
if (beforeRedirect != null) {
beforeRedirect();
}
if (screenBuilder != null) {
switch (theme) {
case ThemeMap.cupertino:
Navigator.of(context).push(CupertinoPageRoute(
builder: screenBuilder,
fullscreenDialog: fullscreenDialog,
));
break;
default:
Navigator.of(context).push(MaterialPageRoute(
builder: screenBuilder,
fullscreenDialog: fullscreenDialog,
));
}
}
}
@override
Widget build(BuildContext context) {
var theme = SettingsProvider.of(context).theme;
if (!material) {
return GestureDetector(
child: child,
onTap: () => _onTap(context, theme),
);
}
return Material(
child: Ink(
color: bgColor ?? Colors.white,
child: InkWell(
child: child,
splashColor: theme == ThemeMap.cupertino ? Colors.transparent : null,
onTap: () {
if (beforeRedirect != null) {
beforeRedirect();
}
if (screenBuilder != null) {
switch (theme) {
case ThemeMap.cupertino:
Navigator.of(context)
.push(CupertinoPageRoute(builder: screenBuilder));
break;
default:
Navigator.of(context)
.push(MaterialPageRoute(builder: screenBuilder));
}
}
},
onTap: () => _onTap(context, theme),
),
),
);

View File

@ -31,7 +31,7 @@ class Loading extends StatelessWidget {
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 40),
padding: EdgeInsets.symmetric(vertical: 100),
child: _buildIndicator(context),
);
}