filters: replaced GIF mime filter by animated webp or gif
This commit is contained in:
parent
9c9c55e8cd
commit
fd5bb222d7
22 changed files with 240 additions and 121 deletions
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>{
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,44 +55,57 @@ 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) {
|
||||||
|
final iconChild = SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.all(1),
|
margin: const EdgeInsets.all(1),
|
||||||
padding: text != null ? EdgeInsets.only(right: iconSize / 4) : null,
|
padding: text != null ? EdgeInsets.only(right: size / 4) : null,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xBB000000),
|
color: const Color(0xBB000000),
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.all(
|
||||||
Radius.circular(iconSize),
|
Radius.circular(size),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: text == null
|
||||||
mainAxisSize: MainAxisSize.min,
|
? iconChild
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Icon(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
icon,
|
children: [
|
||||||
size: iconSize,
|
iconChild,
|
||||||
),
|
const SizedBox(width: 2),
|
||||||
if (text != null) ...[
|
Text(text),
|
||||||
const SizedBox(width: 2),
|
],
|
||||||
Text(text),
|
),
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
Loading…
Reference in a new issue