filters: replaced GIF mime filter by animated webp or gif

This commit is contained in:
Thibault Deckers 2020-04-13 11:20:37 +09:00
parent 9c9c55e8cd
commit fd5bb222d7
22 changed files with 240 additions and 121 deletions

View file

@ -19,8 +19,11 @@ import com.drew.lang.GeoLocation;
import com.drew.metadata.Directory; import com.drew.metadata.Directory;
import com.drew.metadata.Metadata; import com.drew.metadata.Metadata;
import com.drew.metadata.Tag; import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory; import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory; import com.drew.metadata.exif.GpsDirectory;
import com.drew.metadata.gif.GifAnimationDirectory;
import com.drew.metadata.webp.WebpDirectory;
import com.drew.metadata.xmp.XmpDirectory; import com.drew.metadata.xmp.XmpDirectory;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -41,13 +44,30 @@ import io.flutter.plugin.common.MethodChannel;
public class MetadataHandler implements MethodChannel.MethodCallHandler { public class MetadataHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/metadata"; public static final String CHANNEL = "deckers.thibault/aves/metadata";
// catalog metadata
private static final String KEY_DATE_MILLIS = "dateMillis";
private static final String KEY_IS_ANIMATED = "isAnimated";
private static final String KEY_LATITUDE = "latitude";
private static final String KEY_LONGITUDE = "longitude";
private static final String KEY_VIDEO_ROTATION = "videoRotation";
private static final String KEY_XMP_SUBJECTS = "xmpSubjects";
private static final String KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription";
// overlay metadata
private static final String KEY_APERTURE = "aperture";
private static final String KEY_EXPOSURE_TIME = "exposureTime";
private static final String KEY_FOCAL_LENGTH = "focalLength";
private static final String KEY_ISO = "iso";
// XMP
private static final String XMP_DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/"; private static final String XMP_DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/";
private static final String XMP_SUBJECT_PROP_NAME = "dc:subject"; private static final String XMP_SUBJECT_PROP_NAME = "dc:subject";
private static final String XMP_TITLE_PROP_NAME = "dc:title"; private static final String XMP_TITLE_PROP_NAME = "dc:title";
private static final String XMP_DESCRIPTION_PROP_NAME = "dc:description"; private static final String XMP_DESCRIPTION_PROP_NAME = "dc:description";
private static final String XMP_GENERIC_LANG = ""; private static final String XMP_GENERIC_LANG = "";
private static final String XMP_SPECIFIC_LANG = "en-US"; private static final String XMP_SPECIFIC_LANG = "en-US";
private static final Pattern videoLocationPattern = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+)/?");
private static final Pattern VIDEO_LOCATION_PATTERN = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+)/?");
private Context context; private Context context;
@ -179,12 +199,10 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
if (!MimeTypes.MP2T.equals(mimeType)) { if (!MimeTypes.MP2T.equals(mimeType)) {
Metadata metadata = ImageMetadataReader.readMetadata(is); Metadata metadata = ImageMetadataReader.readMetadata(is);
// EXIF Sub-IFD // EXIF
ExifSubIFDDirectory exifSubDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); putDateFromDirectoryTag(metadataMap, KEY_DATE_MILLIS, metadata, ExifSubIFDDirectory.class, ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
if (exifSubDir != null) { if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
if (exifSubDir.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) { putDateFromDirectoryTag(metadataMap, KEY_DATE_MILLIS, metadata, ExifIFD0Directory.class, ExifIFD0Directory.TAG_DATETIME);
metadataMap.put("dateMillis", exifSubDir.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL, null, TimeZone.getDefault()).getTime());
}
} }
// GPS // GPS
@ -192,8 +210,8 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
if (gpsDir != null) { if (gpsDir != null) {
GeoLocation geoLocation = gpsDir.getGeoLocation(); GeoLocation geoLocation = gpsDir.getGeoLocation();
if (geoLocation != null) { if (geoLocation != null) {
metadataMap.put("latitude", geoLocation.getLatitude()); metadataMap.put(KEY_LATITUDE, geoLocation.getLatitude());
metadataMap.put("longitude", geoLocation.getLongitude()); metadataMap.put(KEY_LONGITUDE, geoLocation.getLongitude());
} }
} }
@ -209,30 +227,29 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
XMPProperty item = xmpMeta.getArrayItem(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME, i); XMPProperty item = xmpMeta.getArrayItem(XMP_DC_SCHEMA_NS, XMP_SUBJECT_PROP_NAME, i);
sb.append(";").append(item.getValue()); sb.append(";").append(item.getValue());
} }
metadataMap.put("xmpSubjects", sb.toString()); metadataMap.put(KEY_XMP_SUBJECTS, sb.toString());
} }
// double check retrieved items as the property sometimes is reported to exist but it is actually null putLocalizedTextFromXmp(metadataMap, KEY_XMP_TITLE_DESCRIPTION, xmpMeta, XMP_TITLE_PROP_NAME);
String titleDescription = null; if (!metadataMap.containsKey(KEY_XMP_TITLE_DESCRIPTION)) {
if (xmpMeta.doesPropertyExist(XMP_DC_SCHEMA_NS, XMP_TITLE_PROP_NAME)) { putLocalizedTextFromXmp(metadataMap, KEY_XMP_TITLE_DESCRIPTION, xmpMeta, XMP_DESCRIPTION_PROP_NAME);
XMPProperty item = xmpMeta.getLocalizedText(XMP_DC_SCHEMA_NS, XMP_TITLE_PROP_NAME, XMP_GENERIC_LANG, XMP_SPECIFIC_LANG);
if (item != null) {
titleDescription = item.getValue();
}
}
if (titleDescription == null && xmpMeta.doesPropertyExist(XMP_DC_SCHEMA_NS, XMP_DESCRIPTION_PROP_NAME)) {
XMPProperty item = xmpMeta.getLocalizedText(XMP_DC_SCHEMA_NS, XMP_DESCRIPTION_PROP_NAME, XMP_GENERIC_LANG, XMP_SPECIFIC_LANG);
if (item != null) {
titleDescription = item.getValue();
}
}
if (titleDescription != null) {
metadataMap.put("xmpTitleDescription", titleDescription);
} }
} catch (XMPException e) { } catch (XMPException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
// Animated GIF & WEBP
if (MimeTypes.GIF.equals(mimeType)) {
metadataMap.put(KEY_IS_ANIMATED, metadata.containsDirectoryOfType(GifAnimationDirectory.class));
} else if (MimeTypes.WEBP.equals(mimeType)) {
WebpDirectory webpDir = metadata.getFirstDirectoryOfType(WebpDirectory.class);
if (webpDir != null) {
if (webpDir.containsTag(WebpDirectory.TAG_IS_ANIMATION)) {
metadataMap.put(KEY_IS_ANIMATED, webpDir.getBoolean(WebpDirectory.TAG_IS_ANIMATION));
}
}
}
} }
if (isVideo(mimeType)) { if (isVideo(mimeType)) {
@ -251,14 +268,14 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
long dateMillis = MetadataHelper.parseVideoMetadataDate(dateString); long dateMillis = MetadataHelper.parseVideoMetadataDate(dateString);
// some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time // some entries have an invalid default date (19040101T000000.000Z) that is before Epoch time
if (dateMillis > 0) { if (dateMillis > 0) {
metadataMap.put("dateMillis", dateMillis); metadataMap.put(KEY_DATE_MILLIS, dateMillis);
} }
} }
if (rotationString != null) { if (rotationString != null) {
metadataMap.put("videoRotation", Integer.parseInt(rotationString)); metadataMap.put(KEY_VIDEO_ROTATION, Integer.parseInt(rotationString));
} }
if (locationString != null) { if (locationString != null) {
Matcher locationMatcher = videoLocationPattern.matcher(locationString); Matcher locationMatcher = VIDEO_LOCATION_PATTERN.matcher(locationString);
if (locationMatcher.find() && locationMatcher.groupCount() == 2) { if (locationMatcher.find() && locationMatcher.groupCount() == 2) {
String latitudeString = locationMatcher.group(1); String latitudeString = locationMatcher.group(1);
String longitudeString = locationMatcher.group(2); String longitudeString = locationMatcher.group(2);
@ -267,8 +284,8 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
double latitude = Double.parseDouble(latitudeString); double latitude = Double.parseDouble(latitudeString);
double longitude = Double.parseDouble(longitudeString); double longitude = Double.parseDouble(longitudeString);
if (latitude != 0 && longitude != 0) { if (latitude != 0 && longitude != 0) {
metadataMap.put("latitude", latitude); metadataMap.put(KEY_LATITUDE, latitude);
metadataMap.put("longitude", longitude); metadataMap.put(KEY_LONGITUDE, longitude);
} }
} catch (NumberFormatException ex) { } catch (NumberFormatException ex) {
// ignore // ignore
@ -297,7 +314,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
String path = call.argument("path"); String path = call.argument("path");
String uri = call.argument("uri"); String uri = call.argument("uri");
Map<String, String> metadataMap = new HashMap<>(); Map<String, Object> metadataMap = new HashMap<>();
if (isVideo(mimeType)) { if (isVideo(mimeType)) {
result.success(metadataMap); result.success(metadataMap);
@ -308,17 +325,11 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
Metadata metadata = ImageMetadataReader.readMetadata(is); Metadata metadata = ImageMetadataReader.readMetadata(is);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory != null) { if (directory != null) {
if (directory.containsTag(ExifSubIFDDirectory.TAG_FNUMBER)) { putDescriptionFromTag(metadataMap, KEY_APERTURE, directory, ExifSubIFDDirectory.TAG_FNUMBER);
metadataMap.put("aperture", directory.getDescription(ExifSubIFDDirectory.TAG_FNUMBER)); putStringFromTag(metadataMap, KEY_EXPOSURE_TIME, directory, ExifSubIFDDirectory.TAG_EXPOSURE_TIME);
} putDescriptionFromTag(metadataMap, KEY_FOCAL_LENGTH, directory, ExifSubIFDDirectory.TAG_FOCAL_LENGTH);
if (directory.containsTag(ExifSubIFDDirectory.TAG_EXPOSURE_TIME)) {
metadataMap.put("exposureTime", directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_FOCAL_LENGTH)) {
metadataMap.put("focalLength", directory.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH));
}
if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) { if (directory.containsTag(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)) {
metadataMap.put("iso", "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)); metadataMap.put(KEY_ISO, "ISO" + directory.getDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
} }
} }
result.success(metadataMap); result.success(metadataMap);
@ -330,4 +341,41 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
result.error("getOverlayMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage()); result.error("getOverlayMetadata-exception", "failed to get metadata for uri=" + uri + ", path=" + path, e.getMessage());
} }
} }
// convenience methods
private static <T extends Directory> void putDateFromDirectoryTag(Map<String, Object> metadataMap, String key, Metadata metadata, Class<T> dirClass, int tag) {
Directory dir = metadata.getFirstDirectoryOfType(dirClass);
if (dir != null) {
putDateFromTag(metadataMap, key, dir, tag);
}
}
private static void putDateFromTag(Map<String, Object> metadataMap, String key, Directory dir, int tag) {
if (dir.containsTag(tag)) {
metadataMap.put(key, dir.getDate(tag, null, TimeZone.getDefault()).getTime());
}
}
private static void putDescriptionFromTag(Map<String, Object> metadataMap, String key, Directory dir, int tag) {
if (dir.containsTag(tag)) {
metadataMap.put(key, dir.getDescription(tag));
}
}
private static void putStringFromTag(Map<String, Object> metadataMap, String key, Directory dir, int tag) {
if (dir.containsTag(tag)) {
metadataMap.put(key, dir.getString(tag));
}
}
private static void putLocalizedTextFromXmp(Map<String, Object> metadataMap, String key, XMPMeta xmpMeta, String propName) throws XMPException {
if (xmpMeta.doesPropertyExist(XMP_DC_SCHEMA_NS, propName)) {
XMPProperty item = xmpMeta.getLocalizedText(XMP_DC_SCHEMA_NS, propName, XMP_GENERIC_LANG, XMP_SPECIFIC_LANG);
// double check retrieved items as the property sometimes is reported to exist but it is actually null
if (item != null) {
metadataMap.put(key, item.getValue());
}
}
}
} }

View file

@ -8,6 +8,7 @@ public class MimeTypes {
public static final String JPEG = "image/jpeg"; public static final String JPEG = "image/jpeg";
public static final String PNG = "image/png"; public static final String PNG = "image/png";
public static final String SVG = "image/svg+xml"; public static final String SVG = "image/svg+xml";
public static final String WEBP = "image/webp";
public static final String VIDEO = "video"; public static final String VIDEO = "video";
public static final String AVI = "video/avi"; public static final String AVI = "video/avi";

View file

@ -1,8 +1,8 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
class FavouriteFilter extends CollectionFilter { class FavouriteFilter extends CollectionFilter {
static const type = 'favourite'; static const type = 'favourite';
@ -14,7 +14,7 @@ class FavouriteFilter extends CollectionFilter {
String get label => 'Favourite'; String get label => 'Favourite';
@override @override
Widget iconBuilder(context, size) => Icon(OMIcons.favoriteBorder, size: size); Widget iconBuilder(context, size) => Icon(AIcons.favourite, size: size);
@override @override
String get typeKey => type; String get typeKey => type;

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
class LocationFilter extends CollectionFilter { class LocationFilter extends CollectionFilter {
static const type = 'country'; static const type = 'country';
@ -26,7 +26,7 @@ class LocationFilter extends CollectionFilter {
Widget iconBuilder(context, size) { Widget iconBuilder(context, size) {
final flag = countryCodeToFlag(_countryCode); final flag = countryCodeToFlag(_countryCode);
if (flag != null) return Text(flag, style: TextStyle(fontSize: size)); if (flag != null) return Text(flag, style: TextStyle(fontSize: size));
return Icon(OMIcons.place, size: size); return Icon(AIcons.location, size: size);
} }
@override @override

View file

@ -1,12 +1,16 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/mime_types.dart'; import 'package:aves/model/mime_types.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
class MimeFilter extends CollectionFilter { class MimeFilter extends CollectionFilter {
static const type = 'mime'; static const type = 'mime';
// fake mime type
static const animated = 'aves/animated'; // subset of `image/gif` and `image/webp`
final String mime; final String mime;
bool Function(ImageEntry) _filter; bool Function(ImageEntry) _filter;
String _label; String _label;
@ -14,19 +18,21 @@ class MimeFilter extends CollectionFilter {
MimeFilter(this.mime) { MimeFilter(this.mime) {
var lowMime = mime.toLowerCase(); var lowMime = mime.toLowerCase();
if (lowMime.endsWith('/*')) { if (mime == animated) {
_filter = (entry) => entry.isAnimated;
_label = 'Animated';
_icon = AIcons.animated;
} else if (lowMime.endsWith('/*')) {
lowMime = lowMime.substring(0, lowMime.length - 2); lowMime = lowMime.substring(0, lowMime.length - 2);
_filter = (entry) => entry.mimeType.startsWith(lowMime); _filter = (entry) => entry.mimeType.startsWith(lowMime);
if (lowMime == 'video') { if (lowMime == 'video') {
_label = 'Video'; _label = 'Video';
_icon = OMIcons.movie; _icon = AIcons.video;
} }
_label ??= lowMime.split('/')[0].toUpperCase(); _label ??= lowMime.split('/')[0].toUpperCase();
} else { } else {
_filter = (entry) => entry.mimeType == lowMime; _filter = (entry) => entry.mimeType == lowMime;
if (lowMime == MimeTypes.GIF) { if (lowMime == MimeTypes.SVG) {
_icon = OMIcons.gif;
} else if (lowMime == MimeTypes.SVG) {
_label = 'SVG'; _label = 'SVG';
} }
_label ??= lowMime.split('/')[1].toUpperCase(); _label ??= lowMime.split('/')[1].toUpperCase();

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
class TagFilter extends CollectionFilter { class TagFilter extends CollectionFilter {
static const type = 'tag'; static const type = 'tag';
@ -20,7 +20,7 @@ class TagFilter extends CollectionFilter {
String get label => tag; String get label => tag;
@override @override
Widget iconBuilder(context, size) => Icon(OMIcons.localOffer, size: size); Widget iconBuilder(context, size) => Icon(AIcons.tag, size: size);
@override @override
String get typeKey => type; String get typeKey => type;

View file

@ -106,14 +106,17 @@ class ImageEntry {
bool get isFavourite => favourites.isFavourite(this); bool get isFavourite => favourites.isFavourite(this);
bool get isGif => mimeType == MimeTypes.GIF;
bool get isSvg => mimeType == MimeTypes.SVG; bool get isSvg => mimeType == MimeTypes.SVG;
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
bool get isPhoto => [MimeTypes.HEIC, MimeTypes.HEIF, MimeTypes.JPEG].contains(mimeType);
bool get isVideo => mimeType.startsWith('video'); bool get isVideo => mimeType.startsWith('video');
bool get isCatalogued => _catalogMetadata != null; bool get isCatalogued => _catalogMetadata != null;
bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
bool get canEdit => path != null; bool get canEdit => path != null;
bool get canPrint => !isVideo; bool get canPrint => !isVideo;

View file

@ -29,6 +29,7 @@ class DateMetadata {
class CatalogMetadata { class CatalogMetadata {
final int contentId, dateMillis, videoRotation; final int contentId, dateMillis, videoRotation;
final bool isAnimated;
final String xmpSubjects, xmpTitleDescription; final String xmpSubjects, xmpTitleDescription;
final double latitude, longitude; final double latitude, longitude;
Address address; Address address;
@ -36,6 +37,7 @@ class CatalogMetadata {
CatalogMetadata({ CatalogMetadata({
this.contentId, this.contentId,
this.dateMillis, this.dateMillis,
this.isAnimated,
this.videoRotation, this.videoRotation,
this.xmpSubjects, this.xmpSubjects,
this.xmpTitleDescription, this.xmpTitleDescription,
@ -46,10 +48,12 @@ class CatalogMetadata {
: latitude = latitude == null || latitude < -90.0 || latitude > 90.0 ? null : latitude, : latitude = latitude == null || latitude < -90.0 || latitude > 90.0 ? null : latitude,
longitude = longitude == null || longitude < -180.0 || longitude > 180.0 ? null : longitude; longitude = longitude == null || longitude < -180.0 || longitude > 180.0 ? null : longitude;
factory CatalogMetadata.fromMap(Map map) { factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) {
final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false);
return CatalogMetadata( return CatalogMetadata(
contentId: map['contentId'], contentId: map['contentId'],
dateMillis: map['dateMillis'] ?? 0, dateMillis: map['dateMillis'] ?? 0,
isAnimated: boolAsInteger ? isAnimated != 0 : isAnimated,
videoRotation: map['videoRotation'] ?? 0, videoRotation: map['videoRotation'] ?? 0,
xmpSubjects: map['xmpSubjects'] ?? '', xmpSubjects: map['xmpSubjects'] ?? '',
xmpTitleDescription: map['xmpTitleDescription'] ?? '', xmpTitleDescription: map['xmpTitleDescription'] ?? '',
@ -58,9 +62,10 @@ class CatalogMetadata {
); );
} }
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap({bool boolAsInteger = false}) => {
'contentId': contentId, 'contentId': contentId,
'dateMillis': dateMillis, 'dateMillis': dateMillis,
'isAnimated': boolAsInteger ? (isAnimated ? 1 : 0) : isAnimated,
'videoRotation': videoRotation, 'videoRotation': videoRotation,
'xmpSubjects': xmpSubjects, 'xmpSubjects': xmpSubjects,
'xmpTitleDescription': xmpTitleDescription, 'xmpTitleDescription': xmpTitleDescription,
@ -70,7 +75,7 @@ class CatalogMetadata {
@override @override
String toString() { String toString() {
return 'CatalogMetadata{contentId=$contentId, dateMillis=$dateMillis, videoRotation=$videoRotation, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}'; return 'CatalogMetadata{contentId=$contentId, dateMillis=$dateMillis, isAnimated=$isAnimated, videoRotation=$videoRotation, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}';
} }
} }

View file

@ -24,10 +24,32 @@ class MetadataDb {
_database = openDatabase( _database = openDatabase(
await path, await path,
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('
await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, xmpTitleDescription TEXT, latitude REAL, longitude REAL)'); 'contentId INTEGER PRIMARY KEY'
await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryCode TEXT, countryName TEXT, adminArea TEXT, locality TEXT)'); ', dateMillis INTEGER'
await db.execute('CREATE TABLE $favouriteTable(contentId INTEGER PRIMARY KEY, path TEXT)'); ')');
await db.execute('CREATE TABLE $metadataTable('
'contentId INTEGER PRIMARY KEY'
', dateMillis INTEGER'
', isAnimated INTEGER'
', videoRotation INTEGER'
', xmpSubjects TEXT'
', xmpTitleDescription TEXT'
', latitude REAL'
', longitude REAL'
')');
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, version: 1,
); );
@ -74,7 +96,7 @@ class MetadataDb {
// final stopwatch = Stopwatch()..start(); // final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final maps = await db.query(metadataTable); final maps = await db.query(metadataTable);
final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList(); final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map, boolAsInteger: true)).toList();
// debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); // debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
return metadataEntries; return metadataEntries;
} }
@ -94,7 +116,7 @@ class MetadataDb {
} }
batch.insert( batch.insert(
metadataTable, metadataTable,
metadata.toMap(), metadata.toMap(boolAsInteger: true),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
}); });

View file

@ -29,8 +29,10 @@ class MetadataService {
try { try {
// return map with: // return map with:
// 'dateMillis': date taken in milliseconds since Epoch (long) // 'dateMillis': date taken in milliseconds since Epoch (long)
// 'isAnimated': animated gif/webp (bool)
// 'latitude': latitude (double) // 'latitude': latitude (double)
// 'longitude': longitude (double) // 'longitude': longitude (double)
// 'videoRotation': video rotation degrees (int)
// 'xmpSubjects': ';' separated XMP subjects (string) // 'xmpSubjects': ';' separated XMP subjects (string)
// 'xmpTitleDescription': XMP title or XMP description (string) // 'xmpTitleDescription': XMP title or XMP description (string)
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{ final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{

View file

@ -1,8 +1,12 @@
class MimeTypes { class MimeTypes {
static const String ANY_IMAGE = 'image/*';
static const String GIF = 'image/gif'; static const String GIF = 'image/gif';
static const String HEIC = 'image/heic';
static const String HEIF = 'image/heif';
static const String JPEG = 'image/jpeg'; static const String JPEG = 'image/jpeg';
static const String PNG = 'image/png'; static const String PNG = 'image/png';
static const String SVG = 'image/svg+xml'; static const String SVG = 'image/svg+xml';
static const String WEBP = 'image/webp';
static const String ANY_VIDEO = 'video/*'; static const String ANY_VIDEO = 'video/*';
static const String AVI = 'video/avi'; static const String AVI = 'video/avi';

View file

@ -77,19 +77,19 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
); );
final videoEntry = _FilteredCollectionNavTile( final videoEntry = _FilteredCollectionNavTile(
source: source, source: source,
leading: const Icon(OMIcons.movie), leading: const Icon(AIcons.video),
title: 'Videos', title: 'Videos',
filter: MimeFilter(MimeTypes.ANY_VIDEO), filter: MimeFilter(MimeTypes.ANY_VIDEO),
); );
final gifEntry = _FilteredCollectionNavTile( final animatedEntry = _FilteredCollectionNavTile(
source: source, source: source,
leading: const Icon(OMIcons.gif), leading: const Icon(AIcons.animated),
title: 'GIFs', title: 'Animated',
filter: MimeFilter(MimeTypes.GIF), filter: MimeFilter(MimeFilter.animated),
); );
final favouriteEntry = _FilteredCollectionNavTile( final favouriteEntry = _FilteredCollectionNavTile(
source: source, source: source,
leading: const Icon(OMIcons.favoriteBorder), leading: const Icon(AIcons.favourite),
title: 'Favourites', title: 'Favourites',
filter: FavouriteFilter(), filter: FavouriteFilter(),
); );
@ -103,7 +103,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
final buildTagEntry = (String tag) => _FilteredCollectionNavTile( final buildTagEntry = (String tag) => _FilteredCollectionNavTile(
source: source, source: source,
leading: Icon( leading: Icon(
OMIcons.localOffer, AIcons.tag,
color: stringToColor(tag), color: stringToColor(tag),
), ),
title: tag, title: tag,
@ -130,7 +130,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
style: TextStyle(fontSize: IconTheme.of(context).size), style: TextStyle(fontSize: IconTheme.of(context).size),
) )
: Icon( : Icon(
OMIcons.place, AIcons.location,
color: stringToColor(title), color: stringToColor(title),
), ),
title: title, title: title,
@ -161,7 +161,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
header, header,
allMediaEntry, allMediaEntry,
videoEntry, videoEntry,
gifEntry, animatedEntry,
favouriteEntry, favouriteEntry,
if (specialAlbums.isNotEmpty) ...[ if (specialAlbums.isNotEmpty) ...[
const Divider(), const Divider(),
@ -198,7 +198,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
top: false, top: false,
bottom: false, bottom: false,
child: ExpansionTile( child: ExpansionTile(
leading: const Icon(OMIcons.place), leading: const Icon(AIcons.location),
title: Row( title: Row(
children: [ children: [
const Text('Cities'), const Text('Cities'),
@ -220,7 +220,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
top: false, top: false,
bottom: false, bottom: false,
child: ExpansionTile( child: ExpansionTile(
leading: const Icon(OMIcons.place), leading: const Icon(AIcons.location),
title: Row( title: Row(
children: [ children: [
const Text('Countries'), const Text('Countries'),
@ -242,7 +242,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
top: false, top: false,
bottom: false, bottom: false,
child: ExpansionTile( child: ExpansionTile(
leading: const Icon(OMIcons.localOffer), leading: const Icon(AIcons.tag),
title: Row( title: Row(
children: [ children: [
const Text('Tags'), const Text('Tags'),

View file

@ -62,7 +62,7 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
children: [ children: [
_buildFilterRow( _buildFilterRow(
context: context, context: context,
filters: [FavouriteFilter(), MimeFilter(MimeTypes.ANY_VIDEO), MimeFilter(MimeTypes.GIF), MimeFilter(MimeTypes.SVG)].where((f) => containQuery(f.label)), filters: [FavouriteFilter(), MimeFilter(MimeTypes.ANY_VIDEO), MimeFilter(MimeFilter.animated), MimeFilter(MimeTypes.SVG)].where((f) => containQuery(f.label)),
), ),
_buildFilterRow( _buildFilterRow(
context: context, context: context,

View file

@ -125,8 +125,8 @@ class _ThumbnailOverlay extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (entry.hasGps) GpsIcon(iconSize: iconSize), if (entry.hasGps) GpsIcon(iconSize: iconSize),
if (entry.isGif) if (entry.isAnimated)
GifIcon(iconSize: iconSize) AnimatedImageIcon(iconSize: iconSize)
else if (entry.isVideo) else if (entry.isVideo)
DefaultTextStyle( DefaultTextStyle(
style: TextStyle( style: TextStyle(

View file

@ -8,10 +8,10 @@ import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/album/grid/list_sliver.dart'; import 'package:aves/widgets/album/grid/list_sliver.dart';
import 'package:aves/widgets/album/grid/scaling.dart'; import 'package:aves/widgets/album/grid/scaling.dart';
import 'package:aves/widgets/album/tile_extent_manager.dart'; import 'package:aves/widgets/album/tile_extent_manager.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/scroll_thumb.dart'; import 'package:aves/widgets/common/scroll_thumb.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -40,7 +40,6 @@ class ThumbnailCollection extends StatelessWidget {
return Consumer<CollectionLens>( return Consumer<CollectionLens>(
builder: (context, collection, child) { builder: (context, collection, child) {
// debugPrint('$runtimeType collection builder entries=${collection.entryCount}'); // debugPrint('$runtimeType collection builder entries=${collection.entryCount}');
final sectionKeys = collection.sections.keys.toList();
final showHeaders = collection.showHeaders; final showHeaders = collection.showHeaders;
return GridScaleGestureDetector( return GridScaleGestureDetector(
scrollableKey: _scrollableKey, scrollableKey: _scrollableKey,
@ -119,12 +118,12 @@ class ThumbnailCollection extends StatelessWidget {
Widget _buildEmptyCollectionPlaceholder(CollectionLens collection) { Widget _buildEmptyCollectionPlaceholder(CollectionLens collection) {
return collection.filters.any((filter) => filter is FavouriteFilter) return collection.filters.any((filter) => filter is FavouriteFilter)
? const EmptyContent( ? const EmptyContent(
icon: OMIcons.favoriteBorder, icon: AIcons.favourite,
text: 'No favourites!', text: 'No favourites!',
) )
: collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.ANY_VIDEO) : collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.ANY_VIDEO)
? const EmptyContent( ? const EmptyContent(
icon: OMIcons.movie, icon: AIcons.video,
) )
: const EmptyContent(); : const EmptyContent();
} }

View file

@ -6,6 +6,16 @@ import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
class AIcons {
static const IconData animated = Icons.slideshow;
static const IconData video = OMIcons.movie;
static const IconData favourite = OMIcons.favoriteBorder;
static const IconData favouriteActive = OMIcons.favorite;
static const IconData date = OMIcons.calendarToday;
static const IconData location = OMIcons.place;
static const IconData tag = OMIcons.localOffer;
}
class VideoIcon extends StatelessWidget { class VideoIcon extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final double iconSize; final double iconSize;
@ -16,22 +26,23 @@ class VideoIcon extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: OMIcons.playCircleOutline, icon: OMIcons.playCircleOutline,
iconSize: iconSize, size: iconSize,
text: entry.durationText, text: entry.durationText,
); );
} }
} }
class GifIcon extends StatelessWidget { class AnimatedImageIcon extends StatelessWidget {
final double iconSize; final double iconSize;
const GifIcon({Key key, this.iconSize}) : super(key: key); const AnimatedImageIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: OMIcons.gif, icon: AIcons.animated,
iconSize: iconSize, size: iconSize,
iconSize: iconSize * .8,
); );
} }
} }
@ -44,42 +55,55 @@ class GpsIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: OMIcons.place, icon: AIcons.location,
iconSize: iconSize, size: iconSize,
); );
} }
} }
class OverlayIcon extends StatelessWidget { class OverlayIcon extends StatelessWidget {
final IconData icon; final IconData icon;
final double iconSize; final double size, iconSize;
final String text; final String text;
const OverlayIcon({Key key, this.icon, this.iconSize, this.text}) : super(key: key); const OverlayIcon({
Key key,
@required this.icon,
@required this.size,
double iconSize,
this.text,
}) : iconSize = iconSize ?? size,
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( final iconChild = SizedBox(
margin: const EdgeInsets.all(1), width: size,
padding: text != null ? EdgeInsets.only(right: iconSize / 4) : null, height: size,
decoration: BoxDecoration( child: Icon(
color: const Color(0xBB000000),
borderRadius: BorderRadius.all(
Radius.circular(iconSize),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
icon, icon,
size: iconSize, size: iconSize,
), ),
if (text != null) ...[ );
return Container(
margin: const EdgeInsets.all(1),
padding: text != null ? EdgeInsets.only(right: size / 4) : null,
decoration: BoxDecoration(
color: const Color(0xBB000000),
borderRadius: BorderRadius.all(
Radius.circular(size),
),
),
child: text == null
? iconChild
: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
iconChild,
const SizedBox(width: 2), const SizedBox(width: 2),
Text(text), Text(text),
]
], ],
), ),
); );

View file

@ -68,6 +68,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
if (data != null) if (data != null)
InfoRowGroup({ InfoRowGroup({
'dateMillis': '${data.dateMillis}', 'dateMillis': '${data.dateMillis}',
'isAnimated': '${data.isAnimated}',
'videoRotation': '${data.videoRotation}', 'videoRotation': '${data.videoRotation}',
'latitude': '${data.latitude}', 'latitude': '${data.latitude}',
'longitude': '${data.longitude}', 'longitude': '${data.longitude}',
@ -91,7 +92,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
Text('DB address:${data == null ? ' no row' : ''}'), Text('DB address:${data == null ? ' no row' : ''}'),
if (data != null) if (data != null)
InfoRowGroup({ InfoRowGroup({
'dateMillis': '${data.addressLine}', 'addressLine': '${data.addressLine}',
'countryCode': '${data.countryCode}', 'countryCode': '${data.countryCode}',
'countryName': '${data.countryName}', 'countryName': '${data.countryName}',
'adminArea': '${data.adminArea}', 'adminArea': '${data.adminArea}',
@ -101,6 +102,8 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
); );
}, },
), ),
const Divider(),
Text('Catalog metadata: ${widget.entry.catalogMetadata}'),
], ],
), ),
), ),

View file

@ -28,7 +28,7 @@ class BasicSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final date = entry.bestDate; final date = entry.bestDate;
final dateText = date != null ? '${DateFormat.yMMMd().format(date)}${DateFormat.Hm().format(date)}' : '?'; final dateText = date != null ? '${DateFormat.yMMMd().format(date)}${DateFormat.Hm().format(date)}' : '?';
final showMegaPixels = !entry.isVideo && !entry.isGif && entry.megaPixels != null && entry.megaPixels > 0; final showMegaPixels = entry.isPhoto && entry.megaPixels != null && entry.megaPixels > 0;
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}'; final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase); final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
@ -50,7 +50,7 @@ class BasicSection extends StatelessWidget {
final album = entry.directory; final album = entry.directory;
final filters = [ final filters = [
if (entry.isVideo) MimeFilter(MimeTypes.ANY_VIDEO), if (entry.isVideo) MimeFilter(MimeTypes.ANY_VIDEO),
if (entry.isGif) MimeFilter(MimeTypes.GIF), if (entry.isAnimated) MimeFilter(MimeFilter.animated),
if (isFavourite) FavouriteFilter(), if (isFavourite) FavouriteFilter(),
if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(album)), if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(album)),
...tags.map((tag) => TagFilter(tag)), ...tags.map((tag) => TagFilter(tag)),

View file

@ -5,6 +5,7 @@ import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_app_service.dart'; import 'package:aves/utils/android_app_service.dart';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/info/map_initializer.dart'; import 'package:aves/widgets/fullscreen/info/map_initializer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -93,7 +94,7 @@ class _LocationSectionState extends State<LocationSection> {
if (widget.showTitle) if (widget.showTitle)
const Padding( const Padding(
padding: EdgeInsets.only(bottom: 8), padding: EdgeInsets.only(bottom: 8),
child: SectionRow(OMIcons.place), child: SectionRow(AIcons.location),
), ),
ImageMap( ImageMap(
markerId: entry.uri ?? entry.path, markerId: entry.uri ?? entry.path,

View file

@ -7,6 +7,7 @@ import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
@ -216,7 +217,7 @@ class _LocationRow extends AnimatedWidget {
} }
return Row( return Row(
children: [ children: [
const Icon(OMIcons.place, size: _iconSize), const Icon(AIcons.location, size: _iconSize),
const SizedBox(width: _iconPadding), const SizedBox(width: _iconPadding),
Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)),
], ],
@ -236,7 +237,7 @@ class _DateRow extends StatelessWidget {
final resolution = '${entry.width ?? '?'} × ${entry.height ?? '?'}'; final resolution = '${entry.width ?? '?'} × ${entry.height ?? '?'}';
return Row( return Row(
children: [ children: [
const Icon(OMIcons.calendarToday, size: _iconSize), const Icon(AIcons.date, size: _iconSize),
const SizedBox(width: _iconPadding), const SizedBox(width: _iconPadding),
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
if (!entry.isSvg) Expanded(flex: 2, child: Text(resolution, strutStyle: Constants.overflowStrutStyle)), if (!entry.isSvg) Expanded(flex: 2, child: Text(resolution, strutStyle: Constants.overflowStrutStyle)),

View file

@ -2,12 +2,12 @@ import 'dart:math';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/fx/sweeper.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/fullscreen/fullscreen_actions.dart'; import 'package:aves/widgets/fullscreen/fullscreen_actions.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class FullscreenTopOverlay extends StatelessWidget { class FullscreenTopOverlay extends StatelessWidget {
@ -130,12 +130,12 @@ class FullscreenTopOverlay extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
IconButton( IconButton(
icon: Icon(isFavourite ? OMIcons.favorite : OMIcons.favoriteBorder), icon: Icon(isFavourite ? AIcons.favouriteActive : AIcons.favourite),
onPressed: onPressed, onPressed: onPressed,
tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites', tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites',
), ),
Sweeper( Sweeper(
builder: (context) => Icon(OMIcons.favoriteBorder, color: Colors.redAccent), builder: (context) => Icon(AIcons.favourite, color: Colors.redAccent),
toggledNotifier: entry.isFavouriteNotifier, toggledNotifier: entry.isFavouriteNotifier,
), ),
], ],
@ -181,11 +181,11 @@ class FullscreenTopOverlay extends StatelessWidget {
child = entry.isFavouriteNotifier.value child = entry.isFavouriteNotifier.value
? const MenuRow( ? const MenuRow(
text: 'Remove from favourites', text: 'Remove from favourites',
icon: OMIcons.favorite, icon: AIcons.favouriteActive,
) )
: const MenuRow( : const MenuRow(
text: 'Add to favourites', text: 'Add to favourites',
icon: OMIcons.favoriteBorder, icon: AIcons.favourite,
); );
break; break;
case FullscreenAction.info: case FullscreenAction.info:

View file

@ -11,11 +11,11 @@ import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:charts_flutter/flutter.dart' as charts; import 'package:charts_flutter/flutter.dart' as charts;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:percent_indicator/linear_percent_indicator.dart';
class StatsPage extends StatelessWidget { class StatsPage extends StatelessWidget {
@ -77,7 +77,7 @@ class StatsPage extends StatelessWidget {
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
progressColor: Theme.of(context).accentColor, progressColor: Theme.of(context).accentColor,
animation: true, animation: true,
leading: const Icon(OMIcons.place), leading: const Icon(AIcons.location),
// right padding to match leading, so that inside label is aligned with outside label below // right padding to match leading, so that inside label is aligned with outside label below
padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(right: 24), padding: const EdgeInsets.symmetric(horizontal: 16) + const EdgeInsets.only(right: 24),
center: Text(NumberFormat.percentPattern().format(withGpsPercent)), center: Text(NumberFormat.percentPattern().format(withGpsPercent)),