diff --git a/mobile/lib/models/search/hierarchical/hierarchical_search_filter.dart b/mobile/lib/models/search/hierarchical/hierarchical_search_filter.dart index 9dd616c0b2..4787d89769 100644 --- a/mobile/lib/models/search/hierarchical/hierarchical_search_filter.dart +++ b/mobile/lib/models/search/hierarchical/hierarchical_search_filter.dart @@ -11,6 +11,7 @@ abstract class HierarchicalSearchFilter { //in gallery is when the filter is the initial filter (top level) of the //gallery. final Set matchedUploadedIDs; + bool isApplied = false; HierarchicalSearchFilter({matchedUploadedIDs}) : matchedUploadedIDs = matchedUploadedIDs ?? {}; diff --git a/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart b/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart index 4c6a3ed8a1..7a61c22698 100644 --- a/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart +++ b/mobile/lib/ui/viewer/gallery/state/search_filter_data_provider.dart @@ -25,16 +25,26 @@ class SearchFilterDataProvider { void applyFilters(List filters) { _recommendedFiltersNotifier.removeFilters(filters); + + late final List allFiltersToAdd; if (!isSearchingNotifier.value) { isSearchingNotifier.value = true; - _appliedFiltersNotifier.addFilters([initialGalleryFilter, ...filters]); + allFiltersToAdd = [initialGalleryFilter, ...filters]; } else { - _appliedFiltersNotifier.addFilters(filters); + allFiltersToAdd = filters; } + + for (HierarchicalSearchFilter filter in allFiltersToAdd) { + filter.isApplied = true; + } + _appliedFiltersNotifier.addFilters(allFiltersToAdd); } void removeAppliedFilters(List filters) { _appliedFiltersNotifier.removeFilters(filters); + for (HierarchicalSearchFilter filter in filters) { + filter.isApplied = false; + } _safelyAddToRecommended(filters); } diff --git a/mobile/lib/ui/viewer/hierarchicial_search/applied_filters.dart b/mobile/lib/ui/viewer/hierarchicial_search/applied_filters.dart index 87a2e2957a..1d06fdd8d7 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/applied_filters.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/applied_filters.dart @@ -66,10 +66,14 @@ class _AppliedFiltersState extends State { ) : GenericFilterChip( label: filter.name(), - onTap: () { + apply: () { + _searchFilterDataProvider.applyFilters([filter]); + }, + remove: () { _searchFilterDataProvider.removeAppliedFilters([filter]); }, leadingIcon: filter.icon(), + isApplied: filter.isApplied, ), ); }, diff --git a/mobile/lib/ui/viewer/hierarchicial_search/filter_chip.dart b/mobile/lib/ui/viewer/hierarchicial_search/filter_chip.dart index a380ebe692..5e10a9e1b7 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/filter_chip.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/filter_chip.dart @@ -1,58 +1,114 @@ import "package:flutter/material.dart"; -import "package:flutter/widgets.dart"; import "package:photos/core/constants.dart"; import "package:photos/models/file/file.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/search/result/person_face_widget.dart"; -class GenericFilterChip extends StatelessWidget { +class GenericFilterChip extends StatefulWidget { final String label; final IconData? leadingIcon; - final VoidCallback onTap; + final VoidCallback apply; + final VoidCallback remove; + final bool isApplied; const GenericFilterChip({ required this.label, - required this.onTap, + required this.apply, + required this.remove, + required this.isApplied, this.leadingIcon, super.key, }); + @override + State createState() => _GenericFilterChipState(); +} + +class _GenericFilterChipState extends State { + late bool _isApplied; + + @override + void initState() { + super.initState(); + _isApplied = widget.isApplied; + } + @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap.call, - child: Container( - decoration: BoxDecoration( - color: getEnteColorScheme(context).fillFaint, - borderRadius: - const BorderRadius.all(Radius.circular(kFilterChipHeight / 2)), - border: Border.all( - color: getEnteColorScheme(context).strokeFaint, - width: 0.5, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - leadingIcon != null - ? Icon( - leadingIcon, - size: 16, - ) - : const SizedBox.shrink(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Text( - label, - style: getEnteTextTheme(context).miniBold, - ), + onTap: () { + setState(() { + if (_isApplied) { + widget.remove(); + } else { + widget.apply(); + } + _isApplied = !_isApplied; + }); + }, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + color: getEnteColorScheme(context).fillFaint, + borderRadius: const BorderRadius.all( + Radius.circular(kFilterChipHeight / 2), ), - ], + border: Border.all( + color: getEnteColorScheme(context).strokeFaint, + width: 0.5, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.leadingIcon != null + ? Icon( + widget.leadingIcon, + size: 16, + ) + : const SizedBox.shrink(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + widget.label, + style: getEnteTextTheme(context).miniBold, + ), + ), + ], + ), + ), ), - ), + _isApplied + ? Positioned( + top: -4, + right: -4, + child: Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: getEnteColorScheme(context).backgroundElevated2, + border: Border.all( + color: getEnteColorScheme(context).strokeMuted, + width: 0.5, + ), + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + ), + child: Icon( + Icons.close_rounded, + size: 14, + color: getEnteColorScheme(context).textBase, + ), + ), + ) + : const SizedBox.shrink(), + ], ), ); } diff --git a/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart b/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart new file mode 100644 index 0000000000..0cbceca2b7 --- /dev/null +++ b/mobile/lib/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart @@ -0,0 +1,57 @@ +import "package:flutter/material.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/models/search/hierarchical/face_filter.dart"; +import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; +import "package:photos/ui/viewer/hierarchicial_search/filter_chip.dart"; + +class FilterOptionsBottomSheet extends StatelessWidget { + final SearchFilterDataProvider searchFilterDataProvider; + const FilterOptionsBottomSheet( + this.searchFilterDataProvider, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final recommendations = searchFilterDataProvider.recommendations; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 32), + child: SizedBox( + height: kFilterChipHeight, + child: ListView.builder( + itemBuilder: (context, index) { + final filter = recommendations[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: filter is FaceFilter + ? FaceFilterChip( + personId: filter.personId, + clusterId: filter.clusterId, + faceThumbnailFile: filter.faceFile, + name: filter.name(), + onTap: () { + searchFilterDataProvider.applyFilters([filter]); + }, + ) + : GenericFilterChip( + label: filter.name(), + apply: () { + searchFilterDataProvider.applyFilters([filter]); + }, + remove: () { + searchFilterDataProvider.removeAppliedFilters([filter]); + }, + leadingIcon: filter.icon(), + isApplied: filter.isApplied, + ), + ); + }, + clipBehavior: Clip.none, + scrollDirection: Axis.horizontal, + itemCount: recommendations.length, + padding: const EdgeInsets.symmetric(horizontal: 4), + ), + ), + ); + } +} diff --git a/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters.dart b/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters.dart index 6b5fa257c6..3bda753696 100644 --- a/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters.dart +++ b/mobile/lib/ui/viewer/hierarchicial_search/recommended_filters.dart @@ -2,9 +2,11 @@ import "package:flutter/material.dart"; import "package:photos/core/constants.dart"; import "package:photos/models/search/hierarchical/face_filter.dart"; import "package:photos/models/search/hierarchical/hierarchical_search_filter.dart"; +import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; import "package:photos/ui/viewer/hierarchicial_search/filter_chip.dart"; +import "package:photos/ui/viewer/hierarchicial_search/filter_options_bottom_sheet.dart"; class RecommendedFilters extends StatefulWidget { const RecommendedFilters({super.key}); @@ -67,7 +69,23 @@ class _RecommendedFiltersState extends State { child: ListView.builder( key: ValueKey(_filtersUpdateCount), itemBuilder: (context, index) { - final filter = _recommendations[index]; + if (index == 0) { + return IconButtonWidget( + icon: Icons.sort, + iconButtonType: IconButtonType.rounded, + onTap: () { + showModalBottomSheet( + context: context, + builder: (context) { + return FilterOptionsBottomSheet( + _searchFilterDataProvider, + ); + }, + ); + }, + ); + } + final filter = _recommendations[index - 1]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: filter is FaceFilter @@ -82,16 +100,21 @@ class _RecommendedFiltersState extends State { ) : GenericFilterChip( label: filter.name(), - onTap: () { + apply: () { _searchFilterDataProvider.applyFilters([filter]); }, + remove: () { + _searchFilterDataProvider + .removeAppliedFilters([filter]); + }, leadingIcon: filter.icon(), + isApplied: filter.isApplied, ), ); }, clipBehavior: Clip.none, scrollDirection: Axis.horizontal, - itemCount: _recommendations.length, + itemCount: _recommendations.length + 1, padding: const EdgeInsets.symmetric(horizontal: 4), ), ),