diff --git a/lib/models/auth.dart b/lib/models/auth.dart index 5439584..7ea89c0 100644 --- a/lib/models/auth.dart +++ b/lib/models/auth.dart @@ -337,7 +337,15 @@ class AuthModel with ChangeNotifier { '${activeAccount.domain}/api/v5$p', headers: headers, ); - break; + return; + } + case 'PUT': + { + await http.put( + '${activeAccount.domain}/api/v5$p', + headers: headers, + ); + return; } case 'POST': { @@ -357,6 +365,12 @@ class AuthModel with ChangeNotifier { ); break; } + case 'NO CONTENT': + { + res = await http.get('${activeAccount.domain}/api/v5$p', + headers: headers); + return res; + } default: { res = await http.get('${activeAccount.domain}/api/v5$p', @@ -364,11 +378,8 @@ class AuthModel with ChangeNotifier { break; } } - if (requestType != 'DELETE') { - final info = json.decode(utf8.decode(res.bodyBytes)); - return info; - } - return; + final info = json.decode(utf8.decode(res.bodyBytes)); + return info; } Future fetchGiteeWithPage(String path, diff --git a/lib/models/bitbucket.dart b/lib/models/bitbucket.dart index a4896d7..61c134b 100644 --- a/lib/models/bitbucket.dart +++ b/lib/models/bitbucket.dart @@ -144,3 +144,12 @@ class BbComment { factory BbComment.fromJson(Map json) => _$BbCommentFromJson(json); } + +@JsonSerializable(fieldRename: FieldRename.snake) +class BbBranch { + String name; + String type; + BbBranch(); + factory BbBranch.fromJson(Map json) => + _$BbBranchFromJson(json); +} diff --git a/lib/models/bitbucket.g.dart b/lib/models/bitbucket.g.dart index f403715..c873721 100644 --- a/lib/models/bitbucket.g.dart +++ b/lib/models/bitbucket.g.dart @@ -247,3 +247,14 @@ Map _$BbCommentToJson(BbComment instance) => { 'content': instance.content, 'user': instance.user, }; + +BbBranch _$BbBranchFromJson(Map json) { + return BbBranch() + ..name = json['name'] as String + ..type = json['type'] as String; +} + +Map _$BbBranchToJson(BbBranch instance) => { + 'name': instance.name, + 'type': instance.type, + }; diff --git a/lib/router.dart b/lib/router.dart index 09f0d20..7dab53e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -426,11 +426,14 @@ class BitbucketRouter { '/:login', (context, parameters) => BbUserScreen(parameters['login'].first, isTeam: parameters['team'].first == '1')); - static final repo = RouterScreen( - '/:owner/:name', - (context, parameters) => - BbRepoScreen(parameters['owner'].first, parameters['name'].first), - ); + static final repo = RouterScreen('/:owner/:name', (context, parameters) { + if (parameters['branch'] == null) { + return BbRepoScreen(parameters['owner'].first, parameters['name'].first); + } else { + return BbRepoScreen(parameters['owner'].first, parameters['name'].first, + branch: parameters['branch'].first); + } + }); static final object = RouterScreen( '/:owner/:name/src/:ref', (context, parameters) => BbObjectScreen( diff --git a/lib/screens/bb_repo.dart b/lib/screens/bb_repo.dart index da801a7..8d54d05 100644 --- a/lib/screens/bb_repo.dart +++ b/lib/screens/bb_repo.dart @@ -18,11 +18,12 @@ import '../generated/l10n.dart'; class BbRepoScreen extends StatelessWidget { final String owner; final String name; - BbRepoScreen(this.owner, this.name); + final String branch; + BbRepoScreen(this.owner, this.name, {this.branch}); @override Widget build(BuildContext context) { - return RefreshStatefulScaffold>( + return RefreshStatefulScaffold>>( title: AppBarTitle(S.of(context).repository), fetch: () async { final auth = context.read(); @@ -32,11 +33,17 @@ class BbRepoScreen extends StatelessWidget { '/repositories/$owner/$name/src/${repo.mainbranch.name}/README.md'); final readme = res.statusCode >= 400 ? null : utf8.decode(res.bodyBytes); - return Tuple2(repo, readme); + final branches = await auth + .fetchBbWithPage('/repositories/$owner/$name/refs/branches') + .then((v) { + return [for (var branch in v.data) BbBranch.fromJson(branch)]; + }); + return Tuple3(repo, readme, branches); }, bodyBuilder: (t, setState) { final theme = Provider.of(context); final p = t.item1; + final branches = t.item3; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -56,7 +63,8 @@ class BbRepoScreen extends StatelessWidget { leftIconData: Octicons.code, text: Text('Code'), rightWidget: Text(filesize(p.size)), - url: '/bitbucket/$owner/$name/src/${p.mainbranch.name}', + url: + '/bitbucket/$owner/$name/src/${branch == null ? p.mainbranch.name : branch}', ), TableViewItem( leftIconData: Octicons.issue_opened, @@ -71,8 +79,38 @@ class BbRepoScreen extends StatelessWidget { TableViewItem( leftIconData: Octicons.history, text: Text('Commits'), - url: '/bitbucket/$owner/$name/commits/${p.mainbranch.name}', + url: + '/bitbucket/$owner/$name/commits/${branch == null ? p.mainbranch.name : branch}', ), + if (branches != null) + TableViewItem( + leftIconData: Octicons.git_branch, + text: Text(S.of(context).branches), + rightWidget: Text( + (branch == null ? p.mainbranch.name : branch) + + ' • ' + + branches.length.toString()), + onTap: () async { + if (branches.length < 2) 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, + '/bitbucket/$owner/$name?branch=$ref', + replace: true); + } + }, + ), + ); + }, + ), ], ), CommonStyle.verticalGap, diff --git a/lib/screens/ge_repo.dart b/lib/screens/ge_repo.dart index 0a6bfee..7713796 100644 --- a/lib/screens/ge_repo.dart +++ b/lib/screens/ge_repo.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:git_touch/models/auth.dart'; import 'package:git_touch/models/gitee.dart'; @@ -8,6 +9,7 @@ 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/mutation_button.dart'; import 'package:git_touch/widgets/repo_header.dart'; import 'package:git_touch/widgets/table_view.dart'; import 'package:provider/provider.dart'; @@ -15,6 +17,12 @@ import 'package:tuple/tuple.dart'; import 'package:http/http.dart' as http; import '../generated/l10n.dart'; +class StatusPayload { + bool isWatching; + bool isStarred; + StatusPayload(this.isWatching, this.isStarred); +} + class GeRepoScreen extends StatelessWidget { final String owner; final String name; @@ -24,7 +32,7 @@ class GeRepoScreen extends StatelessWidget { @override Widget build(BuildContext context) { return RefreshStatefulScaffold< - Tuple3>>( + Tuple4, StatusPayload>>( title: AppBarTitle(S.of(context).repository), fetch: () async { final auth = context.read(); @@ -49,7 +57,15 @@ class GeRepoScreen extends StatelessWidget { await auth.fetchGitee('/repos/$owner/$name/branches').then((v) { return [for (var branch in v) GiteeBranch.fromJson(branch)]; }); - return Tuple3(repo, readmeData, branches); + bool isStarred = await auth + .fetchGitee('/user/starred/$owner/$name', requestType: 'NO CONTENT') + .then((v) => v.statusCode == HttpStatus.noContent); + bool isWatching = await auth + .fetchGitee('/user/subscriptions/$owner/$name', + requestType: 'NO CONTENT') + .then((v) => v.statusCode == HttpStatus.noContent); + StatusPayload statusPayload = StatusPayload(isWatching, isStarred); + return Tuple4(repo, readmeData, branches, statusPayload); }, bodyBuilder: (t, setState) { final p = t.item1; @@ -59,17 +75,48 @@ class GeRepoScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ RepoHeader( - avatarUrl: p.owner.avatarUrl, - avatarLink: '/gitee/${p.namespace.path}', - owner: p.namespace.path, - name: p.path, - description: p.description, - homepageUrl: p.homepage, - ), - CommonStyle.border, + avatarUrl: p.owner.avatarUrl, + avatarLink: '/gitee/${p.namespace.path}', + owner: p.namespace.path, + name: p.path, + description: p.description, + homepageUrl: p.homepage, + actions: [ + Row(children: [ + MutationButton( + active: t.item4.isWatching, + text: t.item4.isWatching ? 'Ignore' : 'Watch', + onTap: () async { + final String watchType = + t.item4.isWatching ? 'ignoring' : 'watching'; + await context.read().fetchGitee( + '/user/subscriptions/$owner/$name?watch_type=$watchType', + requestType: t.item4.isWatching ? 'DELETE' : 'PUT'); + setState(() { + t.item4.isWatching = !t.item4.isWatching; + }); + }, + ), + SizedBox(width: 8), + MutationButton( + active: t.item4.isStarred, + text: t.item4.isStarred ? 'Unstar' : 'Star', + onTap: () async { + await context.read().fetchGitee( + '/user/starred/$owner/$name', + requestType: t.item4.isStarred ? 'DELETE' : 'PUT'); + + setState(() { + t.item4.isStarred = !t.item4.isStarred; + }); + }, + ) + ]) + ]), Row( children: [ EntryItem( + count: p.watchersCount, text: 'Watchers', url: '/gitee/$owner/$name/watchers', ),