diff --git a/lib/features/core/data/extensions/object_extensions.dart b/lib/features/core/data/extensions/object_extensions.dart new file mode 100644 index 0000000..d5ea0e8 --- /dev/null +++ b/lib/features/core/data/extensions/object_extensions.dart @@ -0,0 +1,8 @@ +extension ObjectExtensions on Object? { + E asType() => this as E; + E? asNullableType() => this as E?; +} + +extension AsCallback on T { + T Function() get asCallback => () => this; +} diff --git a/lib/features/core/data/extensions/string_extensions.dart b/lib/features/core/data/extensions/string_extensions.dart new file mode 100644 index 0000000..d353aca --- /dev/null +++ b/lib/features/core/data/extensions/string_extensions.dart @@ -0,0 +1,14 @@ +extension StringExtensions on String { + /// Returns true if given word contains atleast all the characters in [targetChars], and `false` otherwise + /// + /// Very efficient `O(n)` instead of naive `O(n*m)` + bool containsAllCharacters({required String targetChars}) { + final Set characterSet = Set.from(targetChars.split('')); + for (final testChar in split('')) { + characterSet.remove(testChar); + if (characterSet.isEmpty) return true; + } + + return false; + } +} diff --git a/lib/features/home/services/images_service.dart b/lib/features/home/services/images_service.dart index 9a1fda7..c69a80f 100644 --- a/lib/features/home/services/images_service.dart +++ b/lib/features/home/services/images_service.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:mc_gallery/features/core/data/extensions/string_extensions.dart'; + import '/features/core/data/constants/const_sorters.dart'; import '/features/core/data/extensions/iterable_extensions.dart'; import '/features/core/data/extensions/map_extensions.dart'; @@ -68,21 +70,25 @@ class ImagesService { /// There are lots of optimizations possible for new inputs, for example reducing search frontier /// by using set-cover/subsetting optimizations on backspace, and so on, but again, not the point, /// I think. - Future> searchImages( - {required SearchOption searchOption, - required String imageNamePart, - bool treatAsInSequence = false}) async { + Future> searchImages({ + required SearchOption searchOption, + required String imageNamePart, + bool treatAsInSequence = false, + }) async { return await _searchMutex.lockAndRun(run: (final unlock) async { try { switch (searchOption) { case SearchOption.local: final rankedKeys = _imageModels.keys - //todo(mehul): Implement atleast-matching-all-parts - .where( - (final imageName) => imageName.contains(treatAsInSequence ? imageNamePart : '')) + // Reduce number of results by atleast occurring + .where((final imageName) => treatAsInSequence + ? imageName.contains(imageNamePart) + : imageName.containsAllCharacters(targetChars: imageNamePart)) .toList(growable: false) + // Sorting by the highest similarity first ..sort((final a, final b) => - ConstSorters.stringsSimilarityTarget(targetWord: imageNamePart, a, b)); + ConstSorters.stringsSimilarityTarget(targetWord: imageNamePart, a, b)) + ..reversed; return _imageModels.valuesByKeys(keys: rankedKeys).toList(growable: false); case SearchOption.web: return (await _imagesApi.searchImages( diff --git a/lib/features/home/views/gallery/gallery_view_model.dart b/lib/features/home/views/gallery/gallery_view_model.dart index ac15c37..65cdc5d 100644 --- a/lib/features/home/views/gallery/gallery_view_model.dart +++ b/lib/features/home/views/gallery/gallery_view_model.dart @@ -64,8 +64,6 @@ class GalleryViewModel extends BaseViewModel { .searchImages( imageNamePart: searchTerm, searchOption: searchOptionListenable.value, - // todo(mehul): When implemented, remove this - treatAsInSequence: true, ) .then( (final fetchedImageModels) => _imageSearchResultsNotifier.value = fetchedImageModels), @@ -79,7 +77,7 @@ class GalleryViewModel extends BaseViewModel { // If transitioning from 'Searching', clear previous results immediately if (_isSearchingNotifier.value) { _imageSearchResultsNotifier.value = []; - _loggingService.info('Clearing results on view mode change'); + _loggingService.info('Clearing of results on view mode change'); } _isSearchingNotifier.value = !_isSearchingNotifier.value; @@ -89,7 +87,12 @@ class GalleryViewModel extends BaseViewModel { void onSearchOptionChanged(SearchOption? option) { _searchOptionNotifier.value = option!; - _loggingService.info('Switching over to $option search'); + _loggingService.info('Switched over to $option search'); + + _imageSearchResultsNotifier.value = []; + _loggingService.info('Cleared resultsw from view'); + + //todo(mehul): Either redo search or force user to type in by clearing field } void onPromptPressed() => _isDisplayingPressingPrompt.value = false; diff --git a/lib/features/home/views/gallery/search_gallery_view.dart b/lib/features/home/views/gallery/search_gallery_view.dart index 19d9e34..7ec68b3 100644 --- a/lib/features/home/views/gallery/search_gallery_view.dart +++ b/lib/features/home/views/gallery/search_gallery_view.dart @@ -24,29 +24,56 @@ class _SearchGalleryView extends StatelessWidget { displayedWidget = const CircularProgressIndicator(); break; case ConnectionState.done: - displayedWidget = Wrap( - runSpacing: 24, - spacing: 8, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - for (final imageResult in resultsImageModels) - Image.network( - imageResult.uri.toString(), - loadingBuilder: (context, final child, final loadingProgress) => - loadingProgress == null - ? child - : Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ), - ), - ], + displayedWidget = ValueListenableBuilder( + valueListenable: galleryViewModel.searchOptionListenable, + builder: (context, final searchOption, child) { + switch (searchOption) { + case SearchOption.local: + return Wrap( + runSpacing: 24, + spacing: 8, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (final resultsImageModel in resultsImageModels) + CachedNetworkImage( + imageUrl: resultsImageModel.uri.toString(), + cacheKey: resultsImageModel.imageIndex.toString(), + progressIndicatorBuilder: (_, __, final progress) => + CircularProgressIndicator( + value: galleryViewModel.downloadProgressValue(progress: progress), + ), + ), + ], + ); + case SearchOption.web: + return Wrap( + runSpacing: 24, + spacing: 8, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (final imageResult in resultsImageModels) + Image.network( + imageResult.uri.toString(), + loadingBuilder: (context, final child, final loadingProgress) => + loadingProgress == null + ? child + : Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ), + ], + ); + } + }, ); }