diff --git a/lib/cubit/issues_bloc.dart b/lib/cubit/issues_bloc.dart new file mode 100644 index 0000000..664178b --- /dev/null +++ b/lib/cubit/issues_bloc.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:gitea_client/model/issues.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import '../service/gitea_service.dart'; + +part 'issues_event.dart'; +part 'issues_state.dart'; + +class IssuesBloc extends Bloc { + final GiteaService giteaService; + final String istate; + final String repoFullName; + IssuesBloc(this.giteaService,this.repoFullName,this.istate) : super(const IssueState()) { + on(_onIssuesFetched, + transformer: (events, mapper) => events.switchMap(mapper),); + } + + Future _onIssuesFetched(IssuesFetched event, Emitter emit) async { + if (state.hasReachedMax) return; + try { + if (state.status == IssueStatus.initial) { + final issues = await giteaService.getRepoIssues(repoFullname: repoFullName,state: istate,page: 1, limit: 10); + return emit(state.copyWith( + status: IssueStatus.success, + issues: issues, + loadedPages: 1, + hasReachedMax: false, + error_message: null, + )); + } + final issues = await giteaService.getRepoIssues(repoFullname: repoFullName,state: istate,page: state.loadedPages+1, limit: 10); + final issueList = List.of(state.issues); + issues.forEach((element) { + if(issueList.where((selement) => selement.id == element.id).isEmpty) { + issueList.add(element); + } + }); + emit(issues.isEmpty + ? state.copyWith(hasReachedMax: true) + : state.copyWith( + status: IssueStatus.success, + issues: issueList, + loadedPages: state.loadedPages+1, + hasReachedMax: false, + error_message: null, + )); + } on Exception catch (e) { + emit(state.copyWith(status: IssueStatus.failure,error_message: e.toString())); + } + } +} diff --git a/lib/cubit/issues_event.dart b/lib/cubit/issues_event.dart new file mode 100644 index 0000000..b006713 --- /dev/null +++ b/lib/cubit/issues_event.dart @@ -0,0 +1,9 @@ +part of 'issues_bloc.dart'; + +abstract class IssuesEvent extends Equatable { + const IssuesEvent(); + @override + List get props => []; +} + +class IssuesFetched extends IssuesEvent {} diff --git a/lib/cubit/issues_state.dart b/lib/cubit/issues_state.dart new file mode 100644 index 0000000..993cb57 --- /dev/null +++ b/lib/cubit/issues_state.dart @@ -0,0 +1,39 @@ +part of 'issues_bloc.dart'; + +enum IssueStatus { initial, success, failure } + +class IssueState extends Equatable { + const IssueState({ + this.status = IssueStatus.initial, + this.issues = const [], + this.loadedPages = 0, + this.hasReachedMax = false, + this.error_message = null + }); + + final IssueStatus status; + final List issues; + final int loadedPages; + final bool hasReachedMax; + final String? error_message; + + IssueState copyWith({ + IssueStatus? status, + List? issues, + int? loadedPages, + bool? hasReachedMax, + String? error_message, + }) { + return IssueState( + status: status ?? this.status, + issues: issues ?? this.issues, + loadedPages: loadedPages ?? this.loadedPages, + hasReachedMax: hasReachedMax ?? this.hasReachedMax, + error_message: error_message ?? this.error_message, + ); + } + + + @override + List get props => [status, issues, hasReachedMax]; +} \ No newline at end of file diff --git a/lib/model/issues.dart b/lib/model/issues.dart new file mode 100644 index 0000000..1588a4a --- /dev/null +++ b/lib/model/issues.dart @@ -0,0 +1,339 @@ +import 'package:gitea_client/model/user.dart'; + +class Issue { + int? id; + String? url; + String? htmlUrl; + int? number; + _User? user; + String? originalAuthor; + int? originalAuthorId; + String? title; + String? body; + String? ref; + List? labels; + Milestone? milestone; + _User? assignee; + List<_User>? assignees; + String? state; + bool? isLocked; + int? comments; + String? createdAt; + String? updatedAt; + String? closedAt; + String? dueDate; + String? pullRequest; + _Repository? repository; + + Issue( + {this.id, + this.url, + this.htmlUrl, + this.number, + this.user, + this.originalAuthor, + this.originalAuthorId, + this.title, + this.body, + this.ref, + this.labels, + this.milestone, + this.assignee, + this.assignees, + this.state, + this.isLocked, + this.comments, + this.createdAt, + this.updatedAt, + this.closedAt, + this.dueDate, + this.pullRequest, + this.repository}); + + Issue.fromJson(Map json) { + id = json['id']; + url = json['url']; + htmlUrl = json['html_url']; + number = json['number']; + user = json['user'] != null ? _User.fromJson(json['user']) : null; + originalAuthor = json['original_author']; + originalAuthorId = json['original_author_id']; + title = json['title']; + body = json['body']; + ref = json['ref']; + if (json['labels'] != null) { + labels = []; + json['labels'].forEach((v) { + labels!.add(Labels.fromJson(v)); + }); + } + milestone = json['milestone'] != null + ? Milestone.fromJson(json['milestone']) + : null; + assignee = + json['assignee'] != null ? _User.fromJson(json['assignee']) : null; + if (json['assignees'] != null) { + assignees = <_User>[]; + json['assignees'].forEach((v) { + assignees!.add(_User.fromJson(v)); + }); + } + state = json['state']; + isLocked = json['is_locked']; + comments = json['comments']; + createdAt = json['created_at']; + updatedAt = json['updated_at']; + closedAt = json['closed_at']; + dueDate = json['due_date']; + pullRequest = json['pull_request']; + repository = json['repository'] != null + ? _Repository.fromJson(json['repository']) + : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['url'] = url; + data['html_url'] = htmlUrl; + data['number'] = number; + if (user != null) { + data['user'] = user!.toJson(); + } + data['original_author'] = originalAuthor; + data['original_author_id'] = originalAuthorId; + data['title'] = title; + data['body'] = body; + data['ref'] = ref; + if (labels != null) { + data['labels'] = labels!.map((v) => v.toJson()).toList(); + } + if (milestone != null) { + data['milestone'] = milestone!.toJson(); + } + if (assignee != null) { + data['assignee'] = assignee!.toJson(); + } + if (assignees != null) { + data['assignees'] = assignees!.map((v) => v.toJson()).toList(); + } + data['state'] = state; + data['is_locked'] = isLocked; + data['comments'] = comments; + data['created_at'] = createdAt; + data['updated_at'] = updatedAt; + data['closed_at'] = closedAt; + data['due_date'] = dueDate; + data['pull_request'] = pullRequest; + if (repository != null) { + data['repository'] = repository!.toJson(); + } + return data; + } +} + +class _User { + int? id; + String? login; + String? fullName; + String? email; + String? avatarUrl; + String? language; + bool? isAdmin; + String? lastLogin; + String? created; + bool? restricted; + bool? active; + bool? prohibitLogin; + String? location; + String? website; + String? description; + String? visibility; + int? followersCount; + int? followingCount; + int? starredReposCount; + String? username; + + _User( + {this.id, + this.login, + this.fullName, + this.email, + this.avatarUrl, + this.language, + this.isAdmin, + this.lastLogin, + this.created, + this.restricted, + this.active, + this.prohibitLogin, + this.location, + this.website, + this.description, + this.visibility, + this.followersCount, + this.followingCount, + this.starredReposCount, + this.username}); + + _User.fromJson(Map json) { + id = json['id']; + login = json['login']; + fullName = json['full_name']; + email = json['email']; + avatarUrl = json['avatar_url']; + language = json['language']; + isAdmin = json['is_admin']; + lastLogin = json['last_login']; + created = json['created']; + restricted = json['restricted']; + active = json['active']; + prohibitLogin = json['prohibit_login']; + location = json['location']; + website = json['website']; + description = json['description']; + visibility = json['visibility']; + followersCount = json['followers_count']; + followingCount = json['following_count']; + starredReposCount = json['starred_repos_count']; + username = json['username']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['login'] = login; + data['full_name'] = fullName; + data['email'] = email; + data['avatar_url'] = avatarUrl; + data['language'] = language; + data['is_admin'] = isAdmin; + data['last_login'] = lastLogin; + data['created'] = created; + data['restricted'] = restricted; + data['active'] = active; + data['prohibit_login'] = prohibitLogin; + data['location'] = location; + data['website'] = website; + data['description'] = description; + data['visibility'] = visibility; + data['followers_count'] = followersCount; + data['following_count'] = followingCount; + data['starred_repos_count'] = starredReposCount; + data['username'] = username; + return data; + } +} + +class Labels { + int id; + String name; + String color; + String description; + String url; + + Labels( + {required this.id, + required this.name, + required this.color, + required this.description, + required this.url}); + + Labels.fromJson(Map json) + : id = json['id'], + name = json['name'], + color = json['color'], + description = json['description'], + url = json['url']; + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['color'] = color; + data['description'] = description; + data['url'] = url; + return data; + } +} + +class Milestone { + int id; + String title; + String description; + String state; + int openIssues; + int closedIssues; + String createdAt; + String updatedAt; + String? closedAt; + String? dueOn; + + Milestone( + {required this.id, + required this.title, + required this.description, + required this.state, + required this.openIssues, + required this.closedIssues, + required this.createdAt, + required this.updatedAt, + this.closedAt, + this.dueOn}); + + Milestone.fromJson(Map json) + : id = json['id'], + title = json['title'], + description = json['description'], + state = json['state'], + openIssues = json['open_issues'], + closedIssues = json['closed_issues'], + createdAt = json['created_at'], + updatedAt = json['updated_at'] { + closedAt = json['closed_at']; + dueOn = json['due_on']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['title'] = title; + data['description'] = description; + data['state'] = state; + data['open_issues'] = openIssues; + data['closed_issues'] = closedIssues; + data['created_at'] = createdAt; + data['updated_at'] = updatedAt; + data['closed_at'] = closedAt; + data['due_on'] = dueOn; + return data; + } +} + +class _Repository { + int id; + String name; + String owner; + String fullName; + + _Repository( + {required this.id, + required this.name, + required this.owner, + required this.fullName}); + + _Repository.fromJson(Map json) + : id = json['id'], + name = json['name'], + owner = json['owner'], + fullName = json['full_name']; + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['owner'] = owner; + data['full_name'] = fullName; + return data; + } +} diff --git a/lib/service/gitea_service.dart b/lib/service/gitea_service.dart index add028e..ece7b55 100644 --- a/lib/service/gitea_service.dart +++ b/lib/service/gitea_service.dart @@ -2,6 +2,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/repository.dart'; import 'package:gitea_client/model/user.dart'; import 'package:http/http.dart' as http; @@ -53,7 +54,31 @@ class GiteaService { } throw Exception('error fetching files'); } -// + + Future> getRepoIssues({required String repoFullname,required String state,int page = 1, int limit = 10}) async{ + if(state != "all" || state != "closed" || state != "open") { + throw Exception("Wrong state provided!"); + } + var response = await http.get( + Uri.https(apiAccess.instance, "api/v1/user/repos/$repoFullname/issues", { + "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 Issue.fromJson(json); + }).toList(); + + result.sort((a,b) => -1*a.updatedAt!.compareTo(b.updatedAt!)); + return result; + } + throw Exception('error fetching posts'); + } + Future> getUserRepositories([int page = 1, int limit = 10]) async{ var response = await http.get( Uri.https(apiAccess.instance, "api/v1/user/repos", { @@ -71,7 +96,7 @@ class GiteaService { result.sort((a,b) => -1*a.updatedAt!.compareTo(b.updatedAt!)); return result; } - throw Exception('error fetching posts'); + throw Exception('error fetching repos'); } Future searchRepo({required String query,int page = 1, int limit = 10}) async{ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1fc8ed3..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/pubspec.lock b/pubspec.lock index 53847b9..d79e764 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,7 +105,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -147,7 +147,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: @@ -258,7 +258,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: transitive description: @@ -307,7 +307,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -335,7 +335,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -501,7 +501,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -543,7 +543,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" typed_data: dependency: transitive description: @@ -613,7 +613,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" watcher: dependency: transitive description: @@ -650,5 +650,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.16.2 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.10.0"