diff --git a/lib/models/gitee.dart b/lib/models/gitee.dart index 9b81c14..8a08f1e 100644 --- a/lib/models/gitee.dart +++ b/lib/models/gitee.dart @@ -79,6 +79,7 @@ class GiteeCommit { GiteeCommitDetail commit; String sha; String htmlUrl; + List files; GiteeCommit(); factory GiteeCommit.fromJson(Map json) => _$GiteeCommitFromJson(json); @@ -152,6 +153,7 @@ class GiteePull { String body; String bodyHtml; String title; + String state; GiteeRepoOwner user; int number; int id; @@ -169,3 +171,42 @@ class GiteeComment { factory GiteeComment.fromJson(Map json) => _$GiteeCommentFromJson(json); } + +@JsonSerializable(fieldRename: FieldRename.snake) +class GiteePatch { + String diff; + GiteePatch(); + factory GiteePatch.fromJson(Map json) => + _$GiteePatchFromJson(json); +} + +// Two different classes because of variable type mismatch +// for additions, deletions, patch +@JsonSerializable(fieldRename: FieldRename.snake) +class GiteePullFile { + String additions; + String deletions; + String blobUrl; + String filename; + String sha; + String status; + GiteePatch patch; + GiteePullFile(); + factory GiteePullFile.fromJson(Map json) => + _$GiteePullFileFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GiteeCommitFile { + int additions; + int deletions; + int changes; + String blobUrl; + String filename; + String sha; + String status; + String patch; + GiteeCommitFile(); + factory GiteeCommitFile.fromJson(Map json) => + _$GiteeCommitFileFromJson(json); +} diff --git a/lib/models/gitee.g.dart b/lib/models/gitee.g.dart index 7787980..9808196 100644 --- a/lib/models/gitee.g.dart +++ b/lib/models/gitee.g.dart @@ -132,7 +132,12 @@ GiteeCommit _$GiteeCommitFromJson(Map json) { ? null : GiteeCommitDetail.fromJson(json['commit'] as Map) ..sha = json['sha'] as String - ..htmlUrl = json['html_url'] as String; + ..htmlUrl = json['html_url'] as String + ..files = (json['files'] as List) + ?.map((e) => e == null + ? null + : GiteeCommitFile.fromJson(e as Map)) + ?.toList(); } Map _$GiteeCommitToJson(GiteeCommit instance) => @@ -141,6 +146,7 @@ Map _$GiteeCommitToJson(GiteeCommit instance) => 'commit': instance.commit, 'sha': instance.sha, 'html_url': instance.htmlUrl, + 'files': instance.files, }; GiteeCommitDetail _$GiteeCommitDetailFromJson(Map json) { @@ -247,6 +253,7 @@ GiteePull _$GiteePullFromJson(Map json) { ..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) @@ -262,6 +269,7 @@ Map _$GiteePullToJson(GiteePull instance) => { 'body': instance.body, 'body_html': instance.bodyHtml, 'title': instance.title, + 'state': instance.state, 'user': instance.user, 'number': instance.number, 'id': instance.id, @@ -282,3 +290,60 @@ Map _$GiteeCommentToJson(GiteeComment instance) => 'created_at': instance.createdAt, 'user': instance.user, }; + +GiteePatch _$GiteePatchFromJson(Map json) { + return GiteePatch()..diff = json['diff'] as String; +} + +Map _$GiteePatchToJson(GiteePatch instance) => + { + 'diff': instance.diff, + }; + +GiteePullFile _$GiteePullFileFromJson(Map json) { + return GiteePullFile() + ..additions = json['additions'] as String + ..deletions = json['deletions'] as String + ..blobUrl = json['blob_url'] as String + ..filename = json['filename'] as String + ..sha = json['sha'] as String + ..status = json['status'] as String + ..patch = json['patch'] == null + ? null + : GiteePatch.fromJson(json['patch'] as Map); +} + +Map _$GiteePullFileToJson(GiteePullFile instance) => + { + 'additions': instance.additions, + 'deletions': instance.deletions, + 'blob_url': instance.blobUrl, + 'filename': instance.filename, + 'sha': instance.sha, + 'status': instance.status, + 'patch': instance.patch, + }; + +GiteeCommitFile _$GiteeCommitFileFromJson(Map json) { + return GiteeCommitFile() + ..additions = json['additions'] as int + ..deletions = json['deletions'] as int + ..changes = json['changes'] as int + ..blobUrl = json['blob_url'] as String + ..filename = json['filename'] as String + ..sha = json['sha'] as String + ..status = json['status'] as String + ..patch = json['patch'] as String; +} + +Map _$GiteeCommitFileToJson(GiteeCommitFile instance) => + { + 'additions': instance.additions, + 'deletions': instance.deletions, + 'changes': instance.changes, + 'blob_url': instance.blobUrl, + 'filename': instance.filename, + 'sha': instance.sha, + 'status': instance.status, + 'patch': instance.patch, + }; diff --git a/lib/router.dart b/lib/router.dart index e6782fe..16aea65 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -10,11 +10,14 @@ import 'package:git_touch/screens/bb_pulls.dart'; 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_commit.dart'; import 'package:git_touch/screens/ge_commits.dart'; +import 'package:git_touch/screens/ge_files.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_pull.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'; @@ -414,6 +417,10 @@ class GiteeRouter { GiteeRouter.issueAdd, // issueAdd should be above issue GiteeRouter.issue, // Due to similarity of uris GiteeRouter.issueComment, + GiteeRouter.pull, + GiteeRouter.pullComment, + GiteeRouter.files, + GiteeRouter.commit, ]; static final search = RouterScreen('/search', (context, parameters) { return GeSearchScreen(); @@ -499,6 +506,31 @@ class GiteeRouter { static final issueComment = RouterScreen( '/:owner/:name/issues/:number/comment', (context, parameters) { return GeIssueCommentScreen(parameters['owner'].first, - parameters['name'].first, parameters['number'].first); + parameters['name'].first, parameters['number'].first, + isPr: false); }); + static final pull = RouterScreen( + '/:owner/:name/pulls/:number', + (context, parameters) { + return GePullScreen(parameters['owner'].first, parameters['name'].first, + parameters['number'].first, + isPr: true); + }, + ); + static final pullComment = RouterScreen('/:owner/:name/pulls/:number/comment', + (context, parameters) { + return GeIssueCommentScreen(parameters['owner'].first, + parameters['name'].first, parameters['number'].first, + isPr: true); + }); + static final files = + RouterScreen('/:owner/:name/pulls/:number/files', (context, parameters) { + return GeFilesScreen(parameters['owner'].first, parameters['name'].first, + parameters['number'].first); + }); + static final commit = RouterScreen( + '/:owner/:name/commits/:sha', + (context, parameters) => GeCommitScreen(parameters['owner'].first, + parameters['name'].first, parameters['sha'].first), + ); } diff --git a/lib/screens/ge_commit.dart b/lib/screens/ge_commit.dart new file mode 100644 index 0000000..194f875 --- /dev/null +++ b/lib/screens/ge_commit.dart @@ -0,0 +1,91 @@ +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_button.dart'; +import 'package:git_touch/widgets/avatar.dart'; +import 'package:git_touch/widgets/files_item.dart'; +import 'package:git_touch/widgets/link.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/theme.dart'; + +class GeCommitScreen extends StatelessWidget { + final String owner; + final String name; + final String sha; + GeCommitScreen(this.owner, this.name, this.sha); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return RefreshStatefulScaffold( + title: Text("Commit: ${sha.substring(0, 7)}"), + fetch: () async { + final auth = context.read(); + final items = await auth.fetchGitee('/repos/$owner/$name/commits/$sha'); + print(GiteeCommit.fromJson(items)); + return GiteeCommit.fromJson(items); + }, + actionBuilder: (data, _) => ActionButton( + title: 'Actions', items: [...ActionItem.getUrlActions(data.htmlUrl)]), + bodyBuilder: (data, _) { + return Column(children: [ + Container( + padding: CommonStyle.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Link( + url: '/gitee/$owner/$name', + child: Row( + children: [ + Avatar( + url: data.author.avatarUrl, + size: AvatarSize.extraSmall, + ), + SizedBox(width: 4), + Text( + '$owner / $name', + style: TextStyle( + fontSize: 17, + color: theme.palette.secondaryText, + ), + ), + SizedBox(width: 4), + Text( + '${sha.substring(0, 7)}', + style: TextStyle( + fontSize: 17, + color: theme.palette.tertiaryText, + ), + ), + ], + ), + ), + SizedBox(height: 8), + Text( + data.commit.message, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ], + )), + Wrap( + children: data.files + .map((file) => FilesItem( + filename: file.filename, + additions: file.additions, + deletions: file.deletions, + status: file.status, + patch: file.patch, + )) + .toList(), + ) + ]); + }, + ); + } +} diff --git a/lib/screens/ge_commits.dart b/lib/screens/ge_commits.dart index 109dbee..36189ad 100644 --- a/lib/screens/ge_commits.dart +++ b/lib/screens/ge_commits.dart @@ -34,7 +34,7 @@ class GeCommitsScreen extends StatelessWidget { avatarLink: '/gitee/${c.author.login}', createdAt: c.commit.author.date, message: c.commit.message, - url: c.htmlUrl, + url: '/gitee/$owner/$name/commits/${c.sha}', ); }, ); diff --git a/lib/screens/ge_files.dart b/lib/screens/ge_files.dart new file mode 100644 index 0000000..510a375 --- /dev/null +++ b/lib/screens/ge_files.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:git_touch/models/gitee.dart'; +import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/widgets/action_button.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/widgets/files_item.dart'; +import 'package:git_touch/models/auth.dart'; +import '../generated/l10n.dart'; + +class GeFilesScreen extends StatelessWidget { + final String owner; + final String name; + final String pullNumber; + GeFilesScreen(this.owner, this.name, this.pullNumber); + + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: AppBarTitle(S.of(context).files), + actionBuilder: () { + return ActionButton( + title: 'Actions', + items: [ + ...ActionItem.getUrlActions( + 'https://gitee.com/$owner/$name/pulls/$pullNumber/files'), + ], + ); + }, + fetch: (page) async { + page = page ?? 1; + final res = await context + .read() + .fetchGiteeWithPage( + '/repos/$owner/$name/pulls/$pullNumber/files?page=$page', + ) + .then((v) { + return [for (var file in v.data) GiteePullFile.fromJson(file)]; + }); + return ListPayload( + cursor: page + 1, + items: res, + hasMore: res.isNotEmpty, + ); + }, + itemBuilder: (v) { + return FilesItem( + filename: v.filename, + additions: int.parse(v.additions), + deletions: int.parse(v.deletions), + status: v.status, + patch: v.patch.diff, + ); + }, + ); + } +} diff --git a/lib/screens/ge_issue_comment.dart b/lib/screens/ge_issue_comment.dart index bac029d..1f2524e 100644 --- a/lib/screens/ge_issue_comment.dart +++ b/lib/screens/ge_issue_comment.dart @@ -11,7 +11,8 @@ class GeIssueCommentScreen extends StatefulWidget { final String owner; final String name; final String number; - GeIssueCommentScreen(this.owner, this.name, this.number); + final bool isPr; + GeIssueCommentScreen(this.owner, this.name, this.number, {this.isPr: false}); @override _GeIssueCommentScreenState createState() => _GeIssueCommentScreenState(); @@ -44,18 +45,35 @@ class _GeIssueCommentScreenState extends State { 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, - ); + if (!widget.isPr) { + 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); + }); + Navigator.pop(context, ''); + await theme.push( + context, + '/gitee/${widget.owner}/${widget.name}/issues/${widget.number}', + replace: true, + ); + } else { + final res = await auth.fetchGitee( + '/repos/${widget.owner}/${widget.name}/pulls/${widget.number}/comments', + isPost: true, + body: {'body': _body, 'repo': widget.name}, + ).then((v) { + return GiteePull.fromJson(v); + }); + Navigator.pop(context, ''); + await theme.push( + context, + '/gitee/${widget.owner}/${widget.name}/pulls/${widget.number}', + replace: true, + ); + } }, ), ], diff --git a/lib/screens/ge_pull.dart b/lib/screens/ge_pull.dart new file mode 100644 index 0000000..ed87dce --- /dev/null +++ b/lib/screens/ge_pull.dart @@ -0,0 +1,225 @@ +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 GePullScreen extends StatelessWidget { + final String owner; + final String name; + final String number; + final bool isPr; + + GePullScreen(this.owner, this.name, this.number, {this.isPr: false}); + + @override + Widget build(BuildContext context) { + return RefreshStatefulScaffold< + Tuple4, List, + List>>( + title: Text("Pull Request: #$number"), + fetch: () async { + final auth = context.read(); + final items = await Future.wait([ + auth.fetchGitee('/repos/$owner/$name/pulls/$number'), + auth.fetchGitee('/repos/$owner/$name/pulls/$number/comments'), + auth.fetchGitee('/repos/$owner/$name/pulls/$number/files'), + auth.fetchGitee('/repos/$owner/$name/pulls/$number/commits'), + ]); + return Tuple4( + GiteePull.fromJson(items[0]), + [for (var v in items[1]) GiteeComment.fromJson(v)], + [for (var v in items[2]) GiteePullFile.fromJson(v)], + [for (var v in items[3]) GiteeCommit.fromJson(v)]); + }, + actionBuilder: (data, _) => ActionEntry( + iconData: Octicons.plus, + url: '/gitee/$owner/$name/pulls/$number/comment', + ), + bodyBuilder: (data, _) { + final pull = data.item1; + final comments = data.item2; + final files = data.item3; + final commits = data.item4; + final theme = context.read(); + var additions = 0; + var deletions = 0; + for (var file in files) { + additions += int.parse(file.additions); + deletions += int.parse(file.deletions); + } + return Column(children: [ + Container( + padding: CommonStyle.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Link( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Link( + url: '/gitee/$owner/$name', + child: Row( + children: [ + Avatar( + url: pull.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( + pull.title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8), + StateLabel( + pull.state == 'open' + ? StateLabelStatus.pullOpened + : StateLabelStatus.pullClosed, + small: true), + SizedBox(height: 16), + CommonStyle.border, + CommonStyle.border, + Link( + url: '/gitee/$owner/$name/pulls/$number/files', + child: Container( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + '${files.length} files changed', + style: TextStyle( + color: theme.palette.secondaryText, + fontSize: 17, + ), + ), + Row( + children: [ + Text( + '+$additions', + style: TextStyle( + color: Colors.green, + fontSize: 15, + ), + ), + SizedBox(width: 2), + Text( + '-$deletions', + style: TextStyle( + color: Colors.red, + fontSize: 15, + ), + ), + Icon( + Icons.chevron_right, + color: theme.palette.border, + ), + ], + ) + ], + ), + ), + ), + CommonStyle.border, + ListTileTheme( + contentPadding: EdgeInsets.zero, + child: ExpansionTile( + title: Text( + 'Commits', + style: TextStyle( + color: theme.palette.primary, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + children: [ + for (var commit in commits) ...[ + Link( + url: + '/gitee/$owner/$name/commits/${commit.sha}', + child: Container( + padding: + EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + '${commit.sha.substring(0, 7)}', + style: TextStyle( + color: theme.palette.primary, + fontSize: 17, + fontFamily: + CommonStyle.monospace, + ), + ), + ], + ), + ), + ) + ] + ], + )), + ]), + ), + url: '/gitee/$owner/$name', + ), + 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), + ], + ]), + ]); + }, + ); + } +} diff --git a/lib/screens/ge_pulls.dart b/lib/screens/ge_pulls.dart index a85d226..0971bc4 100644 --- a/lib/screens/ge_pulls.dart +++ b/lib/screens/ge_pulls.dart @@ -33,7 +33,7 @@ class GePullsScreen extends StatelessWidget { subtitle: '#' + p.number.toString(), title: p.title, updatedAt: DateTime.parse(p.updatedAt), - url: p.htmlUrl, + url: '/gitee/$owner/$name/pulls/${p.number}', ), ); } diff --git a/lib/screens/gh_compare.dart b/lib/screens/gh_compare.dart index 65c0ca6..abad0b6 100644 --- a/lib/screens/gh_compare.dart +++ b/lib/screens/gh_compare.dart @@ -42,7 +42,6 @@ class GhComparisonScreen extends StatelessWidget { additions: vs.additions, deletions: vs.deletions, status: vs.status, - changes: vs.changes, patch: vs.patch, )) .toList(), diff --git a/lib/screens/gh_files.dart b/lib/screens/gh_files.dart index 2bb15eb..b80a588 100644 --- a/lib/screens/gh_files.dart +++ b/lib/screens/gh_files.dart @@ -48,7 +48,6 @@ class GhFilesScreen extends StatelessWidget { additions: v.additions, deletions: v.deletions, status: v.status, - changes: v.changes, patch: v.patch, ); }, diff --git a/lib/widgets/files_item.dart b/lib/widgets/files_item.dart index 9114ad3..927390a 100644 --- a/lib/widgets/files_item.dart +++ b/lib/widgets/files_item.dart @@ -9,7 +9,6 @@ import 'package:flutter_highlight/theme_map.dart'; class FilesItem extends StatelessWidget { final String filename; final String status; - final int changes; final int additions; final int deletions; final String patch; @@ -17,7 +16,6 @@ class FilesItem extends StatelessWidget { FilesItem({ @required this.filename, @required this.status, - @required this.changes, @required this.deletions, @required this.additions, @required this.patch,