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:flutter/material.dart';
|
||||||
|
import 'package:gitea_client/model/user.dart';
|
||||||
import 'package:gitea_client/widget/login_form.dart';
|
import 'package:gitea_client/widget/login_form.dart';
|
||||||
import 'package:gitea_client/widget/login_status.dart';
|
import 'package:gitea_client/widget/login_status.dart';
|
||||||
|
import 'package:gitea_client/widget/repo_list_page.dart';
|
||||||
|
|
||||||
import 'model/ApiAccess.dart';
|
import 'model/ApiAccess.dart';
|
||||||
|
|
||||||
|
@ -32,6 +34,11 @@ class MyApp extends StatelessWidget {
|
||||||
apiAccess: route.arguments as ApiAccess,
|
apiAccess: route.arguments as ApiAccess,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
case "/repolist":
|
||||||
|
return MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: "/repolist"),
|
||||||
|
builder: (context) => RepoListPage(savedUser: route.arguments as SavedUser),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
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';
|
import 'dart:convert';
|
||||||
|
|
||||||
class AuthenticatedUser {
|
import 'package:gitea_client/model/ApiAccess.dart';
|
||||||
|
|
||||||
|
class User {
|
||||||
int? id;
|
int? id;
|
||||||
String? login;
|
String? login;
|
||||||
String? fullName;
|
String? fullName;
|
||||||
|
@ -22,7 +24,7 @@ class AuthenticatedUser {
|
||||||
int? starredReposCount;
|
int? starredReposCount;
|
||||||
String? username;
|
String? username;
|
||||||
|
|
||||||
AuthenticatedUser(
|
User(
|
||||||
{this.id,
|
{this.id,
|
||||||
this.login,
|
this.login,
|
||||||
this.fullName,
|
this.fullName,
|
||||||
|
@ -44,7 +46,7 @@ class AuthenticatedUser {
|
||||||
this.starredReposCount,
|
this.starredReposCount,
|
||||||
this.username});
|
this.username});
|
||||||
|
|
||||||
AuthenticatedUser.fromJson(Map<String, dynamic> json) {
|
User.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
login = json['login'];
|
login = json['login'];
|
||||||
fullName = json['full_name'];
|
fullName = json['full_name'];
|
||||||
|
@ -92,3 +94,9 @@ class AuthenticatedUser {
|
||||||
return data;
|
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 {
|
class AuthenticationChecker {
|
||||||
final ApiAccess apiAccess;
|
final ApiAccess apiAccess;
|
||||||
late GiteaServie service;
|
late GiteaService service;
|
||||||
AuthenticationChecker(this.apiAccess) {
|
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();
|
final user = await service.getAuthenticatedUser();
|
||||||
if(user.username != null) {
|
if(user.username != null) {
|
||||||
return user;
|
return user;
|
||||||
|
|
|
@ -1,25 +1,43 @@
|
||||||
import 'dart:convert';
|
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/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;
|
||||||
|
|
||||||
AuthenticatedUser _parseAuthenticatedUserResponse(String message){
|
User _parseAuthenticatedUserResponse(String message){
|
||||||
return AuthenticatedUser.fromJson(jsonDecode(message));
|
return User.fromJson(jsonDecode(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
class GiteaServie {
|
class GiteaService {
|
||||||
final String apiUrl;
|
final ApiAccess apiAccess;
|
||||||
final String _token;
|
|
||||||
|
|
||||||
const GiteaServie(this.apiUrl, this._token);
|
const GiteaService({ required this.apiAccess});
|
||||||
|
|
||||||
Future<AuthenticatedUser> getAuthenticatedUser() async {
|
Future<User> getAuthenticatedUser() async {
|
||||||
var response = await http.get(
|
var response = await http.get(
|
||||||
Uri.https(apiUrl, "api/v1/user", {
|
Uri.https(apiAccess.instance, "api/v1/user", {
|
||||||
"token": _token,
|
"token": apiAccess.token,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return compute(_parseAuthenticatedUserResponse, response.body);
|
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 {
|
class StatefulLoginStatus extends StatefulWidget {
|
||||||
final ApiAccess apiAccess;
|
final ApiAccess apiAccess;
|
||||||
const StatefulLoginStatus({Key? key, required this.apiAccess}) : super(key: key);
|
|
||||||
|
const StatefulLoginStatus({Key? key, required this.apiAccess})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StatefulLoginStatus createState() => _StatefulLoginStatus();
|
_StatefulLoginStatus createState() => _StatefulLoginStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StatefulLoginStatus extends State<StatefulLoginStatus> {
|
class _StatefulLoginStatus extends State<StatefulLoginStatus> {
|
||||||
|
Future<User>? userRequest;
|
||||||
|
|
||||||
Future<AuthenticatedUser>? userRequest;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
userRequest = AuthenticationChecker(widget.apiAccess).getAuthenticatedUserOrError();
|
userRequest =
|
||||||
|
AuthenticationChecker(widget.apiAccess).getAuthenticatedUserOrError();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,27 +35,62 @@ class _StatefulLoginStatus extends State<StatefulLoginStatus> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FutureBuilder<AuthenticatedUser>(
|
FutureBuilder<User>(
|
||||||
future: userRequest,
|
future: userRequest,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Column(
|
||||||
"Hiba történt: ${snapshot.error}"
|
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!;
|
var user = snapshot.data!;
|
||||||
final username = user.username;
|
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 {
|
} else {
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(),
|
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"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.2"
|
version: "2.8.2"
|
||||||
|
bloc:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bloc
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.3"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -113,6 +120,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.2.3"
|
||||||
|
equatable:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -132,6 +146,13 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -238,6 +259,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -259,6 +287,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -313,6 +348,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -39,6 +39,9 @@ dependencies:
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
|
equatable: ^2.0.3
|
||||||
|
flutter_bloc: ^8.0.1
|
||||||
|
stream_transform: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue