diff --git a/android/app/libs/fijkplayer-full-release.aar b/android/app/libs/fijkplayer-full-release.aar index 72547c9dc..39d501b6d 100644 Binary files a/android/app/libs/fijkplayer-full-release.aar and b/android/app/libs/fijkplayer-full-release.aar differ diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 466d23d84..0d02540ad 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -76,6 +76,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.File +import java.io.InputStream import java.text.ParseException import java.util.* import kotlin.math.roundToLong @@ -90,6 +91,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) } "getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) } "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getExifThumbnails) } + "extractMotionPhotoVideo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractMotionPhotoVideo) } "extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) } "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } else -> result.notImplemented() @@ -775,6 +777,41 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.success(thumbnails) } + private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() + if (mimeType == null || uri == null || sizeBytes == null) { + result.error("extractMotionPhotoVideo-args", "failed because of missing arguments", null) + return + } + + try { + Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> + val metadata = ImageMetadataReader.readMetadata(input) + for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) { + val xmpMeta = dir.xmpMeta + // offset from end + var offsetFromEnd: Int? = null + xmpMeta.getSafeInt(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it } + if (offsetFromEnd != null) { + StorageUtils.openInputStream(context, uri)?.let { original -> + original.skip(sizeBytes - offsetFromEnd!!) + copyEmbeddedBytes(result, MimeTypes.MP4, original) + } + return + } + } + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to extract video from motion photo", e) + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to extract video from motion photo", e) + } + + result.error("extractMotionPhotoVideo-empty", "failed to extract video from motion photo at uri=$uri", null) + } + private fun extractVideoEmbeddedPicture(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { @@ -794,7 +831,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } } embedMimeType?.let { mime -> - copyEmbeddedBytes(bytes, mime, result) + copyEmbeddedBytes(result, mime, bytes.inputStream()) return } } @@ -843,7 +880,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } } - copyEmbeddedBytes(embedBytes, embedMimeType, result) + copyEmbeddedBytes(result, embedMimeType, embedBytes.inputStream()) return } catch (e: XMPException) { result.error("extractXmpDataProp-xmp", "failed to read XMP directory for uri=$uri prop=$dataPropPath", e.message) @@ -859,11 +896,11 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null) } - private fun copyEmbeddedBytes(embedBytes: ByteArray, embedMimeType: String, result: MethodChannel.Result) { + private fun copyEmbeddedBytes(result: MethodChannel.Result, embedMimeType: String, embedByteStream: InputStream) { val embedFile = File.createTempFile("aves", null, context.cacheDir).apply { deleteOnExit() outputStream().use { outputStream -> - embedBytes.inputStream().use { inputStream -> + embedByteStream.use { inputStream -> inputStream.copyTo(outputStream) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index be2f4da9c..177d97119 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -42,6 +42,12 @@ object XMP { fun isDataPath(path: String) = knownDataPaths.contains(path) + // motion photo + + const val GCAMERA_SCHEMA_NS = "http://ns.google.com/photos/1.0/camera/" + + const val GCAMERA_VIDEO_OFFSET_PROP_NAME = "GCamera:MicroVideoOffset" + // panorama // cf https://developers.google.com/streetview/spherical-metadata diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 5786922ed..328777783 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -39,6 +39,7 @@ object MimeTypes { private const val MP2T = "video/mp2t" private const val MP2TS = "video/mp2ts" + const val MP4 = "video/mp4" private const val WEBM = "video/webm" fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE) diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index ec31d08af..c1851231f 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -115,7 +115,9 @@ class VideoMetadataFormatter { save('Channel Layout', _formatChannelLayout(value)); break; case Keys.codecName: - save('Format', _formatCodecName(value)); + if (value != 'none') { + save('Format', _formatCodecName(value)); + } break; case Keys.codecPixelFormat: if (streamType == StreamTypes.video) { @@ -296,6 +298,7 @@ class VideoMetadataFormatter { } class StreamTypes { + static const attachment = 'attachment'; static const audio = 'audio'; static const metadata = 'metadata'; static const subtitle = 'subtitle'; diff --git a/lib/ref/xmp.dart b/lib/ref/xmp.dart index 02a3bfa9e..2dd85b45c 100644 --- a/lib/ref/xmp.dart +++ b/lib/ref/xmp.dart @@ -16,6 +16,7 @@ class XMP { 'GettyImagesGIFT': 'Getty Images', 'GIMP': 'GIMP', 'GCamera': 'Google Camera', + 'GCreations': 'Google Creations', 'GFocus': 'Google Focus', 'GPano': 'Google Panorama', 'illustrator': 'Illustrator', diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 690738336..8ed734735 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -24,6 +24,8 @@ abstract class MetadataService { Future> getExifThumbnails(AvesEntry entry); + Future extractMotionPhotoVideo(AvesEntry entry); + Future extractVideoEmbeddedPicture(String uri); Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType); @@ -167,6 +169,21 @@ class PlatformMetadataService implements MetadataService { return []; } + @override + Future extractMotionPhotoVideo(AvesEntry entry) async { + try { + final result = await platform.invokeMethod('extractMotionPhotoVideo', { + 'mimeType': entry.mimeType, + 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, + }); + return result; + } on PlatformException catch (e) { + debugPrint('extractMotionPhotoVideo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return null; + } + @override Future extractVideoEmbeddedPicture(String uri) async { try { diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 5e76ff780..022da31d8 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -121,6 +121,9 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { Future _openEmbeddedData(BuildContext context, OpenEmbeddedDataNotification notification) async { Map fields; switch (notification.source) { + case EmbeddedDataSource.motionPhotoVideo: + fields = await metadataService.extractMotionPhotoVideo(entry); + break; case EmbeddedDataSource.videoCover: fields = await metadataService.extractVideoEmbeddedPicture(entry.uri); break; diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 1fa504c1e..0efc27272 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -193,6 +193,8 @@ class _MetadataSectionSliverState extends State with Auto String getTypeText(Map stream) { final type = stream[Keys.streamType] ?? StreamTypes.unknown; switch (type) { + case StreamTypes.attachment: + return 'Attachment'; case StreamTypes.audio: return 'Audio'; case StreamTypes.metadata: @@ -209,8 +211,8 @@ class _MetadataSectionSliverState extends State with Auto } final allStreams = (mediaInfo[Keys.streams] as List).cast(); - final unknownStreams = allStreams.where((stream) => stream[Keys.streamType] == StreamTypes.unknown).toList(); - final knownStreams = allStreams.whereNot(unknownStreams.contains); + final attachmentStreams = allStreams.where((stream) => stream[Keys.streamType] == StreamTypes.attachment).toList(); + final knownStreams = allStreams.whereNot(attachmentStreams.contains); // display known streams as separate directories (e.g. video, audio, subs) if (knownStreams.isNotEmpty) { @@ -228,18 +230,18 @@ class _MetadataSectionSliverState extends State with Auto } } - // display unknown streams as attachments (e.g. fonts) - if (unknownStreams.isNotEmpty) { - final unknownCodecCount = >{}; - for (final stream in unknownStreams) { + // group attachments by format (e.g. TTF fonts) + if (attachmentStreams.isNotEmpty) { + final formatCount = >{}; + for (final stream in attachmentStreams) { final codec = (stream[Keys.codecName] as String ?? 'unknown').toUpperCase(); - if (!unknownCodecCount.containsKey(codec)) { - unknownCodecCount[codec] = []; + if (!formatCount.containsKey(codec)) { + formatCount[codec] = []; } - unknownCodecCount[codec].add(stream[Keys.filename]); + formatCount[codec].add(stream[Keys.filename]); } - if (unknownCodecCount.isNotEmpty) { - final rawTags = unknownCodecCount.map((key, value) { + if (formatCount.isNotEmpty) { + final rawTags = formatCount.map((key, value) { final count = value.length; // remove duplicate names, so number of displayed names may not match displayed count final names = value.where((v) => v != null).toSet().toList()..sort(compareAsciiUpperCase); diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 7ee8a308f..613bd66ff 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -4,21 +4,61 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/mwg.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class XmpNamespace { final String namespace; + final Map rawProps; - const XmpNamespace(this.namespace); + const XmpNamespace(this.namespace, this.rawProps); + + factory XmpNamespace.create(String namespace, Map rawProps) { + switch (namespace) { + case XmpBasicNamespace.ns: + return XmpBasicNamespace(rawProps); + case XmpExifNamespace.ns: + return XmpExifNamespace(rawProps); + case XmpGAudioNamespace.ns: + return XmpGAudioNamespace(rawProps); + case XmpGCameraNamespace.ns: + return XmpGCameraNamespace(rawProps); + case XmpGDepthNamespace.ns: + return XmpGDepthNamespace(rawProps); + case XmpGImageNamespace.ns: + return XmpGImageNamespace(rawProps); + case XmpIptcCoreNamespace.ns: + return XmpIptcCoreNamespace(rawProps); + case XmpMgwRegionsNamespace.ns: + return XmpMgwRegionsNamespace(rawProps); + case XmpMMNamespace.ns: + return XmpMMNamespace(rawProps); + case XmpNoteNamespace.ns: + return XmpNoteNamespace(rawProps); + case XmpPhotoshopNamespace.ns: + return XmpPhotoshopNamespace(rawProps); + case XmpTiffNamespace.ns: + return XmpTiffNamespace(rawProps); + default: + return XmpNamespace(namespace, rawProps); + } + } String get displayTitle => XMP.namespaces[namespace] ?? namespace; - List buildNamespaceSection({ - @required List> rawProps, - }) { - final props = rawProps + Map get buildProps => rawProps; + + List buildNamespaceSection() { + final props = buildProps + .entries .map((kv) { final prop = XmpProp(kv.key, kv.value); return extractData(prop) ? null : prop; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart b/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart index 76ac8c865..4ec4f1cee 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/exif.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpExifNamespace extends XmpNamespace { static const ns = 'exif'; - XmpExifNamespace() : super(ns); + XmpExifNamespace(Map rawProps) : super(ns, rawProps); @override String get displayTitle => 'Exif'; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 1e7e82808..24db7a4da 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:tuple/tuple.dart'; abstract class XmpGoogleNamespace extends XmpNamespace { - XmpGoogleNamespace(String ns) : super(ns); + XmpGoogleNamespace(String ns, Map rawProps) : super(ns, rawProps); List> get dataProps; @@ -34,7 +34,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { class XmpGAudioNamespace extends XmpGoogleNamespace { static const ns = 'GAudio'; - XmpGAudioNamespace() : super(ns); + XmpGAudioNamespace(Map rawProps) : super(ns, rawProps); @override List> get dataProps => [Tuple2('$ns:Data', '$ns:Mime')]; @@ -46,7 +46,7 @@ class XmpGAudioNamespace extends XmpGoogleNamespace { class XmpGDepthNamespace extends XmpGoogleNamespace { static const ns = 'GDepth'; - XmpGDepthNamespace() : super(ns); + XmpGDepthNamespace(Map rawProps) : super(ns, rawProps); @override List> get dataProps => [ @@ -61,7 +61,7 @@ class XmpGDepthNamespace extends XmpGoogleNamespace { class XmpGImageNamespace extends XmpGoogleNamespace { static const ns = 'GImage'; - XmpGImageNamespace() : super(ns); + XmpGImageNamespace(Map rawProps) : super(ns, rawProps); @override List> get dataProps => [Tuple2('$ns:Data', '$ns:Mime')]; @@ -69,3 +69,35 @@ class XmpGImageNamespace extends XmpGoogleNamespace { @override String get displayTitle => 'Google Image'; } + +class XmpGCameraNamespace extends XmpNamespace { + static const ns = 'GCamera'; + static const videoOffsetKey = 'GCamera:MicroVideoOffset'; + static const videoDataKey = 'Data'; + + bool _isMotionPhoto; + + XmpGCameraNamespace(Map rawProps) : super(ns, rawProps) { + _isMotionPhoto = rawProps.keys.any((key) => key == videoOffsetKey); + } + + @override + Map get buildProps { + return _isMotionPhoto + ? Map.fromEntries({ + MapEntry(videoDataKey, '[skipped]'), + ...rawProps.entries, + }) + : rawProps; + } + + @override + Map linkifyValues(List props) { + return { + videoDataKey: InfoLinkHandler( + linkText: (context) => context.l10n.viewerInfoOpenLinkText, + onTap: (context) => OpenEmbeddedDataNotification.motionPhotoVideo().dispatch(context), + ), + }; + } +} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart b/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart index 7ccd3feaf..50ddf0385 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart @@ -9,7 +9,7 @@ class XmpIptcCoreNamespace extends XmpNamespace { final creatorContactInfo = {}; - XmpIptcCoreNamespace() : super(ns); + XmpIptcCoreNamespace(Map rawProps) : super(ns, rawProps); @override String get displayTitle => 'IPTC Core'; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart b/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart index 321452173..8516dde69 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart @@ -12,7 +12,7 @@ class XmpMgwRegionsNamespace extends XmpNamespace { final dimensions = {}; final regionList = >{}; - XmpMgwRegionsNamespace() : super(ns); + XmpMgwRegionsNamespace(Map rawProps) : super(ns, rawProps); @override String get displayTitle => 'Regions'; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart index 0b0399bab..73897fe13 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; class XmpPhotoshopNamespace extends XmpNamespace { static const ns = 'photoshop'; - XmpPhotoshopNamespace() : super(ns); + XmpPhotoshopNamespace(Map rawProps) : super(ns, rawProps); @override String get displayTitle => 'Photoshop'; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart b/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart index fbf712f6e..5c901413f 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/tiff.dart @@ -8,7 +8,7 @@ class XmpTiffNamespace extends XmpNamespace { @override String get displayTitle => 'TIFF'; - XmpTiffNamespace() : super(ns); + XmpTiffNamespace(Map rawProps) : super(ns, rawProps); @override String formatValue(XmpProp prop) { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index ce6459333..d39e5c1d4 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -14,7 +14,7 @@ class XmpBasicNamespace extends XmpNamespace { final thumbnails = >{}; - XmpBasicNamespace() : super(ns); + XmpBasicNamespace(Map rawProps) : super(ns, rawProps); @override String get displayTitle => 'Basic'; @@ -61,7 +61,7 @@ class XmpMMNamespace extends XmpNamespace { final ingredients = >{}; final pantry = >{}; - XmpMMNamespace() : super(ns); + XmpMMNamespace(Map rawProps) : super(ns, rawProps); @override String get displayTitle => 'Media Management'; @@ -114,7 +114,7 @@ class XmpNoteNamespace extends XmpNamespace { // `xmpNote:HasExtendedXMP` is structural and should not be displayed to users static const hasExtendedXmp = '$ns:HasExtendedXMP'; - XmpNoteNamespace() : super(ns); + XmpNoteNamespace(Map rawProps) : super(ns, rawProps); @override bool extractData(XmpProp prop) { diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index ef9383dfe..1663aac44 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -4,13 +4,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/xmp.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/mwg.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -36,40 +29,13 @@ class _XmpDirTileState extends State { @override Widget build(BuildContext context) { - final sections = SplayTreeMap>>.of( - groupBy(widget.tags.entries, (kv) { - final fullKey = kv.key; - final i = fullKey.indexOf(XMP.propNamespaceSeparator); - final namespace = i == -1 ? '' : fullKey.substring(0, i); - switch (namespace) { - case XmpBasicNamespace.ns: - return XmpBasicNamespace(); - case XmpExifNamespace.ns: - return XmpExifNamespace(); - case XmpGAudioNamespace.ns: - return XmpGAudioNamespace(); - case XmpGDepthNamespace.ns: - return XmpGDepthNamespace(); - case XmpGImageNamespace.ns: - return XmpGImageNamespace(); - case XmpIptcCoreNamespace.ns: - return XmpIptcCoreNamespace(); - case XmpMgwRegionsNamespace.ns: - return XmpMgwRegionsNamespace(); - case XmpMMNamespace.ns: - return XmpMMNamespace(); - case XmpNoteNamespace.ns: - return XmpNoteNamespace(); - case XmpPhotoshopNamespace.ns: - return XmpPhotoshopNamespace(); - case XmpTiffNamespace.ns: - return XmpTiffNamespace(); - default: - return XmpNamespace(namespace); - } - }), - (a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle), - ); + final sections = groupBy(widget.tags.entries, (kv) { + final fullKey = kv.key; + final i = fullKey.indexOf(XMP.propNamespaceSeparator); + final namespace = i == -1 ? '' : fullKey.substring(0, i); + return namespace; + }).entries.map((kv) => XmpNamespace.create(kv.key, Map.fromEntries(kv.value))).toList() + ..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle)); return AvesExpansionTile( title: 'XMP', expandedNotifier: widget.expandedNotifier, @@ -79,11 +45,7 @@ class _XmpDirTileState extends State { padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: sections.entries - .expand((kv) => kv.key.buildNamespaceSection( - rawProps: kv.value, - )) - .toList(), + children: sections.expand((section) => section.buildNamespaceSection()).toList(), ), ), ], diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index fffebe903..c5d23c7a3 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -28,7 +28,7 @@ class OpenTempEntryNotification extends Notification { String toString() => '$runtimeType#${shortHash(this)}{entry=$entry}'; } -enum EmbeddedDataSource { videoCover, xmp } +enum EmbeddedDataSource { motionPhotoVideo, videoCover, xmp } class OpenEmbeddedDataNotification extends Notification { final EmbeddedDataSource source; @@ -41,6 +41,10 @@ class OpenEmbeddedDataNotification extends Notification { this.mimeType, }); + factory OpenEmbeddedDataNotification.motionPhotoVideo() => OpenEmbeddedDataNotification._private( + source: EmbeddedDataSource.motionPhotoVideo, + ); + factory OpenEmbeddedDataNotification.videoCover() => OpenEmbeddedDataNotification._private( source: EmbeddedDataSource.videoCover, ); diff --git a/pubspec.lock b/pubspec.lock index bd19d121b..7d9e69197 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -197,7 +197,7 @@ packages: description: path: "." ref: aves - resolved-ref: "0f25874db46d1af6fcfbeb8722915cbc211a10fb" + resolved-ref: "0100934c469f35f575f3f84e17116f13c326f393" url: "git://github.com/deckerst/fijkplayer.git" source: git version: "0.8.7"