DB change to merge flags, geotiff identification

This commit is contained in:
Thibault Deckers 2020-12-01 18:12:29 +09:00
parent 60e7b2c5d9
commit 1c415f83dc
11 changed files with 216 additions and 86 deletions

View file

@ -43,6 +43,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString 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
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText 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<String, Any> { private fun getCatalogMetadataByMetadataExtractor(uri: Uri, mimeType: String, path: String?, sizeBytes: Long?): Map<String, Any> {
val metadataMap = HashMap<String, Any>() val metadataMap = HashMap<String, Any>()
var flags = 0
var foundExif = false var foundExif = false
if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) { if (isSupportedByMetadataExtractor(mimeType, sizeBytes)) {
@ -258,7 +260,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
} }
dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) {
val orientation = it val orientation = it
metadataMap[KEY_IS_FLIPPED] = isFlippedForExifCode(orientation) if (isFlippedForExifCode(orientation)) flags = flags or MASK_IS_FLIPPED
metadataMap[KEY_ROTATION_DEGREES] = getRotationDegreesForExifCode(orientation) 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) { when (mimeType) {
MimeTypes.GIF -> { 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 -> { MimeTypes.WEBP -> {
for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) { 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.getSafeDateMillis(ExifInterface.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it }
} }
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { 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 metadataMap[KEY_ROTATION_DEGREES] = exif.rotationDegrees
} }
val latLong = exif.latLong 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) Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=$uri", e)
} }
} }
metadataMap[KEY_FLAGS] = flags
return metadataMap return metadataMap
} }
@ -711,14 +719,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
// catalog metadata // catalog metadata
private const val KEY_MIME_TYPE = "mimeType" private const val KEY_MIME_TYPE = "mimeType"
private const val KEY_DATE_MILLIS = "dateMillis" private const val KEY_DATE_MILLIS = "dateMillis"
private const val KEY_IS_ANIMATED = "isAnimated" private const val KEY_FLAGS = "flags"
private const val KEY_IS_FLIPPED = "isFlipped"
private const val KEY_ROTATION_DEGREES = "rotationDegrees" private const val KEY_ROTATION_DEGREES = "rotationDegrees"
private const val KEY_LATITUDE = "latitude" private const val KEY_LATITUDE = "latitude"
private const val KEY_LONGITUDE = "longitude" private const val KEY_LONGITUDE = "longitude"
private const val KEY_XMP_SUBJECTS = "xmpSubjects" private const val KEY_XMP_SUBJECTS = "xmpSubjects"
private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription" 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 // overlay metadata
private const val KEY_APERTURE = "aperture" private const val KEY_APERTURE = "aperture"
private const val KEY_EXPOSURE_TIME = "exposureTime" private const val KEY_EXPOSURE_TIME = "exposureTime"

View file

@ -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
}

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves.metadata
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.metadata.Directory import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifIFD0Directory
import java.util.* import java.util.*
object MetadataExtractorHelper { object MetadataExtractorHelper {
@ -34,4 +35,25 @@ object MetadataExtractorHelper {
fun Directory.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) { fun Directory.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) {
if (this.containsTag(tag)) save(this.getDate(tag, null, TimeZone.getDefault()).time) 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
}
} }

View file

@ -206,6 +206,8 @@ class ImageEntry {
bool get isAnimated => _catalogMetadata?.isAnimated ?? false; bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
bool get isGeotiff => _catalogMetadata?.isGeotiff ?? false;
bool get canEdit => path != null; bool get canEdit => path != null;
bool get canPrint => !isVideo; bool get canPrint => !isVideo;

View file

@ -30,7 +30,7 @@ class DateMetadata {
class CatalogMetadata { class CatalogMetadata {
final int contentId, dateMillis; final int contentId, dateMillis;
final bool isAnimated; final bool isAnimated, isGeotiff;
bool isFlipped; bool isFlipped;
int rotationDegrees; int rotationDegrees;
final String mimeType, xmpSubjects, xmpTitleDescription; final String mimeType, xmpSubjects, xmpTitleDescription;
@ -38,6 +38,9 @@ class CatalogMetadata {
Address address; Address address;
static const double _precisionErrorTolerance = 1e-9; static const double _precisionErrorTolerance = 1e-9;
static const isAnimatedMask = 1 << 0;
static const isFlippedMask = 1 << 1;
static const isGeotiffMask = 1 << 2;
CatalogMetadata({ CatalogMetadata({
this.contentId, this.contentId,
@ -45,6 +48,7 @@ class CatalogMetadata {
this.dateMillis, this.dateMillis,
this.isAnimated, this.isAnimated,
this.isFlipped, this.isFlipped,
this.isGeotiff,
this.rotationDegrees, this.rotationDegrees,
this.xmpSubjects, this.xmpSubjects,
this.xmpTitleDescription, this.xmpTitleDescription,
@ -69,6 +73,7 @@ class CatalogMetadata {
dateMillis: dateMillis, dateMillis: dateMillis,
isAnimated: isAnimated, isAnimated: isAnimated,
isFlipped: isFlipped, isFlipped: isFlipped,
isGeotiff: isGeotiff,
rotationDegrees: rotationDegrees, rotationDegrees: rotationDegrees,
xmpSubjects: xmpSubjects, xmpSubjects: xmpSubjects,
xmpTitleDescription: xmpTitleDescription, xmpTitleDescription: xmpTitleDescription,
@ -77,15 +82,15 @@ class CatalogMetadata {
); );
} }
factory CatalogMetadata.fromMap(Map map, {bool boolAsInteger = false}) { factory CatalogMetadata.fromMap(Map map) {
final isAnimated = map['isAnimated'] ?? (boolAsInteger ? 0 : false); final flags = map['flags'] ?? 0;
final isFlipped = map['isFlipped'] ?? (boolAsInteger ? 0 : false);
return CatalogMetadata( return CatalogMetadata(
contentId: map['contentId'], contentId: map['contentId'],
mimeType: map['mimeType'], mimeType: map['mimeType'],
dateMillis: map['dateMillis'] ?? 0, dateMillis: map['dateMillis'] ?? 0,
isAnimated: boolAsInteger ? isAnimated != 0 : isAnimated, isAnimated: flags & isAnimatedMask != 0,
isFlipped: boolAsInteger ? isFlipped != 0 : isFlipped, isFlipped: flags & isFlippedMask != 0,
isGeotiff: flags & isGeotiffMask != 0,
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0 // `rotationDegrees` should default to `sourceRotationDegrees`, not 0
rotationDegrees: map['rotationDegrees'], rotationDegrees: map['rotationDegrees'],
xmpSubjects: map['xmpSubjects'] ?? '', xmpSubjects: map['xmpSubjects'] ?? '',
@ -95,12 +100,11 @@ class CatalogMetadata {
); );
} }
Map<String, dynamic> toMap({bool boolAsInteger = false}) => { Map<String, dynamic> toMap() => {
'contentId': contentId, 'contentId': contentId,
'mimeType': mimeType, 'mimeType': mimeType,
'dateMillis': dateMillis, 'dateMillis': dateMillis,
'isAnimated': boolAsInteger ? (isAnimated ? 1 : 0) : isAnimated, 'flags': (isAnimated ? isAnimatedMask : 0) | (isFlipped ? isFlippedMask : 0) | (isGeotiff ? isGeotiffMask : 0),
'isFlipped': boolAsInteger ? (isFlipped ? 1 : 0) : isFlipped,
'rotationDegrees': rotationDegrees, 'rotationDegrees': rotationDegrees,
'xmpSubjects': xmpSubjects, 'xmpSubjects': xmpSubjects,
'xmpTitleDescription': xmpTitleDescription, 'xmpTitleDescription': xmpTitleDescription,
@ -110,7 +114,7 @@ class CatalogMetadata {
@override @override
String toString() { 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}';
} }
} }

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db_upgrade.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
@ -48,8 +49,7 @@ class MetadataDb {
'contentId INTEGER PRIMARY KEY' 'contentId INTEGER PRIMARY KEY'
', mimeType TEXT' ', mimeType TEXT'
', dateMillis INTEGER' ', dateMillis INTEGER'
', isAnimated INTEGER' ', flags INTEGER'
', isFlipped INTEGER'
', rotationDegrees INTEGER' ', rotationDegrees INTEGER'
', xmpSubjects TEXT' ', xmpSubjects TEXT'
', xmpTitleDescription TEXT' ', xmpTitleDescription TEXT'
@ -69,65 +69,8 @@ class MetadataDb {
', path TEXT' ', path TEXT'
')'); ')');
}, },
onUpgrade: (db, oldVersion, newVersion) async { onUpgrade: MetadataDbUpgrader.upgradeDb,
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported version: 3,
// 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,
); );
} }
@ -238,7 +181,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, 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'); // debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
return metadataEntries; return metadataEntries;
} }
@ -246,11 +189,15 @@ class MetadataDb {
Future<void> saveMetadata(Iterable<CatalogMetadata> metadataEntries) async { Future<void> saveMetadata(Iterable<CatalogMetadata> metadataEntries) async {
if (metadataEntries == null || metadataEntries.isEmpty) return; if (metadataEntries == null || metadataEntries.isEmpty) return;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final db = await _database; try {
final batch = db.batch(); final db = await _database;
metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata)); final batch = db.batch();
await batch.commit(noResult: true); metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata));
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); 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<void> updateMetadataId(int oldId, CatalogMetadata metadata) async { Future<void> updateMetadataId(int oldId, CatalogMetadata metadata) async {
@ -273,7 +220,7 @@ class MetadataDb {
} }
batch.insert( batch.insert(
metadataTable, metadataTable,
metadata.toMap(boolAsInteger: true), metadata.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
} }

View file

@ -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<void> 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<void> _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<void> _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;');
});
}
}

View file

@ -61,6 +61,7 @@ class AIcons {
// thumbnail overlay // thumbnail overlay
static const IconData animated = Icons.slideshow; static const IconData animated = Icons.slideshow;
static const IconData geo = Icons.language_outlined;
static const IconData play = Icons.play_circle_outline; static const IconData play = Icons.play_circle_outline;
static const IconData selected = Icons.check_circle_outline; static const IconData selected = Icons.check_circle_outline;
static const IconData unselected = Icons.radio_button_unchecked; static const IconData unselected = Icons.radio_button_unchecked;

View file

@ -36,6 +36,7 @@ class ThumbnailEntryOverlay extends StatelessWidget {
children: [ children: [
if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize), if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize),
if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize), if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize),
if (entry.isGeotiff) GeotiffIcon(iconSize: iconSize),
if (entry.isAnimated) if (entry.isAnimated)
AnimatedImageIcon(iconSize: iconSize) AnimatedImageIcon(iconSize: iconSize)
else if (entry.isVideo) else if (entry.isVideo)

View file

@ -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 { class GpsIcon extends StatelessWidget {
final double iconSize; final double iconSize;

View file

@ -96,6 +96,7 @@ class FullscreenDebugPage extends StatelessWidget {
'isVideo': '${entry.isVideo}', 'isVideo': '${entry.isVideo}',
'isCatalogued': '${entry.isCatalogued}', 'isCatalogued': '${entry.isCatalogued}',
'isAnimated': '${entry.isAnimated}', 'isAnimated': '${entry.isAnimated}',
'isGeotiff': '${entry.isGeotiff}',
'canEdit': '${entry.canEdit}', 'canEdit': '${entry.canEdit}',
'canEditExif': '${entry.canEditExif}', 'canEditExif': '${entry.canEditExif}',
'canPrint': '${entry.canPrint}', 'canPrint': '${entry.canPrint}',