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

feat(gitea): issue, issueComment, labels, refactor(GeIssueComment) (#155)

* feat(gitea): issue, issueAdd, issueComment

* rollback grade.properties change

* refactor: GeIssueComment, fix: url GtIssueComment

* feat: labels
This commit is contained in:
Shreyas Thirumalai 2021-01-10 11:50:03 +05:30 committed by GitHub
parent e9222f414b
commit d74b141279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 489 additions and 51 deletions

View File

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

View File

@ -108,12 +108,23 @@ class GiteaIssue {
GiteaUser user;
int comments;
DateTime updatedAt;
String state;
String htmlUrl;
List<GiteaLabel> labels;
GiteaIssue();
factory GiteaIssue.fromJson(Map<String, dynamic> json) =>
_$GiteaIssueFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GiteaLabel {
String color;
String name;
GiteaLabel();
factory GiteaLabel.fromJson(Map<String, dynamic> json) =>
_$GiteaLabelFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GiteaHeatmapItem {
int timestamp;
@ -122,3 +133,17 @@ class GiteaHeatmapItem {
factory GiteaHeatmapItem.fromJson(Map<String, dynamic> json) =>
_$GiteaHeatmapItemFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GiteaComment {
String body;
DateTime createdAt;
String htmlUrl;
String originalAuthor;
DateTime updatedAt;
int id;
GiteaUser user;
GiteaComment();
factory GiteaComment.fromJson(Map<String, dynamic> json) =>
_$GiteaCommentFromJson(json);
}

View File

@ -187,7 +187,12 @@ GiteaIssue _$GiteaIssueFromJson(Map<String, dynamic> json) {
..updatedAt = json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String)
..htmlUrl = json['html_url'] as String;
..state = json['state'] as String
..htmlUrl = json['html_url'] as String
..labels = (json['labels'] as List)
?.map((e) =>
e == null ? null : GiteaLabel.fromJson(e as Map<String, dynamic>))
?.toList();
}
Map<String, dynamic> _$GiteaIssueToJson(GiteaIssue instance) =>
@ -198,7 +203,21 @@ Map<String, dynamic> _$GiteaIssueToJson(GiteaIssue instance) =>
'user': instance.user,
'comments': instance.comments,
'updated_at': instance.updatedAt?.toIso8601String(),
'state': instance.state,
'html_url': instance.htmlUrl,
'labels': instance.labels,
};
GiteaLabel _$GiteaLabelFromJson(Map<String, dynamic> json) {
return GiteaLabel()
..color = json['color'] as String
..name = json['name'] as String;
}
Map<String, dynamic> _$GiteaLabelToJson(GiteaLabel instance) =>
<String, dynamic>{
'color': instance.color,
'name': instance.name,
};
GiteaHeatmapItem _$GiteaHeatmapItemFromJson(Map<String, dynamic> json) {
@ -212,3 +231,31 @@ Map<String, dynamic> _$GiteaHeatmapItemToJson(GiteaHeatmapItem instance) =>
'timestamp': instance.timestamp,
'contributions': instance.contributions,
};
GiteaComment _$GiteaCommentFromJson(Map<String, dynamic> json) {
return GiteaComment()
..body = json['body'] as String
..createdAt = json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String)
..htmlUrl = json['html_url'] as String
..originalAuthor = json['original_author'] as String
..updatedAt = json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String)
..id = json['id'] as int
..user = json['user'] == null
? null
: GiteaUser.fromJson(json['user'] as Map<String, dynamic>);
}
Map<String, dynamic> _$GiteaCommentToJson(GiteaComment instance) =>
<String, dynamic>{
'body': instance.body,
'created_at': instance.createdAt?.toIso8601String(),
'html_url': instance.htmlUrl,
'original_author': instance.originalAuthor,
'updated_at': instance.updatedAt?.toIso8601String(),
'id': instance.id,
'user': instance.user,
};

View File

@ -35,6 +35,9 @@ import 'package:git_touch/screens/gh_org_repos.dart';
import 'package:git_touch/screens/gl_commit.dart';
import 'package:git_touch/screens/gl_starrers.dart';
import 'package:git_touch/screens/gt_commits.dart';
import 'package:git_touch/screens/gt_issue.dart';
import 'package:git_touch/screens/gt_issue_comment.dart';
import 'package:git_touch/screens/gt_issue_form.dart';
import 'package:git_touch/screens/gt_issues.dart';
import 'package:git_touch/screens/gt_object.dart';
import 'package:git_touch/screens/gt_orgs.dart';
@ -321,6 +324,9 @@ class GiteaRouter {
GiteaRouter.commits,
GiteaRouter.issues,
GiteaRouter.pulls,
GiteaRouter.issueAdd,
GiteaRouter.issue,
GiteaRouter.issueComment,
];
static final status =
RouterScreen('/status', (context, parameters) => GtStatusScreen());
@ -387,6 +393,20 @@ class GiteaRouter {
(context, parameters) => GtIssuesScreen(
parameters['owner'].first, parameters['name'].first,
isPr: true));
static final issueAdd = RouterScreen(
'/:owner/:name/issues/new',
(context, parameters) => GtIssueFormScreen(
parameters['owner'].first, parameters['name'].first));
static final issue = RouterScreen(
'/:owner/:name/issues/:number',
(context, parameters) => GtIssueScreen(parameters['owner'].first,
parameters['name'].first, parameters['number'].first));
static final issueComment = RouterScreen(
'/:owner/:name/issues/:number/comment',
(context, parameters) => GtIssueCommentScreen(parameters['owner'].first,
parameters['name'].first, parameters['number'].first,
body: parameters['body'] != null ? parameters['body'].first : '',
id: parameters['id'] != null ? parameters['id'].first : ''));
}
class BitbucketRouter {

View File

@ -53,53 +53,25 @@ class _GeIssueCommentScreenState extends State<GeIssueCommentScreen> {
CupertinoButton.filled(
child: Text('Comment'),
onPressed: () async {
if (!widget.isPr) {
if (!isEdit) {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/issues/${widget.number}/comments',
requestType: 'POST',
body: {'body': _controller.text, 'repo': widget.name},
);
} 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, '');
await theme.push(
context,
'/gitee/${widget.owner}/${widget.name}/issues/${widget.number}',
replace: true,
if (!isEdit) {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}/comments',
requestType: 'POST',
body: {'body': _controller.text, 'repo': widget.name},
);
} else {
if (!isEdit) {
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/pulls/${widget.number}/comments',
requestType: 'POST',
body: {'body': _controller.text, 'repo': widget.name},
);
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, '');
await theme.push(
context,
'/gitee/${widget.owner}/${widget.name}/pulls/${widget.number}',
replace: true,
final res = await auth.fetchGitee(
'/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/comments/${int.parse(widget.id)}',
requestType: 'PATCH',
body: {'body': _controller.text, 'repo': widget.name},
);
}
Navigator.pop(context, '');
await theme.push(
context,
'/gitee/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}',
replace: true,
);
},
),
],

153
lib/screens/gt_issue.dart Normal file
View File

@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:git_touch/models/gitea.dart';
import 'package:git_touch/scaffolds/refresh_stateful.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/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 GtIssueScreen extends StatelessWidget {
final String owner;
final String name;
final String number;
final bool isPr;
GtIssueScreen(this.owner, this.name, this.number, {this.isPr: false});
List<ActionItem> _buildCommentActionItem(
BuildContext context, GiteaComment comment) {
final auth = context.read<AuthModel>();
final theme = context.read<ThemeModel>();
return [
ActionItem(
iconData: Octicons.pencil,
text: 'Edit',
onTap: (_) {
final uri = Uri(
path: '/gitea/$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.fetchGitea(
'/repos/$owner/$name/issues/comments/${comment.id}',
requestType: 'DELETE');
await theme.push(context, '/gitea/$owner/$name/issues/$number',
replace: true);
},
),
];
}
@override
Widget build(BuildContext context) {
return RefreshStatefulScaffold<Tuple2<GiteaIssue, List<GiteaComment>>>(
title: Text("Issue: #$number"),
fetch: () async {
final auth = context.read<AuthModel>();
final items = await Future.wait([
auth.fetchGitea('/repos/$owner/$name/issues/$number'),
auth.fetchGitea('/repos/$owner/$name/issues/$number/comments')
]);
return Tuple2(GiteaIssue.fromJson(items[0]),
[for (var v in items[1]) GiteaComment.fromJson(v)]);
},
actionBuilder: (data, _) => ActionEntry(
iconData: Octicons.plus,
url: '/gitea/$owner/$name/issues/$number/comment',
),
bodyBuilder: (data, _) {
final issue = data.item1;
final comments = data.item2;
final theme = context.read<ThemeModel>();
final auth = context.read<AuthModel>();
return Column(children: <Widget>[
Container(
padding: CommonStyle.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Link(
url: '/gitea/$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: '/gitea/${comment.user.login}',
),
createdAt: comment.createdAt,
body: comment.body,
login: comment.user.login,
prefix: 'gitea',
commentActionItemList:
_buildCommentActionItem(context, comment),
)),
CommonStyle.border,
SizedBox(height: 16),
],
]),
]);
},
);
}
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.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 GtIssueCommentScreen extends StatefulWidget {
final String owner;
final String name;
final String number;
final bool isPr;
final String body;
final String id;
GtIssueCommentScreen(this.owner, this.name, this.number,
{this.isPr: false, this.body: '', this.id: ''});
@override
_GtIssueCommentScreenState createState() => _GtIssueCommentScreenState();
}
class _GtIssueCommentScreenState extends State<GtIssueCommentScreen> {
bool isEdit = false;
TextEditingController _controller = new TextEditingController();
@override
void initState() {
super.initState();
_controller.text = widget.body;
if (_controller.text != '') {
isEdit = true;
}
}
@override
Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context);
final auth = Provider.of<AuthModel>(context);
return CommonScaffold(
title: Text(isEdit ? 'Update Comment' : 'New Comment'),
body: Column(
children: <Widget>[
Padding(
padding: CommonStyle.padding,
child: CupertinoTextField(
controller: _controller,
style: TextStyle(color: theme.palette.text),
placeholder: 'Body',
maxLines: 10,
),
),
CupertinoButton.filled(
child: Text('Comment'),
onPressed: () async {
if (!isEdit) {
final res = await auth.fetchGitea(
'/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}/comments',
requestType: 'POST',
body: {'body': _controller.text, 'repo': widget.name},
);
} else {
final res = await auth.fetchGitea(
'/repos/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/comments/${int.parse(widget.id)}',
requestType: 'PATCH',
body: {'body': _controller.text, 'repo': widget.name},
);
}
Navigator.pop(context, '');
await theme.push(
context,
'/gitea/${widget.owner}/${widget.name}/${widget.isPr ? 'pulls' : 'issues'}/${widget.number}',
replace: true,
);
},
),
],
),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/gitea.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 GtIssueFormScreen extends StatefulWidget {
final String owner;
final String name;
GtIssueFormScreen(this.owner, this.name);
@override
_GtIssueFormScreenState createState() => _GtIssueFormScreenState();
}
class _GtIssueFormScreenState extends State<GtIssueFormScreen> {
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.fetchGitea(
'/repos/${widget.owner}/${widget.name}/issues',
requestType: 'POST',
body: {'body': _body, 'title': _title},
).then((v) {
return GiteaIssue.fromJson(v);
});
Navigator.pop(context);
await theme.push(
context,
'/gitea/${widget.owner}/${widget.name}/issues',
replace: true,
);
},
),
],
),
);
}
}

View File

@ -3,8 +3,11 @@ import 'package:git_touch/generated/l10n.dart';
import 'package:git_touch/models/auth.dart';
import 'package:git_touch/models/gitea.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:git_touch/widgets/label.dart';
import 'package:provider/provider.dart';
class GtIssuesScreen extends StatelessWidget {
@ -18,7 +21,6 @@ class GtIssuesScreen extends StatelessWidget {
return ListStatefulScaffold<GiteaIssue, int>(
title:
AppBarTitle(isPr ? S.of(context).pullRequests : S.of(context).issues),
// TODO: create issue
fetch: (page) async {
final type = isPr ? 'pulls' : 'issues';
final res = await context.read<AuthModel>().fetchGiteaWithPage(
@ -30,6 +32,12 @@ class GtIssuesScreen extends StatelessWidget {
items: (res.data as List).map((v) => GiteaIssue.fromJson(v)).toList(),
);
},
actionBuilder: () => ActionEntry(
iconData: isPr ? null : Octicons.plus,
url: isPr
? '/gitea/$owner/$name/pulls/new'
: '/gitea/$owner/$name/issues/new',
),
itemBuilder: (p) => IssueItem(
author: p.user.login,
avatarUrl: p.user.avatarUrl,
@ -37,7 +45,17 @@ class GtIssuesScreen extends StatelessWidget {
subtitle: '#' + p.number.toString(),
title: p.title,
updatedAt: p.updatedAt,
url: p.htmlUrl,
url: isPr
? p.htmlUrl // TODO: PR endpoints are not complete in Gitea
: '/gitea/$owner/$name/issues/${p.number}',
labels: isPr
? null
: p.labels.isEmpty
? null
: Wrap(spacing: 4, runSpacing: 4, children: [
for (var label in p.labels)
MyLabel(name: label.name, cssColor: label.color)
]),
),
);
}