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:flutter/foundation.dart';
|
||||||
import 'package:gitea_client/model/ApiAccess.dart';
|
import 'package:gitea_client/model/ApiAccess.dart';
|
||||||
import 'package:gitea_client/model/issues.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/repository.dart';
|
||||||
import 'package:gitea_client/model/user.dart';
|
import 'package:gitea_client/model/user.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
@ -79,6 +80,29 @@ class GiteaService {
|
||||||
throw Exception('error fetching posts: ${response.statusCode}');
|
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{
|
Future<List<Repository>> getUserRepositories([int page = 1, int limit = 10]) async{
|
||||||
var response = await http.get(
|
var response = await http.get(
|
||||||
Uri.https(apiAccess.instance, "api/v1/user/repos", {
|
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:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:gitea_client/cubit/file_list_load_bloc.dart';
|
import 'package:gitea_client/cubit/file_list_load_bloc.dart';
|
||||||
import 'package:gitea_client/cubit/issues_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/issues.dart';
|
||||||
|
import 'package:gitea_client/model/pull.dart';
|
||||||
import 'package:gitea_client/model/repository.dart';
|
import 'package:gitea_client/model/repository.dart';
|
||||||
import 'package:gitea_client/model/user.dart';
|
import 'package:gitea_client/model/user.dart';
|
||||||
import 'package:gitea_client/service/gitea_service.dart';
|
import 'package:gitea_client/service/gitea_service.dart';
|
||||||
|
@ -491,9 +493,167 @@ class RepoPullRequests extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RepoPullRequests extends State<RepoPullRequests> {
|
class _RepoPullRequests extends State<RepoPullRequests> {
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// TODO: implement build
|
final media = MediaQuery.of(context).size;
|
||||||
throw UnimplementedError();
|
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