import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gitea_client/cubit/search_bloc.dart'; import '../model/repository.dart'; import '../model/repouser.dart'; import '../model/user.dart'; class SearchList extends StatefulWidget { final SavedUser user; const SearchList({Key? key, required this.user}) : super(key: key); @override _SearchListState createState() => _SearchListState(); } class _SearchListState extends State { final _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } BuildContext? blocContex; final search = TextEditingController(); @override Widget build(BuildContext context) { final media = MediaQuery.of(context).size; final padding = MediaQuery.of(context).viewPadding; return Column( children: [ Text( "Search", style: Theme.of(context).textTheme.headline4, ), TextField( decoration: const InputDecoration( labelText: "Search text", ), controller: search, onChanged: (text) { if (blocContex != null) { print("Input: ${search.text}"); blocContex!.read().add(SearchInputEvent(search.text)); } }, ), BlocBuilder( builder: (context, state) { blocContex = context; switch (state.status) { case SearchStatus.failure: String error_message = state.error_message!; return Center(child: Text('failed to fetch $error_message')); case SearchStatus.success: if (state.repos.isEmpty) { return const Center(child: Text('No results')); } if (state.repos.length < 5) { context.read().add(SearchFetchedEvent()); } return SizedBox( height: media.height - kToolbarHeight - 130, // Approximate height of search form child: ListView.builder( shrinkWrap: true, itemBuilder: (BuildContext context, int index) { return index >= state.repos.length ? BottomLoader() : RepoListItem( repo: state.repos[index], user: widget.user, ); }, itemCount: state.hasReachedMax ? state.repos.length : state.repos.length + 1, controller: _scrollController, ), ); case SearchStatus.initial: return Container(); default: return const Center(child: CircularProgressIndicator()); } }, ), ], ); } @override void dispose() { _scrollController ..removeListener(_onScroll) ..dispose(); super.dispose(); } void _onScroll() { if (_isBottom) context.read().add(SearchFetchedEvent()); } 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.repo, required this.user}) : super(key: key); final Repository repo; final SavedUser user; @override Widget build(BuildContext context) { return Material( child: Container( color: (repo.private!) ? Colors.yellow[100] : Colors.white, child: ListTile( leading: (repo.private!) ? const Icon(Icons.lock) : (repo.mirror!) ? const Icon(Icons.amp_stories_outlined) : (repo.archived!) ? const Icon(Icons.archive) : const Icon(Icons.book), title: Text('${repo.owner.username}/${repo.name}'), isThreeLine: true, subtitle: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ (repo.description != null) ? Text(repo.description!) : Container(), (repo.mirror!) ? Text("Mirror of ${repo.originalUrl!}") : Container() ], ), dense: true, onTap: () => { Navigator.pushNamed(context, "/repopage", arguments: RepoUser(repo, user)) }, ), ), ); } }