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

Download on the App Store @@ -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,