Merge branch 'pr_view' into l10n

This commit is contained in:
Balazs Toldi 2022-05-22 21:53:41 +02:00
commit 48f97d73a4
Signed by: Bazsalanszky
GPG key ID: 6C7D440036F99D58
6 changed files with 519 additions and 2 deletions

55
lib/cubit/pulls_bloc.dart Normal file
View file

@ -0,0 +1,55 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:gitea_client/model/pull.dart';
import 'package:stream_transform/stream_transform.dart';
import '../service/gitea_service.dart';
part 'pulls_event.dart';
part 'pulls_state.dart';
class PullRequestBloc extends Bloc<PullRequestEvent, PullRequestState> {
final GiteaService giteaService;
final String istate;
final String repoFullName;
PullRequestBloc(this.giteaService,this.repoFullName,this.istate) : super(const PullRequestState()) {
on<PullRequestsFetched>(_onIssuesFetched,
transformer: (events, mapper) => events.switchMap(mapper),);
}
Future<void> _onIssuesFetched(PullRequestsFetched event, Emitter<PullRequestState> emit) async {
if (state.hasReachedMax) return;
try {
if (state.status == PullRequestStatus.initial) {
final pulls = await giteaService.getRepoPulls(owner: repoFullName.split('/')[0],repo: repoFullName.split('/')[1],state: istate,page: 1, limit: 10);
return emit(state.copyWith(
status: PullRequestStatus.success,
pulls: pulls,
loadedPages: 1,
hasReachedMax: false,
error_message: null,
));
}
final pulls = await giteaService.getRepoPulls(owner: repoFullName.split('/')[0],repo: repoFullName.split('/')[1],state: istate,page: state.loadedPages+1, limit: 10);
final prList = List.of(state.pulls);
pulls.forEach((element) {
if(prList.where((selement) => selement.id == element.id).isEmpty) {
prList.add(element);
}
});
emit(pulls.isEmpty
? state.copyWith(hasReachedMax: true)
: state.copyWith(
status: PullRequestStatus.success,
pulls: prList,
loadedPages: state.loadedPages+1,
hasReachedMax: false,
error_message: null,
));
} on Exception catch (e) {
emit(state.copyWith(status: PullRequestStatus.failure,error_message: e.toString()));
}
}
}

View file

@ -0,0 +1,9 @@
part of 'pulls_bloc.dart';
abstract class PullRequestEvent extends Equatable {
const PullRequestEvent();
@override
List<Object?> get props => [];
}
class PullRequestsFetched extends PullRequestEvent {}

View file

@ -0,0 +1,39 @@
part of 'pulls_bloc.dart';
enum PullRequestStatus { initial, success, failure }
class PullRequestState extends Equatable {
const PullRequestState({
this.status = PullRequestStatus.initial,
this.pulls = const <PullRequest>[],
this.loadedPages = 0,
this.hasReachedMax = false,
this.error_message = null
});
final PullRequestStatus status;
final List<PullRequest> pulls;
final int loadedPages;
final bool hasReachedMax;
final String? error_message;
PullRequestState copyWith({
PullRequestStatus? status,
List<PullRequest>? pulls,
int? loadedPages,
bool? hasReachedMax,
String? error_message,
}) {
return PullRequestState(
status: status ?? this.status,
pulls: pulls ?? this.pulls,
loadedPages: loadedPages ?? this.loadedPages,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
error_message: error_message ?? this.error_message,
);
}
@override
List<Object> get props => [status, pulls, hasReachedMax];
}

230
lib/model/pull.dart Normal file
View file

@ -0,0 +1,230 @@
import 'package:gitea_client/model/repository.dart';
import 'package:gitea_client/model/user.dart';
import 'issues.dart';
class PullRequest {
int id;
String url;
int number;
User user;
String title;
String body;
List<Labels>? labels;
Milestone? milestone;
User? assignee;
User? assignees;
String state;
bool isLocked;
int comments;
String htmlUrl;
String diffUrl;
String patchUrl;
bool? mergeable;
bool merged;
String? mergedAt;
String? mergeCommitSha;
User? mergedBy;
bool? allowMaintainerEdit;
Base? base;
Base? head;
String? mergeBase;
String? dueDate;
String createdAt;
String updatedAt;
String? closedAt;
PullRequest(
{required this.id,
required this.url,
required this.number,
required this.user,
required this.title,
required this.body,
this.labels,
this.milestone,
this.assignee,
this.assignees,
required this.state,
required this.isLocked,
required this.comments,
required this.htmlUrl,
required this.diffUrl,
required this.patchUrl,
this.mergeable,
required this.merged,
this.mergedAt,
this.mergeCommitSha,
this.mergedBy,
this.allowMaintainerEdit,
this.base,
this.head,
this.mergeBase,
this.dueDate,
required this.createdAt,
required this.updatedAt,
this.closedAt});
PullRequest.fromJson(Map<String, dynamic> json)
: id = json['id'],
url = json['url'],
number = json['number'],
user = User.fromJson(json['user']),
title = json['title'],
body = json['body'],
comments = json['comments'],
htmlUrl = json['html_url'],
diffUrl = json['diff_url'],
createdAt = json['created_at'],
updatedAt = json['updated_at'],
state = json['state'],
isLocked = json['is_locked'],
patchUrl = json['patch_url'],
merged = json['merged'] {
if (json['labels'] != null) {
labels = <Labels>[];
json['labels'].forEach((v) {
labels!.add( Labels.fromJson(v));
});
}
milestone = json['milestone'] != null
? Milestone.fromJson(json['milestone'])
: null;
assignee = json['assignee'];
assignees = json['assignees'];
mergeable = json['mergeable'];
mergedAt = json['merged_at'];
mergeCommitSha = json['merge_commit_sha'];
mergedBy =
json['merged_by'] != null ? User.fromJson(json['merged_by']) : null;
allowMaintainerEdit = json['allow_maintainer_edit'];
base = json['base'] != null ? Base.fromJson(json['base']) : null;
head = json['head'] != null ? Base.fromJson(json['head']) : null;
mergeBase = json['merge_base'];
dueDate = json['due_date'];
closedAt = json['closed_at'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['url'] = url;
data['number'] = number;
data['user'] = user.toJson();
data['title'] = title;
data['body'] = body;
if (labels != null) {
data['labels'] = labels!.map((v) => v.toJson()).toList();
}
if (milestone != null) {
data['milestone'] = milestone!.toJson();
}
data['assignee'] = assignee;
data['assignees'] = assignees;
data['state'] = state;
data['is_locked'] = isLocked;
data['comments'] = comments;
data['html_url'] = htmlUrl;
data['diff_url'] = diffUrl;
data['patch_url'] = patchUrl;
data['mergeable'] = mergeable;
data['merged'] = merged;
data['merged_at'] = mergedAt;
data['merge_commit_sha'] = mergeCommitSha;
if (mergedBy != null) {
data['merged_by'] = mergedBy!.toJson();
}
data['allow_maintainer_edit'] = allowMaintainerEdit;
if (base != null) {
data['base'] = base!.toJson();
}
if (head != null) {
data['head'] = head!.toJson();
}
data['merge_base'] = mergeBase;
data['due_date'] = dueDate;
data['created_at'] = createdAt;
data['updated_at'] = updatedAt;
data['closed_at'] = closedAt;
return data;
}
}
class Base {
String? label;
String? ref;
String? sha;
int? repoId;
Repository? repo;
Base({this.label, this.ref, this.sha, this.repoId, this.repo});
Base.fromJson(Map<String, dynamic> json) {
label = json['label'];
ref = json['ref'];
sha = json['sha'];
repoId = json['repo_id'];
repo = json['repo'] != null ? Repository.fromJson(json['repo']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['label'] = label;
data['ref'] = ref;
data['sha'] = sha;
data['repo_id'] = repoId;
if (repo != null) {
data['repo'] = repo!.toJson();
}
return data;
}
}
class Permissions {
bool? admin;
bool? push;
bool? pull;
Permissions({this.admin, this.push, this.pull});
Permissions.fromJson(Map<String, dynamic> json) {
admin = json['admin'];
push = json['push'];
pull = json['pull'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['admin'] = admin;
data['push'] = push;
data['pull'] = pull;
return data;
}
}
class InternalTracker {
bool? enableTimeTracker;
bool? allowOnlyContributorsToTrackTime;
bool? enableIssueDependencies;
InternalTracker(
{this.enableTimeTracker,
this.allowOnlyContributorsToTrackTime,
this.enableIssueDependencies});
InternalTracker.fromJson(Map<String, dynamic> json) {
enableTimeTracker = json['enable_time_tracker'];
allowOnlyContributorsToTrackTime =
json['allow_only_contributors_to_track_time'];
enableIssueDependencies = json['enable_issue_dependencies'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['enable_time_tracker'] = enableTimeTracker;
data['allow_only_contributors_to_track_time'] =
allowOnlyContributorsToTrackTime;
data['enable_issue_dependencies'] = enableIssueDependencies;
return data;
}
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:gitea_client/model/ApiAccess.dart';
import 'package:gitea_client/model/issues.dart';
import 'package:gitea_client/model/pull.dart';
import 'package:gitea_client/model/repository.dart';
import 'package:gitea_client/model/user.dart';
import 'package:http/http.dart' as http;
@ -79,6 +80,29 @@ class GiteaService {
throw Exception('error fetching posts: ${response.statusCode}');
}
Future<List<PullRequest>> getRepoPulls({required String owner,required String repo,required String state,int page = 1, int limit = 10}) async{
if(state != "all" && state != "closed" && state != "open") {
throw Exception("Wrong state provided: $state");
}
var response = await http.get(
Uri.https(apiAccess.instance, "api/v1/repos/$owner/$repo/pulls", {
"token": apiAccess.token,
"state": state,
"page" : page.toString(),
"limit" : limit.toString(),
}),
);
if (response.statusCode == 200) {
final body = json.decode(response.body) as List;
var result = body.map((dynamic json) {
return PullRequest.fromJson(json);
}).toList();
return result;
}
throw Exception('error fetching posts: ${response.statusCode}');
}
Future<List<Repository>> getUserRepositories([int page = 1, int limit = 10]) async{
var response = await http.get(
Uri.https(apiAccess.instance, "api/v1/user/repos", {

View file

@ -6,7 +6,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gitea_client/cubit/file_list_load_bloc.dart';
import 'package:gitea_client/cubit/issues_bloc.dart';
import 'package:gitea_client/cubit/pulls_bloc.dart';
import 'package:gitea_client/model/issues.dart';
import 'package:gitea_client/model/pull.dart';
import 'package:gitea_client/model/repository.dart';
import 'package:gitea_client/model/user.dart';
import 'package:gitea_client/service/gitea_service.dart';
@ -491,9 +493,167 @@ class RepoPullRequests extends StatefulWidget {
}
class _RepoPullRequests extends State<RepoPullRequests> {
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
final media = MediaQuery.of(context).size;
return DefaultTabController(
length: 2,
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Container(
color: Colors.lightGreen,
child: SafeArea(
child: Column(
children: <Widget>[
Expanded(child: Container()),
const TabBar(
tabs: [Text("Open"), Text("Closed")],
),
],
),
),
),
),
body: TabBarView(
children: <Widget>[
Column(
children: [
Center(
child: SizedBox(
width: (media.width > 600) ? media.width * 0.5 : media.width * 0.9,
height: media.height-kToolbarHeight-kBottomNavigationBarHeight-kTextTabBarHeight-50,
child: BlocProvider(
create: (context) {
return PullRequestBloc(GiteaService(apiAccess: widget.user.apiAccess),widget.repo.fullName!,"open")
..add(PullRequestsFetched());
},
child: BlocBuilder<PullRequestBloc, PullRequestState>(
builder: (context, state) {
switch (state.status) {
case PullRequestStatus.failure:
String error_message = state.error_message!;
return Center(child: Text('failed to fetch $error_message'));
case PullRequestStatus.success:
if (state.pulls.isEmpty) {
return const Center(
child: Text('No issues found'));
}
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return PullRequestListItem(
issue: state.pulls[index],
onTap: () {},
);
},
itemCount: state.pulls.length,
controller: _scrollController,
);
default:
return const Center(child: CircularProgressIndicator());
}
},
),
),
),
)
],
),
Column(
children: [
Center(
child: SizedBox(
width: (media.width > 600) ? media.width * 0.5 : media.width * 0.9,
height: media.height-kToolbarHeight-kBottomNavigationBarHeight-kTextTabBarHeight-50,
child: BlocProvider(
create: (context) {
return PullRequestBloc(GiteaService(apiAccess: widget.user.apiAccess),widget.repo.fullName!,"closed")
..add(PullRequestsFetched());
},
child: BlocBuilder<PullRequestBloc, PullRequestState>(
builder: (context, state) {
switch (state.status) {
case PullRequestStatus.failure:
String error_message = state.error_message!;
return Center(child: Text('failed to fetch $error_message'));
case PullRequestStatus.success:
if (state.pulls.isEmpty) {
return const Center(
child: Text('No issues found'));
}
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return PullRequestListItem(
issue: state.pulls[index],
onTap: () {},
);
},
itemCount: state.pulls.length,
controller: _scrollController,
);
default:
return const Center(child: CircularProgressIndicator());
}
},
),
),
),
)
],
)
],
),
),
),
);
}
@override
void dispose() {
_scrollController
..removeListener(_onScroll)
..dispose();
super.dispose();
}
void _onScroll() {
if (_isBottom) context.read<IssuesBloc>().add(IssuesFetched());
}
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
}
class PullRequestListItem extends StatelessWidget {
final PullRequest issue;
final Function onTap;
const PullRequestListItem({Key? key, required this.issue, required this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
leading: Text("#${issue.number}"),
title: Text(issue.title),
onTap: () => {onTap()},
);
}
}