diff --git a/README.md b/README.md
index ddb71f4c..ec6cc230 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,9 @@
GitHub,
GitLab,
Bitbucket,
- Gitea and
- Gitee(η δΊ), built with Flutter
+ Gitea,
+ Gitee(η δΊ) and
+ Rhodecode, built with Flutter
@@ -44,6 +45,7 @@ https://github.com/git-touch/git-touch/issues/29
| Gogs | https://try.gogs.io/ | [Gogs API](https://github.com/gogs/docs-api) | π§ | β
|
| Gitea | https://gitea.com/ | [Gitea API](https://try.gitea.io/api/swagger#/) | β
| β
|
| Gitee | https://gitee.com/ | [Gitee API](https://gitee.com/api/v5/swagger) | β
| π¬ |
+| Rhodecode | https://rhodecode.com | [Rhodecode API](https://docs.rhodecode.com/5.x/rce/api/api.html) | π§ | β
|
## Contributing
diff --git a/lib/home.dart b/lib/home.dart
index 7f831a45..bfde8da2 100644
--- a/lib/home.dart
+++ b/lib/home.dart
@@ -24,6 +24,7 @@ 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/login.dart';
+import 'package:git_touch/screens/rh_explore.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:github/github.dart';
import 'package:launch_review/launch_review.dart';
@@ -32,6 +33,8 @@ import 'package:provider/provider.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:universal_io/io.dart';
+import 'screens/rh_user.dart';
+
class Home extends StatefulWidget {
@override
State createState() => _HomeState();
@@ -140,6 +143,14 @@ class _HomeState extends State {
case 1:
return GoUserScreen(auth.activeAccount!.login, isViewer: true);
}
+ break;
+ case PlatformType.rhodecode:
+ switch (index) {
+ case 0:
+ return RhExploreScreen();
+ case 1:
+ return RhUserScreen(null);
+ }
}
}
@@ -232,6 +243,8 @@ class _HomeState extends State {
case PlatformType.gitee:
case PlatformType.gogs:
return [search, me];
+ case PlatformType.rhodecode:
+ return [explore, me];
default:
return [];
}
diff --git a/lib/models/auth.dart b/lib/models/auth.dart
index 4745f533..117ed124 100644
--- a/lib/models/auth.dart
+++ b/lib/models/auth.dart
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'package:ferry/ferry.dart';
import 'package:fimber/fimber.dart';
@@ -11,6 +12,7 @@ import 'package:git_touch/models/gitea.dart';
import 'package:git_touch/models/gitee.dart';
import 'package:git_touch/models/gitlab.dart';
import 'package:git_touch/models/gogs.dart';
+import 'package:git_touch/models/rhodecode.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:github/github.dart';
import 'package:gql_http_link/gql_http_link.dart';
@@ -18,7 +20,6 @@ import 'package:http/http.dart' as http;
import 'package:nanoid/nanoid.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uni_links/uni_links.dart';
-// import 'package:in_app_review/in_app_review.dart';
import 'package:universal_io/io.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -31,6 +32,7 @@ class PlatformType {
static const gitea = 'gitea';
static const gitee = 'gitee';
static const gogs = 'gogs';
+ static const rhodecode = 'rhodecode';
}
class DataWithPage {
@@ -334,6 +336,71 @@ class AuthModel with ChangeNotifier {
}
}
+ Future loginToRhodecode(String domain, String token) async {
+ domain = domain.trim();
+ token = token.trim();
+ final bodys =
+ '{"id":1,"auth_token":"$token","method":"get_user", "args":"{}"}';
+ try {
+ loading = true;
+ notifyListeners();
+ final res = await http.post(Uri.parse('$domain/_admin/api'),
+ headers: {'content-type': 'text/plain'}, body: bodys);
+ final info = json.decode(res.body);
+ if (info['error'] != null) {
+ throw info['error'];
+ }
+ final userResponse = GetUserResponse.fromJson(info);
+
+ await _addAccount(Account(
+ platform: PlatformType.rhodecode,
+ domain: domain,
+ token: token,
+ login: userResponse.result!.username!,
+ avatarUrl: 'TODO', // user.avatarUrl!,
+ ));
+ } finally {
+ loading = false;
+ notifyListeners();
+ }
+ }
+
+// TODO there is no real pagination
+ Future fetchRhodecodeWithPage(
+ String p, {
+ requestType = 'POST',
+ Map body = const {},
+ }) async {
+ final info = await fetchRhodecode(p, requestType: requestType, body: body);
+ var next = null;
+ return DataWithPage(
+ data: info,
+ cursor: next ?? 1,
+ hasMore: next != null,
+ total: info['result'].length ?? kTotalCountFallback,
+ );
+ }
+
+ Future fetchRhodecode(
+ String p, {
+ requestType = 'POST',
+ Map body = const {},
+ }) async {
+ late http.Response res;
+ final encoded = json.encode(body);
+ final fullBody =
+ '{"id":1,"auth_token":"${activeAccount!.token}","method":"$p", "args":$encoded}';
+ res = await http.post(Uri.parse('${activeAccount!.domain}/_admin/api'),
+ headers: {'content-type': 'text/plain'}, body: fullBody);
+ final info = json.decode(res.body);
+ if (info['error'] != null) {
+ // throw Exception(info['error']);
+ throw Error();
+ }
+
+ return info;
+ }
+
// TODO: refactor
Future fetchGogs(
String p, {
diff --git a/lib/models/rhodecode.dart b/lib/models/rhodecode.dart
new file mode 100644
index 00000000..c19644ab
--- /dev/null
+++ b/lib/models/rhodecode.dart
@@ -0,0 +1,246 @@
+import 'dart:ffi';
+
+import 'package:flutter/material.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'rhodecode.g.dart';
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class GetUserResponse {
+ GetUserResponse();
+ factory GetUserResponse.fromJson(Map json) =>
+ _$GetUserResponseFromJson(json);
+ String? error;
+ int? id;
+ RhodecodeUser? result;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhodecodeUser {
+ RhodecodeUser();
+ factory RhodecodeUser.fromJson(Map json) =>
+ _$RhodecodeUserFromJson(json);
+ bool? active;
+ String? username;
+ int? userId;
+ // String? avatarUrl;
+ String? email;
+ String? firstName;
+ String? lastName;
+ String? description;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhRepoListResponse {
+ RhRepoListResponse();
+ factory RhRepoListResponse.fromJson(Map json) =>
+ _$RhRepoListResponseFromJson(json);
+ String? error;
+ int? id;
+ List? result;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhRepoResponse {
+ RhRepoResponse();
+ factory RhRepoResponse.fromJson(Map json) =>
+ _$RhRepoResponseFromJson(json);
+ String? error;
+ int? id;
+ RhRepo? result;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+ disallowUnrecognizedKeys: false,
+ explicitToJson: true
+ )
+class RhRepo {
+ RhRepo();
+ factory RhRepo.fromJson(Map json) => _$RhRepoFromJson(json);
+ String? owner;
+ String? repoName;
+ String? url;
+ bool? private;
+ DateTime? createdOn;
+ String? description;
+ int? repoId;
+ int? forkOfId;
+ RhLastChangeset? lastChangeset;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhLastChangeset {
+ RhLastChangeset();
+ factory RhLastChangeset.fromJson(Map json) =>
+ _$RhLastChangesetFromJson(json);
+ String? author;
+ String? branch;
+ DateTime? date;
+ String? message;
+ // TODO parents
+ String? rawId;
+ int? repoCommitCount;
+ int? revision;
+ String? shortId;
+ // DateTime? updateOn; // TODO
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhRepoRefsResponse {
+ RhRepoRefsResponse();
+ factory RhRepoRefsResponse.fromJson(Map json) =>
+ _$RhRepoRefsResponseFromJson(json);
+ String? error;
+ int? id;
+ RhRepoRefs? result;
+}
+
+@JsonSerializable(fieldRename: FieldRename.snake)
+class RhRepoRefs {
+ RhRepoRefs();
+ factory RhRepoRefs.fromJson(Map json) =>
+ _$RhRepoRefsFromJson(json);
+ // TODO String? bookmarks;
+ Map? branches;
+ // TODO branchesClosed
+ // TODO tags
+}
+
+class RhBranch {
+ RhBranch(this.name, this.changeset);
+ String? name;
+ String? changeset;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhRepoNodesResponse {
+ RhRepoNodesResponse();
+ factory RhRepoNodesResponse.fromJson(Map json) =>
+ _$RhRepoNodesResponseFromJson(json);
+ String? error;
+ int? id;
+ List? result;
+}
+
+@JsonSerializable(fieldRename: FieldRename.snake)
+class RhodecodeTreeItem {
+ RhodecodeTreeItem({required this.type, required this.name});
+ factory RhodecodeTreeItem.fromJson(Map json) =>
+ _$RhodecodeTreeItemFromJson(json);
+ bool? binary;
+ String? extension;
+ int? lines;
+ String? md5;
+ String? mimetype;
+ String name;
+ int? size;
+ String type;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhCommitsResponse {
+ RhCommitsResponse();
+ factory RhCommitsResponse.fromJson(Map json) =>
+ _$RhCommitsResponseFromJson(json);
+ String? error;
+ int? id;
+ List? result;
+}
+
+@JsonSerializable(fieldRename: FieldRename.snake)
+class RhodecodeCommit {
+ RhodecodeCommit(
+ {required this.author,
+ required this.branch,
+ required this.rawId,
+ required this.revision,
+ required this.shortId});
+ factory RhodecodeCommit.fromJson(Map json) =>
+ _$RhodecodeCommitFromJson(json);
+ String author;
+ String? branch;
+ DateTime? date;
+ // diff TODO
+ String? message;
+ String rawId;
+ int revision;
+ String shortId;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhChangesetResponse {
+ RhChangesetResponse();
+ factory RhChangesetResponse.fromJson(Map json) =>
+ _$RhChangesetResponseFromJson(json);
+ String? error;
+ int? id;
+ RhodecodeChangeset? result;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhodecodeStats {
+ RhodecodeStats();
+ factory RhodecodeStats.fromJson(Map json) =>
+ _$RhodecodeStatsFromJson(json);
+ int? added;
+ bool? binary;
+ int? deleted;
+ String? newMode;
+ String? oldMode;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhodecodeDiff {
+ RhodecodeDiff();
+ factory RhodecodeDiff.fromJson(Map json) =>
+ _$RhodecodeDiffFromJson(json);
+ String? filename;
+ String? newRevision;
+ String? oldRevision;
+ String? op;
+ String? rawDiff;
+ RhodecodeStats? stats;
+}
+
+@JsonSerializable(
+ fieldRename: FieldRename.snake,
+)
+class RhodecodeChangeset {
+ RhodecodeChangeset();
+ factory RhodecodeChangeset.fromJson(Map json) =>
+ _$RhodecodeChangesetFromJson(json);
+ String? author;
+ String? branch;
+ DateTime? date;
+ List? diff;
+ String? message;
+ // TODO parents
+ String? rawId;
+ // TODO refs
+ int? revision;
+ String? shortId;
+}
+
diff --git a/lib/models/rhodecode.g.dart b/lib/models/rhodecode.g.dart
new file mode 100644
index 00000000..fa07c276
--- /dev/null
+++ b/lib/models/rhodecode.g.dart
@@ -0,0 +1,307 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'rhodecode.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+GetUserResponse _$GetUserResponseFromJson(Map json) =>
+ GetUserResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = json['result'] == null
+ ? null
+ : RhodecodeUser.fromJson(json['result'] as Map);
+
+Map _$GetUserResponseToJson(GetUserResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhodecodeUser _$RhodecodeUserFromJson(Map json) =>
+ RhodecodeUser()
+ ..active = json['active'] as bool?
+ ..username = json['username'] as String?
+ ..userId = json['user_id'] as int?
+ ..email = json['email'] as String?
+ ..firstName = json['first_name'] as String?
+ ..lastName = json['last_name'] as String?
+ ..description = json['description'] as String?;
+
+Map _$RhodecodeUserToJson(RhodecodeUser instance) =>
+ {
+ 'active': instance.active,
+ 'username': instance.username,
+ 'user_id': instance.userId,
+ 'email': instance.email,
+ 'first_name': instance.firstName,
+ 'last_name': instance.lastName,
+ 'description': instance.description,
+ };
+
+RhRepoListResponse _$RhRepoListResponseFromJson(Map json) =>
+ RhRepoListResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = (json['result'] as List?)
+ ?.map((e) => RhRepo.fromJson(e as Map))
+ .toList();
+
+Map _$RhRepoListResponseToJson(RhRepoListResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhRepoResponse _$RhRepoResponseFromJson(Map json) =>
+ RhRepoResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = json['result'] == null
+ ? null
+ : RhRepo.fromJson(json['result'] as Map);
+
+Map _$RhRepoResponseToJson(RhRepoResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhRepo _$RhRepoFromJson(Map json) => RhRepo()
+ ..owner = json['owner'] as String?
+ ..repoName = json['repo_name'] as String?
+ ..url = json['url'] as String?
+ ..private = json['private'] as bool?
+ ..createdOn = json['created_on'] == null
+ ? null
+ : DateTime.parse(json['created_on'] as String)
+ ..description = json['description'] as String?
+ ..repoId = json['repo_id'] as int?
+ ..forkOfId = json['fork_of_id'] as int?
+ ..lastChangeset = json['last_changeset'] == null
+ ? null
+ : RhLastChangeset.fromJson(
+ json['last_changeset'] as Map);
+
+Map _$RhRepoToJson(RhRepo instance) => {
+ 'owner': instance.owner,
+ 'repo_name': instance.repoName,
+ 'url': instance.url,
+ 'private': instance.private,
+ 'created_on': instance.createdOn?.toIso8601String(),
+ 'description': instance.description,
+ 'repo_id': instance.repoId,
+ 'fork_of_id': instance.forkOfId,
+ };
+
+RhLastChangeset _$RhLastChangesetFromJson(Map json) =>
+ RhLastChangeset()
+ ..author = json['author'] as String?
+ ..branch = json['branch'] as String?
+ ..date =
+ json['date'] == null ? null : DateTime.parse(json['date'] as String)
+ ..message = json['message'] as String?
+ ..rawId = json['raw_id'] as String?
+ ..repoCommitCount = json['repo_commit_count'] as int?
+ ..revision = json['revision'] as int?
+ ..shortId = json['short_id'] as String?;
+
+Map _$RhLastChangesetToJson(RhLastChangeset instance) =>
+ {
+ 'author': instance.author,
+ 'branch': instance.branch,
+ 'date': instance.date?.toIso8601String(),
+ 'message': instance.message,
+ 'raw_id': instance.rawId,
+ 'repo_commit_count': instance.repoCommitCount,
+ 'revision': instance.revision,
+ 'short_id': instance.shortId,
+ };
+
+RhRepoRefsResponse _$RhRepoRefsResponseFromJson(Map json) =>
+ RhRepoRefsResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = json['result'] == null
+ ? null
+ : RhRepoRefs.fromJson(json['result'] as Map);
+
+Map _$RhRepoRefsResponseToJson(RhRepoRefsResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhRepoRefs _$RhRepoRefsFromJson(Map json) => RhRepoRefs()
+ ..branches = (json['branches'] as Map?)?.map(
+ (k, e) => MapEntry(k, e as String),
+ );
+
+Map _$RhRepoRefsToJson(RhRepoRefs instance) =>
+ {
+ 'branches': instance.branches,
+ };
+
+RhRepoNodesResponse _$RhRepoNodesResponseFromJson(Map json) =>
+ RhRepoNodesResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = (json['result'] as List?)
+ ?.map((e) => RhodecodeTreeItem.fromJson(e as Map))
+ .toList();
+
+Map _$RhRepoNodesResponseToJson(
+ RhRepoNodesResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhodecodeTreeItem _$RhodecodeTreeItemFromJson(Map json) =>
+ RhodecodeTreeItem(
+ type: json['type'] as String,
+ name: json['name'] as String,
+ )
+ ..binary = json['binary'] as bool?
+ ..extension = json['extension'] as String?
+ ..lines = json['lines'] as int?
+ ..md5 = json['md5'] as String?
+ ..mimetype = json['mimetype'] as String?
+ ..size = json['size'] as int?;
+
+Map _$RhodecodeTreeItemToJson(RhodecodeTreeItem instance) =>
+ {
+ 'binary': instance.binary,
+ 'extension': instance.extension,
+ 'lines': instance.lines,
+ 'md5': instance.md5,
+ 'mimetype': instance.mimetype,
+ 'name': instance.name,
+ 'size': instance.size,
+ 'type': instance.type,
+ };
+
+RhCommitsResponse _$RhCommitsResponseFromJson(Map json) =>
+ RhCommitsResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = (json['result'] as List?)
+ ?.map((e) => RhodecodeCommit.fromJson(e as Map))
+ .toList();
+
+Map _$RhCommitsResponseToJson(RhCommitsResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhodecodeCommit _$RhodecodeCommitFromJson(Map json) =>
+ RhodecodeCommit(
+ author: json['author'] as String,
+ branch: json['branch'] as String?,
+ rawId: json['raw_id'] as String,
+ revision: json['revision'] as int,
+ shortId: json['short_id'] as String,
+ )
+ ..date =
+ json['date'] == null ? null : DateTime.parse(json['date'] as String)
+ ..message = json['message'] as String?;
+
+Map _$RhodecodeCommitToJson(RhodecodeCommit instance) =>
+ {
+ 'author': instance.author,
+ 'branch': instance.branch,
+ 'date': instance.date?.toIso8601String(),
+ 'message': instance.message,
+ 'raw_id': instance.rawId,
+ 'revision': instance.revision,
+ 'short_id': instance.shortId,
+ };
+
+RhChangesetResponse _$RhChangesetResponseFromJson(Map json) =>
+ RhChangesetResponse()
+ ..error = json['error'] as String?
+ ..id = json['id'] as int?
+ ..result = json['result'] == null
+ ? null
+ : RhodecodeChangeset.fromJson(json['result'] as Map);
+
+Map _$RhChangesetResponseToJson(
+ RhChangesetResponse instance) =>
+ {
+ 'error': instance.error,
+ 'id': instance.id,
+ 'result': instance.result,
+ };
+
+RhodecodeStats _$RhodecodeStatsFromJson(Map json) =>
+ RhodecodeStats()
+ ..added = json['added'] as int?
+ ..binary = json['binary'] as bool?
+ ..deleted = json['deleted'] as int?
+ ..newMode = json['new_mode'] as String?
+ ..oldMode = json['old_mode'] as String?;
+
+Map _$RhodecodeStatsToJson(RhodecodeStats instance) =>
+ {
+ 'added': instance.added,
+ 'binary': instance.binary,
+ 'deleted': instance.deleted,
+ 'new_mode': instance.newMode,
+ 'old_mode': instance.oldMode,
+ };
+
+RhodecodeDiff _$RhodecodeDiffFromJson(Map json) =>
+ RhodecodeDiff()
+ ..filename = json['filename'] as String?
+ ..newRevision = json['new_revision'] as String?
+ ..oldRevision = json['old_revision'] as String?
+ ..op = json['op'] as String?
+ ..rawDiff = json['raw_diff'] as String?
+ ..stats = json['stats'] == null
+ ? null
+ : RhodecodeStats.fromJson(json['stats'] as Map);
+
+Map _$RhodecodeDiffToJson(RhodecodeDiff instance) =>
+ {
+ 'filename': instance.filename,
+ 'new_revision': instance.newRevision,
+ 'old_revision': instance.oldRevision,
+ 'op': instance.op,
+ 'raw_diff': instance.rawDiff,
+ 'stats': instance.stats,
+ };
+
+RhodecodeChangeset _$RhodecodeChangesetFromJson(Map json) =>
+ RhodecodeChangeset()
+ ..author = json['author'] as String?
+ ..branch = json['branch'] as String?
+ ..date =
+ json['date'] == null ? null : DateTime.parse(json['date'] as String)
+ ..diff = (json['diff'] as List?)
+ ?.map((e) => RhodecodeDiff.fromJson(e as Map))
+ .toList()
+ ..message = json['message'] as String?
+ ..rawId = json['raw_id'] as String?
+ ..revision = json['revision'] as int?
+ ..shortId = json['short_id'] as String?;
+
+Map _$RhodecodeChangesetToJson(RhodecodeChangeset instance) =>
+ {
+ 'author': instance.author,
+ 'branch': instance.branch,
+ 'date': instance.date?.toIso8601String(),
+ 'diff': instance.diff,
+ 'message': instance.message,
+ 'raw_id': instance.rawId,
+ 'revision': instance.revision,
+ 'short_id': instance.shortId,
+ };
diff --git a/lib/router.dart b/lib/router.dart
index 89445204..d00da296 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -1,3 +1,6 @@
+import 'dart:developer';
+
+import 'package:flutter/cupertino.dart';
import 'package:git_touch/home.dart';
import 'package:git_touch/screens/bb_commits.dart';
import 'package:git_touch/screens/bb_issue.dart';
@@ -79,10 +82,29 @@ import 'package:git_touch/screens/gt_status.dart';
import 'package:git_touch/screens/gt_user.dart';
import 'package:git_touch/screens/gt_users.dart';
import 'package:git_touch/screens/login.dart';
+import 'package:git_touch/screens/rh_blob.dart';
+import 'package:git_touch/screens/rh_commit.dart';
+import 'package:git_touch/screens/rh_commits.dart';
+import 'package:git_touch/screens/rh_repo.dart';
+import 'package:git_touch/screens/rh_tree.dart';
+import 'package:git_touch/screens/rh_user.dart';
import 'package:git_touch/screens/settings.dart';
import 'package:go_router/go_router.dart';
+class MyNavigatorObserver extends NavigatorObserver {
+ @override
+ void didPush(Route route, Route? previousRoute) {
+ log('did push route ${route.settings.name}');
+ }
+
+ @override
+ void didPop(Route route, Route? previousRoute) {
+ log('did pop route ${route.settings.name}');
+ }
+}
+
final router = GoRouter(
+ observers: [MyNavigatorObserver()],
routes: [
GoRoute(
path: '/',
@@ -876,6 +898,59 @@ final router = GoRouter(
),
],
),
+
+ // rhodecode
+ GoRoute(
+ path: 'rhodecode',
+ builder: (context, state) => Home(),
+ routes: [
+ GoRoute(
+ path: 'user/:id',
+ builder: (context, state) =>
+ RhUserScreen(int.parse(state.params['id']!)),
+ ),
+ GoRoute(
+ path: 'repository/:id',
+ builder: (context, state) => RhRepoScreen(
+ int.parse(state.params['id']!),
+ branch: state.queryParams['branch'] ?? 'master',
+ ),
+ routes: [
+ GoRoute(
+ path: 'commits',
+ builder: (context, state) => RhCommitsScreen(
+ int.parse(state.params['id']!),
+ branch: state.queryParams['branch'],
+ ),
+ ),
+ GoRoute(
+ path: 'commit/:revision',
+ builder: (context, state) => RhCommitScreen(
+ int.parse(state.params['id']!),
+ state.params['revision']!,
+ ),
+ ),
+ GoRoute(
+ path: 'blob/:ref',
+ builder: (context, state) => RhBlobScreen(
+ int.parse(state.params['id']!),
+ state.params['ref']!,
+ path: state.queryParams['path']!,
+ ),
+ ),
+ GoRoute(
+ path: 'tree/:branch',
+ builder: (context, state) => RhTreeScreen(
+ int.parse(state.params['id']!),
+ state.params['branch']!,
+ state.queryParams['revision']!,
+ path: state.queryParams['path'],
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
],
),
],
diff --git a/lib/screens/login.dart b/lib/screens/login.dart
index 42b6a640..ec0eae6c 100644
--- a/lib/screens/login.dart
+++ b/lib/screens/login.dart
@@ -355,6 +355,27 @@ class _LoginScreenState extends State {
}
},
),
+ _buildAddItem(
+ text: 'Rhodecode Account',
+ brand: Ionicons.git_branch_outline, // TODO: brand icon
+ onTap: () async {
+ _domainController.text =
+ 'https://rhodecode.local';
+ final result = await theme.showConfirm(
+ context,
+ _buildPopup(context, showDomain: true),
+ );
+ if (result == true) {
+ try {
+ await auth.loginToRhodecode(
+ _domainController.text, _tokenController.text);
+ _tokenController.clear();
+ } catch (err) {
+ showError(err);
+ }
+ }
+ },
+ ),
],
),
Container(
diff --git a/lib/screens/rh_blob.dart b/lib/screens/rh_blob.dart
new file mode 100644
index 00000000..920e69cc
--- /dev/null
+++ b/lib/screens/rh_blob.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_vector_icons/flutter_vector_icons.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/scaffolds/list_stateful.dart';
+import 'package:git_touch/scaffolds/refresh_stateful.dart';
+import 'package:git_touch/widgets/action_entry.dart';
+import 'package:git_touch/widgets/blob_view.dart';
+import 'package:provider/provider.dart';
+
+class RhBlobScreen extends StatelessWidget {
+ const RhBlobScreen(this.id, this.ref, {required this.path});
+ final int id;
+ final String ref;
+ final String path;
+
+ @override
+ Widget build(BuildContext context) {
+ return RefreshStatefulScaffold(
+ title: Text(path),
+ fetch: () async {
+ final auth = context.read();
+
+ final params = {
+ 'repoid': id,
+ 'commit_id': ref,
+ 'file_path': path.urlencode,
+ 'details': 'full',
+ 'cache': false,
+ 'max_file_bytes': 50000
+ };
+ return auth
+ .fetchRhodecode('get_repo_file', body: params)
+ .onError(
+ (e, s) {
+ return null;
+ },
+ ).then((v) {
+ return (v?['result']['content'] as String?) ?? '';
+ });
+ },
+ action:
+ const ActionEntry(iconData: Ionicons.cog, url: '/choose-code-theme'),
+ bodyBuilder: (data, _) {
+ return BlobView(path, text: data);
+ },
+ );
+ }
+}
diff --git a/lib/screens/rh_commit.dart b/lib/screens/rh_commit.dart
new file mode 100644
index 00000000..fd15a5ab
--- /dev/null
+++ b/lib/screens/rh_commit.dart
@@ -0,0 +1,101 @@
+import 'package:antd_mobile/antd_mobile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/models/code.dart';
+import 'package:git_touch/models/rhodecode.dart';
+import 'package:flutter_gen/gen_l10n/S.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/action_button.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/widgets/avatar.dart';
+
+class RhCommitScreen extends StatelessWidget {
+ // changesets cannot be filtered by branch
+ const RhCommitScreen(this.id, this.revision);
+ final int id;
+ final String revision;
+
+ @override
+ Widget build(BuildContext context) {
+ return RefreshStatefulScaffold(
+ title: Text('${AppLocalizations.of(context)!.commit} $revision'),
+ fetch: () async {
+ final auth = context.read();
+ final parameters = {
+ 'repoid': id,
+ 'revision': revision,
+ 'details': 'full'
+ };
+ final res = await auth.fetchRhodecodeWithPage('get_repo_changeset',
+ body: parameters);
+ return RhodecodeChangeset.fromJson(res.data['result']);
+ },
+ actionBuilder: (data, _) => ActionButton(
+ title: AppLocalizations.of(context)!.actions,
+ items: [...ActionItem.getUrlActions(data.author)]),
+ bodyBuilder: (data, _) {
+ final theme = Provider.of(context);
+ final codeProvider = Provider.of(context);
+ return Column(children: [
+ Container(
+ padding: CommonStyle.padding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ LinkWidget(
+ url: 'TODO',
+ child: Row(
+ children: [
+ Avatar(
+ url: 'TODO',
+ size: AvatarSize.extraSmall,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ 'TODO',
+ style: TextStyle(
+ fontSize: 17,
+ color: AntTheme.of(context).colorTextSecondary,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Text(
+ data.rawId!,
+ style: TextStyle(
+ fontSize: 17,
+ color: AntTheme.of(context).colorWeak,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ data.message!,
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ )),
+ Wrap(
+ children: data.diff!
+ .map((file) => FilesItem(
+ filename: file.filename,
+ additions: file.stats!.added,
+ deletions: file.stats!.deleted,
+ status: file.op,
+ patch: file.rawDiff,
+ ))
+ .toList(),
+ )
+ ]);
+ },
+ );
+ }
+}
diff --git a/lib/screens/rh_commits.dart b/lib/screens/rh_commits.dart
new file mode 100644
index 00000000..71b33e4e
--- /dev/null
+++ b/lib/screens/rh_commits.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/cupertino.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/models/rhodecode.dart';
+import 'package:git_touch/scaffolds/list_stateful.dart';
+import 'package:flutter_gen/gen_l10n/S.dart';
+import 'package:git_touch/widgets/commit_item.dart';
+import 'package:provider/provider.dart';
+
+class RhCommitsScreen extends StatelessWidget {
+ const RhCommitsScreen(this.id, {this.branch});
+ final int id;
+ final String? branch;
+
+
+ @override
+ Widget build(BuildContext context) {
+ return ListStatefulScaffold(
+ title: Text(AppLocalizations.of(context)!.commits),
+ fetch: (page) async {
+ page = page ?? 0;
+ const limit = 10;
+ final auth = context.read();
+ final parameters = {
+ 'repoid': id,
+ 'start_rev': (limit * page).toString(),
+ 'limit': limit,
+ 'details': 'basic'
+ };
+ final res = await auth.fetchRhodecodeWithPage('get_repo_changesets',
+ body: parameters);
+ return ListPayload(
+ cursor: res.cursor,
+ hasMore: res.hasMore,
+ items: (res.data['result'] as List)
+ .map((v) => RhodecodeCommit.fromJson(v))
+ .toList(),
+ );
+ },
+ itemBuilder: (c) {
+ return CommitItem(
+ author: c.author,
+ avatarUrl: null,
+ avatarLink: null,
+ createdAt: c.date,
+ message: c.message,
+ url: '/rhodecode/repository/$id/commit/${c.revision}',
+ );
+ },
+ );
+ }
+}
diff --git a/lib/screens/rh_explore.dart b/lib/screens/rh_explore.dart
new file mode 100644
index 00000000..80419c3c
--- /dev/null
+++ b/lib/screens/rh_explore.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_gen/gen_l10n/S.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/models/rhodecode.dart';
+import 'package:git_touch/scaffolds/list_stateful.dart';
+import 'package:git_touch/widgets/repo_item.dart';
+import 'package:provider/provider.dart';
+
+class RhExploreScreen extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return ListStatefulScaffold(
+ title: Text(AppLocalizations.of(context)!.explore),
+ fetch: (page) async {
+ final auth = context.read();
+ final res = await auth.fetchRhodecodeWithPage('get_repos');
+ return ListPayload(
+ cursor: res.cursor,
+ hasMore: res.hasMore,
+ items: (res.data['result'] as List)
+ .map((v) => RhRepo.fromJson(v))
+ .toList(),
+ );
+ },
+ itemBuilder: (v) {
+ return RepoItem.rh(payload: v);
+ },
+ );
+ }
+}
diff --git a/lib/screens/rh_repo.dart b/lib/screens/rh_repo.dart
new file mode 100644
index 00000000..90acd874
--- /dev/null
+++ b/lib/screens/rh_repo.dart
@@ -0,0 +1,160 @@
+import 'package:antd_mobile/antd_mobile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_vector_icons/flutter_vector_icons.dart';
+import 'package:git_touch/models/theme.dart';
+import 'package:git_touch/utils/utils.dart';
+import 'package:git_touch/widgets/markdown_view.dart';
+import 'package:git_touch/widgets/repo_header.dart';
+import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
+import 'package:flutter_gen/gen_l10n/S.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/models/rhodecode.dart';
+import 'package:git_touch/scaffolds/refresh_stateful.dart';
+import 'package:tuple/tuple.dart';
+
+class RhRepoScreen extends StatelessWidget {
+ RhRepoScreen(this.id, {this.branch = 'master'});
+ final int id;
+ final String branch;
+ String? latestBranchCommit;
+
+ @override
+ Widget build(BuildContext context) {
+ return RefreshStatefulScaffold>>(
+ title: Text(AppLocalizations.of(context)!.repository),
+ fetch: () async {
+ final auth = context.read();
+ final parameters = {'repoid': id};
+ final repo =
+ await auth.fetchRhodecode('get_repo', body: parameters).then((v) {
+ return RhRepoResponse.fromJson(v).result;
+ });
+
+ final rhBranchList = await auth
+ .fetchRhodecode('get_repo_refs', body: parameters)
+ .then((v) {
+ final branches = RhRepoRefsResponse.fromJson(v).result?.branches;
+ final branchList = [];
+ (branches! is List ? {} : branches).forEach((key, value) {
+ branchList.add(RhBranch(key, value));
+ key == branch ? latestBranchCommit = value : '';
+ });
+
+ return branchList;
+ });
+
+ md() {
+ final params = {
+ 'repoid': id,
+ 'commit_id': latestBranchCommit,
+ 'file_path': 'README.md',
+ 'details': 'full',
+ 'cache': false,
+ 'max_file_bytes': 50000
+ };
+
+ return auth
+ .fetchRhodecode('get_repo_file', body: params)
+ .onError(
+ (e, s) {
+ return null;
+ },
+ ).then((v) {
+ return (v?['result']['content'] as String?) ?? '';
+ });
+ }
+
+ return Tuple3(repo!, await md(), rhBranchList);
+ },
+ bodyBuilder: (t, _) {
+ final p = t.item1;
+ final branches = t.item3;
+ final theme = context.read();
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ RepoHeader(
+ avatarUrl: p.owner!, // TODO
+ avatarLink: 'TODO', // TODO
+ owner: p.owner,
+ name: p.repoName,
+ description: p.description,
+ homepageUrl: p.url,
+ ),
+ CommonStyle.border,
+ Row(
+ children: [
+ // TODO
+ // EntryItem(
+ // count: p.watchersCount!,
+ // text: 'Watchers',
+ // ),
+ // EntryItem(
+ // count: p.starsCount!,
+ // text: 'Stars',
+ // ),
+ // EntryItem(
+ // count: p.forksCount!,
+ // text: 'Forks',
+ // ),
+ ],
+ ),
+ CommonStyle.border,
+ AntList(
+ children: [
+ AntListItem(
+ prefix: const Icon(Octicons.code),
+ child: const Text('Code'),
+ onClick: () {
+ context.push(
+ '/rhodecode/repository/$id/tree/$branch?revision=$latestBranchCommit');
+ },
+ ),
+ // TODO
+ // const AntListItem(
+ // prefix: Icon(Octicons.git_pull_request),
+ // child: Text(
+ // 'Pull requests'),
+ // ),
+ AntListItem(
+ prefix: const Icon(Octicons.history),
+ child: const Text('Commits'),
+ onClick: () {
+ context.push(
+ '/rhodecode/repository/$id/commits?branch=$branch');
+ },
+ ),
+ AntListItem(
+ prefix: const Icon(Octicons.git_branch),
+ extra: Text('$branch β’ ${branches.length.toString()}'),
+ onClick: () async {
+ await theme.showPicker(
+ context,
+ PickerGroupItem(
+ value: branch,
+ items: branches
+ .map((b) => PickerItem(b.name, text: b.name))
+ .toList(),
+ onClose: (ref) {
+ if (ref != branch) {
+ context.pushUrl(
+ '/rhodecode/repository/$id?branch=$ref',
+ replace: false);
+ }
+ },
+ ),
+ );
+ },
+ child: Text(AppLocalizations.of(context)!.branches),
+ ),
+ ],
+ ),
+ CommonStyle.verticalGap,
+ MarkdownFlutterView(t.item2),
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/lib/screens/rh_tree.dart b/lib/screens/rh_tree.dart
new file mode 100644
index 00000000..54c5c8fe
--- /dev/null
+++ b/lib/screens/rh_tree.dart
@@ -0,0 +1,58 @@
+import 'package:antd_mobile/antd_mobile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/models/rhodecode.dart';
+import 'package:git_touch/scaffolds/list_stateful.dart';
+import 'package:git_touch/scaffolds/refresh_stateful.dart';
+import 'package:git_touch/widgets/object_tree.dart';
+import 'package:provider/provider.dart';
+import 'package:flutter_gen/gen_l10n/S.dart';
+
+class RhTreeScreen extends StatelessWidget {
+ const RhTreeScreen(this.id, this.branch, this.revision, {this.path});
+ final int id;
+ final String branch;
+ final String revision;
+ final String? path;
+
+ @override
+ Widget build(BuildContext context) {
+ final auth = Provider.of(context);
+
+ return RefreshStatefulScaffold>(
+ title: Text(path ?? AppLocalizations.of(context)!.files),
+ fetch: () async {
+ final parameters = {
+ 'repoid': id,
+ 'revision': revision,
+ 'root_path': path ?? '/',
+ 'details': 'basic',
+ 'max_file_bytes': 5000
+ };
+ final res =
+ await auth.fetchRhodecode('get_repo_nodes', body: parameters);
+ final response = RhRepoNodesResponse.fromJson(res);
+ final itemList = response.result!;
+ return itemList;
+ },
+ bodyBuilder: (data, _) {
+ return AntList(
+ children: [
+ for (var item in data)
+ createObjectTreeItem(
+ type: item.type,
+ name: item.name,
+ size: item.size,
+ //downloadUrl: '', // TODO:
+ url: item.type == 'dir'
+ ? '/rhodecode/repository/$id/tree/$branch?path=${item.name.urlencode}'
+ : item.type == 'file'
+ ? '/rhodecode/repository/$id/blob/$revision?path=${item.name.urlencode}'
+ : '',
+ )
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/lib/screens/rh_user.dart b/lib/screens/rh_user.dart
new file mode 100644
index 00000000..8eb3b113
--- /dev/null
+++ b/lib/screens/rh_user.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_vector_icons/flutter_vector_icons.dart';
+import 'package:git_touch/models/auth.dart';
+import 'package:git_touch/models/rhodecode.dart';
+import 'package:git_touch/scaffolds/refresh_stateful.dart';
+import 'package:flutter_gen/gen_l10n/S.dart';
+import 'package:git_touch/utils/utils.dart';
+import 'package:git_touch/widgets/action_entry.dart';
+import 'package:git_touch/widgets/repo_item.dart';
+import 'package:git_touch/widgets/user_header.dart';
+import 'package:provider/provider.dart';
+import 'package:tuple/tuple.dart';
+import 'package:timeago/timeago.dart' as timeago;
+
+class RhUserScreen extends StatelessWidget {
+ const RhUserScreen(this.id);
+ final int? id;
+ bool get isViewer => id == null;
+
+ @override
+ Widget build(BuildContext context) {
+ return RefreshStatefulScaffold>>(
+ title: Text(isViewer
+ ? AppLocalizations.of(context)!.me
+ : AppLocalizations.of(context)!.user),
+ fetch: () async {
+ final auth = context.read();
+
+ final res = await Future.wait([
+ auth.fetchRhodecode(
+ 'get_user'),
+ auth.fetchRhodecodeWithPage(
+ 'get_repos'),
+ ]);
+
+ return Tuple2(
+ GetUserResponse.fromJson(res[0]).result!,
+ [for (var v in res[1].data['result']) RhRepo.fromJson(v)],
+ );
+ },
+ action: isViewer
+ ? const ActionEntry(
+ iconData: Ionicons.cog,
+ url: '/settings',
+ )
+ : null,
+ bodyBuilder: (data, _) {
+ final user = data.item1;
+ final projects = data.item2;
+
+ return Column(
+ children: [
+ UserHeader(
+ login: user.username,
+ avatarUrl: 'TODO',
+ name: '${user.firstName} ${user.lastName}',
+ createdAt: DateTime.now(), // TODO
+ bio: user.description,
+ isViewer: isViewer,
+ ),
+ CommonStyle.border,
+ Column(
+ children: [
+ for (var v in projects)
+ RepoItem.rh(
+ payload: v,
+ note: 'Updated ${timeago.format(v.lastChangeset!.date!)}',
+ )
+ ],
+ )
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/lib/widgets/files_item.dart b/lib/widgets/files_item.dart
index 6f43eec5..d4831eb7 100644
--- a/lib/widgets/files_item.dart
+++ b/lib/widgets/files_item.dart
@@ -27,7 +27,7 @@ class FilesItem extends StatelessWidget {
final theme = Provider.of(context);
final codeProvider = Provider.of(context);
return AntCollapse(
- activeKey: const {},
+ activeKey: {}, // removed const to fix 'Cannot change an unmodifiable set'
onChange: (_) {
// TODO: set active
},
diff --git a/lib/widgets/object_tree.dart b/lib/widgets/object_tree.dart
index fda81f74..57ca7b3c 100644
--- a/lib/widgets/object_tree.dart
+++ b/lib/widgets/object_tree.dart
@@ -7,11 +7,11 @@ import 'package:git_touch/utils/utils.dart';
Widget _buildIcon(String type, String name) {
switch (type) {
case 'blob': // github gql, gitlab
- case 'file': // github rest, gitea
+ case 'file': // github rest, gitea, rhodecode
case 'commit_file': // bitbucket
return FileIcon(name, size: 26); // TODO: size
case 'tree': // github gql, gitlab
- case 'dir': // github rest, gitea
+ case 'dir': // github rest, gitea, rhodecode
case 'commit_directory': // bitbucket
return const Icon(AntIcons.folderOutline);
case 'commit':
diff --git a/lib/widgets/repo_item.dart b/lib/widgets/repo_item.dart
index f1962e5d..57d77f96 100644
--- a/lib/widgets/repo_item.dart
+++ b/lib/widgets/repo_item.dart
@@ -12,6 +12,8 @@ import 'package:github/github.dart' as github;
import 'package:gql_github/repos.data.gql.dart';
import 'package:timeago/timeago.dart' as timeago;
+import '../models/rhodecode.dart';
+
class RepoItem extends StatelessWidget {
const RepoItem({
required this.owner,
@@ -91,6 +93,21 @@ class RepoItem extends StatelessWidget {
avatarLink = '/github/$owner',
url = '/github/$owner/$name';
+ RepoItem.rh({
+ required RhRepo payload,
+ this.primaryLanguageName,
+ this.primaryLanguageColor,
+ this.note,
+ }) : name = payload.repoName,
+ description = payload.description,
+ url = '/rhodecode/repository/${payload.repoId}',
+ iconData = _buildIconData(payload.private, payload.forkOfId != null),
+ owner = payload.owner,
+ forkCount = 0, //TODO
+ starCount = 0, // TODO
+ avatarLink = 'TODO',
+ avatarUrl = 'TODO';
+
factory RepoItem.gql(GRepoParts v, {String? note}) {
return RepoItem.gh(
owner: v.owner.login,