info: improved listing for directories with same name
This commit is contained in:
parent
b84fde14af
commit
34f8b9cef9
2 changed files with 146 additions and 99 deletions
|
@ -52,10 +52,10 @@ import deckers.thibault.aves.metadata.XMP.isPanorama
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isHeic
|
|
||||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
|
||||||
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.isHeic
|
||||||
|
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.MimeTypes.tiffExtensionPattern
|
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
@ -104,34 +104,45 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
|
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
|
||||||
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
|
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
|
||||||
val uuidDirCount = HashMap<String, Int>()
|
val uuidDirCount = HashMap<String, Int>()
|
||||||
for (dir in metadata.directories.filter {
|
val dirByName = metadata.directories.filter {
|
||||||
it.tagCount > 0
|
it.tagCount > 0
|
||||||
&& it !is FileTypeDirectory
|
&& it !is FileTypeDirectory
|
||||||
&& it !is AviDirectory
|
&& it !is AviDirectory
|
||||||
}) {
|
}.groupBy { dir -> dir.name }
|
||||||
// directory name
|
for (dirEntry in dirByName) {
|
||||||
var dirName = dir.name
|
val baseDirName = dirEntry.key
|
||||||
if (dir is Mp4UuidBoxDirectory) {
|
|
||||||
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
|
|
||||||
dirName += " $uuid"
|
|
||||||
|
|
||||||
val count = uuidDirCount[uuid] ?: 0
|
|
||||||
uuidDirCount[uuid] = count + 1
|
|
||||||
if (count > 0) {
|
|
||||||
dirName += " ($count)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// exclude directories known to be redundant with info derived on the Dart side
|
// exclude directories known to be redundant with info derived on the Dart side
|
||||||
// they are excluded by name instead of runtime type because excluding `Mp4Directory`
|
// they are excluded by name instead of runtime type because excluding `Mp4Directory`
|
||||||
// would also exclude derived directories, such as `Mp4UuidBoxDirectory`
|
// would also exclude derived directories, such as `Mp4UuidBoxDirectory`
|
||||||
if (allMetadataRedundantDirNames.contains(dirName)) continue
|
if (allMetadataRedundantDirNames.contains(baseDirName)) continue
|
||||||
|
|
||||||
|
val sameNameDirs = dirEntry.value
|
||||||
|
val sameNameDirCount = sameNameDirs.size
|
||||||
|
for (dirIndex in 0 until sameNameDirCount) {
|
||||||
|
val dir = sameNameDirs[dirIndex]
|
||||||
|
|
||||||
|
// directory name
|
||||||
|
var thisDirName = baseDirName
|
||||||
|
if (dir is Mp4UuidBoxDirectory) {
|
||||||
|
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
|
||||||
|
thisDirName += " $uuid"
|
||||||
|
|
||||||
|
val count = uuidDirCount[uuid] ?: 0
|
||||||
|
uuidDirCount[uuid] = count + 1
|
||||||
|
if (count > 0) {
|
||||||
|
thisDirName += " ($count)"
|
||||||
|
}
|
||||||
|
} else if (sameNameDirCount > 1 && !allMetadataMergeableDirNames.contains(baseDirName)) {
|
||||||
|
// optional count for multiple directories of the same type
|
||||||
|
thisDirName = "$thisDirName[${dirIndex + 1}]"
|
||||||
|
}
|
||||||
|
|
||||||
// optional parent to distinguish child directories of the same type
|
// optional parent to distinguish child directories of the same type
|
||||||
dir.parent?.name?.let { dirName = "$it/$dirName" }
|
dir.parent?.name?.let { thisDirName = "$it/$thisDirName" }
|
||||||
|
|
||||||
val dirMap = metadataMap[dirName] ?: HashMap()
|
val dirMap = metadataMap[thisDirName] ?: HashMap()
|
||||||
metadataMap[dirName] = dirMap
|
metadataMap[thisDirName] = dirMap
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
val tags = dir.tags
|
val tags = dir.tags
|
||||||
|
@ -183,20 +194,20 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
GSpherical.SPHERICAL_VIDEO_V1_UUID -> {
|
GSpherical.SPHERICAL_VIDEO_V1_UUID -> {
|
||||||
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
||||||
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
|
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
|
||||||
metadataMap.remove(dirName)
|
metadataMap.remove(thisDirName)
|
||||||
}
|
}
|
||||||
QuickTimeMetadata.PROF_UUID -> {
|
QuickTimeMetadata.PROF_UUID -> {
|
||||||
// redundant with info derived on the Dart side
|
// redundant with info derived on the Dart side
|
||||||
metadataMap.remove(dirName)
|
metadataMap.remove(thisDirName)
|
||||||
}
|
}
|
||||||
QuickTimeMetadata.USMT_UUID -> {
|
QuickTimeMetadata.USMT_UUID -> {
|
||||||
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
||||||
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
|
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
|
||||||
if (blocks.isNotEmpty()) {
|
if (blocks.isNotEmpty()) {
|
||||||
metadataMap.remove(dirName)
|
metadataMap.remove(thisDirName)
|
||||||
dirName = "QuickTime User Media"
|
thisDirName = "QuickTime User Media"
|
||||||
val usmt = metadataMap[dirName] ?: HashMap()
|
val usmt = metadataMap[thisDirName] ?: HashMap()
|
||||||
metadataMap[dirName] = usmt
|
metadataMap[thisDirName] = usmt
|
||||||
|
|
||||||
blocks.forEach {
|
blocks.forEach {
|
||||||
var key = it.type
|
var key = it.type
|
||||||
|
@ -218,6 +229,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
||||||
} catch (e: NoClassDefFoundError) {
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
@ -767,6 +779,16 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
"QuickTime Sound",
|
"QuickTime Sound",
|
||||||
"QuickTime Video",
|
"QuickTime Video",
|
||||||
)
|
)
|
||||||
|
private val allMetadataMergeableDirNames = setOf(
|
||||||
|
"Exif SubIFD",
|
||||||
|
"GIF Control",
|
||||||
|
"GIF Image",
|
||||||
|
"HEIF",
|
||||||
|
"ICC Profile",
|
||||||
|
"IPTC",
|
||||||
|
"WebP",
|
||||||
|
"XMP",
|
||||||
|
)
|
||||||
|
|
||||||
// catalog metadata
|
// catalog metadata
|
||||||
private const val KEY_MIME_TYPE = "mimeType"
|
private const val KEY_MIME_TYPE = "mimeType"
|
||||||
|
|
|
@ -40,9 +40,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
|
||||||
|
|
||||||
ValueNotifier<Map<String, MetadataDirectory>> get metadataNotifier => widget.metadataNotifier;
|
ValueNotifier<Map<String, MetadataDirectory>> get metadataNotifier => widget.metadataNotifier;
|
||||||
|
|
||||||
// directory names may contain the name of their parent directory
|
// directory names may contain the name of their parent directory (as prefix + '/')
|
||||||
// if so, they are separated by this character
|
// directory names may contain an index (as suffix in '[]')
|
||||||
static const parentChildSeparator = '/';
|
static final directoryNamePattern = RegExp(r'^((?<parent>.*?)/)?(?<name>.*?)(\[(?<index>\d+)\])?$');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -138,14 +138,27 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
|
||||||
var directoryName = dirKV.key as String;
|
var directoryName = dirKV.key as String;
|
||||||
|
|
||||||
String? parent;
|
String? parent;
|
||||||
final parts = directoryName.split(parentChildSeparator);
|
int? index;
|
||||||
if (parts.length > 1) {
|
final match = directoryNamePattern.firstMatch(directoryName);
|
||||||
parent = parts[0];
|
if (match != null) {
|
||||||
directoryName = parts[1];
|
parent = match.namedGroup('parent');
|
||||||
|
final nameMatch = match.namedGroup('name');
|
||||||
|
if (nameMatch != null) {
|
||||||
|
directoryName = nameMatch;
|
||||||
|
}
|
||||||
|
final indexMatch = match.namedGroup('index');
|
||||||
|
if (indexMatch != null) {
|
||||||
|
index = int.tryParse(indexMatch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final rawTags = dirKV.value as Map;
|
final rawTags = dirKV.value as Map;
|
||||||
return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags));
|
return MetadataDirectory(
|
||||||
|
directoryName,
|
||||||
|
_toSortedTags(rawTags),
|
||||||
|
parent: parent,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) {
|
if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) {
|
||||||
|
@ -157,6 +170,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
|
||||||
if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) {
|
if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) {
|
||||||
title = '${dir.parent}/$title';
|
title = '${dir.parent}/$title';
|
||||||
}
|
}
|
||||||
|
if (dir.index != null) {
|
||||||
|
title += ' ${dir.index}';
|
||||||
|
}
|
||||||
return MapEntry(title, dir);
|
return MapEntry(title, dir);
|
||||||
}).toList()
|
}).toList()
|
||||||
..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
|
..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
|
||||||
|
@ -171,7 +187,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
|
||||||
final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo);
|
final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo);
|
||||||
if (formattedMediaTags.isNotEmpty) {
|
if (formattedMediaTags.isNotEmpty) {
|
||||||
// overwrite generic directory found from the platform side
|
// overwrite generic directory found from the platform side
|
||||||
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, null, _toSortedTags(formattedMediaTags)));
|
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaInfo.containsKey(Keys.streams)) {
|
if (mediaInfo.containsKey(Keys.streams)) {
|
||||||
|
@ -210,7 +226,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
|
||||||
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
|
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
|
||||||
if (formattedStreamTags.isNotEmpty) {
|
if (formattedStreamTags.isNotEmpty) {
|
||||||
final color = stringToColor(typeText);
|
final color = stringToColor(typeText);
|
||||||
directories.add(MetadataDirectory(dirName, null, _toSortedTags(formattedStreamTags), color: color));
|
directories.add(MetadataDirectory(dirName, _toSortedTags(formattedStreamTags), color: color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,7 +248,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
|
||||||
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
|
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
|
||||||
return MapEntry(key, '$count items: ${names.join(', ')}');
|
return MapEntry(key, '$count items: ${names.join(', ')}');
|
||||||
});
|
});
|
||||||
directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags)));
|
directories.add(MetadataDirectory('Attachments', _toSortedTags(rawTags)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,6 +270,7 @@ class MetadataDirectory {
|
||||||
final String name;
|
final String name;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final String? parent;
|
final String? parent;
|
||||||
|
final int? index;
|
||||||
final SplayTreeMap<String, String> allTags;
|
final SplayTreeMap<String, String> allTags;
|
||||||
final SplayTreeMap<String, String> tags;
|
final SplayTreeMap<String, String> tags;
|
||||||
|
|
||||||
|
@ -265,14 +282,22 @@ class MetadataDirectory {
|
||||||
|
|
||||||
const MetadataDirectory(
|
const MetadataDirectory(
|
||||||
this.name,
|
this.name,
|
||||||
this.parent,
|
|
||||||
this.allTags, {
|
this.allTags, {
|
||||||
SplayTreeMap<String, String>? tags,
|
SplayTreeMap<String, String>? tags,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.parent,
|
||||||
|
this.index,
|
||||||
}) : tags = tags ?? allTags;
|
}) : tags = tags ?? allTags;
|
||||||
|
|
||||||
MetadataDirectory filterKeys(bool Function(String key) testKey) {
|
MetadataDirectory filterKeys(bool Function(String key) testKey) {
|
||||||
final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key))));
|
final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key))));
|
||||||
return MetadataDirectory(name, parent, tags, tags: filteredTags, color: color);
|
return MetadataDirectory(
|
||||||
|
name,
|
||||||
|
tags,
|
||||||
|
tags: filteredTags,
|
||||||
|
color: color,
|
||||||
|
parent: parent,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue