DB change to merge flags, geotiff identification
This commit is contained in:
parent
60e7b2c5d9
commit
1c415f83dc
11 changed files with 216 additions and 86 deletions
|
@ -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<String, Any> {
|
||||
val metadataMap = HashMap<String, Any>()
|
||||
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<String, dynamic> toMap({bool boolAsInteger = false}) => {
|
||||
Map<String, dynamic> 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}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void> saveMetadata(Iterable<CatalogMetadata> 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<void> updateMetadataId(int oldId, CatalogMetadata metadata) async {
|
||||
|
@ -273,7 +220,7 @@ class MetadataDb {
|
|||
}
|
||||
batch.insert(
|
||||
metadataTable,
|
||||
metadata.toMap(boolAsInteger: true),
|
||||
metadata.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
|
100
lib/model/metadata_db_upgrade.dart
Normal file
100
lib/model/metadata_db_upgrade.dart
Normal 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;');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}',
|
||||
|
|
Loading…
Reference in a new issue