1
0
mirror of https://github.com/git-touch/git-touch synced 2025-02-21 14:01:02 +01:00

feat: style of home and notification screen

This commit is contained in:
Rongjian Zhang 2019-01-26 22:10:18 +08:00
parent 2458c63a0c
commit 4e28608714
17 changed files with 389 additions and 366 deletions

View File

@ -1,8 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../widgets/event.dart';
import '../providers/event.dart';
import '../models/event.dart';
import 'package:git_flux/utils/utils.dart';
class HomeScreen extends StatefulWidget {
@override
@ -14,6 +13,10 @@ class HomeScreen extends StatefulWidget {
class HomeScreenState extends State<HomeScreen> {
// final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
// GlobalKey<RefreshIndicatorState>();
int page = 1;
bool loading = false;
List<Event> events = [];
ScrollController _controller = ScrollController();
@override
void initState() {
@ -22,50 +25,79 @@ class HomeScreenState extends State<HomeScreen> {
// Future.delayed(Duration(seconds: 0)).then((_) {
// EventProvider.of(context).update.add(true);
// });
_refresh();
_controller.addListener(() {
if (_controller.offset + 100 > _controller.position.maxScrollExtent &&
!_controller.position.outOfRange &&
!loading) {
_loadMore();
}
});
}
Future<void> _refresh() async {
setState(() {
loading = true;
});
page = 1;
var items = await fetchEvents(page);
setState(() {
loading = false;
events = items;
});
}
Future<void> _loadMore() async {
print('more');
setState(() {
loading = true;
});
page = page + 1;
var items = await fetchEvents(page);
setState(() {
loading = false;
events.addAll(items);
});
}
Widget _buildBottomIndicator() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 15),
child: CupertinoActivityIndicator(radius: 12));
}
@override
Widget build(context) {
// Navigator.of(context).pushNamed('/user');
final eventBloc = EventProvider.of(context);
// final eventBloc = EventProvider.of(context);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("GitFlux"),
),
child: StreamBuilder<List<Event>>(
stream: eventBloc.events,
builder: (context, snapshot) {
// print(snapshot.data);
Widget widget;
if (snapshot.hasData) {
// List<Event> events = snapshot.data;
// ScrollController(debugLabel: 'aaa', )
// widget = CupertinoRefreshControl(
// // key: _refreshIndicatorKey,
// onRefresh: () {
// print(1);
// loadFirst();
// },
// );
widget = ListView.builder(itemBuilder: (context, index) {
// print(index);
try {
return EventItem(snapshot.data[index]);
} catch (err) {
return Text(err.toString());
// return null;
}
});
} else if (snapshot.hasError) {
widget = Text("${snapshot.error}");
} else {
widget = CupertinoActivityIndicator();
}
return widget;
},
child: CustomScrollView(
controller: _controller,
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: const Text('News'),
trailing: Icon(Octicons.settings),
),
CupertinoSliverRefreshControl(
onRefresh: _refresh,
),
SliverSafeArea(
top: false,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index == events.length) {
return _buildBottomIndicator();
} else {
return EventItem(events[index]);
}
},
childCount: events.length + 1,
),
),
),
],
),
);
}

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_flux/models/notification.dart';
import 'dart:core';
import 'package:flutter/material.dart' hide Notification;
import 'package:flutter/cupertino.dart' hide Notification;
import 'package:git_flux/providers/notification.dart';
import 'package:git_flux/screens/issue.dart';
import 'package:git_flux/octicons.dart';
import 'package:git_flux/screens/screens.dart';
import 'package:git_flux/utils/utils.dart';
class NotificationScreen extends StatefulWidget {
@override
@ -11,11 +11,6 @@ class NotificationScreen extends StatefulWidget {
}
class NotificationScreenState extends State<NotificationScreen> {
initState() {
super.initState();
// initFetch();
}
Widget _getRouteWidget(String type) {
switch (type) {
case 'Issue':
@ -39,7 +34,7 @@ class NotificationScreenState extends State<NotificationScreen> {
}
}
Widget _buildItem(BuildContext context, NotificationItem item) {
Widget _buildItem(BuildContext context, Notification item) {
return Material(
child: InkWell(
splashColor: Colors.transparent,
@ -59,31 +54,36 @@ class NotificationScreenState extends State<NotificationScreen> {
),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: <Widget>[
Text(item.subject.title, style: TextStyle(height: 1)),
Container(
padding: EdgeInsets.only(top: 4),
child: Text(
item.updatedAt,
style: TextStyle(fontSize: 12),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(item.subject.title,
style: TextStyle(height: 1)),
Padding(padding: EdgeInsets.only(top: 4)),
Text(TimeAgo.format(item.updatedAt),
style: TextStyle(fontSize: 12))
],
),
),
)
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(
CupertinoIcons.right_chevron,
color: CupertinoColors.inactiveGray,
),
),
],
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(
CupertinoIcons.right_chevron,
color: CupertinoColors.inactiveGray,
),
),
],
),
),
@ -147,31 +147,22 @@ class NotificationScreenState extends State<NotificationScreen> {
),
child: Column(
children: <Widget>[
StreamBuilder<bool>(
stream: bloc.loading,
builder: (context, snapshot) {
return Flexible(
child: snapshot.data == null || snapshot.data
? CupertinoActivityIndicator()
: StreamBuilder<List<NotificationGroup>>(
stream: bloc.items,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text('loading...');
}
Container(
child: StreamBuilder<List<NotificationGroup>>(
stream: bloc.items,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text('loading...');
}
List<NotificationGroup> groups = snapshot.data;
return ListView(
children: groups
.map((group) =>
_buildGroupItem(context, group))
.toList());
},
),
);
},
),
return ListView(
shrinkWrap: true,
children: snapshot.data
.map((group) => _buildGroupItem(context, group))
.toList());
},
),
)
],
),
);

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../models/user.dart';
import '../utils.dart';
import 'package:git_flux/utils/utils.dart';
class IosUserPage extends StatelessWidget {
String login;
@ -22,11 +22,11 @@ class IosUserPage extends StatelessWidget {
middle: Text(login),
),
child: FutureBuilder(
future: fetchUser(login),
// future: fetchUser(login),
builder: (context, snapshot) {
Widget widget;
if (snapshot.hasData) {
User user = snapshot.data;
// User user = snapshot.data;
return Text('');
} else if (snapshot.hasError) {
widget = Text("${snapshot.error}");

View File

@ -1,12 +1,9 @@
// import 'package:uri/uri.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_flux/providers/event.dart';
import 'package:git_flux/providers/notification.dart';
import 'package:git_flux/providers/search.dart';
import 'package:git_flux/ios/main.dart';
// import 'package:git_flux/screens/user.dart';
// import 'package:git_flux/screens/repo.dart';
class App extends StatelessWidget {
final isIos = true;
@ -26,7 +23,7 @@ class App extends StatelessWidget {
bloc: eventBloc,
child: MaterialApp(
home: DefaultTextStyle(
style: TextStyle(color: CupertinoColors.black),
style: TextStyle(color: Color(0xff24292e)),
child: IosHomePage(title: 'GitFlux'),
),
// theme: ThemeData(

View File

@ -3,16 +3,6 @@ import 'dart:async';
import 'package:rxdart/subjects.dart';
import '../models/event.dart';
import 'package:rxdart/rxdart.dart';
import '../utils.dart';
Future<List<Event>> fetchEvents([int page = 1]) async {
List data = await getWithCredentials(
'/users/pd4d10/received_events/public?page=$page',
);
return data.map<Event>((item) {
return Event.fromJson(item);
}).toList();
}
class EventBloc {
final _items = BehaviorSubject<List<Event>>(seedValue: []);
@ -24,15 +14,15 @@ class EventBloc {
Sink<bool> get update => _update.sink;
EventBloc() {
_update.stream.listen((bool isRefresh) async {
if (isRefresh) {
_items.add(await fetchEvents());
_page.add(1);
} else {
_items.add(await fetchEvents());
_page.add(_page.value + 1);
}
});
// _update.stream.listen((bool isRefresh) async {
// if (isRefresh) {
// _items.add(await fetchEvents(1));
// _page.add(1);
// } else {
// _items.add(await fetchEvents(1));
// _page.add(_page.value + 1);
// }
// });
}
void dispose() {

View File

@ -1,35 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:rxdart/subjects.dart';
import 'package:rxdart/rxdart.dart';
import '../utils.dart';
import '../models/notification.dart';
class NotificationGroup {
String fullName;
List<NotificationItem> items = [];
NotificationGroup(this.fullName);
}
Future<List<NotificationGroup>> fetchNotifications([int index = 0]) async {
String search = '';
if (index == 1) {
search = '?paticipating=true';
} else if (index == 2) {
search = '?all=true';
}
List data = await getWithCredentials('/notifications$search');
Map<String, NotificationGroup> groupMap = {};
data.forEach((item) {
String repo = item['repository']['full_name'];
if (groupMap[repo] == null) {
groupMap[repo] = NotificationGroup(repo);
}
groupMap[repo].items.add(NotificationItem.fromJson(item));
});
return groupMap.values.toList();
}
import 'package:git_flux/utils/utils.dart';
class NotificationBloc {
final _groups = BehaviorSubject<List<NotificationGroup>>(seedValue: []);

View File

@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart';
import 'dart:async';
import 'package:rxdart/subjects.dart';
import 'package:rxdart/rxdart.dart';
import '../utils.dart';
import 'package:git_flux/utils/utils.dart';
Future search(String keyword, String type) async {
var data = await query('''

View File

@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart';
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:rxdart/subjects.dart';
import 'package:rxdart/rxdart.dart';
import '../utils.dart';
import 'package:git_flux/utils/utils.dart';
Future queryUser(String login) async {
var data = await query('''

3
lib/screens/screens.dart Normal file
View File

@ -0,0 +1,3 @@
export 'repo.dart';
export 'user.dart';
export 'issue.dart';

View File

@ -1,43 +0,0 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'token.dart';
import 'models/user.dart';
final prefix = 'https://api.github.com';
final endpoint = '/graphql';
Future<dynamic> getWithCredentials(String url) async {
final res = await http.get(
prefix + url,
headers: {HttpHeaders.authorizationHeader: 'token $token'},
);
final data = json.decode(res.body);
return data;
}
Future<dynamic> postWithCredentials(String url, String body) async {
final res = await http.post(
prefix + url,
headers: {HttpHeaders.authorizationHeader: 'token $token'},
body: body,
);
final data = json.decode(res.body);
return data;
}
Future<dynamic> query(String query) async {
final data =
await postWithCredentials('/graphql', json.encode({'query': query}));
if (data['errors'] != null) {
throw new Exception(data['errors'].toString());
}
print(data);
return data['data'];
}
Future<User> fetchUser(String login) async {
Map<String, dynamic> data = await getWithCredentials('/users/$login');
return User.fromJson(data);
}

72
lib/utils/github.dart Normal file
View File

@ -0,0 +1,72 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:github/server.dart';
export 'package:github/server.dart';
import '../token.dart';
var ghClient = createGitHubClient(auth: Authentication.withToken(token));
final prefix = 'https://api.github.com';
final endpoint = '/graphql';
Future<dynamic> getWithCredentials(String url) async {
final res = await http.get(
prefix + url,
headers: {HttpHeaders.authorizationHeader: 'token $token'},
);
final data = json.decode(res.body);
return data;
}
Future<dynamic> postWithCredentials(String url, String body) async {
final res = await http.post(
prefix + url,
headers: {HttpHeaders.authorizationHeader: 'token $token'},
body: body,
);
final data = json.decode(res.body);
return data;
}
Future<dynamic> query(String query) async {
final data =
await postWithCredentials('/graphql', json.encode({'query': query}));
if (data['errors'] != null) {
throw new Exception(data['errors'].toString());
}
print(data);
return data['data'];
}
Future<List<Event>> fetchEvents(int page) async {
List data = await getWithCredentials(
'/users/pd4d10/received_events/public?page=$page',
);
return data.map<Event>((item) => Event.fromJSON(item)).toList();
}
class NotificationGroup {
String fullName;
List<Notification> items = [];
NotificationGroup(this.fullName);
}
Future<List<NotificationGroup>> fetchNotifications([int index = 0]) async {
var data = await ghClient.activity
.listNotifications(all: index == 2, participating: index == 1)
.toList();
Map<String, NotificationGroup> groupMap = {};
data.forEach((item) {
String repo = item.repository.fullName;
if (groupMap[repo] == null) {
groupMap[repo] = NotificationGroup(repo);
}
groupMap[repo].items.add(item);
});
return groupMap.values.toList();
}

25
lib/utils/timeago.dart Normal file
View File

@ -0,0 +1,25 @@
import 'dart:core';
class TimeAgo {
static String _ceil(double n) => n.ceil().toString();
static String _pluralize(double time, String unit) {
if (time == 1) {
return '${_ceil(time)} $unit ago';
}
return '${_ceil(time)} ${unit}s ago';
}
static String format(DateTime time) {
double diff =
(DateTime.now().millisecondsSinceEpoch - time.millisecondsSinceEpoch) /
1000;
if (diff < 3600) {
return _pluralize(diff / 60, 'minute');
} else if (diff < 86400) {
return _pluralize(diff / 3600, 'hour');
} else {
return _pluralize(diff / 86400, 'day');
}
}
}

3
lib/utils/utils.dart Normal file
View File

@ -0,0 +1,3 @@
export 'github.dart';
export 'octicons.dart';
export 'timeago.dart';

View File

@ -1,201 +1,180 @@
import '../utils.dart';
import '../models/event.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
// import '../issue.dart';
// import '../user.dart';
// class Strong extends StatelessWidget {
// final String text;
// @override
// build(context) {
// return TextSpan(
// text: text,
// style: TextStyle(
// fontWeight: FontWeight.bold,
// color: Color(0xff24292e),
// ),
// // recognizer: recognizer,
// );
// }
// }
class _Avatar extends StatelessWidget {
final String url;
_Avatar(this.url);
@override
build(context) {
return CircleAvatar(
backgroundImage: NetworkImage(url),
radius: 24.0,
);
}
}
import 'package:git_flux/screens/screens.dart';
import 'package:git_flux/utils/utils.dart';
/// Events types:
///
/// https://developer.github.com/v3/activity/events/types/#event-types--payloads
class EventItem extends StatelessWidget {
final Event event;
EventItem(this.event);
Widget getEventItemByType(BuildContext context) {
TextSpan _buildEvent(BuildContext context) {
switch (event.type) {
case 'IssuesEvent':
return RichText(
text: TextSpan(
style: TextStyle(color: CupertinoColors.black),
children: [
_user(event, context),
TextSpan(text: ' ${event.payload['action']} issue '),
_strong(event.repo.name),
TextSpan(
text: '#' + event.payload['issue']['number'].toString(),
),
TextSpan(
text: event.payload['issue']['title'],
)
],
),
);
return TextSpan(children: [
TextSpan(text: ' ${event.payload['action']} issue '),
_buildIssue(context),
TextSpan(text: ' at '),
_buildRepo(context),
]);
case 'PushEvent':
return RichText(
text: TextSpan(
style: TextStyle(color: CupertinoColors.black),
children: [
_user(event, context),
TextSpan(text: ' pushed to '),
TextSpan(
text: event.payload['ref'],
style: TextStyle(color: CupertinoColors.activeBlue),
),
TextSpan(text: ' in '),
_strong(event.repo.name),
TextSpan(text: '')
],
return TextSpan(children: [
TextSpan(text: ' pushed to '),
TextSpan(
text: event.payload['ref'],
style: TextStyle(color: CupertinoColors.activeBlue),
),
);
TextSpan(text: ' at '),
_buildRepo(context),
TextSpan(text: '')
]);
case 'PullRequestEvent':
return RichText(
text: TextSpan(
style: TextStyle(color: CupertinoColors.black),
children: [
_user(event, context),
TextSpan(text: ' ${event.payload['action']} pull request '),
_strong(event.repo.name),
TextSpan(text: '#' + event.payload['number'].toString()),
TextSpan(text: event.payload['pull_request']['title'])
],
),
);
return TextSpan(children: [
TextSpan(text: ' ${event.payload['action']} pull request '),
_buildPullRequest(context),
TextSpan(text: ' at '),
_buildRepo(context),
]);
case 'PullRequestReviewCommentEvent':
return TextSpan(children: [
TextSpan(text: ' reviewed pull request '),
_buildPullRequest(context),
TextSpan(text: ' at '),
_buildRepo(context),
]);
case 'WatchEvent':
return RichText(
text: TextSpan(
style: TextStyle(color: CupertinoColors.black),
children: [
_user(event, context),
TextSpan(text: ' ${event.payload['action']} '),
_strong(event.repo.name),
],
),
);
return TextSpan(children: [
TextSpan(text: ' ${event.payload['action']} '),
_buildRepo(context)
]);
case 'IssueCommentEvent':
return TextSpan(children: [
TextSpan(text: ' commented on issue '),
_buildIssue(context),
TextSpan(text: ' at '),
_buildRepo(context),
// TextSpan(text: event.payload['comment']['body'])
]);
default:
return Text(
'Not implement yet',
return TextSpan(
text: 'Type ${event.type} Not implement yet',
style: TextStyle(color: CupertinoColors.destructiveRed),
);
}
}
String _buildOriginalComment() {
switch (event.type) {
case 'IssueCommentEvent':
return event.payload['comment']['body'];
case 'IssuesEvent':
return event.payload['issue']['title'];
case 'PullRequestEvent':
return event.payload['pull_request']['title'];
case 'PullRequestReviewCommentEvent':
return event.payload['comment']['body'];
default:
return '';
}
}
String _buildComment() {
return _buildOriginalComment();
}
TextSpan _buildLink(BuildContext context, String text, Function handle) {
return TextSpan(
text: text,
style: TextStyle(color: Color(0xff0366d6)),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) {
return handle();
},
),
);
},
);
}
TextSpan _buildRepo(BuildContext context) {
return _buildLink(context, event.repo.name, () => RepoScreen());
}
TextSpan _buildIssue(BuildContext context) {
return _buildLink(context,
'#' + event.payload['issue']['number'].toString(), () => UserScreen());
}
TextSpan _buildPullRequest(BuildContext context) {
return _buildLink(
context,
'#' + event.payload['pull_request']['number'].toString(),
() => UserScreen());
}
IconData _buildIconData(BuildContext context) {
switch (event.type) {
case 'IssueCommentEvent':
return Octicons.comment_discussion;
case 'IssuesEvent':
return Octicons.issue_opened;
case 'PullRequestEvent':
return Octicons.git_pull_request;
case 'PushEvent':
return Octicons.repo_push;
case 'WatchEvent':
return Octicons.star;
default:
return Octicons.octoface;
}
}
@override
build(context) {
return Container(
padding: EdgeInsets.only(top: 16.0),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border(
top: BorderSide(width: 1.0, color: Color(0xFFFFFFFFFF)),
left: BorderSide(width: 1.0, color: Color(0xFFFFFFFFFF)),
right: BorderSide(width: 1.0, color: Color(0xFFFF000000)),
bottom: BorderSide(width: 1.0, color: Color(0xFFFF000000)),
),
),
child: Row(
children: [
_Avatar(event.actor.avatarUrl),
Expanded(child: getEventItemByType(context)),
],
),
);
}
}
TextSpan _strong(String text, [GestureRecognizer recognizer]) {
return TextSpan(
text: text,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff24292e),
),
recognizer: recognizer,
);
}
TextSpan _user(Event event, context) {
return _strong(
event.actor.login,
// TapGestureRecognizer()
// ..onTap = () {
// Navigator.of(context).push(
// CupertinoPageRoute(
// builder: (context) {
// return IosUserPage(event.actor, event.avatar);
// },
// ),
// );
// },
);
}
class IssuesEvent extends StatelessWidget {
final Event event;
IssuesEvent(this.event);
@override
build(context) {
return RichText(
text: TextSpan(
style: TextStyle(color: CupertinoColors.black),
children: [
_user(event, context),
TextSpan(text: ' ${event.payload['action']} issue '),
_strong(event.repo.name),
TextSpan(
text: '#' + event.payload['issue']['number'].toString(),
border: Border(
bottom: BorderSide(color: CupertinoColors.lightBackgroundGray))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(event.actor.avatarUrl),
radius: 16,
),
Padding(padding: EdgeInsets.only(left: 10)),
Expanded(
child: RichText(
text: TextSpan(
style: TextStyle(color: Color(0xff24292e), height: 1.2),
children: <TextSpan>[
_buildLink(
context, event.actor.login, () => UserScreen()),
_buildEvent(context),
],
),
),
),
Padding(padding: EdgeInsets.only(left: 10)),
Icon(_buildIconData(context),
color: CupertinoColors.inactiveGray),
],
),
TextSpan(
text: event.payload['issue']['title'],
)
],
),
);
}
}
class IssueCommentEvent extends StatelessWidget {
final Event event;
IssueCommentEvent(this.event);
@override
build(context) {
return RichText(
text: TextSpan(
style: TextStyle(color: CupertinoColors.black),
children: [
_user(event, context),
TextSpan(text: ' commented on issue '),
_strong(event.repo.name),
TextSpan(text: '#' + event.payload['issue']['number'].toString()),
TextSpan(text: event.payload['comment']['body'])
Container(
padding: EdgeInsets.only(left: 42, top: 8),
child: Text(_buildComment(),
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Color(0xffaaaaaa))))
],
),
);

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_flux/utils.dart';
import 'package:git_flux/utils/utils.dart';
Future queryUser(String login) async {
var data = await query('''

View File

@ -22,6 +22,8 @@ dependencies:
http: ^0.11.3
rxdart: ^0.20.0
uri: ^0.11.3
github: ^4.1.0
intl: ^0.15.7
dev_dependencies:
flutter_test: