info: present video cover like XMP embedded images
This commit is contained in:
parent
8e10dc7bda
commit
484baaaccb
11 changed files with 219 additions and 163 deletions
|
@ -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<String>("uri")?.let { Uri.parse(it) }
|
||||
if (uri == null) {
|
||||
result.error("getEmbeddedPictures-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val pictures = ArrayList<ByteArray>()
|
||||
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<String>("mimeType")
|
||||
val uri = call.argument<String>("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<String>("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<String>("mimeType")
|
||||
val uri = call.argument<String>("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"
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -22,10 +22,10 @@ abstract class MetadataService {
|
|||
|
||||
Future<String> getContentResolverProp(AvesEntry entry, String prop);
|
||||
|
||||
Future<List<Uint8List>> getEmbeddedPictures(String uri);
|
||||
|
||||
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry);
|
||||
|
||||
Future<Map> extractVideoEmbeddedPicture(String uri);
|
||||
|
||||
Future<Map> extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType);
|
||||
}
|
||||
|
||||
|
@ -152,19 +152,6 @@ class PlatformMetadataService implements MetadataService {
|
|||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
||||
'uri': uri,
|
||||
});
|
||||
return (result as List).cast<Uint8List>();
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getEmbeddedPictures failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async {
|
||||
try {
|
||||
|
@ -180,6 +167,19 @@ class PlatformMetadataService implements MetadataService {
|
|||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map> extractVideoEmbeddedPicture(String uri) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('extractVideoEmbeddedPicture', <String, dynamic>{
|
||||
'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<Map> extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async {
|
||||
try {
|
||||
|
|
|
@ -216,8 +216,9 @@ class _OwnerPropState extends State<OwnerProp> {
|
|||
Future<void> _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;
|
||||
|
|
|
@ -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<String, InfoLinkHandler> 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<OpenEmbeddedDataNotification>(
|
||||
onNotification: (notification) {
|
||||
_openEmbeddedData(context, notification);
|
||||
return true;
|
||||
},
|
||||
child: tile,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -95,4 +109,46 @@ class MetadataDirTile extends StatelessWidget {
|
|||
),
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, InfoLinkHandler> getVideoCoverLinkHandlers(SplayTreeMap<String, String> tags) {
|
||||
return {
|
||||
'Image': InfoLinkHandler(
|
||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||
onTap: (context) => OpenEmbeddedDataNotification.videoCover().dispatch(context),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String> allTags, {SplayTreeMap<String, String> tags, this.color})
|
||||
: allTags = allTags,
|
||||
|
|
|
@ -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<MetadataThumbnails> {
|
|||
@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
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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<XmpDirTile> with FeedbackMixin {
|
||||
class _XmpDirTileState extends State<XmpDirTile> {
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
@override
|
||||
|
@ -83,49 +75,18 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
|
|||
expandedNotifier: widget.expandedNotifier,
|
||||
initiallyExpanded: widget.initiallyExpanded,
|
||||
children: [
|
||||
NotificationListener<OpenEmbeddedDataNotification>(
|
||||
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<void> _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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue