From 484baaaccb35f30094ae0d44b4d2225d23d82cba Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 15 Apr 2021 10:01:08 +0900 Subject: [PATCH] info: present video cover like XMP embedded images --- .../aves/channel/calls/MetadataHandler.kt | 131 +++++++++++------- .../thibault/aves/metadata/Metadata.kt | 3 +- lib/services/metadata_service.dart | 30 ++-- lib/widgets/viewer/info/basic_section.dart | 5 +- .../info/metadata/metadata_dir_tile.dart | 108 +++++++++++---- .../info/metadata/metadata_section.dart | 3 +- .../info/metadata/metadata_thumbnail.dart | 13 +- .../viewer/info/metadata/xmp_namespaces.dart | 26 +++- .../viewer/info/metadata/xmp_ns/google.dart | 2 +- .../viewer/info/metadata/xmp_ns/xmp.dart | 2 +- .../viewer/info/metadata/xmp_tile.dart | 59 ++------ 11 files changed, 219 insertions(+), 163 deletions(-) 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 ccfd75f8e..0aa6ce5e1 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 @@ -88,8 +88,8 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) } "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) } "getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) } - "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) } "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) } + "extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) } "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) } else -> result.notImplemented() } @@ -217,10 +217,17 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { if (isVideo(mimeType)) { // this is used as fallback when the video metadata cannot be found on the Dart side + // and to identify whether there is an accessible cover image // do not include HEIC here val mediaDir = getAllMetadataByMediaMetadataRetriever(uri) if (mediaDir.isNotEmpty()) { metadataMap[Metadata.DIR_MEDIA] = mediaDir + if (mediaDir.containsKey(KEY_HAS_EMBEDDED_PICTURE)) { + metadataMap[Metadata.DIR_COVER_ART] = hashMapOf( + // dummy entry value + "Image" to "data", + ) + } } // Android's `MediaExtractor` and `MediaPlayer` cannot be used for details // about embedded images as they do not list them as separate tracks @@ -241,6 +248,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { for ((code, name) in MediaMetadataRetrieverHelper.allKeys) { retriever.getSafeDescription(code) { dirMap[name] = it } } + if (retriever.embeddedPicture != null) { + // additional key for the Dart side to know whether to add a `Cover` section + dirMap[KEY_HAS_EMBEDDED_PICTURE] = "yes" + } } catch (e: Exception) { Log.w(LOG_TAG, "failed to get video metadata by MediaMetadataRetriever for uri=$uri", e) } finally { @@ -734,28 +745,6 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.success(value?.toString()) } - private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) { - val uri = call.argument("uri")?.let { Uri.parse(it) } - if (uri == null) { - result.error("getEmbeddedPictures-args", "failed because of missing arguments", null) - return - } - - val pictures = ArrayList() - val retriever = StorageUtils.openMetadataRetriever(context, uri) - if (retriever != null) { - try { - retriever.embeddedPicture?.let { pictures.add(it) } - } catch (e: Exception) { - // ignore - } finally { - // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs - retriever.release() - } - } - result.success(pictures) - } - private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -785,6 +774,39 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { result.success(thumbnails) } + private fun extractVideoEmbeddedPicture(call: MethodCall, result: MethodChannel.Result) { + val uri = call.argument("uri")?.let { Uri.parse(it) } + if (uri == null) { + result.error("extractVideoEmbeddedPicture-args", "failed because of missing arguments", null) + return + } + + val retriever = StorageUtils.openMetadataRetriever(context, uri) + if (retriever != null) { + try { + retriever.embeddedPicture?.let { bytes -> + var embedMimeType: String? = null + bytes.inputStream().use { input -> + val metadata = ImageMetadataReader.readMetadata(input) + metadata.getFirstDirectoryOfType(FileTypeDirectory::class.java)?.let { dir -> + dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { embedMimeType = it } + } + } + embedMimeType?.let { mime -> + copyEmbeddedBytes(bytes, mime, result) + return + } + } + } catch (e: Exception) { + result.error("extractVideoEmbeddedPicture-fetch", "failed to fetch picture for uri=$uri", e.message) + } finally { + // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs + retriever.release() + } + } + result.error("extractVideoEmbeddedPicture-empty", "failed to extract picture for uri=$uri", null) + } + private fun extractXmpDataProp(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -820,36 +842,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { } } - val embedFile = File.createTempFile("aves", null, context.cacheDir).apply { - deleteOnExit() - outputStream().use { outputStream -> - embedBytes.inputStream().use { inputStream -> - inputStream.copyTo(outputStream) - } - } - } - val embedUri = Uri.fromFile(embedFile) - val embedFields: FieldMap = hashMapOf( - "uri" to embedUri.toString(), - "mimeType" to embedMimeType, - ) - if (isImage(embedMimeType) || isVideo(embedMimeType)) { - GlobalScope.launch(Dispatchers.IO) { - FileImageProvider().fetchSingle(context, embedUri, embedMimeType, object : ImageProvider.ImageOpCallback { - override fun onSuccess(fields: FieldMap) { - embedFields.putAll(fields) - result.success(embedFields) - } - - override fun onFailure(throwable: Throwable) = result.error("extractXmpDataProp-failure", "failed to get entry for uri=$embedUri mime=$embedMimeType", throwable.message) - }) - } - } else { - result.success(embedFields) - } + copyEmbeddedBytes(embedBytes, embedMimeType, result) return } catch (e: XMPException) { - result.error("extractXmpDataProp-args", "failed to read XMP directory for uri=$uri prop=$dataPropPath", e.message) + result.error("extractXmpDataProp-xmp", "failed to read XMP directory for uri=$uri prop=$dataPropPath", e.message) return } } @@ -862,6 +858,36 @@ 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) { + val embedFile = File.createTempFile("aves", null, context.cacheDir).apply { + deleteOnExit() + outputStream().use { outputStream -> + embedBytes.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + val embedUri = Uri.fromFile(embedFile) + val embedFields: FieldMap = hashMapOf( + "uri" to embedUri.toString(), + "mimeType" to embedMimeType, + ) + if (isImage(embedMimeType) || isVideo(embedMimeType)) { + GlobalScope.launch(Dispatchers.IO) { + FileImageProvider().fetchSingle(context, embedUri, embedMimeType, object : ImageProvider.ImageOpCallback { + override fun onSuccess(fields: FieldMap) { + embedFields.putAll(fields) + result.success(embedFields) + } + + override fun onFailure(throwable: Throwable) = result.error("copyEmbeddedBytes-failure", "failed to get entry for uri=$embedUri mime=$embedMimeType", throwable.message) + }) + } + } else { + result.success(embedFields) + } + } + private fun isMultiPageTiff(uri: Uri) = getTiffPageInfo(uri, 0)?.outDirectoryCount ?: 1 > 1 private fun getTiffPageInfo(uri: Uri, page: Int): TiffBitmapFactory.Options? { @@ -923,5 +949,8 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { private const val KEY_EXPOSURE_TIME = "exposureTime" private const val KEY_FOCAL_LENGTH = "focalLength" private const val KEY_ISO = "iso" + + // additional media key + private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 998465677..cba217c0e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -25,7 +25,8 @@ object Metadata { // directory names, as shown when listing all metadata const val DIR_GPS = "GPS" // from metadata-extractor const val DIR_XMP = "XMP" // from metadata-extractor - const val DIR_MEDIA = "Media" + const val DIR_MEDIA = "Media" // custom + const val DIR_COVER_ART = "Cover" // custom // interpret EXIF code to angle (0, 90, 180 or 270 degrees) fun getRotationDegreesForExifCode(exifOrientation: Int): Int = when (exifOrientation) { diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 83cedd445..690738336 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -22,10 +22,10 @@ abstract class MetadataService { Future getContentResolverProp(AvesEntry entry, String prop); - Future> getEmbeddedPictures(String uri); - Future> getExifThumbnails(AvesEntry entry); + Future extractVideoEmbeddedPicture(String uri); + Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType); } @@ -152,19 +152,6 @@ class PlatformMetadataService implements MetadataService { return null; } - @override - Future> getEmbeddedPictures(String uri) async { - try { - final result = await platform.invokeMethod('getEmbeddedPictures', { - 'uri': uri, - }); - return (result as List).cast(); - } on PlatformException catch (e) { - debugPrint('getEmbeddedPictures failed with code=${e.code}, exception=${e.message}, details=${e.details}'); - } - return []; - } - @override Future> getExifThumbnails(AvesEntry entry) async { try { @@ -180,6 +167,19 @@ class PlatformMetadataService implements MetadataService { return []; } + @override + Future extractVideoEmbeddedPicture(String uri) async { + try { + final result = await platform.invokeMethod('extractVideoEmbeddedPicture', { + 'uri': uri, + }); + return result; + } on PlatformException catch (e) { + debugPrint('extractVideoEmbeddedPicture failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return null; + } + @override Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async { try { diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index e1f58cf21..56dc53e97 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -216,8 +216,9 @@ class _OwnerPropState extends State { Future _getOwner() async { if (entry == null) return; if (_loadedUri.value == entry.uri) return; - if (isVisible) { - _ownerPackage = await metadataService.getContentResolverProp(widget.entry, 'owner_package_name'); + final isMediaContent = entry.uri.startsWith('content://media/external/'); + if (isVisible && isMediaContent) { + _ownerPackage = await metadataService.getContentResolverProp(entry, 'owner_package_name'); _loadedUri.value = entry.uri; } else { _ownerPackage = null; diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index d3285174c..0ce000a79 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -2,20 +2,28 @@ import 'dart:collection'; import 'package:aves/model/entry.dart'; import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/services/services.dart'; import 'package:aves/services/svg_metadata_service.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_thumbnail.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_tile.dart'; +import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:pedantic/pedantic.dart'; -class MetadataDirTile extends StatelessWidget { +class MetadataDirTile extends StatelessWidget with FeedbackMixin { final AvesEntry entry; final String title; final MetadataDirectory dir; @@ -37,43 +45,49 @@ class MetadataDirTile extends StatelessWidget { if (tags.isEmpty) return SizedBox.shrink(); final dirName = dir.name; + Widget tile; if (dirName == MetadataDirectory.xmpDirectory) { - return XmpDirTile( + tile = XmpDirTile( entry: entry, tags: tags, expandedNotifier: expandedDirectoryNotifier, initiallyExpanded: initiallyExpanded, ); - } - - Widget thumbnail; - if (showThumbnails) { + } else { + Map linkHandlers; switch (dirName) { - case MetadataDirectory.exifThumbnailDirectory: - thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry); + case SvgMetadataService.metadataDirectory: + linkHandlers = getSvgLinkHandlers(tags); break; - case MetadataDirectory.mediaDirectory: - thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry); + case MetadataDirectory.coverDirectory: + linkHandlers = getVideoCoverLinkHandlers(tags); break; } - } - return AvesExpansionTile( - title: title, - color: dir.color ?? BrandColors.get(dirName) ?? stringToColor(dirName), - expandedNotifier: expandedDirectoryNotifier, - initiallyExpanded: initiallyExpanded, - children: [ - if (thumbnail != null) thumbnail, - Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - tags, - maxValueLength: Constants.infoGroupMaxValueLength, - linkHandlers: dirName == SvgMetadataService.metadataDirectory ? getSvgLinkHandlers(tags) : null, + tile = AvesExpansionTile( + title: title, + color: dir.color ?? BrandColors.get(dirName) ?? stringToColor(dirName), + expandedNotifier: expandedDirectoryNotifier, + initiallyExpanded: initiallyExpanded, + children: [ + if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry), + Padding( + padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: InfoRowGroup( + tags, + maxValueLength: Constants.infoGroupMaxValueLength, + linkHandlers: linkHandlers, + ), ), - ), - ], + ], + ); + } + return NotificationListener( + onNotification: (notification) { + _openEmbeddedData(context, notification); + return true; + }, + child: tile, ); } @@ -95,4 +109,46 @@ class MetadataDirTile extends StatelessWidget { ), }; } + + static Map getVideoCoverLinkHandlers(SplayTreeMap tags) { + return { + 'Image': InfoLinkHandler( + linkText: (context) => context.l10n.viewerInfoOpenLinkText, + onTap: (context) => OpenEmbeddedDataNotification.videoCover().dispatch(context), + ), + }; + } + + Future _openEmbeddedData(BuildContext context, OpenEmbeddedDataNotification notification) async { + Map fields; + switch (notification.source) { + case EmbeddedDataSource.videoCover: + fields = await metadataService.extractVideoEmbeddedPicture(entry.uri); + break; + case EmbeddedDataSource.xmp: + fields = await metadataService.extractXmpDataProp(entry, notification.propPath, notification.mimeType); + break; + } + if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) { + showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); + return; + } + + final mimeType = fields['mimeType']; + final uri = fields['uri']; + if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { + // open with another app + unawaited(AndroidAppService.open(uri, mimeType).then((success) { + if (!success) { + // fallback to sharing, so that the file can be saved somewhere + AndroidAppService.shareSingle(uri, mimeType).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + } + })); + return; + } + + OpenTempEntryNotification(entry: AvesEntry.fromMap(fields)).dispatch(context); + } } diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 334944b2c..6779617f9 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -276,7 +276,8 @@ class MetadataDirectory { // special directory names static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor static const xmpDirectory = 'XMP'; // from metadata-extractor - static const mediaDirectory = 'Media'; // additional media (video/audio/images) directory + static const mediaDirectory = 'Media'; // custom + static const coverDirectory = 'Cover'; // custom const MetadataDirectory(this.name, this.parent, SplayTreeMap allTags, {SplayTreeMap tags, this.color}) : allTags = allTags, diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 6d6bc926c..170601146 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -6,15 +6,11 @@ import 'package:aves/services/services.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -enum MetadataThumbnailSource { embedded, exif } - class MetadataThumbnails extends StatefulWidget { - final MetadataThumbnailSource source; final AvesEntry entry; const MetadataThumbnails({ Key key, - @required this.source, @required this.entry, }) : super(key: key); @@ -32,14 +28,7 @@ class _MetadataThumbnailsState extends State { @override void initState() { super.initState(); - switch (widget.source) { - case MetadataThumbnailSource.embedded: - _loader = metadataService.getEmbeddedPictures(uri); - break; - case MetadataThumbnailSource.exif: - _loader = metadataService.getExifThumbnails(entry); - break; - } + _loader = metadataService.getExifThumbnails(entry); } @override diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index fd8f4f4c9..d7f1cb1a6 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -117,15 +117,33 @@ class XmpProp { String toString() => '$runtimeType#${shortHash(this)}{path=$path, value=$value}'; } +enum EmbeddedDataSource { videoCover, xmp } + class OpenEmbeddedDataNotification extends Notification { + final EmbeddedDataSource source; final String propPath; final String mimeType; - const OpenEmbeddedDataNotification({ - @required this.propPath, - @required this.mimeType, + const OpenEmbeddedDataNotification._private({ + @required this.source, + this.propPath, + this.mimeType, }); + factory OpenEmbeddedDataNotification.videoCover() => OpenEmbeddedDataNotification._private( + source: EmbeddedDataSource.videoCover, + ); + + factory OpenEmbeddedDataNotification.xmp({ + @required String propPath, + @required String mimeType, + }) => + OpenEmbeddedDataNotification._private( + source: EmbeddedDataSource.xmp, + propPath: propPath, + mimeType: mimeType, + ); + @override - String toString() => '$runtimeType#${shortHash(this)}{propPath=$propPath, mimeType=$mimeType}'; + String toString() => '$runtimeType#${shortHash(this)}{source=$source, propPath=$propPath, mimeType=$mimeType}'; } diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 718ce3f1a..331ad11ae 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -20,7 +20,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { dataProp.displayKey, InfoLinkHandler( linkText: (context) => context.l10n.viewerInfoOpenLinkText, - onTap: (context) => OpenEmbeddedDataNotification( + onTap: (context) => OpenEmbeddedDataNotification.xmp( propPath: dataProp.path, mimeType: mimeProp.value, ).dispatch(context), diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index 9ddd5bb91..1db5e44ea 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -33,7 +33,7 @@ class XmpBasicNamespace extends XmpNamespace { if (struct.containsKey(thumbnailDataDisplayKey)) thumbnailDataDisplayKey: InfoLinkHandler( linkText: (context) => context.l10n.viewerInfoOpenLinkText, - onTap: (context) => OpenEmbeddedDataNotification( + onTap: (context) => OpenEmbeddedDataNotification.xmp( propPath: 'xmp:Thumbnails[$index]/xmpGImg:image', mimeType: MimeTypes.jpeg, ).dispatch(context), diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 141cd9c9c..ef9383dfe 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -1,14 +1,8 @@ import 'dart:collection'; import 'package:aves/model/entry.dart'; -import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/xmp.dart'; -import 'package:aves/services/android_app_service.dart'; -import 'package:aves/services/services.dart'; -import 'package:aves/widgets/common/action_mixins/feedback.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/dialogs/aves_dialog.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'; @@ -17,10 +11,8 @@ 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:aves/widgets/viewer/info/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:pedantic/pedantic.dart'; class XmpDirTile extends StatefulWidget { final AvesEntry entry; @@ -39,7 +31,7 @@ class XmpDirTile extends StatefulWidget { _XmpDirTileState createState() => _XmpDirTileState(); } -class _XmpDirTileState extends State with FeedbackMixin { +class _XmpDirTileState extends State { AvesEntry get entry => widget.entry; @override @@ -83,49 +75,18 @@ class _XmpDirTileState extends State with FeedbackMixin { expandedNotifier: widget.expandedNotifier, initiallyExpanded: widget.initiallyExpanded, children: [ - NotificationListener( - onNotification: (notification) { - _openEmbeddedData(notification.propPath, notification.mimeType); - return true; - }, - child: Padding( - 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(), - ), + Padding( + 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(), ), ), ], ); } - - Future _openEmbeddedData(String propPath, String propMimeType) async { - final fields = await metadataService.extractXmpDataProp(entry, propPath, propMimeType); - if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) { - showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); - return; - } - - final mimeType = fields['mimeType']; - final uri = fields['uri']; - if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { - // open with another app - unawaited(AndroidAppService.open(uri, mimeType).then((success) { - if (!success) { - // fallback to sharing, so that the file can be saved somewhere - AndroidAppService.shareSingle(uri, mimeType).then((success) { - if (!success) showNoMatchingAppDialog(context); - }); - } - })); - return; - } - - OpenTempEntryNotification(entry: AvesEntry.fromMap(fields)).dispatch(context); - } }