Add user search

This commit is contained in:
Nutcake 2023-05-04 18:16:19 +02:00
parent 2a4a23f6aa
commit 2bb3fe6979
3 changed files with 184 additions and 11 deletions

View file

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
class DefaultErrorWidget extends StatelessWidget {
const DefaultErrorWidget({required this.message, this.onRetry, super.key});
const DefaultErrorWidget({this.title, this.message, this.onRetry, this.iconOverride, super.key});
final String message;
final String? title;
final String? message;
final void Function()? onRetry;
final IconData? iconOverride;
@override
Widget build(BuildContext context) {
@ -16,13 +18,19 @@ class DefaultErrorWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Something went wrong: ", style: Theme
Icon(iconOverride ?? Icons.warning, size: 32,),
const SizedBox(height: 16,),
Text(title ?? "Something went wrong: ",
textAlign: TextAlign.center,
style: Theme
.of(context)
.textTheme
.titleMedium,),
Padding(
if (message != null) Padding(
padding: const EdgeInsets.all(16),
child: Text(message),
child: Text(message ?? "",
textAlign: TextAlign.center,
),
),
if (onRetry != null) TextButton.icon(
onPressed: onRetry,

View file

@ -9,8 +9,18 @@ import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friend_list_tile.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart';
import 'package:contacts_plus_plus/widgets/user_search.dart';
import 'package:flutter/material.dart';
class MenuItemDefinition {
final String name;
final IconData icon;
final void Function() onTap;
const MenuItemDefinition({required this.name, required this.icon, required this.onTap});
}
class FriendsList extends StatefulWidget {
const FriendsList({super.key});
@ -85,11 +95,33 @@ class _FriendsListState extends State<FriendsList> {
appBar: AppBar(
title: const Text("Contacts++"),
actions: [
IconButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
Padding(
padding: const EdgeInsets.only(right: 4),
child: PopupMenuButton<MenuItemDefinition>(
icon: const Icon(Icons.more_vert),
onSelected: (MenuItemDefinition itemDef) {
itemDef.onTap();
},
icon: const Icon(Icons.settings),
itemBuilder: (BuildContext context) => [
MenuItemDefinition(name: "Settings", icon: Icons.settings, onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
}),
MenuItemDefinition(name: "Find Users", icon: Icons.person_add, onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const UserSearch()));
})
].map((item) =>
PopupMenuItem<MenuItemDefinition>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.name),
Icon(item.icon),
],
),
),
).toList(),
),
)
],
),
@ -134,7 +166,8 @@ class _FriendsListState extends State<FriendsList> {
},
);
} else if (snapshot.hasError) {
FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
FlutterError.reportError(
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
return DefaultErrorWidget(
message: "${snapshot.error}",
onRetry: () {

View file

@ -0,0 +1,132 @@
import 'dart:async';
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/user.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/user_list_tile.dart';
import 'package:flutter/material.dart';
class SearchError {
final String message;
final IconData icon;
const SearchError({required this.message, required this.icon});
}
class UserSearch extends StatefulWidget {
const UserSearch({super.key});
@override
State<StatefulWidget> createState() => _UserSearchState();
}
class _UserSearchState extends State<UserSearch> {
final TextEditingController _searchInputController = TextEditingController();
Timer? _searchDebouncer;
late Future<List<User>>? _usersFuture = _emptySearch;
bool _isLoading = false;
Future<List<User>> get _emptySearch => Future(() => throw const SearchError(
message: "Start typing to search for users", icon: Icons.search)
);
void _querySearch(BuildContext context, String needle) {
if (needle.isEmpty) {
_usersFuture = _emptySearch;
return;
}
_usersFuture = UserApi.searchUsers(ClientHolder
.of(context).apiClient, needle: needle).then((value) {
final res = value.toList();
if (res.isEmpty) throw SearchError(message: "No user found with username '$needle'", icon: Icons.search_off);
res.sort(
(a, b) => a.username.length.compareTo(b.username.length)
);
return res;
}).whenComplete(() => setState(() => _isLoading = false));
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Find Users"),
),
body: Column(
children: [
if(_isLoading) const LinearProgressIndicator(),
Expanded(
child: FutureBuilder(
future: _usersFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final users = (snapshot.data as List<User>);
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return UserListTile(user: users[index]);
},
);
} else if (snapshot.hasError) {
FlutterError.reportError(
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
final err = snapshot.error;
if (err is SearchError) {
return DefaultErrorWidget(
title: err.message,
iconOverride: err.icon,
);
} else {
return DefaultErrorWidget(title: "${snapshot.error}",);
}
} else {
return const SizedBox.shrink();
}
}
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: TextField(
decoration: InputDecoration(
isDense: true,
hintText: "Search for users...",
contentPadding: const EdgeInsets.all(16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24)
)
),
autocorrect: false,
controller: _searchInputController,
onChanged: (String value) {
_searchDebouncer?.cancel();
if (value.isEmpty) {
setState(() {
_isLoading = false;
_querySearch(context, value);
});
return;
}
setState(() {
_isLoading = true;
});
_searchDebouncer = Timer(const Duration(milliseconds: 300), () {
setState(() {
_querySearch(context, value);
});
});
},
),
),
],
),
);
}
}