feat(gitee): comment actions (issue/PR) (#150)

This commit is contained in:
Shreyas Thirumalai 2021-01-09 08:53:52 +05:30 committed by GitHub
parent efe882a0df
commit e9454c3dce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 234 additions and 70 deletions

View File

@ -278,25 +278,53 @@ class AuthModel with ChangeNotifier {
Future fetchGitee( Future fetchGitee(
String p, { String p, {
isPost = false, requestType = 'GET',
Map<String, dynamic> body = const {}, Map<String, dynamic> body = const {},
}) async { }) async {
http.Response res; http.Response res;
if (isPost) { Map<String, String> headers = {
res = await http.post( 'Authorization': 'token $token',
'${activeAccount.domain}/api/v5$p', HttpHeaders.contentTypeHeader: 'application/json'
headers: { };
'Authorization': 'token $token', switch (requestType) {
HttpHeaders.contentTypeHeader: 'application/json' case 'DELETE':
}, {
body: jsonEncode(body), await http.delete(
); '${activeAccount.domain}/api/v5$p',
} else { headers: headers,
res = await http.get('${activeAccount.domain}/api/v5$p', );
headers: {'Authorization': 'token $token'}); break;
}
case 'POST':
{
res = await http.post(
'${activeAccount.domain}/api/v5$p',
headers: headers,
body: jsonEncode(body),
);
break;
}
case 'PATCH':
{
res = await http.patch(
'${activeAccount.domain}/api/v5$p',
headers: headers,
body: jsonEncode(body),
);
break;
}
default:
{
res = await http.get('${activeAccount.domain}/api/v5$p',
headers: headers);
break;
}
} }
final info = json.decode(utf8.decode(res.bodyBytes)); if (requestType != 'DELETE') {
return info; final info = json.decode(utf8.decode(res.bodyBytes));
return info;
}
return;
} }
Future<DataWithPage> fetchGiteeWithPage(String path, Future<DataWithPage> fetchGiteeWithPage(String path,

View File

@ -164,6 +164,7 @@ class GiteePull {
@JsonSerializable(fieldRename: FieldRename.snake) @JsonSerializable(fieldRename: FieldRename.snake)
class GiteeComment { class GiteeComment {
int id;
String body; String body;
String createdAt; String createdAt;
GiteeRepoOwner user; GiteeRepoOwner user;

View File

@ -277,6 +277,7 @@ Map<String, dynamic> _$GiteePullToJson(GiteePull instance) => <String, dynamic>{
GiteeComment _$GiteeCommentFromJson(Map<String, dynamic> json) { GiteeComment _$GiteeCommentFromJson(Map<String, dynamic> json) {
return GiteeComment() return GiteeComment()
..id = json['id'] as int
..body = json['body'] as String ..body = json['body'] as String
..createdAt = json['created_at'] as String ..createdAt = json['created_at'] as String
..user = json['user'] == null ..user = json['user'] == null
@ -286,6 +287,7 @@ GiteeComment _$GiteeCommentFromJson(Map<String, dynamic> json) {
Map<String, dynamic> _$GiteeCommentToJson(GiteeComment instance) => Map<String, dynamic> _$GiteeCommentToJson(GiteeComment instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id,
'body': instance.body, 'body': instance.body,
'created_at': instance.createdAt, 'created_at': instance.createdAt,
'user': instance.user, 'user': instance.user,

View File

@ -505,9 +505,14 @@ class GiteeRouter {
}); });
static final issueComment = RouterScreen( static final issueComment = RouterScreen(
'/:owner/:name/issues/:number/comment', (context, parameters) { '/:owner/:name/issues/:number/comment', (context, parameters) {
return GeIssueCommentScreen(parameters['owner'].first, return GeIssueCommentScreen(
parameters['name'].first, parameters['number'].first, parameters['owner'].first,
isPr: false); parameters['name'].first,
parameters['number'].first,
isPr: false,
body: parameters['body'] != null ? parameters['body'].first : '',
id: parameters['id'] != null ? parameters['id'].first : '',
);
}); });
static final pull = RouterScreen( static final pull = RouterScreen(
'/:owner/:name/pulls/:number', '/:owner/:name/pulls/:number',
@ -519,9 +524,14 @@ class GiteeRouter {
); );
static final pullComment = RouterScreen('/:owner/:name/pulls/:number/comment', static final pullComment = RouterScreen('/:owner/:name/pulls/:number/comment',
(context, parameters) { (context, parameters) {
return GeIssueCommentScreen(parameters['owner'].first, return GeIssueCommentScreen(
parameters['name'].first, parameters['number'].first, parameters['owner'].first,
isPr: true); parameters['name'].first,
parameters['number'].first,
isPr: true,
body: parameters['body'] != null ? parameters['body'].first : '',
id: parameters['id'] != null ? parameters['id'].first : '',
);
}); });
static final files = static final files =
RouterScreen('/:owner/:name/pulls/:number/files', (context, parameters) { RouterScreen('/:owner/:name/pulls/:number/files', (context, parameters) {

View File

@ -24,7 +24,6 @@ class GeCommitScreen extends StatelessWidget {
fetch: () async { fetch: () async {
final auth = context.read<AuthModel>(); final auth = context.read<AuthModel>();
final items = await auth.fetchGitee('/repos/$owner/$name/commits/$sha'); final items = await auth.fetchGitee('/repos/$owner/$name/commits/$sha');
print(GiteeCommit.fromJson(items));
return GiteeCommit.fromJson(items); return GiteeCommit.fromJson(items);
}, },
actionBuilder: (data, _) => ActionButton( actionBuilder: (data, _) => ActionButton(

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:git_touch/models/gitee.dart'; import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/scaffolds/refresh_stateful.dart'; import 'package:git_touch/scaffolds/refresh_stateful.dart';
import 'package:git_touch/utils/utils.dart'; import 'package:git_touch/utils/utils.dart';
import 'package:git_touch/widgets/action_button.dart';
import 'package:git_touch/widgets/action_entry.dart'; import 'package:git_touch/widgets/action_entry.dart';
import 'package:git_touch/widgets/avatar.dart'; import 'package:git_touch/widgets/avatar.dart';
import 'package:git_touch/widgets/link.dart'; import 'package:git_touch/widgets/link.dart';
@ -20,6 +21,38 @@ class GeIssueScreen extends StatelessWidget {
GeIssueScreen(this.owner, this.name, this.number, {this.isPr: false}); GeIssueScreen(this.owner, this.name, this.number, {this.isPr: false});
List<ActionItem> _buildCommentActionItem(
BuildContext context, GiteeComment comment) {
final auth = context.read<AuthModel>();
final theme = context.read<ThemeModel>();
return [
ActionItem(
iconData: Octicons.pencil,
text: 'Edit',
onTap: (_) {
final uri = Uri(
path: '/gitee/$owner/$name/issues/$number/comment',
queryParameters: {
'body': comment.body,
'id': comment.id.toString(),
},
).toString();
theme.push(context, uri);
}),
ActionItem(
iconData: Octicons.trashcan,
text: 'Delete',
onTap: (_) async {
await auth.fetchGitee(
'/repos/$owner/$name/issues/comments/${comment.id}',
requestType: 'DELETE');
await theme.push(context, '/gitee/$owner/$name/issues/$number',
replace: true);
},
),
];
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshStatefulScaffold<Tuple2<GiteeIssue, List<GiteeComment>>>( return RefreshStatefulScaffold<Tuple2<GiteeIssue, List<GiteeComment>>>(
@ -97,14 +130,17 @@ class GeIssueScreen extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: CommentItem( child: CommentItem(
avatar: Avatar( avatar: Avatar(
url: comment.user.avatarUrl, url: comment.user.avatarUrl,
linkUrl: '/gitee/${comment.user.login}', linkUrl: '/gitee/${comment.user.login}',
), ),
createdAt: DateTime.parse(comment.createdAt), createdAt: DateTime.parse(comment.createdAt),
body: comment.body, body: comment.body,
login: comment.user.login, login: comment.user.login,
prefix: 'gitee')), prefix: 'gitee',
commentActionItemList:
_buildCommentActionItem(context, comment),
)),
CommonStyle.border, CommonStyle.border,
SizedBox(height: 16), SizedBox(height: 16),
], ],

View File

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.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/models/theme.dart';
import 'package:git_touch/scaffolds/common.dart'; import 'package:git_touch/scaffolds/common.dart';
import 'package:git_touch/utils/utils.dart'; import 'package:git_touch/utils/utils.dart';
@ -12,33 +11,42 @@ class GeIssueCommentScreen extends StatefulWidget {
final String name; final String name;
final String number; final String number;
final bool isPr; final bool isPr;
GeIssueCommentScreen(this.owner, this.name, this.number, {this.isPr: false}); final String body;
final String id;
GeIssueCommentScreen(this.owner, this.name, this.number,
{this.isPr: false, this.body: '', this.id: ''});
@override @override
_GeIssueCommentScreenState createState() => _GeIssueCommentScreenState(); _GeIssueCommentScreenState createState() => _GeIssueCommentScreenState();
} }
class _GeIssueCommentScreenState extends State<GeIssueCommentScreen> { class _GeIssueCommentScreenState extends State<GeIssueCommentScreen> {
var _body = ''; bool isEdit = false;
TextEditingController _controller = new TextEditingController();
@override
void initState() {
super.initState();
_controller.text = widget.body;
if (_controller.text != '') {
isEdit = true;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context); final theme = Provider.of<ThemeModel>(context);
final auth = Provider.of<AuthModel>(context); final auth = Provider.of<AuthModel>(context);
return CommonScaffold( return CommonScaffold(
title: Text('New Comment'), title: Text(isEdit ? 'Update Comment' : 'New Comment'),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: CommonStyle.padding, padding: CommonStyle.padding,
child: CupertinoTextField( child: CupertinoTextField(
controller: _controller,
style: TextStyle(color: theme.palette.text), style: TextStyle(color: theme.palette.text),
placeholder: 'Body', placeholder: 'Body',
onChanged: (v) {
setState(() {
_body = v;
});
},
maxLines: 10, maxLines: 10,
), ),
), ),
@ -46,13 +54,19 @@ class _GeIssueCommentScreenState extends State<GeIssueCommentScreen> {
child: Text('Comment'), child: Text('Comment'),
onPressed: () async { onPressed: () async {
if (!widget.isPr) { if (!widget.isPr) {
final res = await auth.fetchGitee( if (!isEdit) {
'/repos/${widget.owner}/${widget.name}/issues/${widget.number}/comments', final res = await auth.fetchGitee(
isPost: true, '/repos/${widget.owner}/${widget.name}/issues/${widget.number}/comments',
body: {'body': _body, 'repo': widget.name}, requestType: 'POST',
).then((v) { body: {'body': _controller.text, 'repo': widget.name},
return GiteeIssue.fromJson(v); );
}); } else {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/issues/comments/${int.parse(widget.id)}',
requestType: 'PATCH',
body: {'body': _controller.text, 'repo': widget.name},
);
}
Navigator.pop(context, ''); Navigator.pop(context, '');
await theme.push( await theme.push(
context, context,
@ -60,13 +74,25 @@ class _GeIssueCommentScreenState extends State<GeIssueCommentScreen> {
replace: true, replace: true,
); );
} else { } else {
final res = await auth.fetchGitee( if (!isEdit) {
'/repos/${widget.owner}/${widget.name}/pulls/${widget.number}/comments', final res = await auth.fetchGitee(
isPost: true, '/repos/${widget.owner}/${widget.name}/pulls/${widget.number}/comments',
body: {'body': _body, 'repo': widget.name}, requestType: 'POST',
).then((v) { body: {'body': _controller.text, 'repo': widget.name},
return GiteePull.fromJson(v); );
}); Navigator.pop(context, '');
await theme.push(
context,
'/gitee/${widget.owner}/${widget.name}/pulls/${widget.number}',
replace: true,
);
} else {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/pulls/comments/${int.parse(widget.id)}',
requestType: 'PATCH',
body: {'body': _controller.text, 'repo': widget.name},
);
}
Navigator.pop(context, ''); Navigator.pop(context, '');
await theme.push( await theme.push(
context, context,

View File

@ -58,7 +58,7 @@ class _GeIssueFormScreenState extends State<GeIssueFormScreen> {
onPressed: () async { onPressed: () async {
final res = await auth.fetchGitee( final res = await auth.fetchGitee(
'/repos/${widget.owner}/issues', '/repos/${widget.owner}/issues',
isPost: true, requestType: 'POST',
body: {'body': _body, 'title': _title, 'repo': widget.name}, body: {'body': _body, 'title': _title, 'repo': widget.name},
).then((v) { ).then((v) {
return GiteeIssue.fromJson(v); return GiteeIssue.fromJson(v);

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:git_touch/models/gitee.dart'; import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/scaffolds/refresh_stateful.dart'; import 'package:git_touch/scaffolds/refresh_stateful.dart';
import 'package:git_touch/utils/utils.dart'; import 'package:git_touch/utils/utils.dart';
import 'package:git_touch/widgets/action_button.dart';
import 'package:git_touch/widgets/action_entry.dart'; import 'package:git_touch/widgets/action_entry.dart';
import 'package:git_touch/widgets/avatar.dart'; import 'package:git_touch/widgets/avatar.dart';
import 'package:git_touch/widgets/link.dart'; import 'package:git_touch/widgets/link.dart';
@ -20,6 +21,38 @@ class GePullScreen extends StatelessWidget {
GePullScreen(this.owner, this.name, this.number, {this.isPr: false}); GePullScreen(this.owner, this.name, this.number, {this.isPr: false});
List<ActionItem> _buildCommentActionItem(
BuildContext context, GiteeComment comment) {
final auth = context.read<AuthModel>();
final theme = context.read<ThemeModel>();
return [
ActionItem(
iconData: Octicons.pencil,
text: 'Edit',
onTap: (_) {
final uri = Uri(
path: '/gitee/$owner/$name/pulls/$number/comment',
queryParameters: {
'body': comment.body,
'id': comment.id.toString(),
},
).toString();
theme.push(context, uri);
}),
ActionItem(
iconData: Octicons.trashcan,
text: 'Delete',
onTap: (_) async {
await auth.fetchGitee(
'/repos/$owner/$name/pulls/comments/${comment.id}',
requestType: 'DELETE');
await theme.push(context, '/gitee/$owner/$name/pulls/$number',
replace: true);
},
),
];
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshStatefulScaffold< return RefreshStatefulScaffold<
@ -206,14 +239,17 @@ class GePullScreen extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: CommentItem( child: CommentItem(
avatar: Avatar( avatar: Avatar(
url: comment.user.avatarUrl, url: comment.user.avatarUrl,
linkUrl: '/gitee/${comment.user.login}', linkUrl: '/gitee/${comment.user.login}',
), ),
createdAt: DateTime.parse(comment.createdAt), createdAt: DateTime.parse(comment.createdAt),
body: comment.body, body: comment.body,
login: comment.user.login, login: comment.user.login,
prefix: 'gitee')), prefix: 'gitee',
commentActionItemList:
_buildCommentActionItem(context, comment),
)),
CommonStyle.border, CommonStyle.border,
SizedBox(height: 16), SizedBox(height: 16),
], ],

View File

@ -24,12 +24,14 @@ class ActionItem {
return [ return [
ActionItem( ActionItem(
text: 'Share', text: 'Share',
iconData: Octicons.rocket,
onTap: (_) { onTap: (_) {
Share.share(url); Share.share(url);
}, },
), ),
ActionItem( ActionItem(
text: 'Open in Browser', text: 'Open in Browser',
iconData: Octicons.globe,
onTap: (_) { onTap: (_) {
launchUrl(url); launchUrl(url);
}, },
@ -68,12 +70,18 @@ class ActionButton extends StatelessWidget {
title: Text(title), title: Text(title),
actions: items.asMap().entries.map((entry) { actions: items.asMap().entries.map((entry) {
return CupertinoActionSheetAction( return CupertinoActionSheetAction(
child: Text( child: Row(
entry.value.text, children: [
style: TextStyle( Icon(entry.value.iconData),
fontWeight: selected == entry.key SizedBox(width: 10),
? FontWeight.w500 Text(
: FontWeight.w400), entry.value.text,
style: TextStyle(
fontWeight: selected == entry.key
? FontWeight.w500
: FontWeight.w400),
),
],
), ),
onPressed: () { onPressed: () {
Navigator.pop(context, entry.key); Navigator.pop(context, entry.key);
@ -106,7 +114,13 @@ class ActionButton extends StatelessWidget {
return items.asMap().entries.map((entry) { return items.asMap().entries.map((entry) {
return PopupMenuItem( return PopupMenuItem(
value: entry.key, value: entry.key,
child: Text(entry.value.text), child: Row(
children: [
Icon(entry.value.iconData),
SizedBox(width: 10),
Text(entry.value.text)
],
),
); );
}).toList(); }).toList();
}, },

View File

@ -139,6 +139,7 @@ class CommentItem extends StatelessWidget {
final String body; final String body;
final String prefix; final String prefix;
final List<Widget> widgets; final List<Widget> widgets;
final List<ActionItem> commentActionItemList;
CommentItem.gh(Map<String, dynamic> payload) CommentItem.gh(Map<String, dynamic> payload)
: avatar = Avatar( : avatar = Avatar(
@ -149,7 +150,8 @@ class CommentItem extends StatelessWidget {
createdAt = DateTime.parse(payload['createdAt']), createdAt = DateTime.parse(payload['createdAt']),
body = payload['body'], body = payload['body'],
widgets = [GhEmojiAction(payload)], widgets = [GhEmojiAction(payload)],
prefix = 'github'; prefix = 'github',
commentActionItemList = []; // TODO
CommentItem({ CommentItem({
@required this.avatar, @required this.avatar,
@ -158,6 +160,7 @@ class CommentItem extends StatelessWidget {
@required this.body, @required this.body,
@required this.prefix, @required this.prefix,
this.widgets, this.widgets,
this.commentActionItemList,
}); });
@override @override
@ -183,6 +186,15 @@ class CommentItem extends StatelessWidget {
], ],
), ),
), ),
Align(
alignment: Alignment.centerRight,
child: ActionButton(
iconData: Octicons.kebab_horizontal,
title: 'Comment Actions',
items: [
if (commentActionItemList != null) ...commentActionItemList
],
)),
]), ]),
SizedBox(height: 12), SizedBox(height: 12),
MarkdownFlutterView(body, padding: EdgeInsets.zero), // TODO: link MarkdownFlutterView(body, padding: EdgeInsets.zero), // TODO: link