diff --git a/lib/ios/home.dart b/lib/ios/home.dart index ab82cc0..8496159 100644 --- a/lib/ios/home.dart +++ b/lib/ios/home.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../widgets/event.dart'; import 'package:git_flux/utils/utils.dart'; +import 'package:git_flux/widgets/widgets.dart'; class HomeScreen extends StatefulWidget { @override diff --git a/lib/screens/issue.dart b/lib/screens/issue.dart index e47854c..f818052 100644 --- a/lib/screens/issue.dart +++ b/lib/screens/issue.dart @@ -1,33 +1,46 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:git_flux/utils/utils.dart'; +import 'package:git_flux/widgets/widgets.dart'; -class IssueScreen extends StatefulWidget { - final int id; - final String repo; - IssueScreen(this.id, this.repo); - - @override - _IssueScreenState createState() => _IssueScreenState(); +Future queryIssue(int id, String owner, String name) async { + var data = await query(''' +{ + repository(owner: "$owner", name: "$name") { + issue(number: $id) { + $graphqlChunk1 + timeline(first: $pageSize) { + pageInfo { + hasNextPage + endCursor + } + nodes { + $graghqlChunk + } + } + } + } +} +'''); + return data['repository']['issue']; } -class _IssueScreenState extends State { - int active = 0; +class IssueScreen extends StatelessWidget { + final int id; + final String owner; + final String name; + + IssueScreen(this.id, this.owner, this.name); @override Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(widget.repo + ' #' + widget.id.toString()), - trailing: Icon(Icons.more_vert, size: 24), - ), - child: SafeArea( - child: Column( - children: [ - Container( - child: Text(widget.id.toString() + widget.repo), - ), - ], - ), + return IssuePullRequestScreen( + id: id, + owner: owner, + name: name, + init: () => queryIssue(id, owner, name), + extra: Row( + children: [Text('test')], ), ); } diff --git a/lib/screens/pull_request.dart b/lib/screens/pull_request.dart index f00a1df..dff07f1 100644 --- a/lib/screens/pull_request.dart +++ b/lib/screens/pull_request.dart @@ -1,38 +1,28 @@ 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 + $graphqlChunk1 merged permalink additions deletions - author { - login - avatarUrl - } commits { totalCount } - timeline(first: $PAGE_SIZE) { + timeline(first: $pageSize) { pageInfo { hasNextPage endCursor } nodes { - __typename + $graghqlChunk ... on ReviewRequestedEvent { createdAt actor { @@ -51,14 +41,6 @@ Future queryPullRequest(int id, String owner, String name) async { login } } - ... on IssueComment { - createdAt - body - author { - login - avatarUrl - } - } ... on LabeledEvent { createdAt label { @@ -69,16 +51,6 @@ Future queryPullRequest(int id, String owner, String name) async { login } } - ... on ReferencedEvent { - createdAt - actor { - login - } - commit { - oid - url - } - } ... on MergedEvent { createdAt mergeRefName @@ -97,15 +69,6 @@ Future queryPullRequest(int id, String owner, String name) async { } headRefName } - ... on Commit { - committedDate - oid - author { - user { - login - } - } - } } } } @@ -115,285 +78,47 @@ Future queryPullRequest(int id, String owner, String name) async { return data['repository']['pullRequest']; } -class PullRequestScreen extends StatefulWidget { +class PullRequestScreen extends StatelessWidget { final int id; final String owner; final String name; + PullRequestScreen(this.id, this.owner, this.name); - @override - _PullRequestScreenState createState() => _PullRequestScreenState(); -} - -class _PullRequestScreenState extends State { - int active = 0; - Map 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: [ - 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: [ - Row(children: [ - Avatar(item['author']['login'], item['author']['avatarUrl']), - Padding(padding: EdgeInsets.only(left: 10)), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ - 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: [ - Container( - // padding: EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _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(), - ), - ], - ), - ) - ]); + // bool merged = payload['merged']; + // int bgColor = merged ? Palette.purple : Palette.green; + // 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: [ + // Icon(iconData, color: Colors.white, size: 15), + // Text(text, + // style: TextStyle( + // color: Colors.white, + // fontWeight: FontWeight.w600, + // )), + // ], + // ), + // ); + return Text('test'); } @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: [ - 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), - ], - ), - ), + return IssuePullRequestScreen( + id: id, + owner: owner, + name: name, + init: () => queryPullRequest(id, owner, name), + extra: Row( + children: [_buildBadge()], ), ); } diff --git a/lib/utils/github.dart b/lib/utils/github.dart index d4ddf1c..4077a6e 100644 --- a/lib/utils/github.dart +++ b/lib/utils/github.dart @@ -36,7 +36,7 @@ Future query(String query) async { if (res['errors'] != null) { throw new Exception(res['errors'].toString()); } - print(res); + // print(res); return res['data']; } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 1a7015b..e5a06aa 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,3 +1,118 @@ +import 'package:flutter/material.dart'; export 'github.dart'; export 'octicons.dart'; export 'timeago.dart'; + +class Palette { + static const green = 0xff2cbe4e; + static const purple = 0xff6f42c1; + static const red = 0xffcb2431; + static const gray = 0xff959da5; +} + +final pageSize = 100; + +final graphqlChunk1 = ''' +title +createdAt +body +author { + login + avatarUrl +} +'''; + +var graghqlChunk = ''' +__typename + +... on IssueComment { + createdAt + body + author { + login + avatarUrl + } +} + +... on Commit { + committedDate + oid + author { + user { + login + } + } +} + ... on ReferencedEvent { + createdAt + isCrossRepository + actor { + login + } + commit { + oid + url + } + commitRepository { + owner { + login + } + name + } + } + + ... on RenamedTitleEvent { + createdAt + previousTitle + currentTitle + actor { + login + } + } + + ... on ClosedEvent { + createdAt + actor { + login + } + } + + ... on ReopenedEvent { + createdAt + actor { + login + } + } + + ... on CrossReferencedEvent { + createdAt + actor { + login + } + source { + __typename + ... on Issue { + number + repository { + owner { + login + } + name + } + } + ... on PullRequest { + number + repository { + owner { + login + } + name + } + } + } + } +'''; + +var warning = Text('xxx', style: TextStyle(color: Colors.redAccent)); +var warningSpan = + TextSpan(text: 'xxx', style: TextStyle(color: Colors.redAccent)); diff --git a/lib/widgets/comment_item.dart b/lib/widgets/comment_item.dart new file mode 100644 index 0000000..3b25624 --- /dev/null +++ b/lib/widgets/comment_item.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:git_flux/widgets/widgets.dart'; +import 'package:git_flux/utils/utils.dart'; + +class CommentItem extends StatelessWidget { + final Map item; + + CommentItem(this.item); + + @override + Widget build(BuildContext context) { + return Column(children: [ + Row(children: [ + Avatar(item['author']['login'], item['author']['avatarUrl']), + Padding(padding: EdgeInsets.only(left: 10)), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + UserName(item['author']['login']), + Text('opened ' + TimeAgo.formatFromString(item['createdAt'])), + ], + ), + ), + ]), + Padding( + padding: const EdgeInsets.only(left: 20, top: 10), + child: MarkdownBody(data: item['body']), + ), + ]); + } +} diff --git a/lib/widgets/event.dart b/lib/widgets/event_item.dart similarity index 94% rename from lib/widgets/event.dart rename to lib/widgets/event_item.dart index d6aecff..00ebd0d 100644 --- a/lib/widgets/event.dart +++ b/lib/widgets/event_item.dart @@ -65,6 +65,10 @@ class EventItem extends StatelessWidget { _buildRepo(context), // TextSpan(text: event.payload['comment']['body']) ]); + case 'ForkEvent': + return TextSpan(children: [ + TextSpan(text: ' forked '), + ]); default: return TextSpan( text: 'Type ${event.type} Not implement yet', @@ -117,9 +121,10 @@ class EventItem extends StatelessWidget { TextSpan _buildIssue(BuildContext context) { int id = event.payload['issue']['number']; - String repo = event.repo.name; + String name = event.repo.name; + var arr = name.split('/'); return _buildLink( - context, '#' + id.toString(), () => IssueScreen(id, repo)); + context, '#' + id.toString(), () => IssueScreen(id, arr[0], arr[1])); } TextSpan _buildPullRequest(BuildContext context, int id) { @@ -141,6 +146,8 @@ class EventItem extends StatelessWidget { return Octicons.repo_push; case 'WatchEvent': return Octicons.star; + case 'ForkEvent': + return Octicons.repo_forked; default: return Octicons.octoface; } diff --git a/lib/widgets/issue_pull_request.dart b/lib/widgets/issue_pull_request.dart new file mode 100644 index 0000000..b8f1843 --- /dev/null +++ b/lib/widgets/issue_pull_request.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:git_flux/widgets/widgets.dart'; + +// Widget of issue screen and pull request screen +class IssuePullRequestScreen extends StatefulWidget { + final int id; + final String owner; + final String name; + final Function init; + final Widget extra; + + IssuePullRequestScreen({ + this.id, + this.owner, + this.name, + this.init, + this.extra, + }); + + @override + _IssuePullRequestScreenState createState() => _IssuePullRequestScreenState(); +} + +class _IssuePullRequestScreenState extends State { + Map payload; + + @override + void initState() { + super.initState(); + widget.init().then((_payload) { + setState(() { + payload = _payload; + }); + }); + } + + get _fullName => widget.owner + '/' + widget.name; + + Widget _buildBody(BuildContext context) { + if (payload == null) { + return CupertinoActivityIndicator(); + } + + List items = payload['timeline']['nodes']; + + return Column(children: [ + Container( + // padding: EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.extra, + Text(payload['title'], + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + height: 1.2, + )), + CommentItem(payload), + // ListView.builder( + // shrinkWrap: true, + // itemCount: comments.length, + // itemBuilder: _buildCommentItem, + // ), + Column( + children: + items.map((item) => TimelineItem(item, payload)).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: _buildBody(context), + ), + ), + ); + } +} diff --git a/lib/widgets/timeline_item.dart b/lib/widgets/timeline_item.dart new file mode 100644 index 0000000..9364e82 --- /dev/null +++ b/lib/widgets/timeline_item.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:git_flux/utils/utils.dart'; +import 'package:git_flux/widgets/widgets.dart'; + +class TimelineItem extends StatelessWidget { + final Map item; + final Map payload; + + TimelineItem(this.item, this.payload); + + TextSpan _buildReviewText(BuildContext context, item) { + switch (item['state']) { + case 'APPROVED': + return TextSpan(text: ' approved these changes'); + case 'COMMENTED': + return TextSpan(text: ' commented '); + default: + return warningSpan; + } + } + + Widget _buildItem({ + String actor, + IconData iconData = Octicons.octoface, + int iconColor = Palette.gray, + TextSpan textSpan, + item, + }) { + return Row( + children: [ + 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 _buildByType(BuildContext context) { + switch (item['__typename']) { + case 'IssueComment': + return CommentItem(item); + case 'ReferencedEvent': + // TODO: isCrossRepository + if (item['commit'] == null) { + return Container(); + } + + return _buildItem( + 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 'RenamedTitleEvent': + return _buildItem( + actor: item['actor']['login'], + iconData: Octicons.pencil, + textSpan: TextSpan(children: [ + TextSpan(text: ' changed the title '), + TextSpan( + text: item['previousTitle'], + style: TextStyle(decoration: TextDecoration.lineThrough), + ), + TextSpan(text: ' to '), + TextSpan(text: item['currentTitle']) + ]), + item: item, + ); + case 'ClosedEvent': + return _buildItem( + actor: item['actor']['login'], + iconData: Octicons.circle_slash, + iconColor: Palette.red, + textSpan: TextSpan(text: ' closed this '), + item: item, + ); + case 'ReopenedEvent': + return _buildItem( + actor: item['actor']['login'], + iconData: Octicons.primitive_dot, + iconColor: Palette.green, + textSpan: TextSpan(text: ' reopened this '), + item: item, + ); + case 'CrossReferencedEvent': + return _buildItem( + actor: item['actor']['login'], + iconData: Octicons.primitive_dot, + iconColor: Palette.green, + textSpan: TextSpan( + text: ' referenced this on #' + + item['source']['number'].toString()), + item: item, + ); + + // + case 'ReviewRequestedEvent': + return _buildItem( + 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 _buildItem( + actor: item['author']['login'], + iconColor: 0xff28a745, + iconData: Octicons.check, + textSpan: _buildReviewText(context, item), + item: item, + ); + case 'LabeledEvent': + return _buildItem( + 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 'MergedEvent': + return _buildItem( + 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 _buildItem( + 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 _buildItem( + 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 warning; + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + border: Border( + bottom: + BorderSide(color: CupertinoColors.extraLightBackgroundGray))), + child: _buildByType(context), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 54c41f9..7fe3e76 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,3 +1,6 @@ export 'avatar.dart'; -export 'event.dart'; +export 'event_item.dart'; export 'user_name.dart'; +export 'timeline_item.dart'; +export 'comment_item.dart'; +export 'issue_pull_request.dart';