#706 viewer: improved overlay field fetch
This commit is contained in:
parent
baa318f0de
commit
ec93b3f21d
8 changed files with 140 additions and 142 deletions
|
@ -109,7 +109,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
||||||
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
|
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
|
||||||
"getOverlayMetadata" -> ioScope.launch { safe(call, result, ::getOverlayMetadata) }
|
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
|
||||||
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
|
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
|
||||||
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
||||||
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
||||||
|
@ -118,7 +118,6 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
||||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
||||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||||
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -807,17 +806,18 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getFields(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
if (mimeType == null || uri == null) {
|
val fields = call.argument<List<String>>("fields")
|
||||||
|
if (mimeType == null || uri == null || fields == null) {
|
||||||
result.error("getOverlayMetadata-args", "missing arguments", null)
|
result.error("getOverlayMetadata-args", "missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadataMap = HashMap<String, Any>()
|
val metadataMap = HashMap<String, Any>()
|
||||||
if (isVideo(mimeType)) {
|
if (fields.isEmpty() || isVideo(mimeType)) {
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -842,12 +842,23 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
val metadata = Helper.safeRead(input)
|
val metadata = Helper.safeRead(input)
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||||
foundExif = true
|
foundExif = true
|
||||||
|
if (fields.contains(KEY_APERTURE)) {
|
||||||
dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
|
dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_DESCRIPTION)) {
|
||||||
|
getDescriptionByMetadataExtractor(metadata)?.let { metadataMap[KEY_DESCRIPTION] = it }
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_EXPOSURE_TIME)) {
|
||||||
dir.getSafeRational(ExifDirectoryBase.TAG_EXPOSURE_TIME, saveExposureTime)
|
dir.getSafeRational(ExifDirectoryBase.TAG_EXPOSURE_TIME, saveExposureTime)
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_FOCAL_LENGTH)) {
|
||||||
dir.getSafeRational(ExifDirectoryBase.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
|
dir.getSafeRational(ExifDirectoryBase.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_ISO)) {
|
||||||
dir.getSafeInt(ExifDirectoryBase.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
|
dir.getSafeInt(ExifDirectoryBase.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||||
} catch (e: NoClassDefFoundError) {
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
@ -862,11 +873,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
try {
|
try {
|
||||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
val exif = ExifInterface(input)
|
val exif = ExifInterface(input)
|
||||||
|
if (fields.contains(KEY_APERTURE)) {
|
||||||
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
|
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_EXPOSURE_TIME)) {
|
||||||
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
|
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_FOCAL_LENGTH)) {
|
||||||
exif.getSafeDouble(ExifInterface.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
|
exif.getSafeDouble(ExifInterface.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
|
||||||
|
}
|
||||||
|
if (fields.contains(KEY_ISO)) {
|
||||||
exif.getSafeInt(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) { metadataMap[KEY_ISO] = it }
|
exif.getSafeInt(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) { metadataMap[KEY_ISO] = it }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// ExifInterface initialization can fail with a RuntimeException
|
// ExifInterface initialization can fail with a RuntimeException
|
||||||
// caused by an internal MediaMetadataRetriever failure
|
// caused by an internal MediaMetadataRetriever failure
|
||||||
|
@ -877,6 +896,47 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return description from these fields (by precedence):
|
||||||
|
// - XMP / dc:description
|
||||||
|
// - IPTC / caption-abstract
|
||||||
|
// - Exif / UserComment
|
||||||
|
// - Exif / ImageDescription
|
||||||
|
private fun getDescriptionByMetadataExtractor(metadata: com.drew.metadata.Metadata): String? {
|
||||||
|
var description: String? = null
|
||||||
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
|
val xmpMeta = dir.xmpMeta
|
||||||
|
try {
|
||||||
|
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
|
||||||
|
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
|
||||||
|
}
|
||||||
|
} catch (e: XMPException) {
|
||||||
|
Log.w(LOG_TAG, "failed to read XMP directory", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (description == null) {
|
||||||
|
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
|
||||||
|
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (description == null) {
|
||||||
|
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||||
|
// user comment field specifies encoding, unlike other string fields
|
||||||
|
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
|
||||||
|
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
|
||||||
|
if (string.isNotBlank()) {
|
||||||
|
description = string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (description == null) {
|
||||||
|
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||||
|
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) {
|
private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
@ -1191,70 +1251,6 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(dateMillis)
|
result.success(dateMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
// return description from these fields (by precedence):
|
|
||||||
// - XMP / dc:description
|
|
||||||
// - IPTC / caption-abstract
|
|
||||||
// - Exif / UserComment
|
|
||||||
// - Exif / ImageDescription
|
|
||||||
private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
|
|
||||||
val mimeType = call.argument<String>("mimeType")
|
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
|
||||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
|
||||||
if (mimeType == null || uri == null) {
|
|
||||||
result.error("getDescription-args", "missing arguments", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: String? = null
|
|
||||||
if (canReadWithMetadataExtractor(mimeType)) {
|
|
||||||
try {
|
|
||||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
|
||||||
val metadata = Helper.safeRead(input)
|
|
||||||
|
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
|
||||||
val xmpMeta = dir.xmpMeta
|
|
||||||
try {
|
|
||||||
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
|
|
||||||
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
|
|
||||||
}
|
|
||||||
} catch (e: XMPException) {
|
|
||||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description == null) {
|
|
||||||
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
|
|
||||||
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description == null) {
|
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
|
||||||
// user comment field specifies encoding, unlike other string fields
|
|
||||||
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
|
|
||||||
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
|
|
||||||
if (string.isNotBlank()) {
|
|
||||||
description = string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description == null) {
|
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
|
||||||
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
|
||||||
} catch (e: NoClassDefFoundError) {
|
|
||||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
|
||||||
} catch (e: AssertionError) {
|
|
||||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.success(description)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
||||||
|
@ -1319,6 +1315,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
|
||||||
// overlay metadata
|
// overlay metadata
|
||||||
private const val KEY_APERTURE = "aperture"
|
private const val KEY_APERTURE = "aperture"
|
||||||
|
private const val KEY_DESCRIPTION = "description"
|
||||||
private const val KEY_EXPOSURE_TIME = "exposureTime"
|
private const val KEY_EXPOSURE_TIME = "exposureTime"
|
||||||
private const val KEY_FOCAL_LENGTH = "focalLength"
|
private const val KEY_FOCAL_LENGTH = "focalLength"
|
||||||
private const val KEY_ISO = "iso"
|
private const val KEY_ISO = "iso"
|
||||||
|
|
|
@ -2,7 +2,7 @@ buildscript {
|
||||||
ext {
|
ext {
|
||||||
kotlin_version = '1.9.21'
|
kotlin_version = '1.9.21'
|
||||||
ksp_version = "$kotlin_version-1.0.15"
|
ksp_version = "$kotlin_version-1.0.15"
|
||||||
agp_version = '8.2.1'
|
agp_version = '8.2.2'
|
||||||
glide_version = '4.16.0'
|
glide_version = '4.16.0'
|
||||||
// AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550
|
// AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550
|
||||||
huawei_agconnect_version = '1.9.1.300'
|
huawei_agconnect_version = '1.9.1.300'
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
|
||||||
|
extension ExtraMetadataSyntheticFieldConvert on MetadataSyntheticField {
|
||||||
|
String? get toPlatform => name;
|
||||||
|
}
|
||||||
|
|
||||||
extension ExtraMetadataFieldConvert on MetadataField {
|
extension ExtraMetadataFieldConvert on MetadataField {
|
||||||
MetadataType get type {
|
MetadataType get type {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
|
|
@ -4,18 +4,17 @@ import 'package:flutter/foundation.dart';
|
||||||
@immutable
|
@immutable
|
||||||
class OverlayMetadata extends Equatable {
|
class OverlayMetadata extends Equatable {
|
||||||
final double? aperture, focalLength;
|
final double? aperture, focalLength;
|
||||||
final String? exposureTime;
|
final String? description, exposureTime;
|
||||||
final int? iso;
|
final int? iso;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [aperture, exposureTime, focalLength, iso];
|
List<Object?> get props => [aperture, description, exposureTime, focalLength, iso];
|
||||||
|
|
||||||
bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null;
|
bool get hasShootingDetails => aperture != null || exposureTime != null || focalLength != null || iso != null;
|
||||||
|
|
||||||
bool get isNotEmpty => !isEmpty;
|
|
||||||
|
|
||||||
const OverlayMetadata({
|
const OverlayMetadata({
|
||||||
this.aperture,
|
this.aperture,
|
||||||
|
this.description,
|
||||||
this.exposureTime,
|
this.exposureTime,
|
||||||
this.focalLength,
|
this.focalLength,
|
||||||
this.iso,
|
this.iso,
|
||||||
|
@ -24,6 +23,7 @@ class OverlayMetadata extends Equatable {
|
||||||
factory OverlayMetadata.fromMap(Map map) {
|
factory OverlayMetadata.fromMap(Map map) {
|
||||||
return OverlayMetadata(
|
return OverlayMetadata(
|
||||||
aperture: map['aperture'] as double?,
|
aperture: map['aperture'] as double?,
|
||||||
|
description: map['description'] as String?,
|
||||||
exposureTime: map['exposureTime'] as String?,
|
exposureTime: map['exposureTime'] as String?,
|
||||||
focalLength: map['focalLength'] as double?,
|
focalLength: map['focalLength'] as double?,
|
||||||
iso: map['iso'] as int?,
|
iso: map['iso'] as int?,
|
||||||
|
|
|
@ -22,7 +22,7 @@ abstract class MetadataFetchService {
|
||||||
|
|
||||||
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
||||||
|
|
||||||
Future<OverlayMetadata?> getOverlayMetadata(AvesEntry entry);
|
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields);
|
||||||
|
|
||||||
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
|
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
|
||||||
|
|
||||||
|
@ -39,8 +39,6 @@ abstract class MetadataFetchService {
|
||||||
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
||||||
|
|
||||||
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
||||||
|
|
||||||
Future<String?> getDescription(AvesEntry entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformMetadataFetchService implements MetadataFetchService {
|
class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
|
@ -112,15 +110,20 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OverlayMetadata?> getOverlayMetadata(AvesEntry entry) async {
|
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
||||||
if (entry.isSvg) return null;
|
if (fields.isNotEmpty && !entry.isSvg) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// returns map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
|
// returns fields on demand, with various value types:
|
||||||
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
// 'aperture' (double),
|
||||||
|
// 'description' (string)
|
||||||
|
// 'exposureTime' (string),
|
||||||
|
// 'focalLength' (double),
|
||||||
|
// 'iso' (int),
|
||||||
|
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
'sizeBytes': entry.sizeBytes,
|
'sizeBytes': entry.sizeBytes,
|
||||||
|
'fields': fields.map((v) => v.toPlatform).toList(),
|
||||||
}) as Map;
|
}) as Map;
|
||||||
return OverlayMetadata.fromMap(result);
|
return OverlayMetadata.fromMap(result);
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
|
@ -128,7 +131,8 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
|
return const OverlayMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -280,20 +284,4 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getDescription(AvesEntry entry) async {
|
|
||||||
try {
|
|
||||||
return await _platform.invokeMethod('getDescription', <String, dynamic>{
|
|
||||||
'mimeType': entry.mimeType,
|
|
||||||
'uri': entry.uri,
|
|
||||||
'sizeBytes': entry.sizeBytes,
|
|
||||||
});
|
|
||||||
} on PlatformException catch (e, stack) {
|
|
||||||
if (entry.isValid) {
|
|
||||||
await reportService.recordError(e, stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,8 @@ mixin EntryEditorMixin {
|
||||||
|
|
||||||
final entry = entries.first;
|
final entry = entries.first;
|
||||||
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
|
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
|
||||||
final initialDescription = await metadataFetchService.getDescription(entry) ?? '';
|
final fields = await metadataFetchService.getFields(entry, {MetadataSyntheticField.description});
|
||||||
|
final initialDescription = fields.description ?? '';
|
||||||
|
|
||||||
return showDialog<Map<DescriptionField, String?>>(
|
return showDialog<Map<DescriptionField, String?>>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import 'package:aves/widgets/viewer/overlay/details/position_title.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart';
|
import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/details/shooting.dart';
|
import 'package:aves/widgets/viewer/overlay/details/shooting.dart';
|
||||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -48,9 +49,9 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
|
|
||||||
AvesEntry? entryForIndex(int index) => index < entries.length ? entries[index] : null;
|
AvesEntry? entryForIndex(int index) => index < entries.length ? entries[index] : null;
|
||||||
|
|
||||||
late Future<List<dynamic>?> _detailLoader;
|
late Future<OverlayMetadata> _detailLoader;
|
||||||
AvesEntry? _lastEntry;
|
AvesEntry? _lastEntry;
|
||||||
List<dynamic>? _lastDetails;
|
OverlayMetadata _lastDetails = const OverlayMetadata();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -70,12 +71,17 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
void _initDetailLoader() {
|
void _initDetailLoader() {
|
||||||
final requestEntry = entry;
|
final requestEntry = entry;
|
||||||
if (requestEntry == null) {
|
if (requestEntry == null) {
|
||||||
_detailLoader = SynchronousFuture(null);
|
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
||||||
} else {
|
} else {
|
||||||
_detailLoader = Future.wait([
|
_detailLoader = metadataFetchService.getFields(requestEntry, {
|
||||||
settings.showOverlayShootingDetails ? metadataFetchService.getOverlayMetadata(requestEntry) : Future.value(null),
|
if (settings.showOverlayShootingDetails) ...{
|
||||||
settings.showOverlayDescription ? metadataFetchService.getDescription(requestEntry) : Future.value(null),
|
MetadataSyntheticField.aperture,
|
||||||
]);
|
MetadataSyntheticField.exposureTime,
|
||||||
|
MetadataSyntheticField.focalLength,
|
||||||
|
MetadataSyntheticField.iso,
|
||||||
|
},
|
||||||
|
if (settings.showOverlayDescription) MetadataSyntheticField.description,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,24 +90,20 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: FutureBuilder<List<dynamic>?>(
|
child: FutureBuilder<OverlayMetadata>(
|
||||||
future: _detailLoader,
|
future: _detailLoader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||||
_lastDetails = snapshot.data;
|
_lastDetails = snapshot.data!;
|
||||||
_lastEntry = entry;
|
_lastEntry = entry;
|
||||||
}
|
}
|
||||||
if (_lastEntry == null) return const SizedBox();
|
if (_lastEntry == null) return const SizedBox();
|
||||||
final mainEntry = _lastEntry!;
|
final mainEntry = _lastEntry!;
|
||||||
|
|
||||||
final shootingDetails = _lastDetails![0];
|
|
||||||
final description = _lastDetails![1];
|
|
||||||
|
|
||||||
final multiPageController = widget.multiPageController;
|
final multiPageController = widget.multiPageController;
|
||||||
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
|
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
|
||||||
pageEntry: pageEntry ?? mainEntry,
|
pageEntry: pageEntry ?? mainEntry,
|
||||||
shootingDetails: shootingDetails,
|
details: _lastDetails,
|
||||||
description: description,
|
|
||||||
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
||||||
availableWidth: widget.availableSize.width,
|
availableWidth: widget.availableSize.width,
|
||||||
multiPageController: multiPageController,
|
multiPageController: multiPageController,
|
||||||
|
@ -122,8 +124,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
|
|
||||||
class ViewerDetailOverlayContent extends StatelessWidget {
|
class ViewerDetailOverlayContent extends StatelessWidget {
|
||||||
final AvesEntry pageEntry;
|
final AvesEntry pageEntry;
|
||||||
final OverlayMetadata? shootingDetails;
|
final OverlayMetadata details;
|
||||||
final String? description;
|
|
||||||
final String? position;
|
final String? position;
|
||||||
final double availableWidth;
|
final double availableWidth;
|
||||||
final MultiPageController? multiPageController;
|
final MultiPageController? multiPageController;
|
||||||
|
@ -140,8 +141,7 @@ class ViewerDetailOverlayContent extends StatelessWidget {
|
||||||
const ViewerDetailOverlayContent({
|
const ViewerDetailOverlayContent({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pageEntry,
|
required this.pageEntry,
|
||||||
required this.shootingDetails,
|
required this.details,
|
||||||
required this.description,
|
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.availableWidth,
|
required this.availableWidth,
|
||||||
required this.multiPageController,
|
required this.multiPageController,
|
||||||
|
@ -244,27 +244,27 @@ class ViewerDetailOverlayContent extends StatelessWidget {
|
||||||
|
|
||||||
Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher(
|
Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher(
|
||||||
context: context,
|
context: context,
|
||||||
visible: description != null,
|
visible: details.description != null,
|
||||||
builder: (context) => OverlayRowExpander(
|
builder: (context) => OverlayRowExpander(
|
||||||
expandedNotifier: expandedNotifier,
|
expandedNotifier: expandedNotifier,
|
||||||
child: OverlayDescriptionRow(description: description!),
|
child: OverlayDescriptionRow(description: details.description!),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher(
|
Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher(
|
||||||
context: context,
|
context: context,
|
||||||
visible: shootingDetails != null && shootingDetails!.isNotEmpty,
|
visible: details.hasShootingDetails,
|
||||||
builder: (context) => SizedBox(
|
builder: (context) => SizedBox(
|
||||||
width: subRowWidth,
|
width: subRowWidth,
|
||||||
child: OverlayShootingRow(details: shootingDetails!),
|
child: OverlayShootingRow(details: details),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildShootingSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher(
|
Widget _buildShootingSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher(
|
||||||
context: context,
|
context: context,
|
||||||
subRowWidth: subRowWidth,
|
subRowWidth: subRowWidth,
|
||||||
visible: shootingDetails != null && shootingDetails!.isNotEmpty,
|
visible: details.hasShootingDetails,
|
||||||
builder: (context) => OverlayShootingRow(details: shootingDetails!),
|
builder: (context) => OverlayShootingRow(details: details),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher(
|
Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher(
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
enum MetadataSyntheticField {
|
||||||
|
aperture,
|
||||||
|
description,
|
||||||
|
exposureTime,
|
||||||
|
focalLength,
|
||||||
|
iso,
|
||||||
|
}
|
||||||
|
|
||||||
enum MetadataField {
|
enum MetadataField {
|
||||||
exifDate,
|
exifDate,
|
||||||
exifDateOriginal,
|
exifDateOriginal,
|
||||||
|
|
Loading…
Reference in a new issue