city filter
This commit is contained in:
parent
94b8ddc854
commit
4ab75fe218
11 changed files with 109 additions and 67 deletions
|
@ -21,7 +21,7 @@ class CollectionLens with ChangeNotifier {
|
||||||
|
|
||||||
CollectionLens({
|
CollectionLens({
|
||||||
@required this.source,
|
@required this.source,
|
||||||
List<CollectionFilter> filters,
|
Iterable<CollectionFilter> filters,
|
||||||
@required GroupFactor groupFactor,
|
@required GroupFactor groupFactor,
|
||||||
@required SortFactor sortFactor,
|
@required SortFactor sortFactor,
|
||||||
}) : this.filters = [if (filters != null) ...filters.where((f) => f != null)].toSet(),
|
}) : this.filters = [if (filters != null) ...filters.where((f) => f != null)].toSet(),
|
||||||
|
@ -53,13 +53,10 @@ class CollectionLens with ChangeNotifier {
|
||||||
CollectionLens derive(CollectionFilter filter) {
|
CollectionLens derive(CollectionFilter filter) {
|
||||||
return CollectionLens(
|
return CollectionLens(
|
||||||
source: source,
|
source: source,
|
||||||
filters: [
|
filters: filters,
|
||||||
...filters,
|
|
||||||
filter,
|
|
||||||
],
|
|
||||||
groupFactor: groupFactor,
|
groupFactor: groupFactor,
|
||||||
sortFactor: sortFactor,
|
sortFactor: sortFactor,
|
||||||
);
|
)..addFilter(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isEmpty => _filteredEntries.isEmpty;
|
bool get isEmpty => _filteredEntries.isEmpty;
|
||||||
|
|
|
@ -11,8 +11,9 @@ class CollectionSource {
|
||||||
final EventBus _eventBus = EventBus();
|
final EventBus _eventBus = EventBus();
|
||||||
|
|
||||||
List<String> sortedAlbums = List.unmodifiable(const Iterable.empty());
|
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> sortedCountries = List.unmodifiable(const Iterable.empty());
|
||||||
|
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
|
||||||
|
|
||||||
List<ImageEntry> get entries => List.unmodifiable(_rawEntries);
|
List<ImageEntry> get entries => List.unmodifiable(_rawEntries);
|
||||||
|
|
||||||
|
@ -117,9 +118,10 @@ class CollectionSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateLocations() {
|
void updateLocations() {
|
||||||
final locatedEntries = _rawEntries.where((entry) => entry.isLocated);
|
final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails);
|
||||||
final countries = locatedEntries.map((entry) => entry.addressDetails.countryName).toSet().toList()..sort(compareAsciiUpperCase);
|
final lister = (String f(AddressDetails a)) => List<String>.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase));
|
||||||
sortedCountries = List.unmodifiable(countries);
|
sortedCountries = lister((address) => address.countryName);
|
||||||
|
sortedCities = lister((address) => address.city);
|
||||||
}
|
}
|
||||||
|
|
||||||
void add(ImageEntry entry) {
|
void add(ImageEntry entry) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:aves/model/filters/album.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/favourite.dart';
|
||||||
import 'package:aves/model/filters/gif.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/query.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/video.dart';
|
import 'package:aves/model/filters/video.dart';
|
||||||
|
@ -18,7 +18,7 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
||||||
VideoFilter.type,
|
VideoFilter.type,
|
||||||
GifFilter.type,
|
GifFilter.type,
|
||||||
AlbumFilter.type,
|
AlbumFilter.type,
|
||||||
CountryFilter.type,
|
LocationFilter.type,
|
||||||
TagFilter.type,
|
TagFilter.type,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
36
lib/model/filters/location.dart
Normal file
36
lib/model/filters/location.dart
Normal 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 }
|
|
@ -76,6 +76,8 @@ class AddressDetails {
|
||||||
final int contentId;
|
final int contentId;
|
||||||
final String addressLine, countryName, adminArea, locality;
|
final String addressLine, countryName, adminArea, locality;
|
||||||
|
|
||||||
|
String get city => locality != null && locality.isNotEmpty ? locality : adminArea;
|
||||||
|
|
||||||
AddressDetails({
|
AddressDetails({
|
||||||
this.contentId,
|
this.contentId,
|
||||||
this.addressLine,
|
this.addressLine,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:ui';
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
import 'package:aves/model/filters/album.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/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/gif.dart';
|
import 'package:aves/model/filters/gif.dart';
|
||||||
|
@ -31,7 +31,7 @@ class CollectionDrawer extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollectionDrawerState extends State<CollectionDrawer> {
|
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;
|
CollectionSource get source => widget.source;
|
||||||
|
|
||||||
|
@ -110,15 +110,15 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
dense: true,
|
dense: true,
|
||||||
filter: TagFilter(tag),
|
filter: TagFilter(tag),
|
||||||
);
|
);
|
||||||
final buildCountryEntry = (country) => _FilteredCollectionNavTile(
|
final buildLocationEntry = (level, location) => _FilteredCollectionNavTile(
|
||||||
source: source,
|
source: source,
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
OMIcons.place,
|
OMIcons.place,
|
||||||
color: stringToColor(country),
|
color: stringToColor(location),
|
||||||
),
|
),
|
||||||
title: country,
|
title: location,
|
||||||
dense: true,
|
dense: true,
|
||||||
filter: CountryFilter(country),
|
filter: LocationFilter(level, location),
|
||||||
);
|
);
|
||||||
|
|
||||||
final regularAlbums = [], appAlbums = [], specialAlbums = [];
|
final regularAlbums = [], appAlbums = [], specialAlbums = [];
|
||||||
|
@ -135,6 +135,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final cities = source.sortedCities;
|
||||||
final countries = source.sortedCountries;
|
final countries = source.sortedCountries;
|
||||||
final tags = source.sortedTags;
|
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)
|
if (countries.isNotEmpty)
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
@ -193,7 +216,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onExpansionChanged: (expanded) => setState(() => _countriesExpanded = expanded),
|
onExpansionChanged: (expanded) => setState(() => _countriesExpanded = expanded),
|
||||||
children: countries.map(buildCountryEntry).toList(),
|
children: countries.map((s) => buildLocationEntry(LocationLevel.country, s)).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (tags.isNotEmpty)
|
if (tags.isNotEmpty)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
import 'package:aves/model/filters/album.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/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/gif.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/query.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/video.dart';
|
import 'package:aves/model/filters/video.dart';
|
||||||
|
@ -71,10 +71,15 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
||||||
title: 'Albums',
|
title: 'Albums',
|
||||||
filters: source.sortedAlbums.where(containQuery).map((s) => AlbumFilter(s, CollectionSource.getUniqueAlbumName(s, source.sortedAlbums))).where((f) => containQuery(f.uniqueName)),
|
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(
|
_buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'Countries',
|
title: 'Countries',
|
||||||
filters: source.sortedCountries.where(containQuery).map((s) => CountryFilter(s)),
|
filters: source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)),
|
||||||
),
|
),
|
||||||
_buildFilterRow(
|
_buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
@ -58,7 +58,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
children: [
|
children: [
|
||||||
if (leading != null) ...[
|
if (leading != null) ...[
|
||||||
leading,
|
leading,
|
||||||
const SizedBox(width: AvesFilterChip.padding * 1.6),
|
const SizedBox(width: AvesFilterChip.padding),
|
||||||
],
|
],
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
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/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/utils/android_app_service.dart';
|
import 'package:aves/utils/android_app_service.dart';
|
||||||
|
@ -74,15 +74,17 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
if (showMap) {
|
if (showMap) {
|
||||||
_loadedUri = entry.uri;
|
_loadedUri = entry.uri;
|
||||||
String location = '';
|
String location = '';
|
||||||
|
final List<LocationFilter> filters = [];
|
||||||
if (entry.isLocated) {
|
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) {
|
} else if (entry.hasGps) {
|
||||||
location = toDMS(entry.latLng).join(', ');
|
location = toDMS(entry.latLng).join(', ');
|
||||||
}
|
}
|
||||||
final country = entry.addressDetails?.countryName;
|
|
||||||
final filters = [
|
|
||||||
if (country != null) CountryFilter(country),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
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/filters.dart';
|
||||||
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
@ -17,14 +17,21 @@ import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class StatsPage extends StatelessWidget {
|
class StatsPage extends StatelessWidget {
|
||||||
final CollectionLens collection;
|
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}) {
|
StatsPage({this.collection}) {
|
||||||
entries.forEach((entry) {
|
entries.forEach((entry) {
|
||||||
final country = entry.addressDetails?.countryName;
|
if (entry.isLocated) {
|
||||||
if (country != null) {
|
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;
|
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
entry.xmpSubjects.forEach((tag) {
|
entry.xmpSubjects.forEach((tag) {
|
||||||
entryCountPerTag[tag] = (entryCountPerTag[tag] ?? 0) + 1;
|
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)),
|
..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue