Merge branch 'pr_view' into l10n
This commit is contained in:
commit
48f97d73a4
6 changed files with 519 additions and 2 deletions
55
lib/cubit/pulls_bloc.dart
Normal file
55
lib/cubit/pulls_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
9
lib/cubit/pulls_event.dart
Normal file
9
lib/cubit/pulls_event.dart
Normal 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 {}
|
39
lib/cubit/pulls_state.dart
Normal file
39
lib/cubit/pulls_state.dart
Normal 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
230
lib/model/pull.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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", {
|
||||
|
|
|
@ -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()},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue