diff --git a/lib/models/github.dart b/lib/models/github.dart index 9f6e7e4..9016d02 100644 --- a/lib/models/github.dart +++ b/lib/models/github.dart @@ -301,3 +301,15 @@ class GithubContentReferenceItem { factory GithubContentReferenceItem.fromJson(Map json) => _$GithubContentReferenceItemFromJson(json); } + +@JsonSerializable(fieldRename: FieldRename.snake) +class GithubContributorItem { + int id; + String login; + String avatarUrl; + String htmlUrl; + int contributions; + GithubContributorItem(); + factory GithubContributorItem.fromJson(Map json) => + _$GithubContributorItemFromJson(json); +} diff --git a/lib/models/github.g.dart b/lib/models/github.g.dart index 30be106..abf8636 100644 --- a/lib/models/github.g.dart +++ b/lib/models/github.g.dart @@ -108,7 +108,11 @@ GithubEventPayload _$GithubEventPayloadFromJson(Map json) { ..checkSuite = json['check_suite'] == null ? null : GithubCheckSuiteItem.fromJson( - json['check_suite'] as Map); + json['check_suite'] as Map) + ..contentReference = json['content_reference'] == null + ? null + : GithubContentReferenceItem.fromJson( + json['content_reference'] as Map); } Map _$GithubEventPayloadToJson(GithubEventPayload instance) => @@ -132,6 +136,7 @@ Map _$GithubEventPayloadToJson(GithubEventPayload instance) => 'installation': instance.installation, 'check_run': instance.checkRun, 'check_suite': instance.checkSuite, + 'content_reference': instance.contentReference, }; GithubEventIssue _$GithubEventIssueFromJson(Map json) { @@ -415,3 +420,23 @@ Map _$GithubContentReferenceItemToJson( 'id': instance.id, 'reference': instance.reference, }; + +GithubContributorItem _$GithubContributorItemFromJson( + Map json) { + return GithubContributorItem() + ..id = json['id'] as int + ..login = json['login'] as String + ..avatarUrl = json['avatar_url'] as String + ..htmlUrl = json['html_url'] as String + ..contributions = json['contributions'] as int; +} + +Map _$GithubContributorItemToJson( + GithubContributorItem instance) => + { + 'id': instance.id, + 'login': instance.login, + 'avatar_url': instance.avatarUrl, + 'html_url': instance.htmlUrl, + 'contributions': instance.contributions, + }; diff --git a/lib/router.dart b/lib/router.dart index 2490eea..645af33 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -5,6 +5,7 @@ import 'package:git_touch/screens/bb_repo.dart'; import 'package:git_touch/screens/bb_user.dart'; import 'package:git_touch/screens/code_theme.dart'; import 'package:git_touch/screens/gh_commits.dart'; +import 'package:git_touch/screens/gh_contributors.dart'; 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'; @@ -71,6 +72,7 @@ class GithubRouter { GithubRouter.object, GithubRouter.stargazers, GithubRouter.watchers, + GithubRouter.contributors, ]; static final user = RouterScreen('/:login', (_, p) { final login = p['login'].first; @@ -135,6 +137,10 @@ class GithubRouter { return GhUsersScreen(p['owner'].first, UsersScreenType.watch, repoName: p['name'].first); }); + static final contributors = + RouterScreen('/:owner/:name/contributors', (_, p) { + return GhContributorsScreen(p['owner'].first, p['name'].first); + }); } class GitlabRouter { diff --git a/lib/screens/gh_contributors.dart b/lib/screens/gh_contributors.dart new file mode 100644 index 0000000..7b9d445 --- /dev/null +++ b/lib/screens/gh_contributors.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:git_touch/models/github.dart'; +import 'package:git_touch/models/notification.dart'; +import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/contributor_item.dart'; +import 'package:github/github.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/widgets/event_item.dart'; +import 'package:git_touch/models/auth.dart'; + +class GhContributorsScreen extends StatelessWidget { + final String owner; + final String name; + GhContributorsScreen(this.owner, this.name); + + Future> _query(BuildContext context, + [int page = 1]) async { + final auth = Provider.of(context); + final res = await auth.ghClient.getJSON>( + '/repos/$owner/$name/contributors?page=$page', + convert: (vs) => [for (var v in vs) GithubContributorItem.fromJson(v)], + ); + return ListPayload( + cursor: page + 1, + items: res, + hasMore: res.isNotEmpty, + ); + } + + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: AppBarTitle('Contributors'), + onRefresh: () => _query(context), + onLoadMore: (cursor) => _query(context, cursor), + itemBuilder: (v) { + final String login = v.login; + return ContributorItem( + avatarUrl: v.avatarUrl, + commits: v.contributions, + login: v.login, + url: '/$login?tab=contributors', + ); + }, + ); + } +} diff --git a/lib/screens/gh_repo.dart b/lib/screens/gh_repo.dart index 4500d92..daa87a4 100644 --- a/lib/screens/gh_repo.dart +++ b/lib/screens/gh_repo.dart @@ -51,6 +51,14 @@ class GhRepoScreen extends StatelessWidget { } } + Future _fetchContributors(BuildContext context) async { + final auth = Provider.of(context); + final res = await auth.ghClient.getJSON( + '/repos/$owner/$name/stats/contributors', + ); + return res.length.toString(); + } + Future _fetchReadme(BuildContext context) async { try { final auth = Provider.of(context); @@ -67,14 +75,16 @@ class GhRepoScreen extends StatelessWidget { Widget build(BuildContext context) { final theme = Provider.of(context); final auth = Provider.of(context); - return RefreshStatefulScaffold>( + return RefreshStatefulScaffold>( title: AppBarTitle('Repository'), fetchData: () async { final rs = await Future.wait([ _query(context), _fetchReadme(context), + _fetchContributors(context), ]); - return Tuple2(rs[0] as GhRepoRepository, rs[1] as String); + + return Tuple3(rs[0] as GhRepoRepository, rs[1] as String, rs[2]); }, actionBuilder: (data, setState) { final repo = data.item1; @@ -96,6 +106,7 @@ class GhRepoScreen extends StatelessWidget { bodyBuilder: (data, setState) { final repo = data.item1; final readme = data.item2; + final contributorsCount = data.item3; final ref = branch == null ? repo.defaultBranchRef : repo.ref; final license = repo.licenseInfo?.spdxId ?? repo.licenseInfo?.name; @@ -321,6 +332,12 @@ class GhRepoScreen extends StatelessWidget { }, ), ], + TableViewItem( + leftIconData: Octicons.organization, + text: Text('Contributors'), + rightWidget: Text(contributorsCount), + url: '/$owner/$name/contributors', + ) ], ), if (readme != null) diff --git a/lib/widgets/contributor_item.dart b/lib/widgets/contributor_item.dart new file mode 100644 index 0000000..38c04f4 --- /dev/null +++ b/lib/widgets/contributor_item.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/theme.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/avatar.dart'; +import 'package:git_touch/widgets/link.dart'; +import 'package:provider/provider.dart'; + +class ContributorItem extends StatelessWidget { + final String login; + final String avatarUrl; + final int commits; + final String url; + + ContributorItem({ + @required this.login, + @required this.avatarUrl, + @required this.commits, + @required this.url, + }); + + @override + Widget build(BuildContext context) { + final theme = Provider.of(context); + return Link( + url: url, + child: Container( + padding: CommonStyle.padding, + child: Row( + children: [ + Avatar(url: avatarUrl, size: AvatarSize.large), + SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + login, + style: TextStyle( + color: theme.palette.primary, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 6), + if (commits != null) + DefaultTextStyle( + style: TextStyle( + color: theme.palette.secondaryText, + fontSize: 16, + ), + child: Text("Commits: " + commits.toString()), + ), + ], + ), + ) + ], + ), + ), + ); + } +}