172 lines
5.2 KiB
Dart
172 lines
5.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:gitea_client/cubit/search_bloc.dart';
|
|
import 'package:flutter_gen/gen_l10n/l10n.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<SearchList> {
|
|
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 l10n = L10n.of(context)!;
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
l10n.search,
|
|
style: Theme.of(context).textTheme.headline4,
|
|
),
|
|
TextField(
|
|
decoration: InputDecoration(
|
|
labelText: l10n.searchText,
|
|
),
|
|
controller: search,
|
|
onChanged: (text) {
|
|
if (blocContex != null) {
|
|
print("Input: ${search.text}");
|
|
blocContex!.read<SearchBloc>().add(SearchInputEvent(search.text));
|
|
}
|
|
},
|
|
),
|
|
BlocBuilder<SearchBloc, SearchState>(
|
|
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<SearchBloc>().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<SearchBloc>().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))
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|