Basic repository list
This commit is contained in:
parent
31874f88a7
commit
69704a20f0
13 changed files with 629 additions and 31 deletions
47
lib/cubit/repo_cubit.dart
Normal file
47
lib/cubit/repo_cubit.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:gitea_client/cubit/repo_event.dart';
|
||||
import 'package:gitea_client/model/repository.dart';
|
||||
import 'package:gitea_client/service/gitea_service.dart';
|
||||
|
||||
part 'repo_state.dart';
|
||||
|
||||
class RepoCubit extends Cubit<RepoState> {
|
||||
RepoCubit() : super(RepoState());
|
||||
}
|
||||
|
||||
class RepoBloc extends Bloc<RepoEvent, RepoState> {
|
||||
RepoBloc({required this.giteaService}) : super(const RepoState()) {
|
||||
on<RepoFetched>(_onRepoFetched);
|
||||
}
|
||||
|
||||
final GiteaService giteaService;
|
||||
|
||||
Future<void> _onRepoFetched(RepoFetched event, Emitter<RepoState> emit) async {
|
||||
if (state.hasReachedMax) return;
|
||||
try {
|
||||
if (state.status == RepoStatus.initial) {
|
||||
final repos = await giteaService.getUserRepositories();
|
||||
return emit(state.copyWith(
|
||||
status: RepoStatus.success,
|
||||
repos: repos,
|
||||
loadedPages: 1,
|
||||
hasReachedMax: false,
|
||||
error_message: null,
|
||||
));
|
||||
}
|
||||
final repos = await giteaService.getUserRepositories(state.loadedPages+1);
|
||||
emit(repos.isEmpty
|
||||
? state.copyWith(hasReachedMax: true)
|
||||
: state.copyWith(
|
||||
status: RepoStatus.success,
|
||||
repos: List.of(state.repos)..addAll(repos),
|
||||
loadedPages: state.loadedPages+1,
|
||||
hasReachedMax: false,
|
||||
error_message: null,
|
||||
));
|
||||
} on Exception catch (e) {
|
||||
emit(state.copyWith(status: RepoStatus.failure,error_message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
8
lib/cubit/repo_event.dart
Normal file
8
lib/cubit/repo_event.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class RepoEvent extends Equatable {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class RepoFetched extends RepoEvent {}
|
41
lib/cubit/repo_state.dart
Normal file
41
lib/cubit/repo_state.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
part of 'repo_cubit.dart';
|
||||
|
||||
enum RepoStatus { initial, success, failure }
|
||||
|
||||
|
||||
class RepoState extends Equatable {
|
||||
const RepoState({
|
||||
this.status = RepoStatus.initial,
|
||||
this.repos = const <Repository>[],
|
||||
this.loadedPages = 0,
|
||||
this.hasReachedMax = false,
|
||||
this.error_message = null
|
||||
});
|
||||
|
||||
final RepoStatus status;
|
||||
final List<Repository> repos;
|
||||
final int loadedPages;
|
||||
final bool hasReachedMax;
|
||||
final String? error_message;
|
||||
|
||||
RepoState copyWith({
|
||||
RepoStatus? status,
|
||||
List<Repository>? repos,
|
||||
int? loadedPages,
|
||||
bool? hasReachedMax,
|
||||
String? error_message,
|
||||
}) {
|
||||
return RepoState(
|
||||
status: status ?? this.status,
|
||||
repos: repos ?? this.repos,
|
||||
loadedPages: loadedPages ?? this.loadedPages,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
error_message: error_message ?? this.error_message,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
List<Object> get props => [status, repos, hasReachedMax];
|
||||
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:gitea_client/model/user.dart';
|
||||
import 'package:gitea_client/widget/login_form.dart';
|
||||
import 'package:gitea_client/widget/login_status.dart';
|
||||
import 'package:gitea_client/widget/repo_list_page.dart';
|
||||
|
||||
import 'model/ApiAccess.dart';
|
||||
|
||||
|
@ -32,6 +34,11 @@ class MyApp extends StatelessWidget {
|
|||
apiAccess: route.arguments as ApiAccess,
|
||||
),
|
||||
);
|
||||
case "/repolist":
|
||||
return MaterialPageRoute(
|
||||
settings: const RouteSettings(name: "/repolist"),
|
||||
builder: (context) => RepoListPage(savedUser: route.arguments as SavedUser),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
|
251
lib/model/repository.dart
Normal file
251
lib/model/repository.dart
Normal file
|
@ -0,0 +1,251 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:gitea_client/model/user.dart';
|
||||
|
||||
class Repository extends Equatable {
|
||||
int id;
|
||||
User owner;
|
||||
String name;
|
||||
String? fullName;
|
||||
String? description;
|
||||
bool? empty;
|
||||
bool? private;
|
||||
bool? fork;
|
||||
bool? template;
|
||||
bool? mirror;
|
||||
int? size;
|
||||
String? language;
|
||||
String? languagesUrl;
|
||||
String? htmlUrl;
|
||||
String? sshUrl;
|
||||
String? cloneUrl;
|
||||
String? originalUrl;
|
||||
String? website;
|
||||
int? starsCount;
|
||||
int? forksCount;
|
||||
int? watchersCount;
|
||||
int? openIssuesCount;
|
||||
int? openPrCounter;
|
||||
int? releaseCounter;
|
||||
String? defaultBranch;
|
||||
bool? archived;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
Permissions? permissions;
|
||||
bool? hasIssues;
|
||||
InternalTracker? internalTracker;
|
||||
bool? hasWiki;
|
||||
bool? hasPullRequests;
|
||||
bool? hasProjects;
|
||||
bool? ignoreWhitespaceConflicts;
|
||||
bool? allowMergeCommits;
|
||||
bool? allowRebase;
|
||||
bool? allowRebaseExplicit;
|
||||
bool? allowSquashMerge;
|
||||
String? defaultMergeStyle;
|
||||
String? avatarUrl;
|
||||
bool? internal;
|
||||
String? mirrorInterval;
|
||||
String? mirrorUpdated;
|
||||
|
||||
Repository(
|
||||
{required this.id,
|
||||
required this.owner,
|
||||
required this.name,
|
||||
this.fullName,
|
||||
this.description,
|
||||
this.empty,
|
||||
this.private,
|
||||
this.fork,
|
||||
this.template,
|
||||
this.mirror,
|
||||
this.size,
|
||||
this.language,
|
||||
this.languagesUrl,
|
||||
this.htmlUrl,
|
||||
this.sshUrl,
|
||||
this.cloneUrl,
|
||||
this.originalUrl,
|
||||
this.website,
|
||||
this.starsCount,
|
||||
this.forksCount,
|
||||
this.watchersCount,
|
||||
this.openIssuesCount,
|
||||
this.openPrCounter,
|
||||
this.releaseCounter,
|
||||
this.defaultBranch,
|
||||
this.archived,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.permissions,
|
||||
this.hasIssues,
|
||||
this.internalTracker,
|
||||
this.hasWiki,
|
||||
this.hasPullRequests,
|
||||
this.hasProjects,
|
||||
this.ignoreWhitespaceConflicts,
|
||||
this.allowMergeCommits,
|
||||
this.allowRebase,
|
||||
this.allowRebaseExplicit,
|
||||
this.allowSquashMerge,
|
||||
this.defaultMergeStyle,
|
||||
this.avatarUrl,
|
||||
this.internal,
|
||||
this.mirrorInterval,
|
||||
this.mirrorUpdated});
|
||||
|
||||
Repository.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'],
|
||||
id = json['id'],
|
||||
owner = User.fromJson(json['owner']) {
|
||||
fullName = json['full_name'];
|
||||
description = json['description'];
|
||||
empty = json['empty'];
|
||||
private = json['private'];
|
||||
fork = json['fork'];
|
||||
template = json['template'];
|
||||
mirror = json['mirror'];
|
||||
size = json['size'];
|
||||
language = json['language'];
|
||||
languagesUrl = json['languages_url'];
|
||||
htmlUrl = json['html_url'];
|
||||
sshUrl = json['ssh_url'];
|
||||
cloneUrl = json['clone_url'];
|
||||
originalUrl = json['original_url'];
|
||||
website = json['website'];
|
||||
starsCount = json['stars_count'];
|
||||
forksCount = json['forks_count'];
|
||||
watchersCount = json['watchers_count'];
|
||||
openIssuesCount = json['open_issues_count'];
|
||||
openPrCounter = json['open_pr_counter'];
|
||||
releaseCounter = json['release_counter'];
|
||||
defaultBranch = json['default_branch'];
|
||||
archived = json['archived'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
permissions = json['permissions'] != null
|
||||
? Permissions.fromJson(json['permissions'])
|
||||
: null;
|
||||
hasIssues = json['has_issues'];
|
||||
internalTracker = json['internal_tracker'] != null
|
||||
? InternalTracker.fromJson(json['internal_tracker'])
|
||||
: null;
|
||||
hasWiki = json['has_wiki'];
|
||||
hasPullRequests = json['has_pull_requests'];
|
||||
hasProjects = json['has_projects'];
|
||||
ignoreWhitespaceConflicts = json['ignore_whitespace_conflicts'];
|
||||
allowMergeCommits = json['allow_merge_commits'];
|
||||
allowRebase = json['allow_rebase'];
|
||||
allowRebaseExplicit = json['allow_rebase_explicit'];
|
||||
allowSquashMerge = json['allow_squash_merge'];
|
||||
defaultMergeStyle = json['default_merge_style'];
|
||||
avatarUrl = json['avatar_url'];
|
||||
internal = json['internal'];
|
||||
mirrorInterval = json['mirror_interval'];
|
||||
mirrorUpdated = json['mirror_updated'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['owner'] = owner.toJson();
|
||||
data['name'] = name;
|
||||
data['full_name'] = fullName;
|
||||
data['description'] = description;
|
||||
data['empty'] = empty;
|
||||
data['private'] = private;
|
||||
data['fork'] = fork;
|
||||
data['template'] = template;
|
||||
data['mirror'] = mirror;
|
||||
data['size'] = size;
|
||||
data['language'] = language;
|
||||
data['languages_url'] = languagesUrl;
|
||||
data['html_url'] = htmlUrl;
|
||||
data['ssh_url'] = sshUrl;
|
||||
data['clone_url'] = cloneUrl;
|
||||
data['original_url'] = originalUrl;
|
||||
data['website'] = website;
|
||||
data['stars_count'] = starsCount;
|
||||
data['forks_count'] = forksCount;
|
||||
data['watchers_count'] = watchersCount;
|
||||
data['open_issues_count'] = openIssuesCount;
|
||||
data['open_pr_counter'] = openPrCounter;
|
||||
data['release_counter'] = releaseCounter;
|
||||
data['default_branch'] = defaultBranch;
|
||||
data['archived'] = archived;
|
||||
data['created_at'] = createdAt;
|
||||
data['updated_at'] = updatedAt;
|
||||
if (permissions != null) {
|
||||
data['permissions'] = permissions!.toJson();
|
||||
}
|
||||
data['has_issues'] = hasIssues;
|
||||
if (internalTracker != null) {
|
||||
data['internal_tracker'] = internalTracker!.toJson();
|
||||
}
|
||||
data['has_wiki'] = hasWiki;
|
||||
data['has_pull_requests'] = hasPullRequests;
|
||||
data['has_projects'] = hasProjects;
|
||||
data['ignore_whitespace_conflicts'] = ignoreWhitespaceConflicts;
|
||||
data['allow_merge_commits'] = allowMergeCommits;
|
||||
data['allow_rebase'] = allowRebase;
|
||||
data['allow_rebase_explicit'] = allowRebaseExplicit;
|
||||
data['allow_squash_merge'] = allowSquashMerge;
|
||||
data['default_merge_style'] = defaultMergeStyle;
|
||||
data['avatar_url'] = avatarUrl;
|
||||
data['internal'] = internal;
|
||||
data['mirror_interval'] = mirrorInterval;
|
||||
data['mirror_updated'] = mirrorUpdated;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [id, owner, name];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class AuthenticatedUser {
|
||||
import 'package:gitea_client/model/ApiAccess.dart';
|
||||
|
||||
class User {
|
||||
int? id;
|
||||
String? login;
|
||||
String? fullName;
|
||||
|
@ -22,7 +24,7 @@ class AuthenticatedUser {
|
|||
int? starredReposCount;
|
||||
String? username;
|
||||
|
||||
AuthenticatedUser(
|
||||
User(
|
||||
{this.id,
|
||||
this.login,
|
||||
this.fullName,
|
||||
|
@ -44,7 +46,7 @@ class AuthenticatedUser {
|
|||
this.starredReposCount,
|
||||
this.username});
|
||||
|
||||
AuthenticatedUser.fromJson(Map<String, dynamic> json) {
|
||||
User.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
login = json['login'];
|
||||
fullName = json['full_name'];
|
||||
|
@ -92,3 +94,9 @@ class AuthenticatedUser {
|
|||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SavedUser {
|
||||
final User authedUser;
|
||||
final ApiAccess apiAccess;
|
||||
const SavedUser({required this.authedUser,required this.apiAccess});
|
||||
}
|
||||
|
|
|
@ -5,12 +5,12 @@ import '../model/user.dart';
|
|||
|
||||
class AuthenticationChecker {
|
||||
final ApiAccess apiAccess;
|
||||
late GiteaServie service;
|
||||
late GiteaService service;
|
||||
AuthenticationChecker(this.apiAccess) {
|
||||
service = GiteaServie(apiAccess.instance, apiAccess.token);
|
||||
service = GiteaService(apiAccess: apiAccess);
|
||||
}
|
||||
|
||||
Future<AuthenticatedUser> getAuthenticatedUserOrError() async{
|
||||
Future<User> getAuthenticatedUserOrError() async{
|
||||
final user = await service.getAuthenticatedUser();
|
||||
if(user.username != null) {
|
||||
return user;
|
||||
|
|
|
@ -1,25 +1,43 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:gitea_client/model/ApiAccess.dart';
|
||||
import 'package:gitea_client/model/repository.dart';
|
||||
import 'package:gitea_client/model/user.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
AuthenticatedUser _parseAuthenticatedUserResponse(String message){
|
||||
return AuthenticatedUser.fromJson(jsonDecode(message));
|
||||
User _parseAuthenticatedUserResponse(String message){
|
||||
return User.fromJson(jsonDecode(message));
|
||||
}
|
||||
|
||||
class GiteaServie {
|
||||
final String apiUrl;
|
||||
final String _token;
|
||||
class GiteaService {
|
||||
final ApiAccess apiAccess;
|
||||
|
||||
const GiteaServie(this.apiUrl, this._token);
|
||||
const GiteaService({ required this.apiAccess});
|
||||
|
||||
Future<AuthenticatedUser> getAuthenticatedUser() async {
|
||||
Future<User> getAuthenticatedUser() async {
|
||||
var response = await http.get(
|
||||
Uri.https(apiUrl, "api/v1/user", {
|
||||
"token": _token,
|
||||
Uri.https(apiAccess.instance, "api/v1/user", {
|
||||
"token": apiAccess.token,
|
||||
}),
|
||||
);
|
||||
return compute(_parseAuthenticatedUserResponse, response.body);
|
||||
}
|
||||
//
|
||||
Future<List<Repository>> getUserRepositories([int page = 1, int limit = 10]) async{
|
||||
var response = await http.get(
|
||||
Uri.https(apiAccess.instance, "api/v1/user/repos", {
|
||||
"token": apiAccess.token,
|
||||
"page" : page.toString(),
|
||||
"limit" : limit.toString(),
|
||||
}),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(response.body) as List;
|
||||
return body.map((dynamic json) {
|
||||
return Repository.fromJson(json);
|
||||
}).toList();
|
||||
}
|
||||
throw Exception('error fetching posts');
|
||||
}
|
||||
}
|
|
@ -6,20 +6,21 @@ import '../service/AuthenticationChecker.dart';
|
|||
|
||||
class StatefulLoginStatus extends StatefulWidget {
|
||||
final ApiAccess apiAccess;
|
||||
const StatefulLoginStatus({Key? key, required this.apiAccess}) : super(key: key);
|
||||
|
||||
const StatefulLoginStatus({Key? key, required this.apiAccess})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_StatefulLoginStatus createState() => _StatefulLoginStatus();
|
||||
}
|
||||
|
||||
class _StatefulLoginStatus extends State<StatefulLoginStatus> {
|
||||
|
||||
|
||||
Future<AuthenticatedUser>? userRequest;
|
||||
Future<User>? userRequest;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
userRequest = AuthenticationChecker(widget.apiAccess).getAuthenticatedUserOrError();
|
||||
userRequest =
|
||||
AuthenticationChecker(widget.apiAccess).getAuthenticatedUserOrError();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -33,30 +34,65 @@ class _StatefulLoginStatus extends State<StatefulLoginStatus> {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
FutureBuilder<AuthenticatedUser>(
|
||||
children: [
|
||||
FutureBuilder<User>(
|
||||
future: userRequest,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError){
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"Hiba történt: ${snapshot.error}"
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: const Icon(
|
||||
Icons.error,
|
||||
color: Colors.redAccent,
|
||||
size: 60,
|
||||
),
|
||||
),
|
||||
Text("Hiba történt: ${snapshot.error}",
|
||||
style: Theme.of(context).textTheme.headline6),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasData){
|
||||
} else if (snapshot.hasData) {
|
||||
var user = snapshot.data!;
|
||||
final username = user.username;
|
||||
return Text("Logged in as $username");
|
||||
|
||||
final avatar = user.avatarUrl;
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 60,
|
||||
),
|
||||
Text(
|
||||
"Logged in as $username",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Image.network(
|
||||
avatar!,
|
||||
width: 100,
|
||||
)),
|
||||
ElevatedButton(
|
||||
onPressed: () => {
|
||||
Navigator.pushNamed(context, "/repolist",arguments:
|
||||
SavedUser(authedUser: user, apiAccess: widget.apiAccess))
|
||||
},
|
||||
child: const Text("Start using Gitea"))
|
||||
],
|
||||
));
|
||||
} else {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
103
lib/widget/repo_list.dart
Normal file
103
lib/widget/repo_list.dart
Normal file
|
@ -0,0 +1,103 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gitea_client/cubit/repo_event.dart';
|
||||
import 'package:gitea_client/model/repository.dart';
|
||||
|
||||
import '../cubit/repo_cubit.dart';
|
||||
|
||||
class ReposList extends StatefulWidget {
|
||||
@override
|
||||
_ReposListState createState() => _ReposListState();
|
||||
}
|
||||
|
||||
class _ReposListState extends State<ReposList> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RepoBloc, RepoState>(
|
||||
builder: (context, state) {
|
||||
switch (state.status) {
|
||||
case RepoStatus.failure:
|
||||
String error_message = state.error_message!;
|
||||
return Center(child: Text('failed to fetch $error_message'));
|
||||
case RepoStatus.success:
|
||||
if (state.repos.isEmpty) {
|
||||
return const Center(child: Text('no posts'));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return index >= state.repos.length
|
||||
? BottomLoader()
|
||||
: RepoListItem(post: state.repos[index]);
|
||||
},
|
||||
itemCount: state.hasReachedMax
|
||||
? state.repos.length
|
||||
: state.repos.length + 1,
|
||||
controller: _scrollController,
|
||||
);
|
||||
default:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController
|
||||
..removeListener(_onScroll)
|
||||
..dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_isBottom) context.read<RepoBloc>().add(RepoFetched());
|
||||
}
|
||||
|
||||
bool get _isBottom {
|
||||
if (!_scrollController.hasClients) return false;
|
||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||
final currentScroll = _scrollController.offset;
|
||||
return currentScroll >= (maxScroll * 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomLoader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 1.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RepoListItem extends StatelessWidget {
|
||||
const RepoListItem({Key? key, required this.post}) : super(key: key);
|
||||
|
||||
final Repository post;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Material(
|
||||
child: ListTile(
|
||||
leading: Text('${post.id}', style: textTheme.caption),
|
||||
title: Text(post.name),
|
||||
isThreeLine: true,
|
||||
subtitle: Text(post.owner.username!),
|
||||
dense: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
34
lib/widget/repo_list_page.dart
Normal file
34
lib/widget/repo_list_page.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gitea_client/cubit/repo_event.dart';
|
||||
import 'package:gitea_client/cubit/repo_cubit.dart';
|
||||
import 'package:gitea_client/model/user.dart';
|
||||
import 'package:gitea_client/service/gitea_service.dart';
|
||||
import 'package:gitea_client/widget/repo_list.dart';
|
||||
|
||||
class RepoListPage extends StatefulWidget {
|
||||
final SavedUser savedUser;
|
||||
|
||||
const RepoListPage({Key? key,required this.savedUser}) : super (key: key);
|
||||
|
||||
@override
|
||||
_RepoListPage createState() => _RepoListPage();
|
||||
|
||||
}
|
||||
|
||||
class _RepoListPage extends State<RepoListPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Repositories"),
|
||||
leading: IconButton(icon: const Icon(Icons.menu),onPressed: ()=> {},),
|
||||
),
|
||||
body: BlocProvider(
|
||||
create: (_) => RepoBloc(giteaService: GiteaService(apiAccess: widget.savedUser.apiAccess))..add(RepoFetched()),
|
||||
child: ReposList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
42
pubspec.lock
42
pubspec.lock
|
@ -29,6 +29,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bloc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.3"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -113,6 +120,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -132,6 +146,13 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -238,6 +259,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -259,6 +287,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -313,6 +348,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
stream_transform:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: stream_transform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -39,6 +39,9 @@ dependencies:
|
|||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
equatable: ^2.0.3
|
||||
flutter_bloc: ^8.0.1
|
||||
stream_transform: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue