#838 info: open MPF embedded images
This commit is contained in:
parent
33ae168eda
commit
82b4c8aaa1
10 changed files with 147 additions and 11 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- Cataloguing: detect/filter `Ultra HDR`
|
- Cataloguing: detect/filter `Ultra HDR`
|
||||||
- Info: show metadata from JPEG MPF
|
- Info: show metadata from JPEG MPF
|
||||||
|
- Info: open images embedded via JPEG MPF
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -11,21 +11,30 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||||
import deckers.thibault.aves.metadata.*
|
import deckers.thibault.aves.metadata.GoogleDeviceContainer
|
||||||
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
|
import deckers.thibault.aves.metadata.MultiPage
|
||||||
|
import deckers.thibault.aves.metadata.XMP
|
||||||
import deckers.thibault.aves.metadata.XMP.doesPropPathExist
|
import deckers.thibault.aves.metadata.XMP.doesPropPathExist
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
||||||
|
import deckers.thibault.aves.metadata.XMPPropName
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||||
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
||||||
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.model.provider.ContentImageProvider
|
import deckers.thibault.aves.model.provider.ContentImageProvider
|
||||||
import deckers.thibault.aves.model.provider.ImageProvider
|
import deckers.thibault.aves.model.provider.ImageProvider
|
||||||
import deckers.thibault.aves.utils.*
|
import deckers.thibault.aves.utils.BitmapUtils
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
||||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
|
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
|
||||||
import deckers.thibault.aves.utils.MimeTypes.extensionFor
|
import deckers.thibault.aves.utils.MimeTypes.extensionFor
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
import deckers.thibault.aves.utils.MimeTypes.isImage
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
@ -42,6 +51,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
|
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
|
||||||
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
|
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
|
||||||
|
"extractJpegMultiPictureFormat" -> ioScope.launch { safe(call, result, ::extractJpegMultiPictureFormat) }
|
||||||
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
|
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
|
||||||
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
|
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
|
||||||
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
|
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
|
||||||
|
@ -141,6 +151,50 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null)
|
result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractJpegMultiPictureFormat(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()
|
||||||
|
val displayName = call.argument<String>("displayName")
|
||||||
|
val id = call.argument<Int>("id")
|
||||||
|
if (mimeType == null || uri == null || sizeBytes == null || id == null) {
|
||||||
|
result.error("extractJpegMultiPictureFormat-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canReadWithMetadataExtractor(mimeType)) {
|
||||||
|
try {
|
||||||
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
|
val metadata = Helper.safeRead(input)
|
||||||
|
metadata.getDirectoriesOfType(MpEntryDirectory::class.java).first { it.id == id }?.let { dir ->
|
||||||
|
val mpEntry = dir.entry
|
||||||
|
MpEntry.getMimeType(dir.entry.format)?.let { embedMimeType ->
|
||||||
|
var dataOffset = mpEntry.dataOffset
|
||||||
|
if (dataOffset > 0) {
|
||||||
|
val baseOffset = MultiPage.getJpegMultiPictureFormatBaseOffset(context, uri, sizeBytes)
|
||||||
|
if (baseOffset != null) {
|
||||||
|
dataOffset += baseOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||||
|
input.skip(dataOffset)
|
||||||
|
copyEmbeddedBytes(result, embedMimeType, displayName, input, mpEntry.size)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to extract file from MPF", e)
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
Log.w(LOG_TAG, "failed to extract file from MPF", e)
|
||||||
|
} catch (e: AssertionError) {
|
||||||
|
Log.w(LOG_TAG, "failed to extract file from MPF", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.error("extractJpegMultiPictureFormat-empty", "failed to extract file index=$id from MPF at uri=$uri", null)
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
|
private fun extractMotionPhotoImage(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) }
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
|
import com.drew.imaging.jpeg.JpegSegmentType
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.metadata.XMP.countPropArrayItems
|
import deckers.thibault.aves.metadata.XMP.countPropArrayItems
|
||||||
import deckers.thibault.aves.metadata.XMP.doesPropExist
|
import deckers.thibault.aves.metadata.XMP.doesPropExist
|
||||||
|
@ -203,6 +204,36 @@ object MultiPage {
|
||||||
return offsetFromEnd
|
return offsetFromEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// starts after `[APP2 marker (1 byte)] [segment size (2 bytes)] [MPF marker (4 bytes)]`
|
||||||
|
fun getJpegMultiPictureFormatBaseOffset(context: Context, uri: Uri, sizeBytes: Long): Int? {
|
||||||
|
val app2Marker = JpegSegmentType.APP2.byteValue
|
||||||
|
val mpfMarker = "MPF".toByteArray() + 0x00
|
||||||
|
|
||||||
|
try {
|
||||||
|
Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, sizeBytes)?.use { input ->
|
||||||
|
var offset = 0
|
||||||
|
while (true) {
|
||||||
|
do {
|
||||||
|
val b = input.read().toByte()
|
||||||
|
offset++
|
||||||
|
} while (b != app2Marker)
|
||||||
|
// skip 2 bytes for segment size
|
||||||
|
input.skip(2)
|
||||||
|
offset += 2
|
||||||
|
val marker = ByteArray(4)
|
||||||
|
input.read(marker, 0, marker.size)
|
||||||
|
offset += 4
|
||||||
|
if (marker.contentEquals(mpfMarker)) {
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to get MPF base offset from uri=$uri", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> {
|
fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> {
|
||||||
fun toMap(page: Int, options: TiffBitmapFactory.Options): FieldMap {
|
fun toMap(page: Int, options: TiffBitmapFactory.Options): FieldMap {
|
||||||
return hashMapOf(
|
return hashMapOf(
|
||||||
|
|
|
@ -47,10 +47,7 @@ class MpEntryDescriptor(directory: MpEntryDirectory?) : TagDescriptor<MpEntryDir
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormatDescription(format: Int): String {
|
fun getFormatDescription(format: Int): String {
|
||||||
return when (format) {
|
return MpEntry.getMimeType(format) ?: "Unknown ($format)"
|
||||||
0 -> MimeTypes.JPEG
|
|
||||||
else -> "Unknown ($format)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTypeDescription(type: Int): String {
|
fun getTypeDescription(type: Int): String {
|
||||||
|
@ -73,4 +70,13 @@ class MpEntryDescriptor(directory: MpEntryDirectory?) : TagDescriptor<MpEntryDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MpEntry(val flags: Int, val format: Int, val type: Int, val size: Long, val dataOffset: Long, val dep1: Short, val dep2: Short)
|
class MpEntry(val flags: Int, val format: Int, val type: Int, val size: Long, val dataOffset: Long, val dep1: Short, val dep2: Short) {
|
||||||
|
companion object {
|
||||||
|
fun getMimeType(format: Int): String? {
|
||||||
|
return when (format) {
|
||||||
|
0 -> MimeTypes.JPEG
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ abstract class EmbeddedDataService {
|
||||||
|
|
||||||
Future<Map> extractMotionPhotoVideo(AvesEntry entry);
|
Future<Map> extractMotionPhotoVideo(AvesEntry entry);
|
||||||
|
|
||||||
|
Future<Map> extractJpegMultiPictureFormat(AvesEntry entry, int index);
|
||||||
|
|
||||||
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
|
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry);
|
||||||
|
|
||||||
Future<Map> extractXmpDataProp(AvesEntry entry, List<dynamic>? props, String? propMimeType);
|
Future<Map> extractXmpDataProp(AvesEntry entry, List<dynamic>? props, String? propMimeType);
|
||||||
|
@ -84,6 +86,23 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map> extractJpegMultiPictureFormat(AvesEntry entry, int id) async {
|
||||||
|
try {
|
||||||
|
final result = await _platform.invokeMethod('extractJpegMultiPictureFormat', <String, dynamic>{
|
||||||
|
'mimeType': entry.mimeType,
|
||||||
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
|
'displayName': ['${entry.bestTitle}', 'MPF #$id'].join(AText.separator),
|
||||||
|
'id': id,
|
||||||
|
});
|
||||||
|
if (result != null) return result as Map;
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry) async {
|
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -340,6 +340,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
|
||||||
theme.textTheme.bodyMedium!.copyWith(
|
theme.textTheme.bodyMedium!.copyWith(
|
||||||
color: colorScheme.onInverseSurface,
|
color: colorScheme.onInverseSurface,
|
||||||
);
|
);
|
||||||
|
final contentTextFontSize = contentTextStyle.fontSize ?? theme.textTheme.bodyMedium!.fontSize!;
|
||||||
final timerChangeShadowColor = colorScheme.primary;
|
final timerChangeShadowColor = colorScheme.primary;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -347,7 +348,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
|
||||||
if (widget.type == FeedbackType.warn) ...[
|
if (widget.type == FeedbackType.warn) ...[
|
||||||
CustomPaint(
|
CustomPaint(
|
||||||
painter: const _WarnIndicator(AColors.warning),
|
painter: const _WarnIndicator(AColors.warning),
|
||||||
size: Size(4, textScaler.scale(contentTextStyle.fontSize!)),
|
size: Size(4, textScaler.scale(contentTextFontSize)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
|
|
|
@ -44,6 +44,8 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
|
||||||
fields = await embeddedDataService.extractGoogleDeviceItem(entry, notification.dataUri!);
|
fields = await embeddedDataService.extractGoogleDeviceItem(entry, notification.dataUri!);
|
||||||
case EmbeddedDataSource.motionPhotoVideo:
|
case EmbeddedDataSource.motionPhotoVideo:
|
||||||
fields = await embeddedDataService.extractMotionPhotoVideo(entry);
|
fields = await embeddedDataService.extractMotionPhotoVideo(entry);
|
||||||
|
case EmbeddedDataSource.mpf:
|
||||||
|
fields = await embeddedDataService.extractJpegMultiPictureFormat(entry, notification.mpfId!);
|
||||||
case EmbeddedDataSource.videoCover:
|
case EmbeddedDataSource.videoCover:
|
||||||
fields = await embeddedDataService.extractVideoEmbeddedPicture(entry);
|
fields = await embeddedDataService.extractVideoEmbeddedPicture(entry);
|
||||||
case EmbeddedDataSource.xmp:
|
case EmbeddedDataSource.xmp:
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum EmbeddedDataSource { googleDevice, motionPhotoVideo, videoCover, xmp }
|
enum EmbeddedDataSource { googleDevice, motionPhotoVideo, mpf, videoCover, xmp }
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class OpenEmbeddedDataNotification extends Notification {
|
class OpenEmbeddedDataNotification extends Notification {
|
||||||
final EmbeddedDataSource source;
|
final EmbeddedDataSource source;
|
||||||
final List<dynamic>? props;
|
final List<dynamic>? props;
|
||||||
final String? mimeType, dataUri;
|
final String? mimeType, dataUri;
|
||||||
|
final int? mpfId;
|
||||||
|
|
||||||
const OpenEmbeddedDataNotification._private({
|
const OpenEmbeddedDataNotification._private({
|
||||||
required this.source,
|
required this.source,
|
||||||
this.props,
|
this.props,
|
||||||
this.mimeType,
|
this.mimeType,
|
||||||
this.dataUri,
|
this.dataUri,
|
||||||
|
this.mpfId,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OpenEmbeddedDataNotification.googleDevice({
|
factory OpenEmbeddedDataNotification.googleDevice({
|
||||||
|
@ -28,6 +30,11 @@ class OpenEmbeddedDataNotification extends Notification {
|
||||||
source: EmbeddedDataSource.motionPhotoVideo,
|
source: EmbeddedDataSource.motionPhotoVideo,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory OpenEmbeddedDataNotification.mpf(int id) => OpenEmbeddedDataNotification._private(
|
||||||
|
source: EmbeddedDataSource.mpf,
|
||||||
|
mpfId: id,
|
||||||
|
);
|
||||||
|
|
||||||
factory OpenEmbeddedDataNotification.videoCover() => const OpenEmbeddedDataNotification._private(
|
factory OpenEmbeddedDataNotification.videoCover() => const OpenEmbeddedDataNotification._private(
|
||||||
source: EmbeddedDataSource.videoCover,
|
source: EmbeddedDataSource.videoCover,
|
||||||
);
|
);
|
||||||
|
@ -43,5 +50,5 @@ class OpenEmbeddedDataNotification extends Notification {
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{source=$source, props=$props, mimeType=$mimeType, dataUri=$dataUri}';
|
String toString() => '$runtimeType#${shortHash(this)}{source=$source, props=$props, mimeType=$mimeType, dataUri=$dataUri, index=$mpfId}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,10 @@ class MetadataDirectory {
|
||||||
// special directory names
|
// special directory names
|
||||||
static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor
|
static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor
|
||||||
static const xmpDirectory = 'XMP'; // from metadata-extractor
|
static const xmpDirectory = 'XMP'; // from metadata-extractor
|
||||||
static const mediaDirectory = 'Media'; // custom
|
|
||||||
static const coverDirectory = 'Cover'; // custom
|
static const coverDirectory = 'Cover'; // custom
|
||||||
static const geoTiffDirectory = 'GeoTIFF'; // custom
|
static const geoTiffDirectory = 'GeoTIFF'; // custom
|
||||||
|
static const mediaDirectory = 'Media'; // custom
|
||||||
|
static const mpfImageDirectoryPrefix = 'MPF Image #'; // custom
|
||||||
|
|
||||||
const MetadataDirectory(
|
const MetadataDirectory(
|
||||||
this.name,
|
this.name,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves/widgets/viewer/info/embedded/notifications.dart';
|
import 'package:aves/widgets/viewer/info/embedded/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/geotiff.dart';
|
import 'package:aves/widgets/viewer/info/metadata/geotiff.dart';
|
||||||
|
@ -109,6 +110,19 @@ class MetadataDirTileBody extends StatelessWidget {
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry),
|
if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry),
|
||||||
|
if (dirName.startsWith(MetadataDirectory.mpfImageDirectoryPrefix))
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: AvesOutlinedButton(
|
||||||
|
label: context.l10n.viewerInfoOpenLinkText,
|
||||||
|
onPressed: () {
|
||||||
|
final id = int.tryParse(dirName.substring(MetadataDirectory.mpfImageDirectoryPrefix.length));
|
||||||
|
if (id != null) {
|
||||||
|
OpenEmbeddedDataNotification.mpf(id).dispatch(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
|
|
Loading…
Reference in a new issue