filters: flag for country filter

This commit is contained in:
Thibault Deckers 2020-04-12 17:29:31 +09:00
parent 28d2dff8b5
commit 9c9c55e8cd
10 changed files with 76 additions and 31 deletions

View file

@ -124,7 +124,7 @@ class CollectionSource {
void updateLocations() { void updateLocations() {
final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails); final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails);
final lister = (String Function(AddressDetails a) f) => List<String>.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase)); final lister = (String Function(AddressDetails a) f) => List<String>.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase));
sortedCountries = lister((address) => address.countryName); sortedCountries = lister((address) => '${address.countryName};${address.countryCode}');
sortedCities = lister((address) => address.city); sortedCities = lister((address) => address.city);
} }

View file

@ -7,18 +7,27 @@ class LocationFilter extends CollectionFilter {
static const type = 'country'; static const type = 'country';
final LocationLevel level; final LocationLevel level;
final String location; String _location;
String _countryCode;
const LocationFilter(this.level, this.location); LocationFilter(this.level, this._location) {
final split = _location.split(';');
if (split.isNotEmpty) _location = split[0];
if (split.length > 1) _countryCode = split[1];
}
@override @override
bool filter(ImageEntry entry) => entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryName == location) || (level == LocationLevel.city && entry.addressDetails.city == location)); bool filter(ImageEntry entry) => entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryName == _location) || (level == LocationLevel.city && entry.addressDetails.city == _location));
@override @override
String get label => location; String get label => _location;
@override @override
Widget iconBuilder(context, size) => Icon(OMIcons.place, size: size); Widget iconBuilder(context, size) {
final flag = countryCodeToFlag(_countryCode);
if (flag != null) return Text(flag, style: TextStyle(fontSize: size));
return Icon(OMIcons.place, size: size);
}
@override @override
String get typeKey => type; String get typeKey => type;
@ -26,11 +35,19 @@ class LocationFilter extends CollectionFilter {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false; if (other.runtimeType != runtimeType) return false;
return other is LocationFilter && other.location == location; return other is LocationFilter && other._location == _location;
} }
@override @override
int get hashCode => hashValues('LocationFilter', location); int get hashCode => hashValues('LocationFilter', _location);
// U+0041 Latin Capital letter A
// U+1F1E6 🇦 REGIONAL INDICATOR SYMBOL LETTER A
static const _countryCodeToFlagDiff = 0x1F1E6 - 0x0041;
static String countryCodeToFlag(String code) {
return code?.length == 2 ? String.fromCharCodes(code.codeUnits.map((letter) => letter += _countryCodeToFlagDiff)) : null;
}
} }
enum LocationLevel { city, country } enum LocationLevel { city, country }

View file

@ -223,6 +223,7 @@ class ImageEntry {
addressDetails = AddressDetails( addressDetails = AddressDetails(
contentId: contentId, contentId: contentId,
addressLine: address.addressLine, addressLine: address.addressLine,
countryCode: address.countryCode,
countryName: address.countryName, countryName: address.countryName,
adminArea: address.adminArea, adminArea: address.adminArea,
locality: address.locality, locality: address.locality,

View file

@ -103,13 +103,14 @@ class OverlayMetadata {
class AddressDetails { class AddressDetails {
final int contentId; final int contentId;
final String addressLine, countryName, adminArea, locality; final String addressLine, countryCode, countryName, adminArea, locality;
String get city => locality != null && locality.isNotEmpty ? locality : adminArea; String get city => locality != null && locality.isNotEmpty ? locality : adminArea;
AddressDetails({ AddressDetails({
this.contentId, this.contentId,
this.addressLine, this.addressLine,
this.countryCode,
this.countryName, this.countryName,
this.adminArea, this.adminArea,
this.locality, this.locality,
@ -119,6 +120,7 @@ class AddressDetails {
return AddressDetails( return AddressDetails(
contentId: map['contentId'], contentId: map['contentId'],
addressLine: map['addressLine'] ?? '', addressLine: map['addressLine'] ?? '',
countryCode: map['countryCode'] ?? '',
countryName: map['countryName'] ?? '', countryName: map['countryName'] ?? '',
adminArea: map['adminArea'] ?? '', adminArea: map['adminArea'] ?? '',
locality: map['locality'] ?? '', locality: map['locality'] ?? '',
@ -128,6 +130,7 @@ class AddressDetails {
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
'contentId': contentId, 'contentId': contentId,
'addressLine': addressLine, 'addressLine': addressLine,
'countryCode': countryCode,
'countryName': countryName, 'countryName': countryName,
'adminArea': adminArea, 'adminArea': adminArea,
'locality': locality, 'locality': locality,
@ -135,7 +138,7 @@ class AddressDetails {
@override @override
String toString() { String toString() {
return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryName=$countryName, adminArea=$adminArea, locality=$locality}'; return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
} }
} }

View file

@ -26,7 +26,7 @@ class MetadataDb {
onCreate: (db, version) async { onCreate: (db, version) async {
await db.execute('CREATE TABLE $dateTakenTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER)'); await db.execute('CREATE TABLE $dateTakenTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER)');
await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, xmpTitleDescription TEXT, latitude REAL, longitude REAL)'); await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, xmpTitleDescription TEXT, latitude REAL, longitude REAL)');
await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryName TEXT, adminArea TEXT, locality TEXT)'); await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryCode TEXT, countryName TEXT, adminArea TEXT, locality TEXT)');
await db.execute('CREATE TABLE $favouriteTable(contentId INTEGER PRIMARY KEY, path TEXT)'); await db.execute('CREATE TABLE $favouriteTable(contentId INTEGER PRIMARY KEY, path TEXT)');
}, },
version: 1, version: 1,

View file

@ -93,14 +93,14 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
title: 'Favourites', title: 'Favourites',
filter: FavouriteFilter(), filter: FavouriteFilter(),
); );
final buildAlbumEntry = (album) => _FilteredCollectionNavTile( final buildAlbumEntry = (String album) => _FilteredCollectionNavTile(
source: source, source: source,
leading: IconUtils.getAlbumIcon(context: context, album: album), leading: IconUtils.getAlbumIcon(context: context, album: album),
title: source.getUniqueAlbumName(album), title: source.getUniqueAlbumName(album),
dense: true, dense: true,
filter: AlbumFilter(album, source.getUniqueAlbumName(album)), filter: AlbumFilter(album, source.getUniqueAlbumName(album)),
); );
final buildTagEntry = (tag) => _FilteredCollectionNavTile( final buildTagEntry = (String tag) => _FilteredCollectionNavTile(
source: source, source: source,
leading: Icon( leading: Icon(
OMIcons.localOffer, OMIcons.localOffer,
@ -110,18 +110,36 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
dense: true, dense: true,
filter: TagFilter(tag), filter: TagFilter(tag),
); );
final buildLocationEntry = (level, location) => _FilteredCollectionNavTile( final buildLocationEntry = (LocationLevel level, String location) {
source: source, String title;
leading: Icon( String flag;
OMIcons.place, if (level == LocationLevel.country) {
color: stringToColor(location), final split = location.split(';');
), String countryCode;
title: location, if (split.isNotEmpty) title = split[0];
dense: true, if (split.length > 1) countryCode = split[1];
filter: LocationFilter(level, location), flag = LocationFilter.countryCodeToFlag(countryCode);
); } else {
title = location;
}
return _FilteredCollectionNavTile(
source: source,
leading: flag != null
? Text(
flag,
style: TextStyle(fontSize: IconTheme.of(context).size),
)
: Icon(
OMIcons.place,
color: stringToColor(title),
),
title: title,
dense: true,
filter: LocationFilter(level, location),
);
};
final regularAlbums = [], appAlbums = [], specialAlbums = []; final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
for (var album in source.sortedAlbums) { for (var album in source.sortedAlbums) {
switch (androidFileUtils.getAlbumType(album)) { switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.Default: case AlbumType.Default:

View file

@ -4,8 +4,6 @@ import 'package:outline_material_icons/outline_material_icons.dart';
typedef FilterCallback = void Function(CollectionFilter filter); typedef FilterCallback = void Function(CollectionFilter filter);
typedef FilterBuilder = CollectionFilter Function(String label);
class AvesFilterChip extends StatefulWidget { class AvesFilterChip extends StatefulWidget {
final CollectionFilter filter; final CollectionFilter filter;
final bool removable; final bool removable;

View file

@ -92,6 +92,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
if (data != null) if (data != null)
InfoRowGroup({ InfoRowGroup({
'dateMillis': '${data.addressLine}', 'dateMillis': '${data.addressLine}',
'countryCode': '${data.countryCode}',
'countryName': '${data.countryName}', 'countryName': '${data.countryName}',
'adminArea': '${data.adminArea}', 'adminArea': '${data.adminArea}',
'locality': '${data.locality}', 'locality': '${data.locality}',

View file

@ -80,7 +80,7 @@ class _LocationSectionState extends State<LocationSection> {
final address = entry.addressDetails; final address = entry.addressDetails;
location = address.addressLine; location = address.addressLine;
final country = address.countryName; final country = address.countryName;
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, country)); if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country;${address.countryCode}'));
final city = address.city; final city = address.city;
if (city != null && city.isNotEmpty) filters.add(LocationFilter(LocationLevel.city, city)); if (city != null && city.isNotEmpty) filters.add(LocationFilter(LocationLevel.city, city));
} else if (entry.hasGps) { } else if (entry.hasGps) {

View file

@ -34,8 +34,9 @@ class StatsPage extends StatelessWidget {
if (city != null && city.isNotEmpty) { if (city != null && city.isNotEmpty) {
entryCountPerCity[city] = (entryCountPerCity[city] ?? 0) + 1; entryCountPerCity[city] = (entryCountPerCity[city] ?? 0) + 1;
} }
final country = address.countryName; var country = address.countryName;
if (country != null && country.isNotEmpty) { if (country != null && country.isNotEmpty) {
country += ';${address.countryCode}';
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1; entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
} }
} }
@ -193,7 +194,12 @@ class StatsPage extends StatelessWidget {
}); });
} }
List<Widget> _buildTopFilters(BuildContext context, String title, Map<String, int> entryCountMap, FilterBuilder filterBuilder) { List<Widget> _buildTopFilters(
BuildContext context,
String title,
Map<String, int> entryCountMap,
CollectionFilter Function(String key) filterBuilder,
) {
if (entryCountMap.isEmpty) return []; if (entryCountMap.isEmpty) return [];
final maxCount = collection.entryCount; final maxCount = collection.entryCount;
@ -214,7 +220,8 @@ class StatsPage extends StatelessWidget {
padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8), padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8),
child: Table( child: Table(
children: sortedEntries.take(5).map((kv) { children: sortedEntries.take(5).map((kv) {
final label = kv.key; final filter = filterBuilder(kv.key);
final label = filter.label;
final count = kv.value; final count = kv.value;
final percent = count / maxCount; final percent = count / maxCount;
return TableRow( return TableRow(
@ -222,7 +229,7 @@ class StatsPage extends StatelessWidget {
Align( Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: AvesFilterChip( child: AvesFilterChip(
filter: filterBuilder(label), filter: filter,
onPressed: (filter) => _goToCollection(context, filter), onPressed: (filter) => _goToCollection(context, filter),
), ),
), ),