From 4f00baf3db327abdfe26badfb593402f6faab746 Mon Sep 17 00:00:00 2001 From: JeeVee11 <88948013+JeeVee11@users.noreply.github.com> Date: Thu, 9 Feb 2023 10:59:55 +0000 Subject: [PATCH] Added a search function to the groups screen (#340) Co-authored-by: Dirk Doesburg --- lib/blocs/groups_cubit.dart | 70 +++++++++++++++++++++----- lib/ui/screens/groups_screen.dart | 84 +++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 13 deletions(-) diff --git a/lib/blocs/groups_cubit.dart b/lib/blocs/groups_cubit.dart index 2033657a9..4e39b1da6 100644 --- a/lib/blocs/groups_cubit.dart +++ b/lib/blocs/groups_cubit.dart @@ -1,8 +1,11 @@ +import 'dart:async'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reaxit/api/api_repository.dart'; import 'package:reaxit/api/exceptions.dart'; -import 'package:reaxit/blocs/detail_state.dart'; -import 'package:reaxit/models/group.dart'; +import 'package:reaxit/blocs.dart'; +import 'package:reaxit/models.dart'; +import 'package:reaxit/config.dart' as config; typedef GroupsState = DetailState>; @@ -19,19 +22,10 @@ class GroupsCubit extends Cubit { if (listResponse.results.isNotEmpty) { emit(ResultState(listResponse.results)); } else { - emit(const ErrorState('There are no boards.')); + emit(const ErrorState('There are no groups.')); } } on ApiException catch (exception) { - emit(ErrorState(_failureMessage(exception))); - } - } - - String _failureMessage(ApiException exception) { - switch (exception) { - case ApiException.noInternet: - return 'Not connected to the internet.'; - default: - return 'An unknown error occurred.'; + emit(ErrorState(exception.message)); } } } @@ -47,3 +41,53 @@ class CommitteesCubit extends GroupsCubit { class SocietiesCubit extends GroupsCubit { SocietiesCubit(ApiRepository api) : super(api, MemberGroupType.society); } + +class AllGroupsCubit extends GroupsCubit { + /// The last used search query. Can be set through `this.search(query)`. + String? _searchQuery; + + /// A timer used to debounce calls to `this.load()` from `this.search()`. + Timer? _searchDebounceTimer; + + // We pass null as MemberGroupType, so we get all groups. + AllGroupsCubit(ApiRepository api) : super(api, null); + + @override + Future load() async { + emit(const LoadingState()); + + try { + final query = _searchQuery; + + final listResponse = + await api.getGroups(limit: 1000, type: groupType, search: query); + + // Don't load if the query changed in the meantime + if (query != _searchQuery) return; + + if (listResponse.results.isNotEmpty) { + emit(ResultState(listResponse.results)); + } else { + if (query?.isEmpty ?? true) { + emit(const ErrorState('There are no results.')); + } + emit(ErrorState('There are no results for "$query".')); + } + } on ApiException catch (exception) { + emit(ErrorState(exception.message)); + } + } + + void search(String? query) { + if (query != _searchQuery) { + _searchQuery = query; + _searchDebounceTimer?.cancel(); + if (query?.isEmpty ?? false) { + // Don't get results when the query is empty. + emit(const LoadingState()); + } else { + _searchDebounceTimer = Timer(config.searchDebounceTime, load); + } + } + } +} diff --git a/lib/ui/screens/groups_screen.dart b/lib/ui/screens/groups_screen.dart index d130c4a06..535122951 100644 --- a/lib/ui/screens/groups_screen.dart +++ b/lib/ui/screens/groups_screen.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:reaxit/blocs/detail_state.dart'; import 'package:reaxit/blocs/groups_cubit.dart'; import 'package:reaxit/models/group.dart'; import 'package:reaxit/ui/widgets.dart'; import 'package:collection/collection.dart'; +import 'package:reaxit/api/api_repository.dart'; class GroupsScreen extends StatefulWidget { final MemberGroupType? currentScreen; @@ -68,6 +70,23 @@ class _GroupsScreenState extends State ], indicatorColor: Theme.of(context).colorScheme.primary, ), + actions: [ + IconButton( + padding: const EdgeInsets.all(16), + icon: const Icon(Icons.search), + onPressed: () async { + final searchCubit = + AllGroupsCubit(RepositoryProvider.of(context)); + + await showSearch( + context: context, + delegate: GroupSearchDelegate(searchCubit), + ); + + searchCubit.close(); + }, + ) + ], ), drawer: MenuDrawer(), body: TabBarView( @@ -173,3 +192,68 @@ class GroupListScrollView extends StatelessWidget { ); } } + +class GroupSearchDelegate extends SearchDelegate { + final AllGroupsCubit _cubit; + + GroupSearchDelegate(this._cubit); + + @override + ThemeData appBarTheme(BuildContext context) { + final theme = super.appBarTheme(context); + return theme.copyWith( + textTheme: theme.textTheme.copyWith( + titleLarge: GoogleFonts.openSans( + textStyle: Theme.of(context).textTheme.titleLarge, + ), + ), + ); + } + + @override + List buildActions(BuildContext context) { + if (query.isNotEmpty) { + return [ + IconButton( + padding: const EdgeInsets.all(16), + tooltip: 'Clear search bar', + icon: const Icon(Icons.clear), + onPressed: () { + query = ''; + }, + ) + ]; + } else { + return []; + } + } + + @override + Widget buildLeading(BuildContext context) { + return BackButton( + onPressed: () => close(context, null), + ); + } + + @override + Widget buildResults(BuildContext context) { + return BlocBuilder( + bloc: _cubit..search(query), + builder: (context, state) { + if (state is ErrorState) { + return ErrorScrollView(state.message!); + } else { + return GroupListScrollView( + key: const PageStorageKey('groups-search'), + groups: state.result ?? [], + ); + } + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + return buildResults(context); + } +}