diff --git a/lib/model/collection_source.dart b/lib/model/collection_source.dart index cdbb14cfc..78483e9ad 100644 --- a/lib/model/collection_source.dart +++ b/lib/model/collection_source.dart @@ -124,7 +124,7 @@ class CollectionSource { void updateLocations() { final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails); final lister = (String Function(AddressDetails a) f) => List.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); } diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 522d63b03..b22a016e8 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -7,18 +7,27 @@ class LocationFilter extends CollectionFilter { static const type = 'country'; 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 - 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 - String get label => location; + String get label => _location; @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 String get typeKey => type; @@ -26,11 +35,19 @@ class LocationFilter extends CollectionFilter { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is LocationFilter && other.location == location; + return other is LocationFilter && other._location == _location; } @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 } diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index d6c1b9393..868351d93 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -223,6 +223,7 @@ class ImageEntry { addressDetails = AddressDetails( contentId: contentId, addressLine: address.addressLine, + countryCode: address.countryCode, countryName: address.countryName, adminArea: address.adminArea, locality: address.locality, diff --git a/lib/model/image_metadata.dart b/lib/model/image_metadata.dart index 81f7dd9c6..6d8f00b69 100644 --- a/lib/model/image_metadata.dart +++ b/lib/model/image_metadata.dart @@ -103,13 +103,14 @@ class OverlayMetadata { class AddressDetails { 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; AddressDetails({ this.contentId, this.addressLine, + this.countryCode, this.countryName, this.adminArea, this.locality, @@ -119,6 +120,7 @@ class AddressDetails { return AddressDetails( contentId: map['contentId'], addressLine: map['addressLine'] ?? '', + countryCode: map['countryCode'] ?? '', countryName: map['countryName'] ?? '', adminArea: map['adminArea'] ?? '', locality: map['locality'] ?? '', @@ -128,6 +130,7 @@ class AddressDetails { Map toMap() => { 'contentId': contentId, 'addressLine': addressLine, + 'countryCode': countryCode, 'countryName': countryName, 'adminArea': adminArea, 'locality': locality, @@ -135,7 +138,7 @@ class AddressDetails { @override 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}'; } } diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index d65e6e845..060bbed8f 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -26,7 +26,7 @@ class MetadataDb { onCreate: (db, version) async { 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 $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)'); }, version: 1, diff --git a/lib/widgets/album/collection_drawer.dart b/lib/widgets/album/collection_drawer.dart index 462bf5ccf..5b5748874 100644 --- a/lib/widgets/album/collection_drawer.dart +++ b/lib/widgets/album/collection_drawer.dart @@ -93,14 +93,14 @@ class _CollectionDrawerState extends State { title: 'Favourites', filter: FavouriteFilter(), ); - final buildAlbumEntry = (album) => _FilteredCollectionNavTile( + final buildAlbumEntry = (String album) => _FilteredCollectionNavTile( source: source, leading: IconUtils.getAlbumIcon(context: context, album: album), title: source.getUniqueAlbumName(album), dense: true, filter: AlbumFilter(album, source.getUniqueAlbumName(album)), ); - final buildTagEntry = (tag) => _FilteredCollectionNavTile( + final buildTagEntry = (String tag) => _FilteredCollectionNavTile( source: source, leading: Icon( OMIcons.localOffer, @@ -110,18 +110,36 @@ class _CollectionDrawerState extends State { dense: true, filter: TagFilter(tag), ); - final buildLocationEntry = (level, location) => _FilteredCollectionNavTile( - source: source, - leading: Icon( - OMIcons.place, - color: stringToColor(location), - ), - title: location, - dense: true, - filter: LocationFilter(level, location), - ); + final buildLocationEntry = (LocationLevel level, String location) { + String title; + String flag; + if (level == LocationLevel.country) { + final split = location.split(';'); + String countryCode; + if (split.isNotEmpty) title = split[0]; + if (split.length > 1) countryCode = split[1]; + 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 = [], appAlbums = [], specialAlbums = []; for (var album in source.sortedAlbums) { switch (androidFileUtils.getAlbumType(album)) { case AlbumType.Default: diff --git a/lib/widgets/common/aves_filter_chip.dart b/lib/widgets/common/aves_filter_chip.dart index 8b13538fc..ca16b64a8 100644 --- a/lib/widgets/common/aves_filter_chip.dart +++ b/lib/widgets/common/aves_filter_chip.dart @@ -4,8 +4,6 @@ import 'package:outline_material_icons/outline_material_icons.dart'; typedef FilterCallback = void Function(CollectionFilter filter); -typedef FilterBuilder = CollectionFilter Function(String label); - class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; final bool removable; diff --git a/lib/widgets/fullscreen/debug.dart b/lib/widgets/fullscreen/debug.dart index c1380dc28..526c7b4be 100644 --- a/lib/widgets/fullscreen/debug.dart +++ b/lib/widgets/fullscreen/debug.dart @@ -92,6 +92,7 @@ class _FullscreenDebugPageState extends State { if (data != null) InfoRowGroup({ 'dateMillis': '${data.addressLine}', + 'countryCode': '${data.countryCode}', 'countryName': '${data.countryName}', 'adminArea': '${data.adminArea}', 'locality': '${data.locality}', diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 0fdb8b032..15b4d8e8f 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -80,7 +80,7 @@ class _LocationSectionState extends State { final address = entry.addressDetails; location = address.addressLine; 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; if (city != null && city.isNotEmpty) filters.add(LocationFilter(LocationLevel.city, city)); } else if (entry.hasGps) { diff --git a/lib/widgets/stats.dart b/lib/widgets/stats.dart index b960d9749..6dee9f129 100644 --- a/lib/widgets/stats.dart +++ b/lib/widgets/stats.dart @@ -34,8 +34,9 @@ class StatsPage extends StatelessWidget { if (city != null && city.isNotEmpty) { entryCountPerCity[city] = (entryCountPerCity[city] ?? 0) + 1; } - final country = address.countryName; + var country = address.countryName; if (country != null && country.isNotEmpty) { + country += ';${address.countryCode}'; entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1; } } @@ -193,7 +194,12 @@ class StatsPage extends StatelessWidget { }); } - List _buildTopFilters(BuildContext context, String title, Map entryCountMap, FilterBuilder filterBuilder) { + List _buildTopFilters( + BuildContext context, + String title, + Map entryCountMap, + CollectionFilter Function(String key) filterBuilder, + ) { if (entryCountMap.isEmpty) return []; final maxCount = collection.entryCount; @@ -214,7 +220,8 @@ class StatsPage extends StatelessWidget { padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.buttonBorderWidth / 2 + 6, end: 8), child: Table( children: sortedEntries.take(5).map((kv) { - final label = kv.key; + final filter = filterBuilder(kv.key); + final label = filter.label; final count = kv.value; final percent = count / maxCount; return TableRow( @@ -222,7 +229,7 @@ class StatsPage extends StatelessWidget { Align( alignment: AlignmentDirectional.centerStart, child: AvesFilterChip( - filter: filterBuilder(label), + filter: filter, onPressed: (filter) => _goToCollection(context, filter), ), ),