#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) {
|
||||
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
||||
"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) }
|
||||
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
||||
"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) }
|
||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
|
||||
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 uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
val metadataMap = HashMap<String, Any>()
|
||||
if (isVideo(mimeType)) {
|
||||
if (fields.isEmpty() || isVideo(mimeType)) {
|
||||
result.success(metadataMap)
|
||||
return
|
||||
}
|
||||
|
@ -842,12 +842,23 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val metadata = Helper.safeRead(input)
|
||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||
foundExif = true
|
||||
if (fields.contains(KEY_APERTURE)) {
|
||||
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)
|
||||
}
|
||||
if (fields.contains(KEY_FOCAL_LENGTH)) {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
|
@ -862,11 +873,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||
val exif = ExifInterface(input)
|
||||
if (fields.contains(KEY_APERTURE)) {
|
||||
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
|
||||
}
|
||||
if (fields.contains(KEY_EXPOSURE_TIME)) {
|
||||
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
|
||||
}
|
||||
if (fields.contains(KEY_FOCAL_LENGTH)) {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ExifInterface initialization can fail with a RuntimeException
|
||||
// caused by an internal MediaMetadataRetriever failure
|
||||
|
@ -877,6 +896,47 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
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) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
|
@ -1191,70 +1251,6 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
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 {
|
||||
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
||||
|
@ -1319,6 +1315,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
// overlay metadata
|
||||
private const val KEY_APERTURE = "aperture"
|
||||
private const val KEY_DESCRIPTION = "description"
|
||||
private const val KEY_EXPOSURE_TIME = "exposureTime"
|
||||
private const val KEY_FOCAL_LENGTH = "focalLength"
|
||||
private const val KEY_ISO = "iso"
|
||||
|
|
|
@ -2,7 +2,7 @@ buildscript {
|
|||
ext {
|
||||
kotlin_version = '1.9.21'
|
||||
ksp_version = "$kotlin_version-1.0.15"
|
||||
agp_version = '8.2.1'
|
||||
agp_version = '8.2.2'
|
||||
glide_version = '4.16.0'
|
||||
// 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'
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import 'package:aves_model/aves_model.dart';
|
||||
|
||||
extension ExtraMetadataSyntheticFieldConvert on MetadataSyntheticField {
|
||||
String? get toPlatform => name;
|
||||
}
|
||||
|
||||
extension ExtraMetadataFieldConvert on MetadataField {
|
||||
MetadataType get type {
|
||||
switch (this) {
|
||||
|
|
|
@ -4,18 +4,17 @@ import 'package:flutter/foundation.dart';
|
|||
@immutable
|
||||
class OverlayMetadata extends Equatable {
|
||||
final double? aperture, focalLength;
|
||||
final String? exposureTime;
|
||||
final String? description, exposureTime;
|
||||
final int? iso;
|
||||
|
||||
@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 isNotEmpty => !isEmpty;
|
||||
bool get hasShootingDetails => aperture != null || exposureTime != null || focalLength != null || iso != null;
|
||||
|
||||
const OverlayMetadata({
|
||||
this.aperture,
|
||||
this.description,
|
||||
this.exposureTime,
|
||||
this.focalLength,
|
||||
this.iso,
|
||||
|
@ -24,6 +23,7 @@ class OverlayMetadata extends Equatable {
|
|||
factory OverlayMetadata.fromMap(Map map) {
|
||||
return OverlayMetadata(
|
||||
aperture: map['aperture'] as double?,
|
||||
description: map['description'] as String?,
|
||||
exposureTime: map['exposureTime'] as String?,
|
||||
focalLength: map['focalLength'] as double?,
|
||||
iso: map['iso'] as int?,
|
||||
|
|
|
@ -22,7 +22,7 @@ abstract class MetadataFetchService {
|
|||
|
||||
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);
|
||||
|
||||
|
@ -39,8 +39,6 @@ abstract class MetadataFetchService {
|
|||
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
||||
|
||||
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
||||
|
||||
Future<String?> getDescription(AvesEntry entry);
|
||||
}
|
||||
|
||||
class PlatformMetadataFetchService implements MetadataFetchService {
|
||||
|
@ -112,15 +110,20 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<OverlayMetadata?> getOverlayMetadata(AvesEntry entry) async {
|
||||
if (entry.isSvg) return null;
|
||||
|
||||
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
||||
if (fields.isNotEmpty && !entry.isSvg) {
|
||||
try {
|
||||
// returns map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
|
||||
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||
// returns fields on demand, with various value types:
|
||||
// 'aperture' (double),
|
||||
// 'description' (string)
|
||||
// 'exposureTime' (string),
|
||||
// 'focalLength' (double),
|
||||
// 'iso' (int),
|
||||
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
'fields': fields.map((v) => v.toPlatform).toList(),
|
||||
}) as Map;
|
||||
return OverlayMetadata.fromMap(result);
|
||||
} on PlatformException catch (e, stack) {
|
||||
|
@ -128,7 +131,8 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return const OverlayMetadata();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -280,20 +284,4 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
}
|
||||
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 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?>>(
|
||||
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/shooting.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/material.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;
|
||||
|
||||
late Future<List<dynamic>?> _detailLoader;
|
||||
late Future<OverlayMetadata> _detailLoader;
|
||||
AvesEntry? _lastEntry;
|
||||
List<dynamic>? _lastDetails;
|
||||
OverlayMetadata _lastDetails = const OverlayMetadata();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -70,12 +71,17 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
|||
void _initDetailLoader() {
|
||||
final requestEntry = entry;
|
||||
if (requestEntry == null) {
|
||||
_detailLoader = SynchronousFuture(null);
|
||||
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
||||
} else {
|
||||
_detailLoader = Future.wait([
|
||||
settings.showOverlayShootingDetails ? metadataFetchService.getOverlayMetadata(requestEntry) : Future.value(null),
|
||||
settings.showOverlayDescription ? metadataFetchService.getDescription(requestEntry) : Future.value(null),
|
||||
]);
|
||||
_detailLoader = metadataFetchService.getFields(requestEntry, {
|
||||
if (settings.showOverlayShootingDetails) ...{
|
||||
MetadataSyntheticField.aperture,
|
||||
MetadataSyntheticField.exposureTime,
|
||||
MetadataSyntheticField.focalLength,
|
||||
MetadataSyntheticField.iso,
|
||||
},
|
||||
if (settings.showOverlayDescription) MetadataSyntheticField.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,24 +90,20 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
|||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: FutureBuilder<List<dynamic>?>(
|
||||
child: FutureBuilder<OverlayMetadata>(
|
||||
future: _detailLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||
_lastDetails = snapshot.data;
|
||||
_lastDetails = snapshot.data!;
|
||||
_lastEntry = entry;
|
||||
}
|
||||
if (_lastEntry == null) return const SizedBox();
|
||||
final mainEntry = _lastEntry!;
|
||||
|
||||
final shootingDetails = _lastDetails![0];
|
||||
final description = _lastDetails![1];
|
||||
|
||||
final multiPageController = widget.multiPageController;
|
||||
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
shootingDetails: shootingDetails,
|
||||
description: description,
|
||||
details: _lastDetails,
|
||||
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
|
||||
availableWidth: widget.availableSize.width,
|
||||
multiPageController: multiPageController,
|
||||
|
@ -122,8 +124,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
|||
|
||||
class ViewerDetailOverlayContent extends StatelessWidget {
|
||||
final AvesEntry pageEntry;
|
||||
final OverlayMetadata? shootingDetails;
|
||||
final String? description;
|
||||
final OverlayMetadata details;
|
||||
final String? position;
|
||||
final double availableWidth;
|
||||
final MultiPageController? multiPageController;
|
||||
|
@ -140,8 +141,7 @@ class ViewerDetailOverlayContent extends StatelessWidget {
|
|||
const ViewerDetailOverlayContent({
|
||||
super.key,
|
||||
required this.pageEntry,
|
||||
required this.shootingDetails,
|
||||
required this.description,
|
||||
required this.details,
|
||||
required this.position,
|
||||
required this.availableWidth,
|
||||
required this.multiPageController,
|
||||
|
@ -244,27 +244,27 @@ class ViewerDetailOverlayContent extends StatelessWidget {
|
|||
|
||||
Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher(
|
||||
context: context,
|
||||
visible: description != null,
|
||||
visible: details.description != null,
|
||||
builder: (context) => OverlayRowExpander(
|
||||
expandedNotifier: expandedNotifier,
|
||||
child: OverlayDescriptionRow(description: description!),
|
||||
child: OverlayDescriptionRow(description: details.description!),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher(
|
||||
context: context,
|
||||
visible: shootingDetails != null && shootingDetails!.isNotEmpty,
|
||||
visible: details.hasShootingDetails,
|
||||
builder: (context) => SizedBox(
|
||||
width: subRowWidth,
|
||||
child: OverlayShootingRow(details: shootingDetails!),
|
||||
child: OverlayShootingRow(details: details),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildShootingSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher(
|
||||
context: context,
|
||||
subRowWidth: subRowWidth,
|
||||
visible: shootingDetails != null && shootingDetails!.isNotEmpty,
|
||||
builder: (context) => OverlayShootingRow(details: shootingDetails!),
|
||||
visible: details.hasShootingDetails,
|
||||
builder: (context) => OverlayShootingRow(details: details),
|
||||
);
|
||||
|
||||
Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher(
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
enum MetadataSyntheticField {
|
||||
aperture,
|
||||
description,
|
||||
exposureTime,
|
||||
focalLength,
|
||||
iso,
|
||||
}
|
||||
|
||||
enum MetadataField {
|
||||
exifDate,
|
||||
exifDateOriginal,
|
||||
|
|
Loading…
Reference in a new issue