diff --git a/lib/home.dart b/lib/home.dart index dcbde55e..7687c6a7 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -8,6 +8,8 @@ import 'package:git_touch/screens/bb_teams.dart'; import 'package:git_touch/screens/bb_user.dart'; import 'package:git_touch/screens/ge_user.dart'; import 'package:git_touch/screens/gl_search.dart'; +import 'package:git_touch/screens/go_search.dart'; +import 'package:git_touch/screens/go_user.dart'; import 'package:git_touch/screens/gt_orgs.dart'; import 'package:git_touch/screens/gt_user.dart'; import 'package:git_touch/screens/gl_explore.dart'; @@ -98,6 +100,13 @@ class _HomeState extends State { return GeUserScreen(auth.activeAccount.login, isViewer: true); } break; + case PlatformType.gogs: + switch (index) { + case 0: + return GoSearchScreen(); + case 1: + return GoUserScreen(auth.activeAccount.login, isViewer: true); + } } } @@ -206,6 +215,14 @@ class _HomeState extends State { icon: Icon(Icons.person), label: S.of(context).me), ]; break; + case PlatformType.gogs: + navigationItems = [ + BottomNavigationBarItem( + icon: Icon(Icons.search), label: S.of(context).search), + BottomNavigationBarItem( + icon: Icon(Icons.person), label: S.of(context).me), + ]; + break; } switch (theme.theme) { diff --git a/lib/main.dart b/lib/main.dart index 3ec7192f..910d728e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,7 +66,10 @@ void main() async { themeModel.router.define(GiteeRouter.prefix + screen.path, handler: Handler(handlerFunc: screen.handler)); }); - + GogsRouter.routes.forEach((screen) { + themeModel.router.define(GogsRouter.prefix + screen.path, + handler: Handler(handlerFunc: screen.handler)); + }); // To match status bar color to app bar color SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( statusBarColor: Colors.transparent, diff --git a/lib/models/auth.dart b/lib/models/auth.dart index 7ea89c0c..0a968605 100644 --- a/lib/models/auth.dart +++ b/lib/models/auth.dart @@ -20,6 +20,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../utils/utils.dart'; import 'account.dart'; import 'gitlab.dart'; +import 'gogs.dart'; const clientId = 'df930d7d2e219f26142a'; @@ -29,6 +30,7 @@ class PlatformType { static const bitbucket = 'bitbucket'; static const gitea = 'gitea'; static const gitee = 'gitee'; + static const gogs = 'gogs'; } class DataWithPage { @@ -320,6 +322,109 @@ class AuthModel with ChangeNotifier { ); } + Future loginToGogs(String domain, String token) async { + domain = domain.trim(); + token = token.trim(); + try { + loading = true; + notifyListeners(); + final res = await http.get('$domain/api/v1/user', + headers: {'Authorization': 'token $token'}); + final info = json.decode(res.body); + if (info['message'] != null) { + throw info['message']; + } + final user = GogsUser.fromJson(info); + + await _addAccount(Account( + platform: PlatformType.gogs, + domain: domain, + token: token, + login: user.username, + avatarUrl: user.avatarUrl, + )); + } finally { + loading = false; + notifyListeners(); + } + } + + // TODO: refactor + Future fetchGogs( + String p, { + requestType = 'GET', + Map body = const {}, + }) async { + http.Response res; + Map 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 fetchGogsWithPage(String path, + {int page, int limit}) async { + page = page ?? 1; + limit = limit ?? pageSize; + + var uri = Uri.parse('${activeAccount.domain}/api/v1$path'); + uri = uri.replace( + queryParameters: { + 'page': page.toString(), + 'limit': limit.toString(), + ...uri.queryParameters, + }, + ); + final res = await http.get(uri, headers: {'Authorization': 'token $token'}); + final info = json.decode(utf8.decode(res.bodyBytes)); + + return DataWithPage( + data: info, + cursor: page + 1, + hasMore: info is List && info.length > 0, + total: int.tryParse(res.headers['x-total-count'] ?? ''), + ); + } + Future fetchGitee( String p, { requestType = 'GET', diff --git a/lib/models/gogs.dart b/lib/models/gogs.dart new file mode 100644 index 00000000..732f74eb --- /dev/null +++ b/lib/models/gogs.dart @@ -0,0 +1,133 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'gogs.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsUser { + int id; + String username; + String fullName; + String avatarUrl; + String email; + GogsUser(); + factory GogsUser.fromJson(Map json) => + _$GogsUserFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsRepository { + int id; + String fullName; + bool private; + GogsUser owner; + String htmlUrl; + String description; + String defaultBranch; + DateTime createdAt; + DateTime updatedAt; + int starsCount; + int forksCount; + String website; + int watchersCount; + GogsRepository(); + factory GogsRepository.fromJson(Map json) => + _$GogsRepositoryFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsOrg { + int id; + String username; + String fullName; + String avatarUrl; + String description; + String location; + String website; + GogsOrg(); + factory GogsOrg.fromJson(Map json) => + _$GogsOrgFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsTree { + String type; + String name; + String path; + int size; + String downloadUrl; + GogsTree(); + factory GogsTree.fromJson(Map json) => + _$GogsTreeFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsBlob extends GogsTree { + String content; + GogsBlob(); + factory GogsBlob.fromJson(Map json) => + _$GogsBlobFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsBranch { + String name; + GogsBranch(); + factory GogsBranch.fromJson(Map json) => + _$GogsBranchFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsCommit { + GogsUser author; + GogsCommitDetail commit; + String sha; + String htmlUrl; + GogsCommit(); + factory GogsCommit.fromJson(Map json) => + _$GogsCommitFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsCommitDetail { + String message; + GogsCommitAuthor author; + GogsCommitAuthor committer; + GogsCommitDetail(); + factory GogsCommitDetail.fromJson(Map json) => + _$GogsCommitDetailFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsCommitAuthor { + String name; + String email; + DateTime date; + GogsCommitAuthor(); + factory GogsCommitAuthor.fromJson(Map json) => + _$GogsCommitAuthorFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsIssue { + int number; + String state; + String title; + String body; + GogsUser user; + List labels; + DateTime createdAt; + DateTime updatedAt; + int comments; + GogsIssue(); + factory GogsIssue.fromJson(Map json) => + _$GogsIssueFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class GogsLabel { + String name; + String color; + GogsLabel(); + factory GogsLabel.fromJson(Map json) => + _$GogsLabelFromJson(json); +} diff --git a/lib/models/gogs.g.dart b/lib/models/gogs.g.dart new file mode 100644 index 00000000..eafbf8f5 --- /dev/null +++ b/lib/models/gogs.g.dart @@ -0,0 +1,228 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'gogs.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GogsUser _$GogsUserFromJson(Map json) { + return GogsUser() + ..id = json['id'] as int + ..username = json['username'] as String + ..fullName = json['full_name'] as String + ..avatarUrl = json['avatar_url'] as String + ..email = json['email'] as String; +} + +Map _$GogsUserToJson(GogsUser instance) => { + 'id': instance.id, + 'username': instance.username, + 'full_name': instance.fullName, + 'avatar_url': instance.avatarUrl, + 'email': instance.email, + }; + +GogsRepository _$GogsRepositoryFromJson(Map json) { + return GogsRepository() + ..id = json['id'] as int + ..fullName = json['full_name'] as String + ..private = json['private'] as bool + ..owner = json['owner'] == null + ? null + : GogsUser.fromJson(json['owner'] as Map) + ..htmlUrl = json['html_url'] as String + ..description = json['description'] as String + ..defaultBranch = json['default_branch'] as String + ..createdAt = json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String) + ..updatedAt = json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String) + ..starsCount = json['stars_count'] as int + ..forksCount = json['forks_count'] as int + ..website = json['website'] as String + ..watchersCount = json['watchers_count'] as int; +} + +Map _$GogsRepositoryToJson(GogsRepository instance) => + { + 'id': instance.id, + 'full_name': instance.fullName, + 'private': instance.private, + 'owner': instance.owner, + 'html_url': instance.htmlUrl, + 'description': instance.description, + 'default_branch': instance.defaultBranch, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'stars_count': instance.starsCount, + 'forks_count': instance.forksCount, + 'website': instance.website, + 'watchers_count': instance.watchersCount, + }; + +GogsOrg _$GogsOrgFromJson(Map json) { + return GogsOrg() + ..id = json['id'] as int + ..username = json['username'] as String + ..fullName = json['full_name'] as String + ..avatarUrl = json['avatar_url'] as String + ..description = json['description'] as String + ..location = json['location'] as String + ..website = json['website'] as String; +} + +Map _$GogsOrgToJson(GogsOrg instance) => { + 'id': instance.id, + 'username': instance.username, + 'full_name': instance.fullName, + 'avatar_url': instance.avatarUrl, + 'description': instance.description, + 'location': instance.location, + 'website': instance.website, + }; + +GogsTree _$GogsTreeFromJson(Map json) { + return GogsTree() + ..type = json['type'] as String + ..name = json['name'] as String + ..path = json['path'] as String + ..size = json['size'] as int + ..downloadUrl = json['download_url'] as String; +} + +Map _$GogsTreeToJson(GogsTree instance) => { + 'type': instance.type, + 'name': instance.name, + 'path': instance.path, + 'size': instance.size, + 'download_url': instance.downloadUrl, + }; + +GogsBlob _$GogsBlobFromJson(Map json) { + return GogsBlob() + ..type = json['type'] as String + ..name = json['name'] as String + ..path = json['path'] as String + ..size = json['size'] as int + ..downloadUrl = json['download_url'] as String + ..content = json['content'] as String; +} + +Map _$GogsBlobToJson(GogsBlob instance) => { + 'type': instance.type, + 'name': instance.name, + 'path': instance.path, + 'size': instance.size, + 'download_url': instance.downloadUrl, + 'content': instance.content, + }; + +GogsBranch _$GogsBranchFromJson(Map json) { + return GogsBranch()..name = json['name'] as String; +} + +Map _$GogsBranchToJson(GogsBranch instance) => + { + 'name': instance.name, + }; + +GogsCommit _$GogsCommitFromJson(Map json) { + return GogsCommit() + ..author = json['author'] == null + ? null + : GogsUser.fromJson(json['author'] as Map) + ..commit = json['commit'] == null + ? null + : GogsCommitDetail.fromJson(json['commit'] as Map) + ..sha = json['sha'] as String + ..htmlUrl = json['html_url'] as String; +} + +Map _$GogsCommitToJson(GogsCommit instance) => + { + 'author': instance.author, + 'commit': instance.commit, + 'sha': instance.sha, + 'html_url': instance.htmlUrl, + }; + +GogsCommitDetail _$GogsCommitDetailFromJson(Map json) { + return GogsCommitDetail() + ..message = json['message'] as String + ..author = json['author'] == null + ? null + : GogsCommitAuthor.fromJson(json['author'] as Map) + ..committer = json['committer'] == null + ? null + : GogsCommitAuthor.fromJson(json['committer'] as Map); +} + +Map _$GogsCommitDetailToJson(GogsCommitDetail instance) => + { + 'message': instance.message, + 'author': instance.author, + 'committer': instance.committer, + }; + +GogsCommitAuthor _$GogsCommitAuthorFromJson(Map json) { + return GogsCommitAuthor() + ..name = json['name'] as String + ..email = json['email'] as String + ..date = + json['date'] == null ? null : DateTime.parse(json['date'] as String); +} + +Map _$GogsCommitAuthorToJson(GogsCommitAuthor instance) => + { + 'name': instance.name, + 'email': instance.email, + 'date': instance.date?.toIso8601String(), + }; + +GogsIssue _$GogsIssueFromJson(Map json) { + return GogsIssue() + ..number = json['number'] as int + ..state = json['state'] as String + ..title = json['title'] as String + ..body = json['body'] as String + ..user = json['user'] == null + ? null + : GogsUser.fromJson(json['user'] as Map) + ..labels = (json['labels'] as List) + ?.map((e) => + e == null ? null : GogsLabel.fromJson(e as Map)) + ?.toList() + ..createdAt = json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String) + ..updatedAt = json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String) + ..comments = json['comments'] as int; +} + +Map _$GogsIssueToJson(GogsIssue instance) => { + 'number': instance.number, + 'state': instance.state, + 'title': instance.title, + 'body': instance.body, + 'user': instance.user, + 'labels': instance.labels, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'comments': instance.comments, + }; + +GogsLabel _$GogsLabelFromJson(Map json) { + return GogsLabel() + ..name = json['name'] as String + ..color = json['color'] as String; +} + +Map _$GogsLabelToJson(GogsLabel instance) => { + 'name': instance.name, + 'color': instance.color, + }; diff --git a/lib/router.dart b/lib/router.dart index 0d43b8ca..7b4e58a8 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -35,6 +35,14 @@ import 'package:git_touch/screens/gh_org_repos.dart'; import 'package:git_touch/screens/gl_commit.dart'; import 'package:git_touch/screens/gl_issue_form.dart'; import 'package:git_touch/screens/gl_starrers.dart'; +import 'package:git_touch/screens/go_commits.dart'; +import 'package:git_touch/screens/go_issues.dart'; +import 'package:git_touch/screens/go_object.dart'; +import 'package:git_touch/screens/go_orgs.dart'; +import 'package:git_touch/screens/go_repo.dart'; +import 'package:git_touch/screens/go_repos.dart'; +import 'package:git_touch/screens/go_user.dart'; +import 'package:git_touch/screens/go_users.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'; @@ -652,3 +660,63 @@ class GiteeRouter { parameters['owner'].first, parameters['name'].first), ); } + +class GogsRouter { + static const prefix = '/gogs'; + static final routes = [ + GogsRouter.user, + GogsRouter.repo, + GogsRouter.object, + GogsRouter.commits, + GogsRouter.issues, + ]; + static final user = RouterScreen('/:login', (context, parameters) { + final login = parameters['login'].first; + final tab = parameters['tab']?.first; + final isViewer = parameters['isViewer']?.first; + switch (tab) { + case 'followers': + return GoUsersScreen.followers(login); + case 'following': + return GoUsersScreen.following(login); + case 'repositories': + return GoReposScreen(login, + isViewer: isViewer == 'false' ? false : true); + case 'organizations': + return GoOrgsScreen.ofUser(login, + isViewer: isViewer == 'false' ? false : true); // handle better? + default: + return GoUserScreen(parameters['login'].first); + } + }); + static final repo = RouterScreen( + '/:owner/:name', + (context, parameters) { + if (parameters['branch'] == null) { + return GoRepoScreen( + parameters['owner'].first, parameters['name'].first); + } else { + return GoRepoScreen(parameters['owner'].first, parameters['name'].first, + branch: parameters['branch'].first); + } + }, + ); + static final object = RouterScreen( + '/:owner/:name/blob', + (context, parameters) => GoObjectScreen( + parameters['owner'].first, + parameters['name'].first, + path: parameters['path']?.first, + ref: parameters['ref']?.first, + ), + ); + static final commits = RouterScreen( + '/:owner/:name/commits', + (context, parameters) => GoCommitsScreen( + parameters['owner'].first, parameters['name'].first, + branch: parameters['ref']?.first)); + static final issues = RouterScreen( + '/:owner/:name/issues', + (context, parameters) => + GoIssuesScreen(parameters['owner'].first, parameters['name'].first)); +} diff --git a/lib/screens/go_commits.dart b/lib/screens/go_commits.dart new file mode 100644 index 00000000..f71ec461 --- /dev/null +++ b/lib/screens/go_commits.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gogs.dart'; +import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/commit_item.dart'; +import 'package:provider/provider.dart'; +import '../generated/l10n.dart'; + +class GoCommitsScreen extends StatelessWidget { + final String owner; + final String name; + final String branch; + GoCommitsScreen(this.owner, this.name, {this.branch = 'master'}); + + // TODO: API only returns most recent commit. No provision for all commits. + @override + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: AppBarTitle(S.of(context).commits), + fetch: (page) async { + final res = await context.read().fetchGogsWithPage( + '/repos/$owner/$name/commits/$branch', + page: page); + return ListPayload( + cursor: res.cursor, + hasMore: res.hasMore, + items: [GogsCommit.fromJson(res.data)], + ); + }, + itemBuilder: (c) { + return CommitItem( + author: c.author?.username ?? c.commit.author.name, + avatarUrl: c.author.avatarUrl, + avatarLink: '/gogs/${c.author.username}', + createdAt: c.commit.author.date, + message: c.commit.message, + url: c.htmlUrl, + ); + }, + ); + } +} diff --git a/lib/screens/go_issues.dart b/lib/screens/go_issues.dart new file mode 100644 index 00000000..3f1f3733 --- /dev/null +++ b/lib/screens/go_issues.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/generated/l10n.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gogs.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 GoIssuesScreen extends StatelessWidget { + final String owner; + final String name; + final bool isPr; + GoIssuesScreen(this.owner, this.name, {this.isPr = false}); + + @override + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: + AppBarTitle(isPr ? S.of(context).pullRequests : S.of(context).issues), + fetch: (page) async { + final type = isPr ? 'pulls' : 'issues'; + final res = await context.read().fetchGogsWithPage( + '/repos/$owner/$name/issues?state=open&type=$type', + page: page); + return ListPayload( + cursor: res.cursor, + hasMore: res.hasMore, + items: (res.data as List).map((v) => GogsIssue.fromJson(v)).toList(), + ); + }, + actionBuilder: () => ActionEntry( + iconData: isPr ? null : Octicons.plus, + url: isPr + ? '/gogs/$owner/$name/pulls/new' // TODO + : '/gogs/$owner/$name/issues/new', + ), + itemBuilder: (p) => IssueItem( + author: p.user.username, + avatarUrl: p.user.avatarUrl, + commentCount: p.comments, + subtitle: '#' + p.number.toString(), + title: p.title, + updatedAt: p.updatedAt, + url: isPr + ? 'https://gogs.io' // TODO: PR endpoints are not supported in Gogs, htmlUrl prop non-existent + : '/gogs/$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) + ]), + ), + ); + } +} diff --git a/lib/screens/go_object.dart b/lib/screens/go_object.dart new file mode 100644 index 00000000..f9774d4c --- /dev/null +++ b/lib/screens/go_object.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/generated/l10n.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gogs.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/app_bar_title.dart'; +import 'package:git_touch/widgets/blob_view.dart'; +import 'package:git_touch/widgets/object_tree.dart'; +import 'package:provider/provider.dart'; + +class GoObjectScreen extends StatelessWidget { + final String owner; + final String name; + final String path; + final String ref; + GoObjectScreen(this.owner, this.name, {this.path, this.ref}); + + @override + Widget build(BuildContext context) { + return RefreshStatefulScaffold( + title: AppBarTitle(path ?? S.of(context).files), + fetch: () async { + final suffix = path == null ? '' : '/$path'; + final res = await context + .read() + .fetchGogs('/repos/$owner/$name/contents$suffix?ref=$ref'); + return res; + }, + actionBuilder: (p, _) { + if (p is List) { + return null; + } else { + return ActionEntry( + iconData: Icons.settings, + url: '/choose-code-theme', + ); + } + }, + bodyBuilder: (p, _) { + if (p is List) { + final items = p.map((t) => GogsTree.fromJson(t)).toList(); + items.sort((a, b) { + return sortByKey('dir', a.type, b.type); + }); + return ObjectTree(items: [ + for (var v in items) + ObjectTreeItem( + name: v.name, + type: v.type, + size: v.type == 'file' ? v.size : null, + url: + '/gogs/$owner/$name/blob?path=${v.path.urlencode}&ref=$ref', + downloadUrl: v.downloadUrl, + ), + ]); + } else { + final v = GogsBlob.fromJson(p); + return BlobView(v.name, + base64Text: v.content == null ? '' : v.content); + } + }, + ); + } +} diff --git a/lib/screens/go_orgs.dart b/lib/screens/go_orgs.dart new file mode 100644 index 00000000..b64c2c3f --- /dev/null +++ b/lib/screens/go_orgs.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gogs.dart'; +import 'package:git_touch/scaffolds/refresh_stateful.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/user_item.dart'; +import 'package:provider/provider.dart'; +import '../generated/l10n.dart'; + +class GoOrgsScreen extends StatelessWidget { + final String api; + final bool isViewer; + // TODO: implement list of orgs screen when API is available + GoOrgsScreen.ofUser(String login, {this.isViewer}) + : api = isViewer ? '/users/$login/orgs' : '/user/orgs'; + + @override + Widget build(BuildContext context) { + return RefreshStatefulScaffold>( + title: AppBarTitle(S.of(context).organizations), + fetch: () async { + final res = await context.read().fetchGogs(api); + return [for (var v in res) GogsOrg.fromJson(v)]; + }, + bodyBuilder: (p, _) { + return Column( + children: [ + for (var org in p) ...[ + UserItem.gogs( + avatarUrl: org.avatarUrl, + login: org.username, + name: org.fullName, + bio: Text(org.description ?? org.website ?? org.location), + ), + CommonStyle.border, + ] + ], + ); + }, + ); + } +} diff --git a/lib/screens/go_repo.dart b/lib/screens/go_repo.dart new file mode 100644 index 00000000..1c85bad5 --- /dev/null +++ b/lib/screens/go_repo.dart @@ -0,0 +1,152 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gogs.dart'; +import 'package:git_touch/models/theme.dart'; +import 'package:git_touch/scaffolds/refresh_stateful.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/entry_item.dart'; +import 'package:git_touch/widgets/markdown_view.dart'; +import 'package:git_touch/widgets/repo_header.dart'; +import 'package:git_touch/widgets/table_view.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; +import 'package:http/http.dart' as http; +import '../generated/l10n.dart'; + +class GoRepoScreen extends StatelessWidget { + final String owner; + final String name; + final String branch; + GoRepoScreen(this.owner, this.name, {this.branch}); + + @override + Widget build(BuildContext context) { + return RefreshStatefulScaffold< + Tuple3>>( + title: AppBarTitle(S.of(context).repository), + fetch: () async { + final auth = context.read(); + final repo = await auth.fetchGogs('/repos/$owner/$name').then((v) { + return GogsRepository.fromJson(v); + }); + + final md = () => + auth.fetchGogs('/repos/$owner/$name/contents/README.md').then((v) { + return (v['content'] as String)?.base64ToUtf8; + }); + final html = () => md().then((v) async { + final res = await http.post( + '${auth.activeAccount.domain}/api/v1/markdown/raw', + headers: {'Authorization': 'token ${auth.token}'}, + body: v, + ); + return utf8.decode(res.bodyBytes).normalizedHtml; + }); + final readmeData = MarkdownViewData(context, md: md, html: html); + final branches = + await auth.fetchGogs('/repos/$owner/$name/branches').then((v) { + if (!(v is List)) + return null; // Valid API Response only returned if repo contains >= 2 branches + return [for (var branch in v) GogsBranch.fromJson(branch)]; + }); + + return Tuple3(repo, readmeData, branches); + }, + bodyBuilder: (t, setState) { + final p = t.item1; + final branches = t.item3; + final theme = context.read(); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RepoHeader( + avatarUrl: p.owner.avatarUrl, + avatarLink: '/gogs/${p.owner.username}', + owner: p.owner.username, + name: p.fullName.split('/')[1], + description: p.description, + homepageUrl: p.website, + ), + CommonStyle.border, + Row( + children: [ + // TODO: when API is available + EntryItem( + count: p.watchersCount, + text: 'Watchers', + ), + EntryItem( + count: p.starsCount, + text: 'Stars', + ), + EntryItem( + count: p.forksCount, + text: 'Forks', + ), + ], + ), + CommonStyle.border, + TableView( + hasIcon: true, + items: [ + TableViewItem( + leftIconData: Octicons.code, + text: Text('Code'), + url: + '/gogs/$owner/$name/blob?ref=${branch == null ? 'master' : branch}', + ), + TableViewItem( + leftIconData: Octicons.issue_opened, + text: Text('Issues'), + url: '/gogs/$owner/$name/issues', + ), + TableViewItem( + leftIconData: Octicons.git_pull_request, + text: Text( + 'Pull requests'), // TODO: when API endpoint is available + ), + TableViewItem( + leftIconData: Octicons.history, + text: Text('Commits'), + url: + '/gogs/$owner/$name/commits?ref=${branch == null ? 'master' : branch}', + ), + TableViewItem( + leftIconData: Octicons.git_branch, + text: Text(S.of(context).branches), + rightWidget: Text((branch == null ? 'master' : branch) + + ' • ' + + '${branches == null ? '1' : branches.length.toString()}'), + onTap: () async { + if (branches == null) return; + + await theme.showPicker( + context, + PickerGroupItem( + value: branch, + items: branches + .map((b) => PickerItem(b.name, text: b.name)) + .toList(), + onClose: (ref) { + if (ref != branch) { + theme.push( + context, '/gogs/$owner/$name?branch=$ref', + replace: true); + } + }, + ), + ); + }, + ), + ], + ), + CommonStyle.verticalGap, + MarkdownView(t.item2), + ], + ); + }, + ); + } +} diff --git a/lib/screens/go_repos.dart b/lib/screens/go_repos.dart new file mode 100644 index 00000000..61a8f094 --- /dev/null +++ b/lib/screens/go_repos.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/gogs.dart'; +import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/widgets/repository_item.dart'; + +class GoReposScreen extends StatelessWidget { + final String api; + final String title; + final bool isViewer; + + GoReposScreen(String owner, {this.isViewer = false}) + : api = isViewer ? '/users/$owner/repos' : '/user/repos', + title = 'Repositories'; + GoReposScreen.org(String owner) + : api = '/orgs/$owner/repos', + title = 'Repositories', + isViewer = false; + + @override + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: AppBarTitle(title), + fetch: (page) async { + final res = + await context.read().fetchGogsWithPage(api, page: page); + return ListPayload( + cursor: res.cursor, + hasMore: res.hasMore, + items: [for (var v in res.data) GogsRepository.fromJson(v)], + ); + }, + itemBuilder: (v) { + return RepositoryItem.go( + payload: v, + name: v.fullName.split('/')[1], + owner: v.owner.username, + ); + }, + ); + } +} diff --git a/lib/screens/go_search.dart b/lib/screens/go_search.dart new file mode 100644 index 00000000..6dcc0128 --- /dev/null +++ b/lib/screens/go_search.dart @@ -0,0 +1,9 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class GoSearchScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center(child: Text('Coming Soon...')); + } +} diff --git a/lib/screens/go_user.dart b/lib/screens/go_user.dart new file mode 100644 index 00000000..8d99f4a0 --- /dev/null +++ b/lib/screens/go_user.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:git_touch/models/gogs.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/entry_item.dart'; +import 'package:git_touch/widgets/repository_item.dart'; +import 'package:git_touch/widgets/table_view.dart'; +import 'package:git_touch/widgets/user_header.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; + +class GoUserScreen extends StatelessWidget { + final String login; + final bool isViewer; + GoUserScreen(this.login, {this.isViewer = false}); + + @override + Widget build(BuildContext context) { + return RefreshStatefulScaffold>>( + title: Text(isViewer ? 'Me' : login), + fetch: () async { + final auth = context.read(); + final res = await Future.wait([ + auth.fetchGogs(isViewer ? '/user' : '/users/$login'), + auth.fetchGogsWithPage( + isViewer ? '/user/repos' : '/users/$login/repos', + limit: 6), + ]); + + return Tuple2(GogsUser.fromJson(res[0]), + [for (var repo in res[1].data) GogsRepository.fromJson(repo)]); + }, + action: isViewer + ? ActionEntry( + iconData: Icons.settings, + url: '/settings', + ) + : null, + bodyBuilder: (p, _) { + final user = p.item1; + final repos = p.item2; + if (p.item1 != null) { + return Column( + children: [ + UserHeader( + login: user.username, + avatarUrl: user.avatarUrl, + name: user.fullName, + createdAt: + null, // TODO: API response does not have this attribute + isViewer: isViewer, + bio: null, // TODO: API response does not have this attribute + ), + CommonStyle.border, + Row(children: [ + EntryItem( + text: 'Repositories', + url: '/gogs/$login?tab=repositories&isViewer=$isViewer', + ), + EntryItem( + text: 'Followers', + url: '/gogs/$login?tab=followers', + ), + EntryItem( + text: 'Following', + url: '/gogs/$login?tab=following', + ), + ]), + CommonStyle.border, + TableView( + hasIcon: true, + items: [ + TableViewItem( + leftIconData: Octicons.home, + text: Text('Organizations'), + url: + '/gogs/${user.username}?tab=organizations&isViewer=$isViewer', + ), + ], + ), + CommonStyle.border, + Column( + children: [ + for (var v in repos) ...[ + RepositoryItem.go( + payload: v, + name: v.fullName.split('/')[1], + owner: v.owner.username, + ), + CommonStyle.border, + ] + ], + ), + ], + ); + } else { + return Text('404'); // TODO: + } + }, + ); + } +} diff --git a/lib/screens/go_users.dart b/lib/screens/go_users.dart new file mode 100644 index 00000000..ac5ce9f3 --- /dev/null +++ b/lib/screens/go_users.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/gogs.dart'; +import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/user_item.dart'; +import 'package:git_touch/models/auth.dart'; +import 'package:provider/provider.dart'; + +class GoUsersScreen extends StatelessWidget { + final String api; + final String title; + + GoUsersScreen.followers(String login) + : api = '/users/$login/followers', + title = 'Followers'; + GoUsersScreen.following(String login) + : api = '/users/$login/following', + title = "Following"; + + @override + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: AppBarTitle(title), + fetch: (page) async { + final res = + await context.read().fetchGogsWithPage(api, page: page); + return ListPayload( + cursor: res.cursor, + hasMore: res.hasMore, + items: [for (var v in res.data) GogsUser.fromJson(v)], + ); + }, + itemBuilder: (payload) { + return UserItem.gogs( + login: payload.username, + name: payload.fullName, + avatarUrl: payload.avatarUrl, + bio: null, + ); + }, + ); + } +} diff --git a/lib/screens/login.dart b/lib/screens/login.dart index 9e2c896e..8d3ed932 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -348,6 +348,26 @@ class _LoginScreenState extends State { } }, ), + _buildAddItem( + text: 'Gogs Account', + brand: Octicons.git_branch, // TODO: brand icon + onTap: () async { + _domainController.text = 'https://gogs.com'; + final result = await theme.showConfirm( + context, + _buildPopup(context, showDomain: true), + ); + if (result == true) { + try { + await auth.loginToGogs( + _domainController.text, _tokenController.text); + _tokenController.clear(); + } catch (err) { + showError(err); + } + } + }, + ), Container( padding: CommonStyle.padding, child: Text( diff --git a/lib/widgets/repository_item.dart b/lib/widgets/repository_item.dart index 625c0c8e..96b11e50 100644 --- a/lib/widgets/repository_item.dart +++ b/lib/widgets/repository_item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:git_touch/models/bitbucket.dart'; import 'package:git_touch/models/gitlab.dart'; +import 'package:git_touch/models/gogs.dart'; import 'package:git_touch/models/theme.dart'; import 'package:git_touch/utils/utils.dart'; import 'package:git_touch/widgets/avatar.dart'; @@ -39,6 +40,21 @@ class RepositoryItem extends StatelessWidget { @required this.avatarLink, }); + RepositoryItem.go({ + @required GogsRepository payload, + this.primaryLanguageName, + this.primaryLanguageColor, + this.note, + this.owner, + this.name, + }) : url = '/gogs/${payload.fullName}', + avatarUrl = payload.owner.avatarUrl, + avatarLink = '/gogs/${payload.fullName}', + description = payload.description, + forkCount = payload.forksCount, + starCount = payload.starsCount, + iconData = payload.private ? Octicons.lock : null; + RepositoryItem.bb({ @required BbRepo payload, this.primaryLanguageName, diff --git a/lib/widgets/user_item.dart b/lib/widgets/user_item.dart index 09e93824..5abffbf8 100644 --- a/lib/widgets/user_item.dart +++ b/lib/widgets/user_item.dart @@ -63,6 +63,13 @@ class UserItem extends StatelessWidget { @required this.bio, }) : url = '/bitbucket/$login?team=1'; + UserItem.gogs({ + @required this.login, + @required this.name, + @required this.avatarUrl, + @required this.bio, + }) : url = '/gogs/$login'; + @override Widget build(BuildContext context) { final theme = Provider.of(context);