diff --git a/lib/cubit/issues_bloc.dart b/lib/cubit/issues_bloc.dart index 664178b..53db34e 100644 --- a/lib/cubit/issues_bloc.dart +++ b/lib/cubit/issues_bloc.dart @@ -23,7 +23,7 @@ class IssuesBloc extends Bloc { if (state.hasReachedMax) return; try { if (state.status == IssueStatus.initial) { - final issues = await giteaService.getRepoIssues(repoFullname: repoFullName,state: istate,page: 1, limit: 10); + final issues = await giteaService.getRepoIssues(owner: repoFullName.split('/')[0],repo: repoFullName.split('/')[1],state: istate,page: 1, limit: 10); return emit(state.copyWith( status: IssueStatus.success, issues: issues, @@ -32,7 +32,7 @@ class IssuesBloc extends Bloc { error_message: null, )); } - final issues = await giteaService.getRepoIssues(repoFullname: repoFullName,state: istate,page: state.loadedPages+1, limit: 10); + final issues = await giteaService.getRepoIssues(owner: repoFullName.split('/')[0],repo: repoFullName.split('/')[1],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) { diff --git a/lib/service/gitea_service.dart b/lib/service/gitea_service.dart index ece7b55..053d658 100644 --- a/lib/service/gitea_service.dart +++ b/lib/service/gitea_service.dart @@ -55,12 +55,12 @@ 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!"); + Future> getRepoIssues({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/user/repos/$repoFullname/issues", { + Uri.https(apiAccess.instance, "api/v1/repos/$owner/$repo/issues", { "token": apiAccess.token, "state": state, "page" : page.toString(), @@ -76,7 +76,7 @@ class GiteaService { result.sort((a,b) => -1*a.updatedAt!.compareTo(b.updatedAt!)); return result; } - throw Exception('error fetching posts'); + throw Exception('error fetching posts: ${response.statusCode}'); } Future> getUserRepositories([int page = 1, int limit = 10]) async{ diff --git a/lib/widget/repo_overview.dart b/lib/widget/repo_overview.dart index 51d50bc..ca62a6d 100644 --- a/lib/widget/repo_overview.dart +++ b/lib/widget/repo_overview.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:gitea_client/cubit/file_list_load_bloc.dart'; +import 'package:gitea_client/cubit/issues_bloc.dart'; +import 'package:gitea_client/model/issues.dart'; import 'package:gitea_client/model/repository.dart'; import 'package:gitea_client/model/user.dart'; import 'package:gitea_client/service/gitea_service.dart'; @@ -308,8 +310,17 @@ class RepoIssues extends StatefulWidget { } class _RepoIssues extends State { + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + @override Widget build(BuildContext context) { + final media = MediaQuery.of(context).size; return DefaultTabController( length: 2, child: DefaultTabController( @@ -334,10 +345,90 @@ class _RepoIssues extends State { body: TabBarView( children: [ Column( - children: const [Text("Open Issues")], + 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 IssuesBloc(GiteaService(apiAccess: widget.user.apiAccess),widget.repo.fullName!,"open") + ..add(IssuesFetched()); + }, + child: BlocBuilder( + builder: (context, state) { + switch (state.status) { + case IssueStatus.failure: + String error_message = state.error_message!; + return Center(child: Text('failed to fetch $error_message')); + case IssueStatus.success: + if (state.issues.isEmpty) { + return const Center( + child: Text('No issues found')); + } + + return ListView.builder( + itemBuilder: (BuildContext context, int index) { + return IssuesListItem( + issue: state.issues[index], + onTap: () {}, + ); + }, + itemCount: state.issues.length, + controller: _scrollController, + ); + default: + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ), + ), + ) + ], ), Column( - children: const [Text("Closed Issues")], + 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 IssuesBloc(GiteaService(apiAccess: widget.user.apiAccess),widget.repo.fullName!,"closed") + ..add(IssuesFetched()); + }, + child: BlocBuilder( + builder: (context, state) { + switch (state.status) { + case IssueStatus.failure: + String error_message = state.error_message!; + return Center(child: Text('failed to fetch $error_message')); + case IssueStatus.success: + if (state.issues.isEmpty) { + return const Center( + child: Text('No issues found')); + } + + return ListView.builder( + itemBuilder: (BuildContext context, int index) { + return IssuesListItem( + issue: state.issues[index], + onTap: () {}, + ); + }, + itemCount: state.issues.length, + controller: _scrollController, + ); + default: + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ), + ), + ) + ], ) ], ), @@ -345,6 +436,42 @@ class _RepoIssues extends State { ), ); } + + @override + void dispose() { + _scrollController + ..removeListener(_onScroll) + ..dispose(); + super.dispose(); + } + + void _onScroll() { + if (_isBottom) context.read().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 IssuesListItem extends StatelessWidget { + final Issue issue; + final Function onTap; + + const IssuesListItem({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()}, + ); + } } class RepoPullRequests extends StatefulWidget {