1
0
mirror of https://github.com/git-touch/git-touch synced 2025-02-23 06:47:46 +01:00

feat(gitee): issues, issue, pullRequests screens (#144)

* feat: gitee pulls, issue screens

* add action button - issue screen

* remove unnecessary comment

* feat: gitee create issue in repo

* feat: commenting in issue screen

* fix: prepend '#' to subtitle props
This commit is contained in:
Shreyas Thirumalai 2021-01-06 10:22:58 +05:30 committed by GitHub
parent 4679ca10ee
commit 1ac27f4853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 551 additions and 20 deletions

View File

@ -276,9 +276,25 @@ class AuthModel with ChangeNotifier {
);
}
Future fetchGitee(String p) async {
final res = await http.get('${activeAccount.domain}/api/v5$p',
headers: {'Authorization': 'token $token'});
Future fetchGitee(
String p, {
isPost = false,
Map<String, dynamic> body = const {},
}) async {
http.Response res;
if (isPost) {
res = await http.post(
'${activeAccount.domain}/api/v5$p',
headers: {
'Authorization': 'token $token',
HttpHeaders.contentTypeHeader: 'application/json'
},
body: jsonEncode(body),
);
} else {
res = await http.get('${activeAccount.domain}/api/v5$p',
headers: {'Authorization': 'token $token'});
}
final info = json.decode(utf8.decode(res.bodyBytes));
return info;
}

View File

@ -122,3 +122,49 @@ class GiteeBlob {
factory GiteeBlob.fromJson(Map<String, dynamic> json) =>
_$GiteeBlobFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GiteeIssue {
int comments;
String commentsUrl;
String createdAt;
String htmlUrl;
String updatedAt;
String body;
String bodyHtml;
String title;
String state;
GiteeRepoOwner user;
String number;
int id;
GiteeIssue();
factory GiteeIssue.fromJson(Map<String, dynamic> json) =>
_$GiteeIssueFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GiteePull {
String commentsUrl;
String createdAt;
String htmlUrl;
String updatedAt;
String body;
String bodyHtml;
String title;
GiteeRepoOwner user;
int number;
int id;
GiteePull();
factory GiteePull.fromJson(Map<String, dynamic> json) =>
_$GiteePullFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GiteeComment {
String body;
String createdAt;
GiteeRepoOwner user;
GiteeComment();
factory GiteeComment.fromJson(Map<String, dynamic> json) =>
_$GiteeCommentFromJson(json);
}

View File

@ -199,3 +199,82 @@ GiteeBlob _$GiteeBlobFromJson(Map<String, dynamic> json) {
Map<String, dynamic> _$GiteeBlobToJson(GiteeBlob instance) => <String, dynamic>{
'content': instance.content,
};
GiteeIssue _$GiteeIssueFromJson(Map<String, dynamic> json) {
return GiteeIssue()
..comments = json['comments'] as int
..commentsUrl = json['comments_url'] as String
..createdAt = json['created_at'] as String
..htmlUrl = json['html_url'] as String
..updatedAt = json['updated_at'] as String
..body = json['body'] as String
..bodyHtml = json['body_html'] as String
..title = json['title'] as String
..state = json['state'] as String
..user = json['user'] == null
? null
: GiteeRepoOwner.fromJson(json['user'] as Map<String, dynamic>)
..number = json['number'] as String
..id = json['id'] as int;
}
Map<String, dynamic> _$GiteeIssueToJson(GiteeIssue instance) =>
<String, dynamic>{
'comments': instance.comments,
'comments_url': instance.commentsUrl,
'created_at': instance.createdAt,
'html_url': instance.htmlUrl,
'updated_at': instance.updatedAt,
'body': instance.body,
'body_html': instance.bodyHtml,
'title': instance.title,
'state': instance.state,
'user': instance.user,
'number': instance.number,
'id': instance.id,
};
GiteePull _$GiteePullFromJson(Map<String, dynamic> json) {
return GiteePull()
..commentsUrl = json['comments_url'] as String
..createdAt = json['created_at'] as String
..htmlUrl = json['html_url'] as String
..updatedAt = json['updated_at'] as String
..body = json['body'] as String
..bodyHtml = json['body_html'] as String
..title = json['title'] as String
..user = json['user'] == null
? null
: GiteeRepoOwner.fromJson(json['user'] as Map<String, dynamic>)
..number = json['number'] as int
..id = json['id'] as int;
}
Map<String, dynamic> _$GiteePullToJson(GiteePull instance) => <String, dynamic>{
'comments_url': instance.commentsUrl,
'created_at': instance.createdAt,
'html_url': instance.htmlUrl,
'updated_at': instance.updatedAt,
'body': instance.body,
'body_html': instance.bodyHtml,
'title': instance.title,
'user': instance.user,
'number': instance.number,
'id': instance.id,
};
GiteeComment _$GiteeCommentFromJson(Map<String, dynamic> json) {
return GiteeComment()
..body = json['body'] as String
..createdAt = json['created_at'] as String
..user = json['user'] == null
? null
: GiteeRepoOwner.fromJson(json['user'] as Map<String, dynamic>);
}
Map<String, dynamic> _$GiteeCommentToJson(GiteeComment instance) =>
<String, dynamic>{
'body': instance.body,
'created_at': instance.createdAt,
'user': instance.user,
};

View File

@ -8,6 +8,11 @@ import 'package:git_touch/screens/bb_user.dart';
import 'package:git_touch/screens/code_theme.dart';
import 'package:git_touch/screens/ge_blob.dart';
import 'package:git_touch/screens/ge_commits.dart';
import 'package:git_touch/screens/ge_issue.dart';
import 'package:git_touch/screens/ge_issue_comment.dart';
import 'package:git_touch/screens/ge_issue_form.dart';
import 'package:git_touch/screens/ge_issues.dart';
import 'package:git_touch/screens/ge_pulls.dart';
import 'package:git_touch/screens/ge_repo.dart';
import 'package:git_touch/screens/ge_repos.dart';
import 'package:git_touch/screens/ge_search.dart';
@ -392,6 +397,11 @@ class GiteeRouter {
GiteeRouter.commits,
GiteeRouter.tree,
GiteeRouter.blob,
GiteeRouter.issues,
GiteeRouter.pulls,
GiteeRouter.issueAdd, // issueAdd should be above issue
GiteeRouter.issue, // Due to similarity of uris
GiteeRouter.issueComment,
];
static final search = RouterScreen('/search', (context, parameters) {
return GeSearchScreen();
@ -449,4 +459,34 @@ class GiteeRouter {
);
},
);
static final issues = RouterScreen(
'/:owner/:name/issues',
(context, parameters) {
return GeIssuesScreen(parameters['owner'].first, parameters['name'].first,
isPr: false);
},
);
static final issue =
RouterScreen('/:owner/:name/issues/:number', (context, parameters) {
return GeIssueScreen(parameters['owner'].first, parameters['name'].first,
parameters['number'].first,
isPr: false);
});
static final pulls = RouterScreen(
'/:owner/:name/pulls',
(context, parameters) {
return GePullsScreen(parameters['owner'].first, parameters['name'].first,
isPr: true);
},
);
static final issueAdd =
RouterScreen('/:owner/:name/issues/new', (context, parameters) {
return GeIssueFormScreen(
parameters['owner'].first, parameters['name'].first);
});
static final issueComment = RouterScreen(
'/:owner/:name/issues/:number/comment', (context, parameters) {
return GeIssueCommentScreen(parameters['owner'].first,
parameters['name'].first, parameters['number'].first);
});
}

View File

@ -37,7 +37,7 @@ class BbIssuesScreen extends StatelessWidget {
avatarUrl: v.reporter.avatarUrl,
author: v.reporter.displayName,
title: v.title,
number: issueNumber,
subtitle: '#' + issueNumber.toString(),
commentCount: 0,
updatedAt: v.createdOn,
url: '${auth.activeAccount.domain}/$owner/$name/issues/$issueNumber',

View File

@ -36,7 +36,7 @@ class BbPullsScreen extends StatelessWidget {
avatarUrl: v.author.avatarUrl,
author: v.author.displayName,
title: v.title,
number: pullNumber,
subtitle: '#' + pullNumber.toString(),
commentCount: 0,
updatedAt: v.createdOn,
url: '${auth.activeAccount.domain}/$owner/$name/issues/$pullNumber',

116
lib/screens/ge_issue.dart Normal file
View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/scaffolds/refresh_stateful.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:git_touch/widgets/action_entry.dart';
import 'package:git_touch/widgets/avatar.dart';
import 'package:git_touch/widgets/link.dart';
import 'package:git_touch/widgets/comment_item.dart';
import 'package:primer/primer.dart';
import 'package:provider/provider.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/theme.dart';
import 'package:tuple/tuple.dart';
class GeIssueScreen extends StatelessWidget {
final String owner;
final String name;
final String number;
final bool isPr;
GeIssueScreen(this.owner, this.name, this.number, {this.isPr: false});
@override
Widget build(BuildContext context) {
return RefreshStatefulScaffold<Tuple2<GiteeIssue, List<GiteeComment>>>(
title: Text("Issue: #$number"),
fetch: () async {
final auth = context.read<AuthModel>();
final items = await Future.wait([
auth.fetchGitee('/repos/$owner/$name/issues/$number'),
auth.fetchGitee('/repos/$owner/$name/issues/$number/comments')
]);
return Tuple2(GiteeIssue.fromJson(items[0]),
[for (var v in items[1]) GiteeComment.fromJson(v)]);
},
actionBuilder: (data, _) => ActionEntry(
iconData: Octicons.plus,
url: '/gitee/$owner/$name/issues/$number/comment',
),
bodyBuilder: (data, _) {
final issue = data.item1;
final comments = data.item2;
final theme = context.read<ThemeModel>();
return Column(children: <Widget>[
Container(
padding: CommonStyle.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Link(
url: '/gitee/$owner/$name',
child: Row(
children: <Widget>[
Avatar(
url: issue.user.avatarUrl,
size: AvatarSize.extraSmall,
),
SizedBox(width: 4),
Text(
'$owner / $name',
style: TextStyle(
fontSize: 17,
color: theme.palette.secondaryText,
),
),
SizedBox(width: 4),
Text(
'#$number',
style: TextStyle(
fontSize: 17,
color: theme.palette.tertiaryText,
),
),
],
),
),
SizedBox(height: 8),
Text(
issue.title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8),
StateLabel(
issue.state == 'open'
? StateLabelStatus.issueOpened
: StateLabelStatus.issueClosed,
small: true),
SizedBox(height: 16),
CommonStyle.border,
],
)),
Column(children: [
for (var comment in comments) ...[
Padding(
padding: EdgeInsets.only(left: 10),
child: CommentItem(
avatar: Avatar(
url: comment.user.avatarUrl,
linkUrl: '/gitee/${comment.user.login}',
),
createdAt: DateTime.parse(comment.createdAt),
body: comment.body,
login: comment.user.login,
prefix: 'gitee')),
CommonStyle.border,
SizedBox(height: 16),
],
]),
]);
},
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/models/theme.dart';
import 'package:git_touch/scaffolds/common.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:provider/provider.dart';
class GeIssueCommentScreen extends StatefulWidget {
final String owner;
final String name;
final String number;
GeIssueCommentScreen(this.owner, this.name, this.number);
@override
_GeIssueCommentScreenState createState() => _GeIssueCommentScreenState();
}
class _GeIssueCommentScreenState extends State<GeIssueCommentScreen> {
var _body = '';
@override
Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context);
final auth = Provider.of<AuthModel>(context);
return CommonScaffold(
title: Text('New Comment'),
body: Column(
children: <Widget>[
Padding(
padding: CommonStyle.padding,
child: CupertinoTextField(
style: TextStyle(color: theme.palette.text),
placeholder: 'Body',
onChanged: (v) {
setState(() {
_body = v;
});
},
maxLines: 10,
),
),
CupertinoButton.filled(
child: Text('Comment'),
onPressed: () async {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/issues/${widget.number}/comments',
isPost: true,
body: {'body': _body, 'repo': widget.name},
).then((v) {
return GiteeIssue.fromJson(v);
});
await theme.push(
context,
'/gitee/${widget.owner}/${widget.name}/issues/${widget.number}',
replace: true,
);
},
),
],
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/models/theme.dart';
import 'package:git_touch/scaffolds/common.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:provider/provider.dart';
class GeIssueFormScreen extends StatefulWidget {
final String owner;
final String name;
GeIssueFormScreen(this.owner, this.name);
@override
_GeIssueFormScreenState createState() => _GeIssueFormScreenState();
}
class _GeIssueFormScreenState extends State<GeIssueFormScreen> {
var _title = '';
var _body = '';
@override
Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context);
final auth = Provider.of<AuthModel>(context);
return CommonScaffold(
title: Text('Submit an issue'),
body: Column(
children: <Widget>[
Padding(
padding: CommonStyle.padding,
child: CupertinoTextField(
style: TextStyle(color: theme.palette.text),
placeholder: 'Title',
onChanged: (v) {
setState(() {
_title = v;
});
},
),
),
Padding(
padding: CommonStyle.padding,
child: CupertinoTextField(
style: TextStyle(color: theme.palette.text),
placeholder: 'Body',
onChanged: (v) {
setState(() {
_body = v;
});
},
maxLines: 10,
),
),
CupertinoButton.filled(
child: Text('Submit'),
onPressed: () async {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/issues',
isPost: true,
body: {'body': _body, 'title': _title, 'repo': widget.name},
).then((v) {
return GiteeIssue.fromJson(v);
});
await theme.push(
context,
'/gitee/${widget.owner}/${widget.name}/issues/${res.number}',
replace: true,
);
},
),
],
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/scaffolds/list_stateful.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:git_touch/widgets/action_entry.dart';
import 'package:git_touch/widgets/app_bar_title.dart';
import 'package:git_touch/widgets/issue_item.dart';
import 'package:provider/provider.dart';
class GeIssuesScreen extends StatelessWidget {
final String owner;
final String name;
final bool isPr;
GeIssuesScreen(this.owner, this.name, {this.isPr = false});
@override
Widget build(BuildContext context) {
return ListStatefulScaffold<GiteeIssue, int>(
title: AppBarTitle(isPr ? 'Pull Requests' : 'Issues'),
fetch: (page) async {
final res = await context
.read<AuthModel>()
.fetchGiteeWithPage('/repos/$owner/$name/issues', page: page);
return ListPayload(
cursor: res.cursor,
hasMore: res.hasMore,
items: [for (var v in res.data) GiteeIssue.fromJson(v)],
);
},
actionBuilder: () => ActionEntry(
iconData: Octicons.plus,
url: '/gitee/$owner/$name/issues/new',
),
itemBuilder: (p) => IssueItem(
author: p.user.login,
avatarUrl: p.user.avatarUrl,
commentCount: p.comments,
subtitle: '#' + p.number,
title: p.title,
updatedAt: DateTime.parse(p.updatedAt),
url: '/gitee/$owner/$name/issues/${p.number}',
),
);
}
}

40
lib/screens/ge_pulls.dart Normal file
View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/scaffolds/list_stateful.dart';
import 'package:git_touch/widgets/app_bar_title.dart';
import 'package:git_touch/widgets/issue_item.dart';
import 'package:provider/provider.dart';
class GePullsScreen extends StatelessWidget {
final String owner;
final String name;
final bool isPr;
GePullsScreen(this.owner, this.name, {this.isPr = false});
@override
Widget build(BuildContext context) {
return ListStatefulScaffold<GiteePull, int>(
title: AppBarTitle(isPr ? 'Pull Requests' : 'Issues'),
fetch: (page) async {
final res = await context
.read<AuthModel>()
.fetchGiteeWithPage('/repos/$owner/$name/pulls', page: page);
return ListPayload(
cursor: res.cursor,
hasMore: res.hasMore,
items: [for (var v in res.data) GiteePull.fromJson(v)],
);
},
itemBuilder: (p) => IssueItem(
author: p.user.login,
avatarUrl: p.user.avatarUrl,
commentCount: 0, // fix this
subtitle: '#' + p.number.toString(),
title: p.title,
updatedAt: DateTime.parse(p.updatedAt),
url: p.htmlUrl,
),
);
}
}

View File

@ -92,13 +92,13 @@ class GeRepoScreen extends StatelessWidget {
leftIconData: Octicons.issue_opened,
text: Text('Issues'),
rightWidget: Text(numberFormat.format(p.openIssuesCount)),
url: 'https://gitee.com/$owner/$name/issues', // TODO:
url: '/gitee/$owner/$name/issues',
),
if (p.pullRequestsEnabled)
TableViewItem(
leftIconData: Octicons.git_pull_request,
text: Text('Pull requests'),
url: 'https://gitee.com/$owner/$name/pulls', // TODO:
url: '/gitee/$owner/$name/pulls',
),
TableViewItem(
leftIconData: Octicons.history,

View File

@ -42,7 +42,7 @@ class GhIssuesScreen extends StatelessWidget {
author: p.author?.login,
avatarUrl: p.author?.avatarUrl,
commentCount: p.comments.totalCount,
number: p.number,
subtitle: '#' + p.number.toString(),
title: p.title,
updatedAt: p.updatedAt,
labels: p.labels.nodes.isEmpty

View File

@ -37,7 +37,7 @@ class GhPullsScreen extends StatelessWidget {
author: p.author?.login,
avatarUrl: p.author?.avatarUrl,
commentCount: p.comments.totalCount,
number: p.number,
subtitle: '#' + p.number.toString(),
title: p.title,
updatedAt: p.updatedAt,
labels: p.labels.nodes.isEmpty

View File

@ -198,7 +198,7 @@ class _GhSearchScreenState extends State<GhSearchScreen> {
author: p['author']['login'],
avatarUrl: p['author']['avatarUrl'],
commentCount: p['comments']['totalCount'],
number: p['number'],
subtitle: '#' + p['number'].toString(),
title: p['title'],
updatedAt: DateTime.parse(p['updatedAt']),
url: '/github' + Uri.parse(p['url']).path,

View File

@ -52,6 +52,7 @@ class GlIssueScreen extends StatelessWidget {
createdAt: issue.createdAt,
body: issue.description,
login: issue.author.username,
prefix: 'gitlab',
),
),
CommonStyle.border,
@ -80,6 +81,7 @@ class GlIssueScreen extends StatelessWidget {
createdAt: note.createdAt,
body: note.body,
login: note.author.username,
prefix: 'gitlab',
),
)
],

View File

@ -34,7 +34,7 @@ class GlIssuesScreen extends StatelessWidget {
author: p.author.username,
avatarUrl: p.author.avatarUrl,
commentCount: p.userNotesCount,
number: p.iid,
subtitle: '#' + p.iid.toString(),
title: p.title,
updatedAt: p.updatedAt,
labels: p.labels.isEmpty

View File

@ -33,7 +33,7 @@ class GlMergeRequestsScreen extends StatelessWidget {
author: p.author.username,
avatarUrl: p.author.avatarUrl,
commentCount: p.userNotesCount,
number: p.iid,
subtitle: '#' + p.iid.toString(),
title: p.title,
updatedAt: p.updatedAt,
labels: p.labels.isEmpty

View File

@ -34,7 +34,7 @@ class GtIssuesScreen extends StatelessWidget {
author: p.user.login,
avatarUrl: p.user.avatarUrl,
commentCount: p.comments,
number: p.number,
subtitle: '#' + p.number.toString(),
title: p.title,
updatedAt: p.updatedAt,
url: p.htmlUrl,

View File

@ -137,6 +137,7 @@ class CommentItem extends StatelessWidget {
final String login;
final DateTime createdAt;
final String body;
final String prefix;
final List<Widget> widgets;
CommentItem.gh(Map<String, dynamic> payload)
@ -147,13 +148,15 @@ class CommentItem extends StatelessWidget {
login = payload['author']['login'],
createdAt = DateTime.parse(payload['createdAt']),
body = payload['body'],
widgets = [GhEmojiAction(payload)];
widgets = [GhEmojiAction(payload)],
prefix = 'github';
CommentItem({
@required this.avatar,
@required this.login,
@required this.createdAt,
@required this.body,
@required this.prefix,
this.widgets,
});
@ -170,7 +173,7 @@ class CommentItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
UserName(login),
UserName(login, prefix),
SizedBox(height: 2),
Text(
timeago.format(createdAt),

View File

@ -34,7 +34,7 @@ comments {
class IssueItem extends StatelessWidget {
final String url;
final int number;
final String subtitle;
final String title;
final int commentCount;
final DateTime updatedAt;
@ -45,7 +45,7 @@ class IssueItem extends StatelessWidget {
IssueItem({
@required this.url,
@required this.number,
@required this.subtitle,
@required this.title,
@required this.commentCount,
@required this.updatedAt,
@ -83,7 +83,7 @@ class IssueItem extends StatelessWidget {
children: [
TextSpan(text: '$title '),
TextSpan(
text: '#$number',
text: '$subtitle',
style: TextStyle(
color: theme.palette.tertiaryText,
fontWeight: FontWeight.normal,

View File

@ -5,14 +5,15 @@ import 'package:provider/provider.dart';
class UserName extends StatelessWidget {
final String login;
final String prefix;
UserName(this.login);
UserName(this.login, this.prefix);
@override
Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context);
return Link(
url: '/github/$login',
url: '/$prefix/$login',
child: Container(
// padding: EdgeInsets.all(2),
decoration: BoxDecoration(