#706 viewer: improved overlay field fetch

This commit is contained in:
Thibault Deckers 2024-01-27 00:37:08 +01:00
parent baa318f0de
commit ec93b3f21d
8 changed files with 140 additions and 142 deletions

View file

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

View file

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

View file

@ -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) {

View file

@ -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?,

View file

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

View file

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

View file

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

View file

@ -1,3 +1,11 @@
enum MetadataSyntheticField {
aperture,
description,
exposureTime,
focalLength,
iso,
}
enum MetadataField { enum MetadataField {
exifDate, exifDate,
exifDateOriginal, exifDateOriginal,