feat(gogs): setup (#170)

closes #117
This commit is contained in:
Shreyas Thirumalai 2021-01-23 19:38:05 +05:30 committed by GitHub
parent cb9bc89778
commit 4f1e0441d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1165 additions and 1 deletions

View File

@ -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<Home> {
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<Home> {
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) {

View File

@ -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,

View File

@ -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<T> {
@ -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<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> 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',

133
lib/models/gogs.dart Normal file
View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) =>
_$GogsOrgFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsTree {
String type;
String name;
String path;
int size;
String downloadUrl;
GogsTree();
factory GogsTree.fromJson(Map<String, dynamic> json) =>
_$GogsTreeFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsBlob extends GogsTree {
String content;
GogsBlob();
factory GogsBlob.fromJson(Map<String, dynamic> json) =>
_$GogsBlobFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsBranch {
String name;
GogsBranch();
factory GogsBranch.fromJson(Map<String, dynamic> json) =>
_$GogsBranchFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsCommit {
GogsUser author;
GogsCommitDetail commit;
String sha;
String htmlUrl;
GogsCommit();
factory GogsCommit.fromJson(Map<String, dynamic> json) =>
_$GogsCommitFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsCommitDetail {
String message;
GogsCommitAuthor author;
GogsCommitAuthor committer;
GogsCommitDetail();
factory GogsCommitDetail.fromJson(Map<String, dynamic> json) =>
_$GogsCommitDetailFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsCommitAuthor {
String name;
String email;
DateTime date;
GogsCommitAuthor();
factory GogsCommitAuthor.fromJson(Map<String, dynamic> json) =>
_$GogsCommitAuthorFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsIssue {
int number;
String state;
String title;
String body;
GogsUser user;
List<GogsLabel> labels;
DateTime createdAt;
DateTime updatedAt;
int comments;
GogsIssue();
factory GogsIssue.fromJson(Map<String, dynamic> json) =>
_$GogsIssueFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class GogsLabel {
String name;
String color;
GogsLabel();
factory GogsLabel.fromJson(Map<String, dynamic> json) =>
_$GogsLabelFromJson(json);
}

228
lib/models/gogs.g.dart Normal file
View File

@ -0,0 +1,228 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'gogs.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GogsUser _$GogsUserFromJson(Map<String, dynamic> 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<String, dynamic> _$GogsUserToJson(GogsUser instance) => <String, dynamic>{
'id': instance.id,
'username': instance.username,
'full_name': instance.fullName,
'avatar_url': instance.avatarUrl,
'email': instance.email,
};
GogsRepository _$GogsRepositoryFromJson(Map<String, dynamic> 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<String, dynamic>)
..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<String, dynamic> _$GogsRepositoryToJson(GogsRepository instance) =>
<String, dynamic>{
'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<String, dynamic> 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<String, dynamic> _$GogsOrgToJson(GogsOrg instance) => <String, dynamic>{
'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<String, dynamic> 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<String, dynamic> _$GogsTreeToJson(GogsTree instance) => <String, dynamic>{
'type': instance.type,
'name': instance.name,
'path': instance.path,
'size': instance.size,
'download_url': instance.downloadUrl,
};
GogsBlob _$GogsBlobFromJson(Map<String, dynamic> 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<String, dynamic> _$GogsBlobToJson(GogsBlob instance) => <String, dynamic>{
'type': instance.type,
'name': instance.name,
'path': instance.path,
'size': instance.size,
'download_url': instance.downloadUrl,
'content': instance.content,
};
GogsBranch _$GogsBranchFromJson(Map<String, dynamic> json) {
return GogsBranch()..name = json['name'] as String;
}
Map<String, dynamic> _$GogsBranchToJson(GogsBranch instance) =>
<String, dynamic>{
'name': instance.name,
};
GogsCommit _$GogsCommitFromJson(Map<String, dynamic> json) {
return GogsCommit()
..author = json['author'] == null
? null
: GogsUser.fromJson(json['author'] as Map<String, dynamic>)
..commit = json['commit'] == null
? null
: GogsCommitDetail.fromJson(json['commit'] as Map<String, dynamic>)
..sha = json['sha'] as String
..htmlUrl = json['html_url'] as String;
}
Map<String, dynamic> _$GogsCommitToJson(GogsCommit instance) =>
<String, dynamic>{
'author': instance.author,
'commit': instance.commit,
'sha': instance.sha,
'html_url': instance.htmlUrl,
};
GogsCommitDetail _$GogsCommitDetailFromJson(Map<String, dynamic> json) {
return GogsCommitDetail()
..message = json['message'] as String
..author = json['author'] == null
? null
: GogsCommitAuthor.fromJson(json['author'] as Map<String, dynamic>)
..committer = json['committer'] == null
? null
: GogsCommitAuthor.fromJson(json['committer'] as Map<String, dynamic>);
}
Map<String, dynamic> _$GogsCommitDetailToJson(GogsCommitDetail instance) =>
<String, dynamic>{
'message': instance.message,
'author': instance.author,
'committer': instance.committer,
};
GogsCommitAuthor _$GogsCommitAuthorFromJson(Map<String, dynamic> 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<String, dynamic> _$GogsCommitAuthorToJson(GogsCommitAuthor instance) =>
<String, dynamic>{
'name': instance.name,
'email': instance.email,
'date': instance.date?.toIso8601String(),
};
GogsIssue _$GogsIssueFromJson(Map<String, dynamic> 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<String, dynamic>)
..labels = (json['labels'] as List)
?.map((e) =>
e == null ? null : GogsLabel.fromJson(e as Map<String, dynamic>))
?.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<String, dynamic> _$GogsIssueToJson(GogsIssue instance) => <String, dynamic>{
'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<String, dynamic> json) {
return GogsLabel()
..name = json['name'] as String
..color = json['color'] as String;
}
Map<String, dynamic> _$GogsLabelToJson(GogsLabel instance) => <String, dynamic>{
'name': instance.name,
'color': instance.color,
};

View File

@ -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));
}

View File

@ -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<GogsCommit, int>(
title: AppBarTitle(S.of(context).commits),
fetch: (page) async {
final res = await context.read<AuthModel>().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,
);
},
);
}
}

View File

@ -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<GogsIssue, int>(
title:
AppBarTitle(isPr ? S.of(context).pullRequests : S.of(context).issues),
fetch: (page) async {
final type = isPr ? 'pulls' : 'issues';
final res = await context.read<AuthModel>().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)
]),
),
);
}
}

View File

@ -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<AuthModel>()
.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);
}
},
);
}
}

43
lib/screens/go_orgs.dart Normal file
View File

@ -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<List<GogsOrg>>(
title: AppBarTitle(S.of(context).organizations),
fetch: () async {
final res = await context.read<AuthModel>().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,
]
],
);
},
);
}
}

152
lib/screens/go_repo.dart Normal file
View File

@ -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<GogsRepository, MarkdownViewData, List<GogsBranch>>>(
title: AppBarTitle(S.of(context).repository),
fetch: () async {
final auth = context.read<AuthModel>();
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<ThemeModel>();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
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: <Widget>[
// 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),
],
);
},
);
}
}

44
lib/screens/go_repos.dart Normal file
View File

@ -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<GogsRepository, int>(
title: AppBarTitle(title),
fetch: (page) async {
final res =
await context.read<AuthModel>().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,
);
},
);
}
}

View File

@ -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...'));
}
}

105
lib/screens/go_user.dart Normal file
View File

@ -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<Tuple2<GogsUser, List<GogsRepository>>>(
title: Text(isViewer ? 'Me' : login),
fetch: () async {
final auth = context.read<AuthModel>();
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: <Widget>[
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: <Widget>[
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:
}
},
);
}
}

43
lib/screens/go_users.dart Normal file
View File

@ -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<GogsUser, int>(
title: AppBarTitle(title),
fetch: (page) async {
final res =
await context.read<AuthModel>().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,
);
},
);
}
}

View File

@ -348,6 +348,26 @@ class _LoginScreenState extends State<LoginScreen> {
}
},
),
_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(

View File

@ -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,

View File

@ -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<ThemeModel>(context);