From 1c415f83dca3e2fe7857389b8f8be1946e34e5d5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 1 Dec 2020 18:12:29 +0900 Subject: [PATCH] DB change to merge flags, geotiff identification --- .../aves/channel/calls/MetadataHandler.kt | 27 +++-- .../deckers/thibault/aves/metadata/Geotiff.kt | 27 +++++ .../aves/metadata/MetadataExtractorHelper.kt | 22 ++++ lib/model/image_entry.dart | 2 + lib/model/image_metadata.dart | 24 +++-- lib/model/metadata_db.dart | 83 +++------------ lib/model/metadata_db_upgrade.dart | 100 ++++++++++++++++++ lib/theme/icons.dart | 1 + lib/widgets/collection/thumbnail/overlay.dart | 1 + lib/widgets/common/identity/aves_icons.dart | 14 +++ .../fullscreen/fullscreen_debug_page.dart | 1 + 11 files changed, 216 insertions(+), 86 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/metadata/Geotiff.kt create mode 100644 lib/model/metadata_db_upgrade.dart diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 09aea57f2..46056b88c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -43,6 +43,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString +import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.XMP.getSafeDateMillis import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText @@ -219,6 +220,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?, sizeBytes: Long?): Map { val metadataMap = HashMap() + var flags = 0 var foundExif = false if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { @@ -258,7 +260,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { val orientation = it - metadataMap[KEY_IS_FLIPPED] = isFlippedForExifCode(orientation) + if (isFlippedForExifCode(orientation)) flags = flags or MASK_IS_FLIPPED metadataMap[KEY_ROTATION_DEGREES] = getRotationDegreesForExifCode(orientation) } } @@ -293,17 +295,22 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } } - // Animated GIF & WEBP + // identification of animated GIF & WEBP, GeoTIFF when (mimeType) { MimeTypes.GIF -> { - metadataMap[KEY_IS_ANIMATED] = metadata.containsDirectoryOfType(GifAnimationDirectory::class.java) + if (metadata.containsDirectoryOfType(GifAnimationDirectory::class.java)) flags = flags or MASK_IS_ANIMATED } MimeTypes.WEBP -> { for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) { - dir.getSafeBoolean(WebpDirectory.TAG_IS_ANIMATION) { metadataMap[KEY_IS_ANIMATED] = it } + dir.getSafeBoolean(WebpDirectory.TAG_IS_ANIMATION) { + if (it) flags = flags or MASK_IS_ANIMATED + } } } - else -> { + MimeTypes.TIFF -> { + for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { + if (dir.isGeoTiff()) flags = flags or MASK_IS_GEOTIFF + } } } } @@ -324,7 +331,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { exif.getSafeDateMillis(ExifInterface.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it } } exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { - metadataMap[KEY_IS_FLIPPED] = exif.isFlipped + if (exif.isFlipped) flags = flags or MASK_IS_FLIPPED metadataMap[KEY_ROTATION_DEGREES] = exif.rotationDegrees } val latLong = exif.latLong @@ -339,6 +346,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=$uri", e) } } + metadataMap[KEY_FLAGS] = flags return metadataMap } @@ -711,14 +719,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { // catalog metadata private const val KEY_MIME_TYPE = "mimeType" private const val KEY_DATE_MILLIS = "dateMillis" - private const val KEY_IS_ANIMATED = "isAnimated" - private const val KEY_IS_FLIPPED = "isFlipped" + private const val KEY_FLAGS = "flags" private const val KEY_ROTATION_DEGREES = "rotationDegrees" private const val KEY_LATITUDE = "latitude" private const val KEY_LONGITUDE = "longitude" private const val KEY_XMP_SUBJECTS = "xmpSubjects" private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription" + private const val MASK_IS_ANIMATED = 1 shl 0 + private const val MASK_IS_FLIPPED = 1 shl 1 + private const val MASK_IS_GEOTIFF = 1 shl 2 + // overlay metadata private const val KEY_APERTURE = "aperture" private const val KEY_EXPOSURE_TIME = "exposureTime" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Geotiff.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Geotiff.kt new file mode 100644 index 000000000..5b40d833b --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Geotiff.kt @@ -0,0 +1,27 @@ +package deckers.thibault.aves.metadata + +object Geotiff { + // 33550 + // ModelPixelScaleTag (optional) + val TAG_MODEL_PIXEL_SCALE = 0x830e + + // 33922 + // ModelTiepointTag (conditional) + val TAG_MODEL_TIEPOINT = 0x8482 + + // 34264 + // ModelTransformationTag (conditional) + val TAG_MODEL_TRANSFORMATION = 0x85d8 + + // 34735 + // GeoKeyDirectoryTag (mandatory) + val TAG_GEO_KEY_DIRECTORY = 0x87af + + // 34736 + // GeoDoubleParamsTag (optional) + val TAG_GEO_DOUBLE_PARAMS = 0x87b0 + + // 34737 + // GeoAsciiParamsTag (optional) + val TAG_GEO_ASCII_PARAMS = 0x87b1 +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt index ec60169ba..74c63bf4a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt @@ -2,6 +2,7 @@ package deckers.thibault.aves.metadata import com.drew.lang.Rational import com.drew.metadata.Directory +import com.drew.metadata.exif.ExifIFD0Directory import java.util.* object MetadataExtractorHelper { @@ -34,4 +35,25 @@ object MetadataExtractorHelper { fun Directory.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) { if (this.containsTag(tag)) save(this.getDate(tag, null, TimeZone.getDefault()).time) } + + // geotiff + + /* + cf http://docs.opengeospatial.org/is/19-008r4/19-008r4.html#_underlying_tiff_requirements + - One of ModelTiepointTag or ModelTransformationTag SHALL be included in an Image File Directory (IFD) + - If the ModelTransformationTag is included in an IFD, then a ModelPixelScaleTag SHALL NOT be included + - If the ModelPixelScaleTag is included in an IFD, then a ModelTiepointTag SHALL also be included. + */ + fun ExifIFD0Directory.isGeoTiff(): Boolean { + if (!this.containsTag(Geotiff.TAG_GEO_KEY_DIRECTORY)) return false + + val modelTiepoint = this.containsTag(Geotiff.TAG_MODEL_TIEPOINT) + val modelTransformation = this.containsTag(Geotiff.TAG_MODEL_TRANSFORMATION) + if (!modelTiepoint && !modelTransformation) return false + + val modelPixelScale = this.containsTag(Geotiff.TAG_MODEL_PIXEL_SCALE) + if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiepoint)) return false + + return true + } } \ No newline at end of file diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 2de981063..eb0fc7923 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -206,6 +206,8 @@ class ImageEntry { bool get isAnimated => _catalogMetadata?.isAnimated ?? false; + bool get isGeotiff => _catalogMetadata?.isGeotiff ?? false; + bool get canEdit => path != null; bool get canPrint => !isVideo; diff --git a/lib/model/image_metadata.dart b/lib/model/image_metadata.dart index ee984acc7..bfc6b84b7 100644 --- a/lib/model/image_metadata.dart +++ b/lib/model/image_metadata.dart @@ -30,7 +30,7 @@ class DateMetadata { class CatalogMetadata { final int contentId, dateMillis; - final bool isAnimated; + final bool isAnimated, isGeotiff; bool isFlipped; int rotationDegrees; final String mimeType, xmpSubjects, xmpTitleDescription; @@ -38,6 +38,9 @@ class CatalogMetadata { Address address; static const double _precisionErrorTolerance = 1e-9; + static const isAnimatedMask = 1 << 0; + static const isFlippedMask = 1 << 1; + static const isGeotiffMask = 1 << 2; CatalogMetadata({ this.contentId, @@ -45,6 +48,7 @@ class CatalogMetadata { this.dateMillis, this.isAnimated, this.isFlipped, + this.isGeotiff, this.rotationDegrees, this.xmpSubjects, this.xmpTitleDescription, @@ -69,6 +73,7 @@ class CatalogMetadata { dateMillis: dateMillis, isAnimated: isAnimated, isFlipped: isFlipped, + isGeotiff: isGeotiff, rotationDegrees: rotationDegrees, xmpSubjects: xmpSubjects, xmpTitleDescription: xmpTitleDescription, @@ -77,15 +82,15 @@ class CatalogMetadata { ); } - factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) { - final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false); - final isFlipped = map['isFlipped'] ?? (boolAsInteger ? 0 : false); + factory CatalogMetadata.fromMap(Map map) { + final flags = map['flags'] ?? 0; return CatalogMetadata( contentId: map['contentId'], mimeType: map['mimeType'], dateMillis: map['dateMillis'] ?? 0, - isAnimated: boolAsInteger ? isAnimated != 0 : isAnimated, - isFlipped: boolAsInteger ? isFlipped != 0 : isFlipped, + isAnimated: flags & isAnimatedMask != 0, + isFlipped: flags & isFlippedMask != 0, + isGeotiff: flags & isGeotiffMask != 0, // `rotationDegrees` should default to `sourceRotationDegrees`, not 0 rotationDegrees: map['rotationDegrees'], xmpSubjects: map['xmpSubjects'] ?? '', @@ -95,12 +100,11 @@ class CatalogMetadata { ); } - Map toMap({bool boolAsInteger = false}) => { + Map toMap() => { 'contentId': contentId, 'mimeType': mimeType, 'dateMillis': dateMillis, - 'isAnimated': boolAsInteger ? (isAnimated ? 1 : 0) : isAnimated, - 'isFlipped': boolAsInteger ? (isFlipped ? 1 : 0) : isFlipped, + 'flags': (isAnimated ? isAnimatedMask : 0) | (isFlipped ? isFlippedMask : 0) | (isGeotiff ? isGeotiffMask : 0), 'rotationDegrees': rotationDegrees, 'xmpSubjects': xmpSubjects, 'xmpTitleDescription': xmpTitleDescription, @@ -110,7 +114,7 @@ class CatalogMetadata { @override String toString() { - return 'CatalogMetadata{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, rotationDegrees=$rotationDegrees, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}'; + return 'CatalogMetadata{contentId=$contentId, mimeType=$mimeType, dateMillis=$dateMillis, isAnimated=$isAnimated, isFlipped=$isFlipped, isGeotiff=$isGeotiff, rotationDegrees=$rotationDegrees, latitude=$latitude, longitude=$longitude, xmpSubjects=$xmpSubjects, xmpTitleDescription=$xmpTitleDescription}'; } } diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 681a38605..48a8b5a3b 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; +import 'package:aves/model/metadata_db_upgrade.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; @@ -48,8 +49,7 @@ class MetadataDb { 'contentId INTEGER PRIMARY KEY' ', mimeType TEXT' ', dateMillis INTEGER' - ', isAnimated INTEGER' - ', isFlipped INTEGER' + ', flags INTEGER' ', rotationDegrees INTEGER' ', xmpSubjects TEXT' ', xmpTitleDescription TEXT' @@ -69,65 +69,8 @@ class MetadataDb { ', path TEXT' ')'); }, - onUpgrade: (db, oldVersion, newVersion) async { - // warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported - // on SQLite <3.25.0, bundled on older Android devices - while (oldVersion < newVersion) { - if (oldVersion == 1) { - // rename column 'orientationDegrees' to 'sourceRotationDegrees' - await db.transaction((txn) async { - const newEntryTable = '${entryTable}TEMP'; - await db.execute('CREATE TABLE $newEntryTable(' - 'contentId INTEGER PRIMARY KEY' - ', uri TEXT' - ', path TEXT' - ', sourceMimeType TEXT' - ', width INTEGER' - ', height INTEGER' - ', sourceRotationDegrees INTEGER' - ', sizeBytes INTEGER' - ', title TEXT' - ', dateModifiedSecs INTEGER' - ', sourceDateTakenMillis INTEGER' - ', durationMillis INTEGER' - ')'); - await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)' - ' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis' - ' FROM $entryTable;'); - await db.execute('DROP TABLE $entryTable;'); - await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;'); - }); - - // rename column 'videoRotation' to 'rotationDegrees' - await db.transaction((txn) async { - const newMetadataTable = '${metadataTable}TEMP'; - await db.execute('CREATE TABLE $newMetadataTable(' - 'contentId INTEGER PRIMARY KEY' - ', mimeType TEXT' - ', dateMillis INTEGER' - ', isAnimated INTEGER' - ', rotationDegrees INTEGER' - ', xmpSubjects TEXT' - ', xmpTitleDescription TEXT' - ', latitude REAL' - ', longitude REAL' - ')'); - await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' - ' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude' - ' FROM $metadataTable;'); - await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;'); - await db.execute('DROP TABLE $metadataTable;'); - await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); - }); - - // new column 'isFlipped' - await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;'); - - oldVersion++; - } - } - }, - version: 2, + onUpgrade: MetadataDbUpgrader.upgradeDb, + version: 3, ); } @@ -238,7 +181,7 @@ class MetadataDb { // final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(metadataTable); - final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map, boolAsInteger: true)).toList(); + final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList(); // debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); return metadataEntries; } @@ -246,11 +189,15 @@ class MetadataDb { Future saveMetadata(Iterable metadataEntries) async { if (metadataEntries == null || metadataEntries.isEmpty) return; final stopwatch = Stopwatch()..start(); - final db = await _database; - final batch = db.batch(); - metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata)); - await batch.commit(noResult: true); - debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); + try { + final db = await _database; + final batch = db.batch(); + metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata)); + await batch.commit(noResult: true); + debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); + } catch (exception, stack) { + debugPrint('$runtimeType failed to save metadata with exception=$exception\n$stack'); + } } Future updateMetadataId(int oldId, CatalogMetadata metadata) async { @@ -273,7 +220,7 @@ class MetadataDb { } batch.insert( metadataTable, - metadata.toMap(boolAsInteger: true), + metadata.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } diff --git a/lib/model/metadata_db_upgrade.dart b/lib/model/metadata_db_upgrade.dart new file mode 100644 index 000000000..b343eaf12 --- /dev/null +++ b/lib/model/metadata_db_upgrade.dart @@ -0,0 +1,100 @@ +import 'package:aves/model/metadata_db.dart'; +import 'package:flutter/foundation.dart'; +import 'package:sqflite/sqflite.dart'; + +class MetadataDbUpgrader { + static const entryTable = MetadataDb.entryTable; + static const metadataTable = MetadataDb.metadataTable; + + // warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported + // on SQLite <3.25.0, bundled on older Android devices + static Future upgradeDb(Database db, int oldVersion, int newVersion) async { + while (oldVersion < newVersion) { + switch (oldVersion) { + case 1: + await _upgradeFrom1(db); + break; + case 2: + await _upgradeFrom2(db); + break; + } + oldVersion++; + } + } + + static Future _upgradeFrom1(Database db) async { + debugPrint('upgrading DB from v1'); + // rename column 'orientationDegrees' to 'sourceRotationDegrees' + await db.transaction((txn) async { + const newEntryTable = '${entryTable}TEMP'; + await db.execute('CREATE TABLE $newEntryTable(' + 'contentId INTEGER PRIMARY KEY' + ', uri TEXT' + ', path TEXT' + ', sourceMimeType TEXT' + ', width INTEGER' + ', height INTEGER' + ', sourceRotationDegrees INTEGER' + ', sizeBytes INTEGER' + ', title TEXT' + ', dateModifiedSecs INTEGER' + ', sourceDateTakenMillis INTEGER' + ', durationMillis INTEGER' + ')'); + await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)' + ' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis' + ' FROM $entryTable;'); + await db.execute('DROP TABLE $entryTable;'); + await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;'); + }); + + // rename column 'videoRotation' to 'rotationDegrees' + await db.transaction((txn) async { + const newMetadataTable = '${metadataTable}TEMP'; + await db.execute('CREATE TABLE $newMetadataTable(' + 'contentId INTEGER PRIMARY KEY' + ', mimeType TEXT' + ', dateMillis INTEGER' + ', isAnimated INTEGER' + ', rotationDegrees INTEGER' + ', xmpSubjects TEXT' + ', xmpTitleDescription TEXT' + ', latitude REAL' + ', longitude REAL' + ')'); + await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' + ' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude' + ' FROM $metadataTable;'); + await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;'); + await db.execute('DROP TABLE $metadataTable;'); + await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); + }); + + // new column 'isFlipped' + await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;'); + } + + static Future _upgradeFrom2(Database db) async { + debugPrint('upgrading DB from v2'); + // merge columns 'isAnimated' and 'isFlipped' into 'flags' + await db.transaction((txn) async { + const newMetadataTable = '${metadataTable}TEMP'; + await db.execute('CREATE TABLE $newMetadataTable(' + 'contentId INTEGER PRIMARY KEY' + ', mimeType TEXT' + ', dateMillis INTEGER' + ', flags INTEGER' + ', rotationDegrees INTEGER' + ', xmpSubjects TEXT' + ', xmpTitleDescription TEXT' + ', latitude REAL' + ', longitude REAL' + ')'); + await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)' + ' SELECT contentId,mimeType,dateMillis,ifnull(isAnimated,0)+ifnull(isFlipped,0)*2,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude' + ' FROM $metadataTable;'); + await db.execute('DROP TABLE $metadataTable;'); + await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;'); + }); + } +} diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 6d33fce2b..d198c454a 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -61,6 +61,7 @@ class AIcons { // thumbnail overlay static const IconData animated = Icons.slideshow; + static const IconData geo = Icons.language_outlined; static const IconData play = Icons.play_circle_outline; static const IconData selected = Icons.check_circle_outline; static const IconData unselected = Icons.radio_button_unchecked; diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 6e9179bd3..a834070cc 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -36,6 +36,7 @@ class ThumbnailEntryOverlay extends StatelessWidget { children: [ if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize), if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize), + if (entry.isGeotiff) GeotiffIcon(iconSize: iconSize), if (entry.isAnimated) AnimatedImageIcon(iconSize: iconSize) else if (entry.isVideo) diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index c6ed8fb77..382b26f75 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -45,6 +45,20 @@ class AnimatedImageIcon extends StatelessWidget { } } +class GeotiffIcon extends StatelessWidget { + final double iconSize; + + const GeotiffIcon({Key key, this.iconSize}) : super(key: key); + + @override + Widget build(BuildContext context) { + return OverlayIcon( + icon: AIcons.geo, + size: iconSize, + ); + } +} + class GpsIcon extends StatelessWidget { final double iconSize; diff --git a/lib/widgets/fullscreen/fullscreen_debug_page.dart b/lib/widgets/fullscreen/fullscreen_debug_page.dart index 3c796a253..0afd2e51a 100644 --- a/lib/widgets/fullscreen/fullscreen_debug_page.dart +++ b/lib/widgets/fullscreen/fullscreen_debug_page.dart @@ -96,6 +96,7 @@ class FullscreenDebugPage extends StatelessWidget { 'isVideo': '${entry.isVideo}', 'isCatalogued': '${entry.isCatalogued}', 'isAnimated': '${entry.isAnimated}', + 'isGeotiff': '${entry.isGeotiff}', 'canEdit': '${entry.canEdit}', 'canEditExif': '${entry.canEditExif}', 'canPrint': '${entry.canPrint}',