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.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"
|
||||||
|
|
|
@ -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.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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
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
|
// 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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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}',
|
||||||
|
|
Loading…
Reference in a new issue