fixed album/country/tag background image and count update on source change

This commit is contained in:
Thibault Deckers 2021-02-25 19:17:56 +09:00
parent c86534d600
commit c7aef79f37
8 changed files with 97 additions and 31 deletions

View file

@ -121,6 +121,7 @@ mixin AlbumMixin on SourceBase {
directories.forEach(_filterEntryCountMap.remove); directories.forEach(_filterEntryCountMap.remove);
directories.forEach(_filterRecentEntryMap.remove); directories.forEach(_filterRecentEntryMap.remove);
} }
eventBus.fire(AlbumSummaryInvalidatedEvent(directories));
} }
int albumEntryCount(AlbumFilter filter) { int albumEntryCount(AlbumFilter filter) {
@ -133,3 +134,9 @@ mixin AlbumMixin on SourceBase {
} }
class AlbumsChangedEvent {} class AlbumsChangedEvent {}
class AlbumSummaryInvalidatedEvent {
final Set<String> directories;
const AlbumSummaryInvalidatedEvent(this.directories);
}

View file

@ -131,16 +131,22 @@ mixin LocationMixin on SourceBase {
void updateLocations() { void updateLocations() {
final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).toList(); final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).toList();
sortedPlaces = List<String>.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 // the same country code could be found with different country names
// e.g. if the locale changed between geolocating calls // e.g. if the locale changed between geolocating calls
// so we merge countries by code, keeping only one name for each code // 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)); final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty));
sortedCountries = List<String>.unmodifiable(countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase)); final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase);
if (!listEquals(updatedCountries, sortedCountries)) {
invalidateCountryFilterSummary(); sortedCountries = List.unmodifiable(updatedCountries);
eventBus.fire(LocationsChangedEvent()); invalidateCountryFilterSummary();
eventBus.fire(CountriesChangedEvent());
}
} }
// filter summary // filter summary
@ -150,14 +156,16 @@ mixin LocationMixin on SourceBase {
final Map<String, AvesEntry> _filterRecentEntryMap = {}; final Map<String, AvesEntry> _filterRecentEntryMap = {};
void invalidateCountryFilterSummary([Set<AvesEntry> entries]) { void invalidateCountryFilterSummary([Set<AvesEntry> entries]) {
Set<String> countryCodes;
if (entries == null) { if (entries == null) {
_filterEntryCountMap.clear(); _filterEntryCountMap.clear();
_filterRecentEntryMap.clear(); _filterRecentEntryMap.clear();
} else { } 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.remove(null);
countryCodes.forEach(_filterEntryCountMap.remove); countryCodes.forEach(_filterEntryCountMap.remove);
} }
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));
} }
int countryEntryCount(LocationFilter filter) { int countryEntryCount(LocationFilter filter) {
@ -171,4 +179,12 @@ mixin LocationMixin on SourceBase {
class AddressMetadataChangedEvent {} class AddressMetadataChangedEvent {}
class LocationsChangedEvent {} class PlacesChangedEvent {}
class CountriesChangedEvent {}
class CountrySummaryInvalidatedEvent {
final Set<String> countryCodes;
const CountrySummaryInvalidatedEvent(this.countryCodes);
}

View file

@ -56,11 +56,12 @@ mixin TagMixin on SourceBase {
} }
void updateTags() { void updateTags() {
final tags = visibleEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase); final updatedTags = visibleEntries.expand((entry) => entry.xmpSubjects).toSet().toList()..sort(compareAsciiUpperCase);
sortedTags = List.unmodifiable(tags); if (!listEquals(updatedTags, sortedTags)) {
sortedTags = List.unmodifiable(updatedTags);
invalidateTagFilterSummary(); invalidateTagFilterSummary();
eventBus.fire(TagsChangedEvent()); eventBus.fire(TagsChangedEvent());
}
} }
// filter summary // filter summary
@ -70,13 +71,15 @@ mixin TagMixin on SourceBase {
final Map<String, AvesEntry> _filterRecentEntryMap = {}; final Map<String, AvesEntry> _filterRecentEntryMap = {};
void invalidateTagFilterSummary([Set<AvesEntry> entries]) { void invalidateTagFilterSummary([Set<AvesEntry> entries]) {
Set<String> tags;
if (entries == null) { if (entries == null) {
_filterEntryCountMap.clear(); _filterEntryCountMap.clear();
_filterRecentEntryMap.clear(); _filterRecentEntryMap.clear();
} else { } 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); tags.forEach(_filterEntryCountMap.remove);
} }
eventBus.fire(TagSummaryInvalidatedEvent(tags));
} }
int tagEntryCount(TagFilter filter) { int tagEntryCount(TagFilter filter) {
@ -91,3 +94,9 @@ mixin TagMixin on SourceBase {
class CatalogMetadataChangedEvent {} class CatalogMetadataChangedEvent {}
class TagsChangedEvent {} class TagsChangedEvent {}
class TagSummaryInvalidatedEvent {
final Set<String> tags;
const TagSummaryInvalidatedEvent(this.tags);
}

View file

@ -177,7 +177,7 @@ class _AppDrawerState extends State<AppDrawer> {
icon: AIcons.location, icon: AIcons.location,
title: 'Countries', title: 'Countries',
trailing: StreamBuilder( trailing: StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(), stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, _) => Text('${source.sortedCountries.length}'), builder: (context, _) => Text('${source.sortedCountries.length}'),
), ),
routeName: CountryListPage.routeName, routeName: CountryListPage.routeName,

View file

@ -1,10 +1,14 @@
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.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/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/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
@ -20,7 +24,6 @@ import 'package:provider/provider.dart';
class DecoratedFilterChip extends StatelessWidget { class DecoratedFilterChip extends StatelessWidget {
final CollectionFilter filter; final CollectionFilter filter;
final AvesEntry entry;
final double extent; final double extent;
final bool pinned, highlightable; final bool pinned, highlightable;
final FilterCallback onTap; final FilterCallback onTap;
@ -29,7 +32,6 @@ class DecoratedFilterChip extends StatelessWidget {
const DecoratedFilterChip({ const DecoratedFilterChip({
Key key, Key key,
@required this.filter, @required this.filter,
@required this.entry,
@required this.extent, @required this.extent,
this.pinned = false, this.pinned = false,
this.highlightable = true, this.highlightable = true,
@ -39,6 +41,42 @@ class DecoratedFilterChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<CollectionSource>(
builder: (context, source, child) {
switch (filter.runtimeType) {
case AlbumFilter:
{
final album = (filter as AlbumFilter).album;
return StreamBuilder<AlbumSummaryInvalidatedEvent>(
stream: source.eventBus.on<AlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories.contains(album)),
builder: (context, snapshot) => _buildChip(source),
);
}
case LocationFilter:
{
final countryCode = (filter as LocationFilter).countryCode;
return StreamBuilder<CountrySummaryInvalidatedEvent>(
stream: source.eventBus.on<CountrySummaryInvalidatedEvent>().where((event) => event.countryCodes == null || event.countryCodes.contains(countryCode)),
builder: (context, snapshot) => _buildChip(source),
);
}
case TagFilter:
{
final tag = (filter as TagFilter).tag;
return StreamBuilder<TagSummaryInvalidatedEvent>(
stream: source.eventBus.on<TagSummaryInvalidatedEvent>().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 final backgroundImage = entry == null
? Container(color: Colors.white) ? Container(color: Colors.white)
: entry.isSvg : entry.isSvg
@ -57,7 +95,7 @@ class DecoratedFilterChip extends StatelessWidget {
filter: filter, filter: filter,
showGenericIcon: false, showGenericIcon: false,
background: backgroundImage, background: backgroundImage,
details: _buildDetails(filter), details: _buildDetails(source, filter),
borderRadius: borderRadius, borderRadius: borderRadius,
padding: titlePadding, padding: titlePadding,
onTap: onTap, onTap: onTap,
@ -86,7 +124,7 @@ class DecoratedFilterChip extends StatelessWidget {
return child; return child;
} }
Widget _buildDetails(CollectionFilter filter) { Widget _buildDetails(CollectionSource source, CollectionFilter filter) {
final padding = min<double>(8.0, extent / 16); final padding = min<double>(8.0, extent / 16);
final iconSize = min<double>(14.0, extent / 8); final iconSize = min<double>(14.0, extent / 8);
final fontSize = min<double>(14.0, extent / 6); final fontSize = min<double>(14.0, extent / 6);
@ -115,13 +153,11 @@ class DecoratedFilterChip extends StatelessWidget {
size: iconSize, size: iconSize,
), ),
), ),
Consumer<CollectionSource>( Text(
builder: (context, source, child) => Text( '${source.count(filter)}',
'${source.count(filter)}', style: TextStyle(
style: TextStyle( color: FilterGridPage.detailColor,
color: FilterGridPage.detailColor, fontSize: fontSize,
fontSize: fontSize,
),
), ),
), ),
], ],

View file

@ -117,7 +117,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
child: DecoratedFilterChip( child: DecoratedFilterChip(
key: Key(filter.key), key: Key(filter.key),
filter: filter, filter: filter,
entry: entry,
extent: _tileExtentNotifier.value, extent: _tileExtentNotifier.value,
pinned: pinnedFilters.contains(filter), pinned: pinnedFilters.contains(filter),
onTap: onTap, onTap: onTap,
@ -252,7 +251,6 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
final filter = item.filter; final filter = item.filter;
return DecoratedFilterChip( return DecoratedFilterChip(
filter: filter, filter: filter,
entry: item.entry,
extent: extent, extent: extent,
pinned: pinnedFilters.contains(filter), pinned: pinnedFilters.contains(filter),
highlightable: false, highlightable: false,

View file

@ -26,7 +26,7 @@ class CountryListPage extends StatelessWidget {
selector: (context, s) => Tuple2(s.countrySortFactor, s.pinnedFilters), selector: (context, s) => Tuple2(s.countrySortFactor, s.pinnedFilters),
builder: (context, s, child) { builder: (context, s, child) {
return StreamBuilder( return StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(), stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage<LocationFilter>( builder: (context, snapshot) => FilterNavigationPage<LocationFilter>(
source: source, source: source,
title: 'Countries', title: 'Countries',

View file

@ -110,7 +110,7 @@ class CollectionSearchDelegate {
); );
}), }),
StreamBuilder( StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(), stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, snapshot) { builder: (context, snapshot) {
final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList();
return _buildFilterRow( return _buildFilterRow(
@ -120,7 +120,7 @@ class CollectionSearchDelegate {
); );
}), }),
StreamBuilder( StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(), stream: source.eventBus.on<PlacesChangedEvent>(),
builder: (context, snapshot) { builder: (context, snapshot) {
final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)); final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s));
final noFilter = LocationFilter(LocationLevel.place, ''); final noFilter = LocationFilter(LocationLevel.place, '');