diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 5fecb0848..8f78d6c18 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -121,6 +121,7 @@ mixin AlbumMixin on SourceBase { directories.forEach(_filterEntryCountMap.remove); directories.forEach(_filterRecentEntryMap.remove); } + eventBus.fire(AlbumSummaryInvalidatedEvent(directories)); } int albumEntryCount(AlbumFilter filter) { @@ -133,3 +134,9 @@ mixin AlbumMixin on SourceBase { } class AlbumsChangedEvent {} + +class AlbumSummaryInvalidatedEvent { + final Set directories; + + const AlbumSummaryInvalidatedEvent(this.directories); +} diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index c0f7b3715..124c9a8d1 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -131,16 +131,22 @@ mixin LocationMixin on SourceBase { void updateLocations() { final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).toList(); - sortedPlaces = List.unmodifiable(locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase)); + final updatedPlaces = locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase); + if (!listEquals(updatedPlaces, sortedPlaces)) { + sortedPlaces = List.unmodifiable(updatedPlaces); + eventBus.fire(PlacesChangedEvent()); + } // the same country code could be found with different country names // e.g. if the locale changed between geolocating calls // so we merge countries by code, keeping only one name for each code final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty)); - sortedCountries = List.unmodifiable(countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase)); - - invalidateCountryFilterSummary(); - eventBus.fire(LocationsChangedEvent()); + final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase); + if (!listEquals(updatedCountries, sortedCountries)) { + sortedCountries = List.unmodifiable(updatedCountries); + invalidateCountryFilterSummary(); + eventBus.fire(CountriesChangedEvent()); + } } // filter summary @@ -150,14 +156,16 @@ mixin LocationMixin on SourceBase { final Map _filterRecentEntryMap = {}; void invalidateCountryFilterSummary([Set entries]) { + Set countryCodes; if (entries == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); } else { - final countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails.countryCode).toSet(); + countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails.countryCode).toSet(); countryCodes.remove(null); countryCodes.forEach(_filterEntryCountMap.remove); } + eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes)); } int countryEntryCount(LocationFilter filter) { @@ -171,4 +179,12 @@ mixin LocationMixin on SourceBase { class AddressMetadataChangedEvent {} -class LocationsChangedEvent {} +class PlacesChangedEvent {} + +class CountriesChangedEvent {} + +class CountrySummaryInvalidatedEvent { + final Set countryCodes; + + const CountrySummaryInvalidatedEvent(this.countryCodes); +} diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 9dbd72934..897ba577e 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -56,11 +56,12 @@ mixin TagMixin on SourceBase { } void updateTags() { - final tags = visibleEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); - sortedTags = List.unmodifiable(tags); - - invalidateTagFilterSummary(); - eventBus.fire(TagsChangedEvent()); + final updatedTags = visibleEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); + if (!listEquals(updatedTags, sortedTags)) { + sortedTags = List.unmodifiable(updatedTags); + invalidateTagFilterSummary(); + eventBus.fire(TagsChangedEvent()); + } } // filter summary @@ -70,13 +71,15 @@ mixin TagMixin on SourceBase { final Map _filterRecentEntryMap = {}; void invalidateTagFilterSummary([Set entries]) { + Set tags; if (entries == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); } else { - final tags = entries.where((entry) => entry.isCatalogued).expand((entry) => entry.xmpSubjects).toSet(); + tags = entries.where((entry) => entry.isCatalogued).expand((entry) => entry.xmpSubjects).toSet(); tags.forEach(_filterEntryCountMap.remove); } + eventBus.fire(TagSummaryInvalidatedEvent(tags)); } int tagEntryCount(TagFilter filter) { @@ -91,3 +94,9 @@ mixin TagMixin on SourceBase { class CatalogMetadataChangedEvent {} class TagsChangedEvent {} + +class TagSummaryInvalidatedEvent { + final Set tags; + + const TagSummaryInvalidatedEvent(this.tags); +} diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index fb942856a..c3322e4dd 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -177,7 +177,7 @@ class _AppDrawerState extends State { icon: AIcons.location, title: 'Countries', trailing: StreamBuilder( - stream: source.eventBus.on(), + stream: source.eventBus.on(), builder: (context, _) => Text('${source.sortedCountries.length}'), ), routeName: CountryListPage.routeName, diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 102ccea1f..a0646b566 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -1,10 +1,14 @@ import 'dart:math'; import 'dart:ui'; -import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/location.dart'; +import 'package:aves/model/source/tag.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -20,7 +24,6 @@ import 'package:provider/provider.dart'; class DecoratedFilterChip extends StatelessWidget { final CollectionFilter filter; - final AvesEntry entry; final double extent; final bool pinned, highlightable; final FilterCallback onTap; @@ -29,7 +32,6 @@ class DecoratedFilterChip extends StatelessWidget { const DecoratedFilterChip({ Key key, @required this.filter, - @required this.entry, @required this.extent, this.pinned = false, this.highlightable = true, @@ -39,6 +41,42 @@ class DecoratedFilterChip extends StatelessWidget { @override Widget build(BuildContext context) { + return Consumer( + builder: (context, source, child) { + switch (filter.runtimeType) { + case AlbumFilter: + { + final album = (filter as AlbumFilter).album; + return StreamBuilder( + stream: source.eventBus.on().where((event) => event.directories == null || event.directories.contains(album)), + builder: (context, snapshot) => _buildChip(source), + ); + } + case LocationFilter: + { + final countryCode = (filter as LocationFilter).countryCode; + return StreamBuilder( + stream: source.eventBus.on().where((event) => event.countryCodes == null || event.countryCodes.contains(countryCode)), + builder: (context, snapshot) => _buildChip(source), + ); + } + case TagFilter: + { + final tag = (filter as TagFilter).tag; + return StreamBuilder( + stream: source.eventBus.on().where((event) => event.tags == null || event.tags.contains(tag)), + builder: (context, snapshot) => _buildChip(source), + ); + } + default: + return SizedBox(); + } + }, + ); + } + + Widget _buildChip(CollectionSource source) { + final entry = source.recentEntry(filter); final backgroundImage = entry == null ? Container(color: Colors.white) : entry.isSvg @@ -57,7 +95,7 @@ class DecoratedFilterChip extends StatelessWidget { filter: filter, showGenericIcon: false, background: backgroundImage, - details: _buildDetails(filter), + details: _buildDetails(source, filter), borderRadius: borderRadius, padding: titlePadding, onTap: onTap, @@ -86,7 +124,7 @@ class DecoratedFilterChip extends StatelessWidget { return child; } - Widget _buildDetails(CollectionFilter filter) { + Widget _buildDetails(CollectionSource source, CollectionFilter filter) { final padding = min(8.0, extent / 16); final iconSize = min(14.0, extent / 8); final fontSize = min(14.0, extent / 6); @@ -115,13 +153,11 @@ class DecoratedFilterChip extends StatelessWidget { size: iconSize, ), ), - Consumer( - builder: (context, source, child) => Text( - '${source.count(filter)}', - style: TextStyle( - color: FilterGridPage.detailColor, - fontSize: fontSize, - ), + Text( + '${source.count(filter)}', + style: TextStyle( + color: FilterGridPage.detailColor, + fontSize: fontSize, ), ), ], diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index f3f2ec4fd..c36427f40 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -117,7 +117,6 @@ class FilterGridPage extends StatelessWidget { child: DecoratedFilterChip( key: Key(filter.key), filter: filter, - entry: entry, extent: _tileExtentNotifier.value, pinned: pinnedFilters.contains(filter), onTap: onTap, @@ -252,7 +251,6 @@ class _SectionedContentState extends State<_Sectione final filter = item.filter; return DecoratedFilterChip( filter: filter, - entry: item.entry, extent: extent, pinned: pinnedFilters.contains(filter), highlightable: false, diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 0d8b00fa8..e2d7a5b5a 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -26,7 +26,7 @@ class CountryListPage extends StatelessWidget { selector: (context, s) => Tuple2(s.countrySortFactor, s.pinnedFilters), builder: (context, s, child) { return StreamBuilder( - stream: source.eventBus.on(), + stream: source.eventBus.on(), builder: (context, snapshot) => FilterNavigationPage( source: source, title: 'Countries', diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 927bf82fa..dfcbc4a21 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -110,7 +110,7 @@ class CollectionSearchDelegate { ); }), StreamBuilder( - stream: source.eventBus.on(), + stream: source.eventBus.on(), builder: (context, snapshot) { final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); return _buildFilterRow( @@ -120,7 +120,7 @@ class CollectionSearchDelegate { ); }), StreamBuilder( - stream: source.eventBus.on(), + stream: source.eventBus.on(), builder: (context, snapshot) { final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)); final noFilter = LocationFilter(LocationLevel.place, '');