city filter

This commit is contained in:
Thibault Deckers 2020-03-30 17:09:58 +09:00
parent 94b8ddc854
commit 4ab75fe218
11 changed files with 109 additions and 67 deletions

View file

@ -21,7 +21,7 @@ class CollectionLens with ChangeNotifier {
CollectionLens({
@required this.source,
List<CollectionFilter> filters,
Iterable<CollectionFilter> filters,
@required GroupFactor groupFactor,
@required SortFactor sortFactor,
}) : this.filters = [if (filters != null) ...filters.where((f) => f != null)].toSet(),
@ -53,13 +53,10 @@ class CollectionLens with ChangeNotifier {
CollectionLens derive(CollectionFilter filter) {
return CollectionLens(
source: source,
filters: [
...filters,
filter,
],
filters: filters,
groupFactor: groupFactor,
sortFactor: sortFactor,
);
)..addFilter(filter);
}
bool get isEmpty => _filteredEntries.isEmpty;

View file

@ -11,8 +11,9 @@ class CollectionSource {
final EventBus _eventBus = EventBus();
List<String> sortedAlbums = List.unmodifiable(const Iterable.empty());
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
List<String> sortedCities = List.unmodifiable(const Iterable.empty());
List<String> sortedCountries = List.unmodifiable(const Iterable.empty());
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
List<ImageEntry> get entries => List.unmodifiable(_rawEntries);
@ -117,9 +118,10 @@ class CollectionSource {
}
void updateLocations() {
final locatedEntries = _rawEntries.where((entry) => entry.isLocated);
final countries = locatedEntries.map((entry) => entry.addressDetails.countryName).toSet().toList()..sort(compareAsciiUpperCase);
sortedCountries = List.unmodifiable(countries);
final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails);
final lister = (String f(AddressDetails a)) => List<String>.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase));
sortedCountries = lister((address) => address.countryName);
sortedCities = lister((address) => address.city);
}
void add(ImageEntry entry) {

View file

@ -1,33 +0,0 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart';
import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
class CountryFilter extends CollectionFilter {
static const type = 'country';
final String country;
const CountryFilter(this.country);
@override
bool filter(ImageEntry entry) => entry.isLocated && entry.addressDetails.countryName == country;
@override
String get label => country;
@override
Widget iconBuilder(context, size) => Icon(OMIcons.place, size: size);
@override
String get typeKey => type;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is CountryFilter && other.country == country;
}
@override
int get hashCode => hashValues('CountryFilter', country);
}

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/country.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/gif.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/filters/video.dart';
@ -18,7 +18,7 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
VideoFilter.type,
GifFilter.type,
AlbumFilter.type,
CountryFilter.type,
LocationFilter.type,
TagFilter.type,
];

View file

@ -0,0 +1,36 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart';
import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
class LocationFilter extends CollectionFilter {
static const type = 'country';
final LocationLevel level;
final String location;
const LocationFilter(this.level, this.location);
@override
bool filter(ImageEntry entry) => entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryName == location) || (level == LocationLevel.city && entry.addressDetails.city == location));
@override
String get label => location;
@override
Widget iconBuilder(context, size) => Icon(OMIcons.place, size: size);
@override
String get typeKey => type;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is LocationFilter && other.location == location;
}
@override
int get hashCode => hashValues('LocationFilter', location);
}
enum LocationLevel { city, country }

View file

@ -76,6 +76,8 @@ class AddressDetails {
final int contentId;
final String addressLine, countryName, adminArea, locality;
String get city => locality != null && locality.isNotEmpty ? locality : adminArea;
AddressDetails({
this.contentId,
this.addressLine,

View file

@ -3,7 +3,7 @@ import 'dart:ui';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/collection_source.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/country.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/gif.dart';
@ -31,7 +31,7 @@ class CollectionDrawer extends StatefulWidget {
}
class _CollectionDrawerState extends State<CollectionDrawer> {
bool _albumsExpanded = false, _tagsExpanded = false, _countriesExpanded = false;
bool _albumsExpanded = false, _citiesExpanded = false, _countriesExpanded = false, _tagsExpanded = false;
CollectionSource get source => widget.source;
@ -110,15 +110,15 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
dense: true,
filter: TagFilter(tag),
);
final buildCountryEntry = (country) => _FilteredCollectionNavTile(
final buildLocationEntry = (level, location) => _FilteredCollectionNavTile(
source: source,
leading: Icon(
OMIcons.place,
color: stringToColor(country),
color: stringToColor(location),
),
title: country,
title: location,
dense: true,
filter: CountryFilter(country),
filter: LocationFilter(level, location),
);
final regularAlbums = [], appAlbums = [], specialAlbums = [];
@ -135,6 +135,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
break;
}
}
final cities = source.sortedCities;
final countries = source.sortedCountries;
final tags = source.sortedTags;
@ -174,6 +175,28 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
],
),
),
if (cities.isNotEmpty)
SafeArea(
top: false,
bottom: false,
child: ExpansionTile(
leading: const Icon(OMIcons.place),
title: Row(
children: [
const Text('Cities'),
const Spacer(),
Text(
'${cities.length}',
style: TextStyle(
color: (_citiesExpanded ? Theme.of(context).accentColor : Colors.white).withOpacity(.6),
),
),
],
),
onExpansionChanged: (expanded) => setState(() => _citiesExpanded = expanded),
children: cities.map((s) => buildLocationEntry(LocationLevel.city, s)).toList(),
),
),
if (countries.isNotEmpty)
SafeArea(
top: false,
@ -193,7 +216,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
],
),
onExpansionChanged: (expanded) => setState(() => _countriesExpanded = expanded),
children: countries.map(buildCountryEntry).toList(),
children: countries.map((s) => buildLocationEntry(LocationLevel.country, s)).toList(),
),
),
if (tags.isNotEmpty)

View file

@ -1,10 +1,10 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/collection_source.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/country.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/gif.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/filters/video.dart';
@ -71,10 +71,15 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
title: 'Albums',
filters: source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, CollectionSource.getUniqueAlbumName(s, source.sortedAlbums))).where((f) => containQuery(f.uniqueName)),
),
_buildFilterRow(
context: context,
title: 'Cities',
filters: source.sortedCities.where(containQuery).map((s) => LocationFilter(LocationLevel.city, s)),
),
_buildFilterRow(
context: context,
title: 'Countries',
filters: source.sortedCountries.where(containQuery).map((s) => CountryFilter(s)),
filters: source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)),
),
_buildFilterRow(
context: context,

View file

@ -58,7 +58,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
children: [
if (leading != null) ...[
leading,
const SizedBox(width: AvesFilterChip.padding * 1.6),
const SizedBox(width: AvesFilterChip.padding),
],
Flexible(
child: Text(

View file

@ -1,5 +1,5 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/filters/country.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_app_service.dart';
@ -74,15 +74,17 @@ class _LocationSectionState extends State<LocationSection> {
if (showMap) {
_loadedUri = entry.uri;
String location = '';
final List<LocationFilter> filters = [];
if (entry.isLocated) {
location = entry.addressDetails.addressLine;
final address = entry.addressDetails;
location = address.addressLine;
final country = address.countryName;
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, country));
final city = address.city;
if (city != null && city.isNotEmpty) filters.add(LocationFilter(LocationLevel.city, city));
} else if (entry.hasGps) {
location = toDMS(entry.latLng).join(', ');
}
final country = entry.addressDetails?.countryName;
final filters = [
if (country != null) CountryFilter(country),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,

View file

@ -1,6 +1,6 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/filters/country.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/image_entry.dart';
import 'package:aves/utils/color_utils.dart';
@ -17,14 +17,21 @@ import 'package:percent_indicator/linear_percent_indicator.dart';
class StatsPage extends StatelessWidget {
final CollectionLens collection;
final Map<String, int> entryCountPerCountry = Map<String, int>(), entryCountPerTag = Map<String, int>();
final Map<String, int> entryCountPerCity = Map(), entryCountPerCountry = Map(), entryCountPerTag = Map();
StatsPage({this.collection}) {
entries.forEach((entry) {
final country = entry.addressDetails?.countryName;
if (country != null) {
if (entry.isLocated) {
final address = entry.addressDetails;
final city = address.city;
if (city != null && city.isNotEmpty) {
entryCountPerCity[city] = (entryCountPerCity[city] ?? 0) + 1;
}
final country = address.countryName;
if (country != null && country.isNotEmpty) {
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
}
}
entry.xmpSubjects.forEach((tag) {
entryCountPerTag[tag] = (entryCountPerTag[tag] ?? 0) + 1;
});
@ -76,7 +83,8 @@ class StatsPage extends StatelessWidget {
],
),
),
..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => CountryFilter(s)),
..._buildTopFilters(context, 'Top cities', entryCountPerCity, (s) => LocationFilter(LocationLevel.city, s)),
..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)),
..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)),
],
),