Ver Fonte

Searching improvements (#1625)

Vishnu Mohandas há 1 ano atrás
pai
commit
c25212d9d8

+ 47 - 0
lib/models/search/index_of_indexed_stack.dart

@@ -0,0 +1,47 @@
+import "package:flutter/material.dart";
+
+enum SearchState {
+  empty,
+  searching,
+  notEmpty,
+}
+
+class IndexOfStackNotifier with ChangeNotifier {
+  int _prevIndex = 0;
+  int _index = 0;
+  bool _isSearchQueryEmpty = true;
+  SearchState _searchState = SearchState.empty;
+
+  static IndexOfStackNotifier? _instance;
+
+  IndexOfStackNotifier._();
+
+  factory IndexOfStackNotifier() => _instance ??= IndexOfStackNotifier._();
+
+  set isSearchQueryEmpty(bool value) {
+    _isSearchQueryEmpty = value;
+    setIndex();
+  }
+
+  set searchState(SearchState value) {
+    _searchState = value;
+    setIndex();
+  }
+
+  setIndex() {
+    _prevIndex = _index;
+
+    if (_isSearchQueryEmpty) {
+      _index = 0;
+    } else {
+      if (_searchState == SearchState.empty) {
+        _index = 2;
+      } else {
+        _index = 1;
+      }
+    }
+    _prevIndex != _index ? notifyListeners() : null;
+  }
+
+  get index => _index;
+}

+ 3 - 1
lib/models/typedefs.dart

@@ -11,7 +11,9 @@ typedef VoidCallbackParamDouble = Function(double);
 typedef VoidCallbackParamBool = void Function(bool);
 typedef VoidCallbackParamListDouble = void Function(List<double>);
 typedef VoidCallbackParamLocation = void Function(Location);
-typedef VoidCallbackParamSearchResults = void Function(List<SearchResult>);
+typedef VoidCallbackParamSearchResutlsStream = void Function(
+  Stream<List<SearchResult>>,
+);
 
 typedef FutureVoidCallback = Future<void> Function();
 typedef FutureOrVoidCallback = FutureOr<void> Function();

+ 0 - 53
lib/states/search_results_state.dart

@@ -1,53 +0,0 @@
-import "package:flutter/cupertino.dart";
-import "package:photos/models/search/search_result.dart";
-import "package:photos/models/typedefs.dart";
-
-class SearchResultsProvider extends StatefulWidget {
-  final Widget child;
-  const SearchResultsProvider({
-    required this.child,
-    super.key,
-  });
-
-  @override
-  State<SearchResultsProvider> createState() => _SearchResultsProviderState();
-}
-
-class _SearchResultsProviderState extends State<SearchResultsProvider> {
-  var searchResults = <SearchResult>[];
-  @override
-  Widget build(BuildContext context) {
-    return InheritedSearchResults(
-      searchResults,
-      updateSearchResults,
-      child: widget.child,
-    );
-  }
-
-  void updateSearchResults(List<SearchResult> newResult) {
-    setState(() {
-      searchResults = newResult;
-    });
-  }
-}
-
-class InheritedSearchResults extends InheritedWidget {
-  final List<SearchResult> results;
-  final VoidCallbackParamSearchResults updateResults;
-  const InheritedSearchResults(
-    this.results,
-    this.updateResults, {
-    required super.child,
-    super.key,
-  });
-
-  static InheritedSearchResults of(BuildContext context) {
-    return context
-        .dependOnInheritedWidgetOfExactType<InheritedSearchResults>()!;
-  }
-
-  @override
-  bool updateShouldNotify(covariant InheritedSearchResults oldWidget) {
-    return results != oldWidget.results;
-  }
-}

+ 25 - 21
lib/ui/search_tab.dart

@@ -2,15 +2,13 @@ import "package:fade_indexed_stack/fade_indexed_stack.dart";
 import "package:flutter/material.dart";
 import "package:flutter_animate/flutter_animate.dart";
 import "package:photos/core/constants.dart";
-import "package:photos/models/search/search_result.dart";
+import "package:photos/models/search/index_of_indexed_stack.dart";
 import "package:photos/models/search/search_types.dart";
 import "package:photos/states/all_sections_examples_state.dart";
-import "package:photos/states/search_results_state.dart";
 import "package:photos/ui/common/loading_widget.dart";
 import "package:photos/ui/viewer/search/result/no_result_widget.dart";
 import "package:photos/ui/viewer/search/search_section.dart";
 import "package:photos/ui/viewer/search/search_suggestions.dart";
-import 'package:photos/ui/viewer/search/search_widget.dart';
 import "package:photos/ui/viewer/search/tab_empty_state.dart";
 
 class SearchTab extends StatefulWidget {
@@ -21,34 +19,40 @@ class SearchTab extends StatefulWidget {
 }
 
 class _SearchTabState extends State<SearchTab> {
-  var _searchResults = <SearchResult>[];
-  int index = 0;
+  late int index;
+  final indexOfStackNotifier = IndexOfStackNotifier();
 
   @override
-  void didChangeDependencies() {
-    super.didChangeDependencies();
-    _searchResults = InheritedSearchResults.of(context).results;
-    if (_searchResults.isEmpty) {
-      if (isSearchQueryEmpty) {
-        index = 0;
-      } else {
-        index = 2;
-      }
-    } else {
-      index = 1;
-    }
+  void initState() {
+    super.initState();
+    index = indexOfStackNotifier.index;
+    indexOfStackNotifier.addListener(indexNotifierListener);
+  }
+
+  void indexNotifierListener() {
+    setState(() {
+      index = indexOfStackNotifier.index;
+    });
+  }
+
+  @override
+  void dispose() {
+    indexOfStackNotifier.removeListener(indexNotifierListener);
+    indexOfStackNotifier.dispose();
+    super.dispose();
   }
 
   @override
   Widget build(BuildContext context) {
     return AllSectionsExamplesProvider(
       child: FadeIndexedStack(
+        lazy: false,
         duration: const Duration(milliseconds: 150),
         index: index,
-        children: [
-          const AllSearchSections(),
-          SearchSuggestionsWidget(_searchResults),
-          const NoResultWidget(),
+        children: const [
+          AllSearchSections(),
+          SearchSuggestionsWidget(),
+          NoResultWidget(),
         ],
       ),
     );

+ 78 - 81
lib/ui/tabs/home_widget.dart

@@ -33,7 +33,6 @@ import 'package:photos/services/local_sync_service.dart';
 import "package:photos/services/notification_service.dart";
 import 'package:photos/services/update_service.dart';
 import 'package:photos/services/user_service.dart';
-import "package:photos/states/search_results_state.dart";
 import 'package:photos/states/user_details_state.dart';
 import 'package:photos/theme/colors.dart';
 import "package:photos/theme/effects.dart";
@@ -393,90 +392,88 @@ class _HomeWidgetState extends State<HomeWidget> {
             !LocalSyncService.instance.hasGrantedLimitedPermissions() &&
             CollectionsService.instance.getActiveCollections().isEmpty;
 
-    return SearchResultsProvider(
-      child: Stack(
-        children: [
-          Builder(
-            builder: (context) {
-              return ExtentsPageView(
-                onPageChanged: (page) {
-                  Bus.instance.fire(
-                    TabChangedEvent(
-                      page,
-                      TabChangedEventSource.pageView,
-                    ),
-                  );
-                },
-                controller: _pageController,
-                openDrawer: Scaffold.of(context).openDrawer,
-                physics: const BouncingScrollPhysics(),
-                children: [
-                  _showShowBackupHook
-                      ? const StartBackupHookWidget(headerWidget: _headerWidget)
-                      : HomeGalleryWidget(
-                          header: _headerWidget,
-                          footer: const SizedBox(
-                            height: 160,
-                          ),
-                          selectedFiles: _selectedFiles,
-                        ),
-                  _userCollectionsTab,
-                  _sharedCollectionTab,
-                  _searchTab,
-                ],
-              );
-            },
-          ),
-          Align(
-            alignment: Alignment.bottomCenter,
-            child: ValueListenableBuilder(
-              valueListenable: isOnSearchTabNotifier,
-              builder: (context, value, child) {
-                return Container(
-                  decoration: value
-                      ? BoxDecoration(
-                          color: getEnteColorScheme(context).backgroundElevated,
-                          boxShadow: shadowFloatFaintLight,
-                        )
-                      : null,
-                  child: Column(
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      value
-                          ? const SearchWidget()
-                              .animate()
-                              .fadeIn(
-                                duration: const Duration(milliseconds: 225),
-                                curve: Curves.easeInOutSine,
-                              )
-                              .scale(
-                                begin: const Offset(0.8, 0.8),
-                                end: const Offset(1, 1),
-                                duration: const Duration(
-                                  milliseconds: 225,
-                                ),
-                                curve: Curves.easeInOutSine,
-                              )
-                              .slide(
-                                begin: const Offset(0, 0.4),
-                                curve: Curves.easeInOutSine,
-                                duration: const Duration(
-                                  milliseconds: 225,
-                                ),
-                              )
-                          : const SizedBox.shrink(),
-                      HomeBottomNavigationBar(
-                        _selectedFiles,
-                        selectedTabIndex: _selectedTabIndex,
-                      ),
-                    ],
+    return Stack(
+      children: [
+        Builder(
+          builder: (context) {
+            return ExtentsPageView(
+              onPageChanged: (page) {
+                Bus.instance.fire(
+                  TabChangedEvent(
+                    page,
+                    TabChangedEventSource.pageView,
                   ),
                 );
               },
-            ),
+              controller: _pageController,
+              openDrawer: Scaffold.of(context).openDrawer,
+              physics: const BouncingScrollPhysics(),
+              children: [
+                _showShowBackupHook
+                    ? const StartBackupHookWidget(headerWidget: _headerWidget)
+                    : HomeGalleryWidget(
+                        header: _headerWidget,
+                        footer: const SizedBox(
+                          height: 160,
+                        ),
+                        selectedFiles: _selectedFiles,
+                      ),
+                _userCollectionsTab,
+                _sharedCollectionTab,
+                _searchTab,
+              ],
+            );
+          },
+        ),
+        Align(
+          alignment: Alignment.bottomCenter,
+          child: ValueListenableBuilder(
+            valueListenable: isOnSearchTabNotifier,
+            builder: (context, value, child) {
+              return Container(
+                decoration: value
+                    ? BoxDecoration(
+                        color: getEnteColorScheme(context).backgroundElevated,
+                        boxShadow: shadowFloatFaintLight,
+                      )
+                    : null,
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    value
+                        ? const SearchWidget()
+                            .animate()
+                            .fadeIn(
+                              duration: const Duration(milliseconds: 225),
+                              curve: Curves.easeInOutSine,
+                            )
+                            .scale(
+                              begin: const Offset(0.8, 0.8),
+                              end: const Offset(1, 1),
+                              duration: const Duration(
+                                milliseconds: 225,
+                              ),
+                              curve: Curves.easeInOutSine,
+                            )
+                            .slide(
+                              begin: const Offset(0, 0.4),
+                              curve: Curves.easeInOutSine,
+                              duration: const Duration(
+                                milliseconds: 225,
+                              ),
+                            )
+                        : const SizedBox.shrink(),
+                    HomeBottomNavigationBar(
+                      _selectedFiles,
+                      selectedTabIndex: _selectedTabIndex,
+                    ),
+                  ],
+                ),
+              );
+            },
           ),
-        ],
-      ),
+        ),
+      ],
     );
   }
 

+ 1 - 2
lib/ui/viewer/search/search_suffix_icon_widget.dart

@@ -11,8 +11,7 @@ class SearchSuffixIcon extends StatefulWidget {
   State<SearchSuffixIcon> createState() => _SearchSuffixIconState();
 }
 
-class _SearchSuffixIconState extends State<SearchSuffixIcon>
-    with TickerProviderStateMixin {
+class _SearchSuffixIconState extends State<SearchSuffixIcon> {
   @override
   Widget build(BuildContext context) {
     final colorScheme = getEnteColorScheme(context);

+ 166 - 49
lib/ui/viewer/search/search_suggestions.dart

@@ -1,29 +1,126 @@
+import "dart:async";
+
 import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
+import "package:flutter_animate/flutter_animate.dart";
+import "package:logging/logging.dart";
 import "package:photos/core/event_bus.dart";
 import "package:photos/events/clear_and_unfocus_search_bar_event.dart";
 import "package:photos/generated/l10n.dart";
-import 'package:photos/models/search/album_search_result.dart';
-import 'package:photos/models/search/generic_search_result.dart';
+import "package:photos/models/search/album_search_result.dart";
+import "package:photos/models/search/generic_search_result.dart";
+import "package:photos/models/search/index_of_indexed_stack.dart";
 import 'package:photos/models/search/search_result.dart';
 import "package:photos/services/collections_service.dart";
 import "package:photos/theme/ente_theme.dart";
-import 'package:photos/ui/viewer/gallery/collection_page.dart';
-import 'package:photos/ui/viewer/search/result/search_result_widget.dart';
-import 'package:photos/utils/navigation_util.dart';
-
-class SearchSuggestionsWidget extends StatelessWidget {
-  final List<SearchResult> results;
+import "package:photos/ui/common/loading_widget.dart";
+import "package:photos/ui/viewer/gallery/collection_page.dart";
+import "package:photos/ui/viewer/search/result/search_result_widget.dart";
+import "package:photos/ui/viewer/search/search_widget.dart";
+import "package:photos/utils/navigation_util.dart";
 
-  const SearchSuggestionsWidget(
-    this.results, {
+///Not using StreamBuilder in this widget for rebuilding on every new event as
+///StreamBuilder is not lossless. It misses some events if the stream fires too
+///fast. Instead, we usi a queue to store the events and then generate the
+///widgets from the queue at regular intervals.
+class SearchSuggestionsWidget extends StatefulWidget {
+  const SearchSuggestionsWidget({
     Key? key,
   }) : super(key: key);
 
+  @override
+  State<SearchSuggestionsWidget> createState() =>
+      _SearchSuggestionsWidgetState();
+}
+
+class _SearchSuggestionsWidgetState extends State<SearchSuggestionsWidget> {
+  Stream<List<SearchResult>>? resultsStream;
+  final queueOfSearchResults = <List<SearchResult>>[];
+  var searchResultWidgets = <Widget>[];
+  StreamSubscription<List<SearchResult>>? subscription;
+  Timer? timer;
+
+  ///This is the interval at which the queue is checked for new events and
+  ///the search result widgets are generated from the queue.
+  static const _surfaceNewResultsInterval = 50;
+
+  @override
+  void initState() {
+    super.initState();
+    SearchWidgetState.searchResultsStreamNotifier.addListener(() {
+      IndexOfStackNotifier().searchState = SearchState.searching;
+      final resultsStream = SearchWidgetState.searchResultsStreamNotifier.value;
+
+      searchResultWidgets.clear();
+      releaseResources();
+
+      subscription = resultsStream!.listen(
+        (searchResults) {
+          //Currently, we add searchResults even if the list is empty. So we are adding
+          //empty list to the queue, which will trigger rebuilds with no change in UI
+          //(see [generateResultWidgetsInIntervalsFromQueue]'s setState()).
+          //This is needed to clear the search results in this widget when the
+          //search bar is cleared, and the event fired by the stream will be an
+          //empty list. Can optimize rebuilds if there are performance issues in future.
+          if (searchResults.isNotEmpty) {
+            IndexOfStackNotifier().searchState = SearchState.notEmpty;
+          }
+          queueOfSearchResults.add(searchResults);
+        },
+        onDone: () {
+          Future.delayed(
+              const Duration(milliseconds: _surfaceNewResultsInterval + 20),
+              () {
+            if (searchResultWidgets.isEmpty) {
+              IndexOfStackNotifier().searchState = SearchState.empty;
+            }
+          });
+          SearchWidgetState.isLoading.value = false;
+        },
+      );
+
+      generateResultWidgetsInIntervalsFromQueue();
+    });
+  }
+
+  void releaseResources() {
+    subscription?.cancel();
+    timer?.cancel();
+  }
+
+  ///This method generates searchResultsWidgets from the queueOfEvents by checking
+  ///every [_surfaceNewResultsInterval] if the queue is empty or not. If the
+  ///queue is not empty, it generates the widgets and clears the queue and
+  ///updates the UI.
+  void generateResultWidgetsInIntervalsFromQueue() {
+    timer = Timer.periodic(
+        const Duration(milliseconds: _surfaceNewResultsInterval), (timer) {
+      if (queueOfSearchResults.isNotEmpty) {
+        for (List<SearchResult> event in queueOfSearchResults) {
+          for (SearchResult result in event) {
+            searchResultWidgets.add(
+              SearchResultsWidgetGenerator(result).animate().fadeIn(
+                    duration: const Duration(milliseconds: 80),
+                    curve: Curves.easeIn,
+                  ),
+            );
+          }
+        }
+        queueOfSearchResults.clear();
+        setState(() {});
+      }
+    });
+  }
+
+  @override
+  void dispose() {
+    releaseResources();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
-    late final String title;
-    final resultsCount = results.length;
+    String title;
+    final resultsCount = searchResultWidgets.length;
     title = S.of(context).searchResultCount(resultsCount);
     return Scaffold(
       appBar: AppBar(
@@ -38,53 +135,38 @@ class SearchSuggestionsWidget extends StatelessWidget {
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            Text(
-              title,
-              style: getEnteTextTheme(context).largeBold,
+            SizedBox(
+              height: 44,
+              child: SearchWidgetState.isLoading.value
+                  ? EnteLoadingWidget(
+                      size: 14,
+                      padding: 4,
+                      color: getEnteColorScheme(context).strokeMuted,
+                      alignment: Alignment.topLeft,
+                    )
+                  : Text(
+                      title,
+                      style: getEnteTextTheme(context).largeBold,
+                    ).animate().fadeIn(
+                        duration: const Duration(milliseconds: 60),
+                        curve: Curves.easeIn,
+                      ),
             ),
-            const SizedBox(height: 20),
             Expanded(
               child: Padding(
                 padding: const EdgeInsets.symmetric(horizontal: 4),
                 child: ListView.separated(
                   itemBuilder: (context, index) {
-                    final result = results[index];
-                    if (result is AlbumSearchResult) {
-                      final AlbumSearchResult albumSearchResult = result;
-                      return SearchResultWidget(
-                        result,
-                        resultCount: CollectionsService.instance.getFileCount(
-                          albumSearchResult.collectionWithThumbnail.collection,
-                        ),
-                        onResultTap: () => routeToPage(
-                          context,
-                          CollectionPage(
-                            albumSearchResult.collectionWithThumbnail,
-                            tagPrefix: result.heroTag(),
-                          ),
-                        ),
-                      );
-                    } else if (result is GenericSearchResult) {
-                      return SearchResultWidget(
-                        result,
-                        onResultTap: result.onResultTap != null
-                            ? () => result.onResultTap!(context)
-                            : null,
-                      );
-                    } else {
-                      Logger('SearchSuggestionsWidget')
-                          .info("Invalid/Unsupported value");
-                      return const SizedBox.shrink();
-                    }
+                    return searchResultWidgets[index];
                   },
-                  padding: EdgeInsets.only(
-                    bottom: (MediaQuery.sizeOf(context).height / 2) + 50,
-                  ),
                   separatorBuilder: (context, index) {
                     return const SizedBox(height: 12);
                   },
-                  itemCount: results.length,
+                  itemCount: searchResultWidgets.length,
                   physics: const BouncingScrollPhysics(),
+                  padding: EdgeInsets.only(
+                    bottom: (MediaQuery.sizeOf(context).height / 2) + 50,
+                  ),
                 ),
               ),
             ),
@@ -94,3 +176,38 @@ class SearchSuggestionsWidget extends StatelessWidget {
     );
   }
 }
+
+class SearchResultsWidgetGenerator extends StatelessWidget {
+  final SearchResult result;
+  const SearchResultsWidgetGenerator(this.result, {super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    if (result is AlbumSearchResult) {
+      final AlbumSearchResult albumSearchResult = result as AlbumSearchResult;
+      return SearchResultWidget(
+        result,
+        resultCount: CollectionsService.instance.getFileCount(
+          albumSearchResult.collectionWithThumbnail.collection,
+        ),
+        onResultTap: () => routeToPage(
+          context,
+          CollectionPage(
+            albumSearchResult.collectionWithThumbnail,
+            tagPrefix: result.heroTag(),
+          ),
+        ),
+      );
+    } else if (result is GenericSearchResult) {
+      return SearchResultWidget(
+        result,
+        onResultTap: (result as GenericSearchResult).onResultTap != null
+            ? () => (result as GenericSearchResult).onResultTap!(context)
+            : null,
+      );
+    } else {
+      Logger('SearchResultsWidgetGenerator').info("Invalid/Unsupported value");
+      return const SizedBox.shrink();
+    }
+  }
+}

+ 150 - 25
lib/ui/viewer/search/search_widget.dart

@@ -6,16 +6,14 @@ import "package:logging/logging.dart";
 import "package:photos/core/event_bus.dart";
 import "package:photos/events/clear_and_unfocus_search_bar_event.dart";
 import "package:photos/events/tab_changed_event.dart";
+import "package:photos/models/search/index_of_indexed_stack.dart";
 import "package:photos/models/search/search_result.dart";
 import "package:photos/services/search_service.dart";
-import "package:photos/states/search_results_state.dart";
 import "package:photos/theme/ente_theme.dart";
 import "package:photos/ui/viewer/search/search_suffix_icon_widget.dart";
 import "package:photos/utils/date_time_util.dart";
 import "package:photos/utils/debouncer.dart";
 
-bool isSearchQueryEmpty = true;
-
 class SearchWidget extends StatefulWidget {
   const SearchWidget({Key? key}) : super(key: key);
 
@@ -24,7 +22,15 @@ class SearchWidget extends StatefulWidget {
 }
 
 class SearchWidgetState extends State<SearchWidget> {
+  static final ValueNotifier<Stream<List<SearchResult>>?>
+      searchResultsStreamNotifier = ValueNotifier(null);
+
+  ///This stores the query that is being searched for. When going to other tabs
+  ///when searching, this state gets disposed and when coming back to the
+  ///search tab, this query is used to populate the search bar.
   static String query = "";
+  //Debouncing + querying
+  static final isLoading = ValueNotifier(false);
   final _searchService = SearchService.instance;
   final _debouncer = Debouncer(const Duration(milliseconds: 200));
   final Logger _logger = Logger((SearchWidgetState).toString());
@@ -64,6 +70,9 @@ class SearchWidgetState extends State<SearchWidget> {
 
       textController.addListener(textControllerListener);
     });
+
+    //Populate the serach tab with the latest query when coming back
+    //to the serach tab.
     textController.text = query;
 
     _clearAndUnfocusSearchBar =
@@ -95,25 +104,15 @@ class SearchWidgetState extends State<SearchWidget> {
   }
 
   Future<void> textControllerListener() async {
-    //query in local varialbe
-    final value = textController.text;
-    isSearchQueryEmpty = value.isEmpty;
-    //latest query in global variable
-    query = textController.text;
-
-    final List<SearchResult> allResults =
-        await getSearchResultsForQuery(context, value);
-    /*checking if query == value to make sure that the results are from the current query
-                      and not from the previous query (race condition).*/
-    //checking if query == value to make sure that the latest query's result
-    //(allResults) is passed to updateResult. Due to race condition, the previous
-    //query's allResults could be passed to updateResult after the lastest query's
-    //allResults is passed.
-
-    if (mounted && query == value) {
-      final inheritedSearchResults = InheritedSearchResults.of(context);
-      inheritedSearchResults.updateResults(allResults);
-    }
+    isLoading.value = true;
+    _debouncer.run(() async {
+      if (mounted) {
+        query = textController.text;
+        IndexOfStackNotifier().isSearchQueryEmpty = query.isEmpty;
+        searchResultsStreamNotifier.value =
+            _getSearchResultsStream(context, query);
+      }
+    });
   }
 
   @override
@@ -177,14 +176,14 @@ class SearchWidgetState extends State<SearchWidget> {
                       /*Using valueListenableBuilder inside a stateful widget because this widget is only rebuild when
                       setState is called when deboucncing is over and the spinner needs to be shown while debouncing */
                       suffixIcon: ValueListenableBuilder(
-                        valueListenable: _debouncer.debounceActiveNotifier,
+                        valueListenable: isLoading,
                         builder: (
                           BuildContext context,
-                          bool isDebouncing,
+                          bool isSearching,
                           Widget? child,
                         ) {
                           return SearchSuffixIcon(
-                            isDebouncing,
+                            isSearching,
                           );
                         },
                       ),
@@ -274,6 +273,132 @@ class SearchWidgetState extends State<SearchWidget> {
     completer.complete(allResults);
   }
 
+  Stream<List<SearchResult>> _getSearchResultsStream(
+    BuildContext context,
+    String query,
+  ) {
+    int resultCount = 0;
+    final maxResultCount = _isYearValid(query) ? 11 : 10;
+    final streamController = StreamController<List<SearchResult>>();
+
+    if (query.isEmpty) {
+      streamController.sink.add([]);
+      streamController.close();
+      return streamController.stream;
+    }
+    if (_isYearValid(query)) {
+      _searchService.getYearSearchResults(query).then((yearSearchResults) {
+        streamController.sink.add(yearSearchResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      });
+    }
+
+    _searchService.getHolidaySearchResults(context, query).then(
+      (holidayResults) {
+        streamController.sink.add(holidayResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getFileTypeResults(context, query).then(
+      (fileTypeSearchResults) {
+        streamController.sink.add(fileTypeSearchResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getCaptionAndNameResults(query).then(
+      (captionAndDisplayNameResult) {
+        streamController.sink.add(captionAndDisplayNameResult);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getFileExtensionResults(query).then(
+      (fileExtnResult) {
+        streamController.sink.add(fileExtnResult);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getLocationResults(query).then(
+      (locationResult) {
+        streamController.sink.add(locationResult);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getCollectionSearchResults(query).then(
+      (collectionResults) {
+        streamController.sink.add(collectionResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getMonthSearchResults(context, query).then(
+      (monthResults) {
+        streamController.sink.add(monthResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getDateResults(context, query).then(
+      (possibleEvents) {
+        streamController.sink.add(possibleEvents);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getMagicSearchResults(context, query).then(
+      (magicResults) {
+        streamController.sink.add(magicResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    _searchService.getContactSearchResults(query).then(
+      (contactResults) {
+        streamController.sink.add(contactResults);
+        resultCount++;
+        if (resultCount == maxResultCount) {
+          streamController.close();
+        }
+      },
+    );
+
+    return streamController.stream;
+  }
+
   bool _isYearValid(String year) {
     final yearAsInt = int.tryParse(year); //returns null if cannot be parsed
     return yearAsInt != null && yearAsInt <= currentYear;