feat: home screen style, add pull request screen

This commit is contained in:
Rongjian Zhang 2019-01-28 00:37:44 +08:00
parent 4e28608714
commit 16240278f6
23 changed files with 760 additions and 436 deletions

View File

@ -26,11 +26,30 @@ class _IosHomePageState extends State<IosHomePage> {
tabBar: CupertinoTabBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
icon: Icon(Icons.rss_feed),
title: Text('News'),
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications),
icon: StreamBuilder<int>(builder: (context, snapshot) {
int count = snapshot.data;
print(count);
// https://stackoverflow.com/a/45434404
if (count != null && count > 0) {
return Stack(children: <Widget>[
Icon(Icons.notifications),
Positioned(
// draw a red marble
top: 0,
right: 0,
child: Icon(Icons.brightness_1,
size: 8.0, color: Colors.redAccent),
)
]);
} else {
return Icon(Icons.notifications);
}
}),
title: Text('Notification'),
),
BottomNavigationBarItem(

View File

@ -5,17 +5,37 @@ import 'package:git_flux/providers/notification.dart';
import 'package:git_flux/screens/screens.dart';
import 'package:git_flux/utils/utils.dart';
class NotificationGroup {
String fullName;
List<Notification> items = [];
NotificationGroup(this.fullName);
}
class NotificationScreen extends StatefulWidget {
@override
NotificationScreenState createState() => NotificationScreenState();
}
class NotificationScreenState extends State<NotificationScreen> {
Widget _getRouteWidget(String type) {
int active = 0;
bool loading = false;
List<NotificationGroup> groups = [];
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 0)).then((_) {
_onSwitchTab(context, 0);
});
}
Widget _buildRoute(Notification item) {
String type = item.subject.type;
switch (type) {
case 'Issue':
case 'PullRequest':
return IssueScreen();
// return IssueScreen(item.repository.);
default:
throw new Exception('Unhandled notification type: $type');
}
@ -40,8 +60,7 @@ class NotificationScreenState extends State<NotificationScreen> {
splashColor: Colors.transparent,
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => _getRouteWidget(item.subject.type)),
CupertinoPageRoute(builder: (context) => _buildRoute(item)),
);
},
child: Container(
@ -91,7 +110,9 @@ class NotificationScreenState extends State<NotificationScreen> {
);
}
Widget _buildGroupItem(BuildContext context, NotificationGroup group) {
Widget _buildGroupItem(BuildContext context, int index) {
var group = groups[index];
return Container(
// padding: EdgeInsets.all(10),
child: Column(
@ -113,6 +134,34 @@ class NotificationScreenState extends State<NotificationScreen> {
);
}
void _onSwitchTab(BuildContext context, int index) async {
setState(() {
active = index;
loading = true;
});
var ns = await ghClient.activity
.listNotifications(all: index == 2, participating: index == 1)
.toList();
NotificationProvider.of(context).countUpdate.add(ns.length);
Map<String, NotificationGroup> groupMap = {};
ns.forEach((item) {
String repo = item.repository.fullName;
if (groupMap[repo] == null) {
groupMap[repo] = NotificationGroup(repo);
}
groupMap[repo].items.add(item);
});
setState(() {
groups = groupMap.values.toList();
loading = false;
});
}
@override
Widget build(context) {
NotificationBloc bloc = NotificationProvider.of(context);
@ -120,49 +169,35 @@ class NotificationScreenState extends State<NotificationScreen> {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: StreamBuilder<int>(
stream: bloc.active,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text("loading...");
}
return SizedBox.expand(
child: DefaultTextStyle(
style: _textStyle,
child: CupertinoSegmentedControl(
groupValue: snapshot.data,
onValueChanged: (int index) {
bloc.activeUpdate.add(index);
},
children: {
0: Text('Unread'),
1: Text('Paticipating'),
2: Text('All')
},
),
),
);
},
middle: SizedBox.expand(
child: DefaultTextStyle(
style: _textStyle,
child: CupertinoSegmentedControl(
groupValue: active,
onValueChanged: (index) => _onSwitchTab(context, index),
children: {
0: Text('Unread'),
1: Text('Paticipating'),
2: Text('All')
},
),
),
),
),
child: Column(
children: <Widget>[
// CupertinoSliverRefreshControl(
// onRefresh: () async {
// return Future.delayed(Duration(seconds: 3));
// },
// ),
Container(
child: StreamBuilder<List<NotificationGroup>>(
stream: bloc.items,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text('loading...');
}
return ListView(
shrinkWrap: true,
children: snapshot.data
.map((group) => _buildGroupItem(context, group))
.toList());
},
child: ListView.builder(
shrinkWrap: true,
itemCount: groups.length,
itemBuilder: _buildGroupItem,
),
)
),
],
),
);

View File

@ -1,9 +1,8 @@
import 'package:flutter/cupertino.dart';
import '../widgets/user.dart';
class ProfileScreen extends StatelessWidget {
@override
Widget build(context) {
return UserWidget('pd4d10');
return Text("profile");
}
}

View File

@ -1,38 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'event.g.dart';
@JsonSerializable()
class Actor {
Actor(this.login, this.avatarUrl);
String login;
String avatarUrl;
factory Actor.fromJson(Map<String, dynamic> json) => _$ActorFromJson(json);
Map<String, dynamic> toJson() => _$ActorToJson(this);
}
@JsonSerializable()
class Repo {
Repo(this.name);
String name;
factory Repo.fromJson(Map<String, dynamic> json) => _$RepoFromJson(json);
Map<String, dynamic> toJson() => _$RepoToJson(this);
}
@JsonSerializable()
class Event {
Event(this.id, this.type, this.actor, this.repo);
String id;
String type;
Actor actor;
Repo repo;
Map<String, dynamic> payload;
factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);
Map<String, dynamic> toJson() => _$EventToJson(this);
}

View File

@ -1,44 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'event.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Actor _$ActorFromJson(Map<String, dynamic> json) {
return Actor(json['login'] as String, json['avatar_url'] as String);
}
Map<String, dynamic> _$ActorToJson(Actor instance) => <String, dynamic>{
'login': instance.login,
'avatar_url': instance.avatarUrl
};
Repo _$RepoFromJson(Map<String, dynamic> json) {
return Repo(json['name'] as String);
}
Map<String, dynamic> _$RepoToJson(Repo instance) =>
<String, dynamic>{'name': instance.name};
Event _$EventFromJson(Map<String, dynamic> json) {
return Event(
json['id'] as String,
json['type'] as String,
json['actor'] == null
? null
: Actor.fromJson(json['actor'] as Map<String, dynamic>),
json['repo'] == null
? null
: Repo.fromJson(json['repo'] as Map<String, dynamic>))
..payload = json['payload'] as Map<String, dynamic>;
}
Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
'id': instance.id,
'type': instance.type,
'actor': instance.actor,
'repo': instance.repo,
'payload': instance.payload
};

View File

@ -1,56 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'notification.g.dart';
@JsonSerializable()
class Subject {
Subject(this.title, this.type);
String title;
String type;
factory Subject.fromJson(Map<String, dynamic> json) =>
_$SubjectFromJson(json);
Map<String, dynamic> toJson() => _$SubjectToJson(this);
}
@JsonSerializable()
class Owner {
Owner(this.login, this.avatarUrl);
String login;
String avatarUrl;
factory Owner.fromJson(Map<String, dynamic> json) => _$OwnerFromJson(json);
Map<String, dynamic> toJson() => _$OwnerToJson(this);
}
@JsonSerializable()
class Repository {
Repository(this.fullName, this.type, this.onwer);
String fullName;
String type;
Owner onwer;
factory Repository.fromJson(Map<String, dynamic> json) =>
_$RepositoryFromJson(json);
Map<String, dynamic> toJson() => _$RepositoryToJson(this);
}
@JsonSerializable()
class NotificationItem {
NotificationItem(
this.id, this.type, this.updatedAt, this.repository, this.subject);
String id;
String type;
String updatedAt;
Repository repository;
Subject subject;
Map<String, dynamic> payload;
factory NotificationItem.fromJson(Map<String, dynamic> json) =>
_$NotificationItemFromJson(json);
Map<String, dynamic> toJson() => _$NotificationItemToJson(this);
}

View File

@ -1,63 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Subject _$SubjectFromJson(Map<String, dynamic> json) {
return Subject(json['title'] as String, json['type'] as String);
}
Map<String, dynamic> _$SubjectToJson(Subject instance) =>
<String, dynamic>{'title': instance.title, 'type': instance.type};
Owner _$OwnerFromJson(Map<String, dynamic> json) {
return Owner(json['login'] as String, json['avatar_url'] as String);
}
Map<String, dynamic> _$OwnerToJson(Owner instance) => <String, dynamic>{
'login': instance.login,
'avatar_url': instance.avatarUrl
};
Repository _$RepositoryFromJson(Map<String, dynamic> json) {
return Repository(
json['full_name'] as String,
json['type'] as String,
json['onwer'] == null
? null
: Owner.fromJson(json['onwer'] as Map<String, dynamic>));
}
Map<String, dynamic> _$RepositoryToJson(Repository instance) =>
<String, dynamic>{
'full_name': instance.fullName,
'type': instance.type,
'onwer': instance.onwer
};
NotificationItem _$NotificationItemFromJson(Map<String, dynamic> json) {
return NotificationItem(
json['id'] as String,
json['type'] as String,
json['updated_at'] as String,
json['repository'] == null
? null
: Repository.fromJson(json['repository'] as Map<String, dynamic>),
json['subject'] == null
? null
: Subject.fromJson(json['subject'] as Map<String, dynamic>))
..payload = json['payload'] as Map<String, dynamic>;
}
Map<String, dynamic> _$NotificationItemToJson(NotificationItem instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'updated_at': instance.updatedAt,
'repository': instance.repository,
'subject': instance.subject,
'payload': instance.payload
};

View File

@ -1,8 +1,7 @@
import 'package:flutter/widgets.dart';
import 'dart:async';
import 'package:rxdart/subjects.dart';
import '../models/event.dart';
import 'package:flutter/widgets.dart';
import 'package:rxdart/rxdart.dart';
import 'package:git_flux/utils/utils.dart';
class EventBloc {
final _items = BehaviorSubject<List<Event>>(seedValue: []);

View File

@ -1,24 +1,18 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:rxdart/subjects.dart';
import 'package:rxdart/rxdart.dart';
import 'package:git_flux/utils/utils.dart';
class NotificationBloc {
final _groups = BehaviorSubject<List<NotificationGroup>>(seedValue: []);
final _active = BehaviorSubject(seedValue: 0);
final _loading = BehaviorSubject(seedValue: false);
final _count = BehaviorSubject(seedValue: 0);
final _updater = StreamController();
Stream<List<NotificationGroup>> get items => _groups.stream;
Stream<int> get active => _active.stream;
Stream<bool> get loading => _loading.stream;
Sink<int> get activeUpdate => _active.sink;
Stream<int> get count => _count.stream;
Sink get countUpdate => _updater.sink;
NotificationBloc() {
_active.stream.listen((int index) async {
_loading.add(true);
_groups.add(await fetchNotifications(index));
_loading.add(false);
_updater.stream.listen((_) {
_count.add(99);
});
}
}

View File

@ -0,0 +1,4 @@
export 'event.dart';
export 'notification.dart';
export 'search.dart';
export 'user.dart';

View File

@ -1,13 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class IssueScreen extends StatelessWidget {
class IssueScreen extends StatefulWidget {
final int id;
final String repo;
IssueScreen(this.id, this.repo);
@override
Widget build(context) {
_IssueScreenState createState() => _IssueScreenState();
}
class _IssueScreenState extends State<IssueScreen> {
int active = 0;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("issue"),
middle: Text(widget.repo + ' #' + widget.id.toString()),
trailing: Icon(Icons.more_vert, size: 24),
),
child: SafeArea(
child: Column(
children: <Widget>[
Container(
child: Text(widget.id.toString() + widget.repo),
),
],
),
),
child: Text("issue"),
);
}
}

View File

@ -0,0 +1,400 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:git_flux/utils/utils.dart';
import 'package:git_flux/widgets/widgets.dart';
const PAGE_SIZE = 100;
Future queryPullRequest(int id, String owner, String name) async {
var data = await query('''
{
repository(owner: "$owner", name: "$name") {
pullRequest(number: $id) {
title
createdAt
body
merged
permalink
additions
deletions
author {
login
avatarUrl
}
commits {
totalCount
}
timeline(first: $PAGE_SIZE) {
pageInfo {
hasNextPage
endCursor
}
nodes {
__typename
... on ReviewRequestedEvent {
createdAt
actor {
login
}
requestedReviewer {
... on User {
login
}
}
}
... on PullRequestReview {
createdAt
state
author {
login
}
}
... on IssueComment {
createdAt
body
author {
login
avatarUrl
}
}
... on LabeledEvent {
createdAt
label {
name
url
}
actor {
login
}
}
... on ReferencedEvent {
createdAt
actor {
login
}
commit {
oid
url
}
}
... on MergedEvent {
createdAt
mergeRefName
actor {
login
}
commit {
oid
url
}
}
... on HeadRefDeletedEvent {
createdAt
actor {
login
}
headRefName
}
... on Commit {
committedDate
oid
author {
user {
login
}
}
}
}
}
}
}
}
''');
return data['repository']['pullRequest'];
}
class PullRequestScreen extends StatefulWidget {
final int id;
final String owner;
final String name;
PullRequestScreen(this.id, this.owner, this.name);
@override
_PullRequestScreenState createState() => _PullRequestScreenState();
}
class _PullRequestScreenState extends State<PullRequestScreen> {
int active = 0;
Map<String, dynamic> payload;
@override
void initState() {
super.initState();
queryPullRequest(widget.id, widget.owner, widget.name).then((_payload) {
setState(() {
payload = _payload;
});
});
}
get _fullName => widget.owner + '/' + widget.name;
Widget _buildBadge() {
bool merged = payload['merged'];
int bgColor = merged ? 0xff6f42c1 : 0xff2cbe4e;
IconData iconData = merged ? Octicons.git_merge : Octicons.git_pull_request;
String text = merged ? 'Merged' : 'Open';
return Container(
decoration: BoxDecoration(
color: Color(bgColor),
borderRadius: BorderRadius.all(Radius.circular(4)),
),
padding: EdgeInsets.all(6),
child: Row(
children: <Widget>[
Icon(iconData, color: Colors.white, size: 15),
Text(text,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
)),
],
),
);
}
TextSpan _buildReviewText(BuildContext context, item) {
switch (item['state']) {
case 'APPROVED':
return TextSpan(text: ' approved these changes');
default:
return TextSpan(text: 'not implement');
}
}
TextSpan _buildCommitText(BuildContext context, item) {}
Widget _buildComment(BuildContext context, item) {
// return Text('comment');
return Column(children: <Widget>[
Row(children: <Widget>[
Avatar(item['author']['login'], item['author']['avatarUrl']),
Padding(padding: EdgeInsets.only(left: 10)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UserName(item['author']['login']),
Text('opened ' + TimeAgo.formatFromString(item['createdAt'])),
],
),
),
]),
Padding(
padding: const EdgeInsets.only(left: 20, top: 10),
child: MarkdownBody(data: item['body']),
),
]);
}
Widget _buildItemItem({
String actor,
IconData iconData = Octicons.octoface,
int iconColor = 0xff959da5,
TextSpan textSpan,
item,
}) {
return Row(
children: <Widget>[
Icon(iconData, color: Color(iconColor), size: 16),
Padding(padding: EdgeInsets.only(left: 4)),
Expanded(
child: RichText(
text: TextSpan(style: TextStyle(color: Colors.black), children: [
createUserSpan(actor),
textSpan,
// TextSpan(text: ' ' + TimeAgo.formatFromString(item['createdAt']))
]),
),
),
],
);
}
Widget _buildItem(BuildContext context, item) {
switch (item['__typename']) {
case 'IssueComment':
return _buildComment(context, item);
case 'ReviewRequestedEvent':
return _buildItemItem(
iconData: Octicons.eye,
actor: payload['author']['login'],
textSpan: TextSpan(children: [
TextSpan(text: ' requested a review from '),
createUserSpan(item['requestedReviewer']['login']),
]),
item: item,
);
case 'PullRequestReview':
return _buildItemItem(
actor: item['author']['login'],
iconColor: 0xff28a745,
iconData: Octicons.check,
textSpan: _buildReviewText(context, item),
item: item,
);
case 'LabeledEvent':
return _buildItemItem(
actor: item['actor']['login'],
iconData: Octicons.tag,
textSpan: TextSpan(children: [
TextSpan(text: ' added the '),
TextSpan(text: item['label']['name']),
TextSpan(text: 'label'),
]),
item: item,
);
case 'ReferencedEvent':
return _buildItemItem(
actor: item['actor']['login'],
iconData: Octicons.bookmark,
textSpan: TextSpan(children: [
TextSpan(text: ' referenced this pull request from commit '),
TextSpan(text: item['commit']['oid'].substring(0, 8)),
]),
item: item,
);
case 'MergedEvent':
return _buildItemItem(
actor: item['actor']['login'],
iconData: Octicons.git_merge,
iconColor: 0xff6f42c1,
textSpan: TextSpan(children: [
TextSpan(text: ' merged commit '),
TextSpan(text: item['commit']['oid'].substring(0, 8)),
TextSpan(text: ' into '),
TextSpan(text: item['mergeRefName']),
]),
item: item,
);
case 'HeadRefDeletedEvent':
return _buildItemItem(
actor: item['actor']['login'],
iconData: Octicons.git_branch,
textSpan: TextSpan(children: [
TextSpan(text: ' deleted the '),
TextSpan(text: item['headRefName']),
TextSpan(text: ' branch'),
]),
item: item,
);
case 'Commit':
return _buildItemItem(
actor: item['author']['user']['login'],
iconData: Octicons.git_commit,
textSpan: TextSpan(children: [
TextSpan(text: ' added commit '),
TextSpan(text: item['oid'].substring(0, 8))
]),
item: item,
);
default:
return Text('no data', style: TextStyle(color: Colors.redAccent));
}
}
Widget _buildBody(BuildContext context) {
if (payload == null) {
return CupertinoActivityIndicator();
}
List items = payload['timeline']['nodes'];
return Column(children: <Widget>[
Container(
// padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
_buildBadge(),
],
),
Text(payload['title'],
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
height: 2,
)),
_buildComment(context, payload),
// ListView.builder(
// shrinkWrap: true,
// itemCount: comments.length,
// itemBuilder: _buildCommentItem,
// ),
Column(
children: items.map((item) {
return Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color:
CupertinoColors.extraLightBackgroundGray))),
child: _buildItem(context, item),
);
}).toList(),
),
],
),
)
]);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(_fullName + ' #' + widget.id.toString()),
trailing: Icon(Icons.more_vert, size: 24),
),
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(
width: double.infinity,
height: 48,
child: CupertinoSegmentedControl(
groupValue: active,
onValueChanged: (value) async {
switch (value) {
case 1:
launch(
'https://github.com/$_fullName/pull/${widget.id}/commits');
break;
case 2:
launch(
'https://github.com/$_fullName/pull/${widget.id}/files');
break;
}
setState(() {});
},
children: {
0: Text('Conversation'),
1: Text('Commits'),
2: Text('Changes')
},
),
),
_buildBody(context),
],
),
),
),
);
}
}

View File

@ -1,13 +1,19 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class RepoScreen extends StatefulWidget {
final String owner;
final String name;
RepoScreen(this.owner, this.name);
class RepoScreen extends StatelessWidget {
@override
Widget build(context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("repo"),
),
child: Text("repo"),
_RepoScreenState createState() => _RepoScreenState();
}
class _RepoScreenState extends State<RepoScreen> {
@override
Widget build(BuildContext context) {
return Container(
child: Text(widget.owner),
);
}
}

View File

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

View File

@ -1,9 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../widgets/user.dart';
import 'package:git_flux/utils/utils.dart';
class UserScreen extends StatelessWidget {
@override
Widget build(context) {
return UserWidget('pd4d10');
Future queryUser(String login) async {
var data = await query('''
{
user(login: "$login") {
name
avatarUrl
bio
email
repositories {
totalCount
}
starredRepositories {
totalCount
}
followers {
totalCount
}
following {
totalCount
}
}
}
''');
return data['user'];
}
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
new GlobalKey<RefreshIndicatorState>();
class UserScreen extends StatefulWidget {
final String login;
UserScreen(this.login);
_UserScreenState createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
var user = null;
@override
void initState() {
super.initState();
queryUser(widget.login).then((_user) {
setState(() {
user = _user;
});
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () async {
var _user = await queryUser(widget.login);
setState(() {
user = _user;
});
},
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
child: ClipOval(
child: Image.network(
user['avatarUrl'],
fit: BoxFit.fill,
width: 64,
height: 64,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
GestureDetector(
child: Column(
children: <Widget>[
Text(user['followers']['totalCount'].toString()),
Text('Followers'),
],
),
onTap: () {
// print(1);
},
),
Column(
children: <Widget>[
Text(user['following']['totalCount'].toString()),
Text('Following')
],
)
],
),
],
),
),
);
}
}

View File

@ -31,13 +31,13 @@ Future<dynamic> postWithCredentials(String url, String body) async {
}
Future<dynamic> query(String query) async {
final data =
final res =
await postWithCredentials('/graphql', json.encode({'query': query}));
if (data['errors'] != null) {
throw new Exception(data['errors'].toString());
if (res['errors'] != null) {
throw new Exception(res['errors'].toString());
}
print(data);
return data['data'];
print(res);
return res['data'];
}
Future<List<Event>> fetchEvents(int page) async {
@ -46,27 +46,3 @@ Future<List<Event>> fetchEvents(int page) async {
);
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();
}

View File

@ -10,6 +10,10 @@ class TimeAgo {
return '${_ceil(time)} ${unit}s ago';
}
static String formatFromString(String str) {
return format(DateTime.parse(str));
}
static String format(DateTime time) {
double diff =
(DateTime.now().millisecondsSinceEpoch - time.millisecondsSinceEpoch) /

29
lib/widgets/avatar.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_flux/screens/screens.dart';
class Avatar extends StatelessWidget {
final String login;
final String url;
Avatar(this.login, this.url);
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
splashColor: Colors.transparent,
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => UserScreen(login)),
);
},
child: CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(url),
radius: 18,
),
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:git_flux/screens/screens.dart';
import 'package:git_flux/utils/utils.dart';
import 'package:git_flux/widgets/widgets.dart';
/// Events types:
///
@ -34,14 +35,14 @@ class EventItem extends StatelessWidget {
case 'PullRequestEvent':
return TextSpan(children: [
TextSpan(text: ' ${event.payload['action']} pull request '),
_buildPullRequest(context),
_buildPullRequest(context, event.payload['pull_request']['number']),
TextSpan(text: ' at '),
_buildRepo(context),
]);
case 'PullRequestReviewCommentEvent':
return TextSpan(children: [
TextSpan(text: ' reviewed pull request '),
_buildPullRequest(context),
_buildPullRequest(context, event.payload['pull_request']['number']),
TextSpan(text: ' at '),
_buildRepo(context),
]);
@ -51,9 +52,15 @@ class EventItem extends StatelessWidget {
_buildRepo(context)
]);
case 'IssueCommentEvent':
bool isIssue = event.payload['issue']['pull_request'] == null;
String resource = isIssue ? 'issue' : 'pull request';
TextSpan link = isIssue
? _buildIssue(context)
: _buildPullRequest(context, event.payload['issue']['number']);
return TextSpan(children: [
TextSpan(text: ' commented on issue '),
_buildIssue(context),
TextSpan(text: ' commented on $resource '),
link,
TextSpan(text: ' at '),
_buildRepo(context),
// TextSpan(text: event.payload['comment']['body'])
@ -103,19 +110,23 @@ class EventItem extends StatelessWidget {
}
TextSpan _buildRepo(BuildContext context) {
return _buildLink(context, event.repo.name, () => RepoScreen());
String name = event.repo.name;
var arr = name.split('/');
return _buildLink(context, name, () => RepoScreen(arr[0], arr[1]));
}
TextSpan _buildIssue(BuildContext context) {
return _buildLink(context,
'#' + event.payload['issue']['number'].toString(), () => UserScreen());
int id = event.payload['issue']['number'];
String repo = event.repo.name;
return _buildLink(
context, '#' + id.toString(), () => IssueScreen(id, repo));
}
TextSpan _buildPullRequest(BuildContext context) {
return _buildLink(
context,
'#' + event.payload['pull_request']['number'].toString(),
() => UserScreen());
TextSpan _buildPullRequest(BuildContext context, int id) {
String name = event.repo.name;
var arr = name.split('/');
return _buildLink(context, '#' + id.toString(),
() => PullRequestScreen(id, arr[0], arr[1]));
}
IconData _buildIconData(BuildContext context) {
@ -147,19 +158,15 @@ class EventItem extends StatelessWidget {
children: <Widget>[
Row(
children: <Widget>[
CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(event.actor.avatarUrl),
radius: 16,
),
Avatar(event.actor.login, event.actor.avatarUrl),
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()),
_buildLink(context, event.actor.login,
() => UserScreen(event.actor.login)),
_buildEvent(context),
],
),

View File

@ -1,107 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_flux/utils/utils.dart';
Future queryUser(String login) async {
var data = await query('''
{
user(login: "$login") {
name
avatarUrl
bio
email
repositories {
totalCount
}
starredRepositories {
totalCount
}
followers {
totalCount
}
following {
totalCount
}
}
}
''');
return data['user'];
}
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
new GlobalKey<RefreshIndicatorState>();
class UserWidget extends StatefulWidget {
final String login;
UserWidget(this.login);
_UserWidgetState createState() => _UserWidgetState();
}
class _UserWidgetState extends State<UserWidget> {
var user = null;
@override
void initState() {
super.initState();
queryUser(widget.login).then((_user) {
setState(() {
user = _user;
});
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () async {
var _user = await queryUser(widget.login);
setState(() {
user = _user;
});
},
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
child: ClipOval(
child: Image.network(
user['avatarUrl'],
fit: BoxFit.fill,
width: 64,
height: 64,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
GestureDetector(
child: Column(
children: <Widget>[
Text(user['followers']['totalCount'].toString()),
Text('Followers'),
],
),
onTap: () {
// print(1);
},
),
Column(
children: <Widget>[
Text(user['following']['totalCount'].toString()),
Text('Following')
],
)
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_flux/screens/screens.dart';
final style = TextStyle(fontWeight: FontWeight.w600);
TextSpan createUserSpan(String login) {
return TextSpan(text: login, style: style);
}
class UserName extends StatelessWidget {
final String login;
UserName(this.login);
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
splashColor: Colors.transparent,
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => UserScreen(login)),
);
},
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Text(login, style: style),
),
),
);
}
}

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

@ -0,0 +1,3 @@
export 'avatar.dart';
export 'event.dart';
export 'user_name.dart';

View File

@ -24,6 +24,8 @@ dependencies:
uri: ^0.11.3
github: ^4.1.0
intl: ^0.15.7
url_launcher: ^4.2.0
flutter_markdown: ^0.2.0
dev_dependencies:
flutter_test: