Add user search
This commit is contained in:
parent
2a4a23f6aa
commit
2bb3fe6979
3 changed files with 184 additions and 11 deletions
|
@ -1,10 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class DefaultErrorWidget extends StatelessWidget {
|
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 void Function()? onRetry;
|
||||||
|
final IconData? iconOverride;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -16,13 +18,19 @@ class DefaultErrorWidget extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
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)
|
.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleMedium,),
|
.titleMedium,),
|
||||||
Padding(
|
if (message != null) Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(message),
|
child: Text(message ?? "",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (onRetry != null) TextButton.icon(
|
if (onRetry != null) TextButton.icon(
|
||||||
onPressed: onRetry,
|
onPressed: onRetry,
|
||||||
|
|
|
@ -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/expanding_input_fab.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friend_list_tile.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/settings_page.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/user_search.dart';
|
||||||
import 'package:flutter/material.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 {
|
class FriendsList extends StatefulWidget {
|
||||||
const FriendsList({super.key});
|
const FriendsList({super.key});
|
||||||
|
|
||||||
|
@ -85,11 +95,33 @@ class _FriendsListState extends State<FriendsList> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Contacts++"),
|
title: const Text("Contacts++"),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
Padding(
|
||||||
onPressed: () {
|
padding: const EdgeInsets.only(right: 4),
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
|
child: PopupMenuButton<MenuItemDefinition>(
|
||||||
},
|
icon: const Icon(Icons.more_vert),
|
||||||
icon: const Icon(Icons.settings),
|
onSelected: (MenuItemDefinition itemDef) {
|
||||||
|
itemDef.onTap();
|
||||||
|
},
|
||||||
|
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) {
|
} else if (snapshot.hasError) {
|
||||||
FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
|
FlutterError.reportError(
|
||||||
|
FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));
|
||||||
return DefaultErrorWidget(
|
return DefaultErrorWidget(
|
||||||
message: "${snapshot.error}",
|
message: "${snapshot.error}",
|
||||||
onRetry: () {
|
onRetry: () {
|
||||||
|
|
132
lib/widgets/user_search.dart
Normal file
132
lib/widgets/user_search.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue