#952 renaming: processors for tags/make/model
This commit is contained in:
parent
55c96ad1c1
commit
bd9a89e5d4
12 changed files with 225 additions and 25 deletions
|
@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Collection: support for Fairphone burst pattern
|
- Collection: support for Fairphone burst pattern
|
||||||
|
- Collection: allow using tags/make/model when bulk renaming
|
||||||
- Settings: hidden items can be toggled
|
- Settings: hidden items can be toggled
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
@ -29,6 +29,7 @@ import com.drew.metadata.webp.WebpDirectory
|
||||||
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.metadata.ExifGeoTiffTags
|
import deckers.thibault.aves.metadata.ExifGeoTiffTags
|
||||||
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
|
||||||
|
@ -110,7 +111,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
||||||
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
|
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
|
||||||
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
|
"getOverlayMetadata" -> ioScope.launch { safe(call, result, ::getOverlayMetadata) }
|
||||||
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
|
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
|
||||||
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
||||||
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
||||||
|
@ -119,6 +120,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
||||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
||||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||||
|
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -815,7 +817,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFields(call: MethodCall, result: MethodChannel.Result) {
|
private fun getOverlayMetadata(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) }
|
||||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
|
@ -1250,6 +1252,71 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(dateMillis)
|
result.success(dateMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFields(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 fields = call.argument<List<String>>("fields")
|
||||||
|
if (mimeType == null || uri == null || fields == null) {
|
||||||
|
result.error("getFields-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val metadataMap = HashMap<String, Any>()
|
||||||
|
if (fields.isEmpty() || isVideo(mimeType)) {
|
||||||
|
result.success(metadataMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundExif = false
|
||||||
|
if (canReadWithMetadataExtractor(mimeType)) {
|
||||||
|
try {
|
||||||
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
|
val metadata = Helper.safeRead(input)
|
||||||
|
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) {
|
||||||
|
foundExif = true
|
||||||
|
val allTags = ExifInterfaceHelper.allTags
|
||||||
|
fields.forEach { tag ->
|
||||||
|
allTags[tag]?.let { mapper ->
|
||||||
|
val tagType = mapper.type
|
||||||
|
dir.getDescription(tagType)?.let { value -> metadataMap[tag] = value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||||
|
} catch (e: AssertionError) {
|
||||||
|
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundExif && canReadWithExifInterface(mimeType)) {
|
||||||
|
// fallback to read EXIF via ExifInterface
|
||||||
|
try {
|
||||||
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
|
val exif = ExifInterface(input)
|
||||||
|
fields.forEach { tag ->
|
||||||
|
if (exif.hasAttribute(tag)) {
|
||||||
|
val value = exif.getAttribute(tag)
|
||||||
|
if (value != null) {
|
||||||
|
metadataMap[tag] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ExifInterface initialization can fail with a RuntimeException
|
||||||
|
// caused by an internal MediaMetadataRetriever failure
|
||||||
|
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for mimeType=$mimeType uri=$uri", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success(metadataMap)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
||||||
|
|
|
@ -43,6 +43,8 @@ extension ExtraMetadataFieldConvert on MetadataField {
|
||||||
case MetadataField.exifGpsTrackRef:
|
case MetadataField.exifGpsTrackRef:
|
||||||
case MetadataField.exifGpsVersionId:
|
case MetadataField.exifGpsVersionId:
|
||||||
case MetadataField.exifImageDescription:
|
case MetadataField.exifImageDescription:
|
||||||
|
case MetadataField.exifMake:
|
||||||
|
case MetadataField.exifModel:
|
||||||
case MetadataField.exifUserComment:
|
case MetadataField.exifUserComment:
|
||||||
return MetadataType.exif;
|
return MetadataType.exif;
|
||||||
case MetadataField.mp4GpsCoordinates:
|
case MetadataField.mp4GpsCoordinates:
|
||||||
|
@ -145,6 +147,10 @@ extension ExtraMetadataFieldConvert on MetadataField {
|
||||||
return 'GPSVersionID';
|
return 'GPSVersionID';
|
||||||
case MetadataField.exifImageDescription:
|
case MetadataField.exifImageDescription:
|
||||||
return 'ImageDescription';
|
return 'ImageDescription';
|
||||||
|
case MetadataField.exifMake:
|
||||||
|
return 'Make';
|
||||||
|
case MetadataField.exifModel:
|
||||||
|
return 'Model';
|
||||||
case MetadataField.exifUserComment:
|
case MetadataField.exifUserComment:
|
||||||
return 'UserComment';
|
return 'UserComment';
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
import 'package:aves/convert/metadata/fields.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -38,6 +42,12 @@ class NamingPattern {
|
||||||
if (processorOptions != null) {
|
if (processorOptions != null) {
|
||||||
processors.add(DateNamingProcessor(processorOptions.trim()));
|
processors.add(DateNamingProcessor(processorOptions.trim()));
|
||||||
}
|
}
|
||||||
|
case TagsNamingProcessor.key:
|
||||||
|
processors.add(TagsNamingProcessor(processorOptions?.trim() ?? ''));
|
||||||
|
case MetadataFieldNamingProcessor.key:
|
||||||
|
if (processorOptions != null) {
|
||||||
|
processors.add(MetadataFieldNamingProcessor(processorOptions.trim()));
|
||||||
|
}
|
||||||
case NameNamingProcessor.key:
|
case NameNamingProcessor.key:
|
||||||
processors.add(const NameNamingProcessor());
|
processors.add(const NameNamingProcessor());
|
||||||
case CounterNamingProcessor.key:
|
case CounterNamingProcessor.key:
|
||||||
|
@ -95,21 +105,33 @@ class NamingPattern {
|
||||||
switch (processorKey) {
|
switch (processorKey) {
|
||||||
case DateNamingProcessor.key:
|
case DateNamingProcessor.key:
|
||||||
return '<$processorKey, yyyyMMdd-HHmmss>';
|
return '<$processorKey, yyyyMMdd-HHmmss>';
|
||||||
|
case TagsNamingProcessor.key:
|
||||||
|
return '<$processorKey, ->';
|
||||||
case CounterNamingProcessor.key:
|
case CounterNamingProcessor.key:
|
||||||
case NameNamingProcessor.key:
|
case NameNamingProcessor.key:
|
||||||
default:
|
default:
|
||||||
|
if (processorKey.startsWith(MetadataFieldNamingProcessor.key)) {
|
||||||
|
final field = MetadataFieldNamingProcessor.fieldFromKey(processorKey);
|
||||||
|
return '<${MetadataFieldNamingProcessor.key}, $field>';
|
||||||
|
}
|
||||||
return '<$processorKey>';
|
return '<$processorKey>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String apply(AvesEntry entry, int index) => processors.map((v) => v.process(entry, index) ?? '').join().trimLeft();
|
Future<String> apply(AvesEntry entry, int index) async {
|
||||||
|
final fields = processors.expand((v) => v.getRequiredFields()).toSet();
|
||||||
|
final fieldValues = await metadataFetchService.getFields(entry, fields);
|
||||||
|
return processors.map((v) => v.process(entry, index, fieldValues) ?? '').join().trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
abstract class NamingProcessor extends Equatable {
|
abstract class NamingProcessor extends Equatable {
|
||||||
const NamingProcessor();
|
const NamingProcessor();
|
||||||
|
|
||||||
String? process(AvesEntry entry, int index);
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues);
|
||||||
|
|
||||||
|
Set<MetadataField> getRequiredFields() => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -122,7 +144,7 @@ class LiteralNamingProcessor extends NamingProcessor {
|
||||||
const LiteralNamingProcessor(this.text);
|
const LiteralNamingProcessor(this.text);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) => text;
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -137,12 +159,60 @@ class DateNamingProcessor extends NamingProcessor {
|
||||||
DateNamingProcessor(String pattern) : format = DateFormat(pattern);
|
DateNamingProcessor(String pattern) : format = DateFormat(pattern);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) {
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
return date != null ? format.format(date) : null;
|
return date != null ? format.format(date) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class TagsNamingProcessor extends NamingProcessor {
|
||||||
|
static const key = 'tags';
|
||||||
|
static const defaultSeparator = ' ';
|
||||||
|
|
||||||
|
final String separator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [separator];
|
||||||
|
|
||||||
|
TagsNamingProcessor(String separator) : separator = separator.isEmpty ? defaultSeparator : separator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||||
|
return entry.tags.join(separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class MetadataFieldNamingProcessor extends NamingProcessor {
|
||||||
|
static const key = 'field';
|
||||||
|
|
||||||
|
static String keyWithField(MetadataField field) => '$key-${field.name}';
|
||||||
|
|
||||||
|
// loose, for user to see and later parse
|
||||||
|
static String fieldFromKey(String keyWithField) => keyWithField.substring(key.length + 1);
|
||||||
|
|
||||||
|
late final MetadataField? field;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [field];
|
||||||
|
|
||||||
|
MetadataFieldNamingProcessor(String field) {
|
||||||
|
final lowerField = field.toLowerCase();
|
||||||
|
this.field = MetadataField.values.firstWhereOrNull((v) => v.name.toLowerCase() == lowerField);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<MetadataField> getRequiredFields() {
|
||||||
|
return {field}.whereNotNull().toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||||
|
return fieldValues[field?.toPlatform]?.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class NameNamingProcessor extends NamingProcessor {
|
class NameNamingProcessor extends NamingProcessor {
|
||||||
static const key = 'name';
|
static const key = 'name';
|
||||||
|
@ -153,7 +223,7 @@ class NameNamingProcessor extends NamingProcessor {
|
||||||
const NameNamingProcessor();
|
const NameNamingProcessor();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) => entry.filenameWithoutExtension;
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => entry.filenameWithoutExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -174,5 +244,5 @@ class CounterNamingProcessor extends NamingProcessor {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) => '${index + start}'.padLeft(padding, '0');
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => '${index + start}'.padLeft(padding, '0');
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ abstract class MetadataFetchService {
|
||||||
|
|
||||||
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
||||||
|
|
||||||
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields);
|
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields);
|
||||||
|
|
||||||
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
|
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ abstract class MetadataFetchService {
|
||||||
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
||||||
|
|
||||||
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformMetadataFetchService implements MetadataFetchService {
|
class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
|
@ -110,7 +112,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
||||||
if (fields.isNotEmpty && !entry.isSvg) {
|
if (fields.isNotEmpty && !entry.isSvg) {
|
||||||
try {
|
try {
|
||||||
// returns fields on demand, with various value types:
|
// returns fields on demand, with various value types:
|
||||||
|
@ -119,7 +121,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
// 'exposureTime' (string),
|
// 'exposureTime' (string),
|
||||||
// 'focalLength' (double),
|
// 'focalLength' (double),
|
||||||
// 'iso' (int),
|
// 'iso' (int),
|
||||||
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
'sizeBytes': entry.sizeBytes,
|
'sizeBytes': entry.sizeBytes,
|
||||||
|
@ -284,4 +286,24 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields) async {
|
||||||
|
if (fields.isNotEmpty && !entry.isSvg) {
|
||||||
|
try {
|
||||||
|
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
||||||
|
'mimeType': entry.mimeType,
|
||||||
|
'uri': entry.uri,
|
||||||
|
'sizeBytes': entry.sizeBytes,
|
||||||
|
'fields': fields.map((v) => v.toPlatform).toList(),
|
||||||
|
});
|
||||||
|
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
if (entry.isValid) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,7 @@ class AIcons {
|
||||||
static const info = Icons.info_outlined;
|
static const info = Icons.info_outlined;
|
||||||
static const layers = Icons.layers_outlined;
|
static const layers = Icons.layers_outlined;
|
||||||
static const map = Icons.map_outlined;
|
static const map = Icons.map_outlined;
|
||||||
|
static const more = Icons.more_horiz_outlined;
|
||||||
static final move = MdiIcons.fileMoveOutline;
|
static final move = MdiIcons.fileMoveOutline;
|
||||||
static const mute = Icons.volume_off_outlined;
|
static const mute = Icons.volume_off_outlined;
|
||||||
static const unmute = Icons.volume_up_outlined;
|
static const unmute = Icons.volume_up_outlined;
|
||||||
|
|
|
@ -11,6 +11,10 @@ extension ExtraMetadataFieldView on MetadataField {
|
||||||
return 'Exif digitized date';
|
return 'Exif digitized date';
|
||||||
case MetadataField.exifGpsDatestamp:
|
case MetadataField.exifGpsDatestamp:
|
||||||
return 'Exif GPS date';
|
return 'Exif GPS date';
|
||||||
|
case MetadataField.exifMake:
|
||||||
|
return 'Exif make';
|
||||||
|
case MetadataField.exifModel:
|
||||||
|
return 'Exif model';
|
||||||
case MetadataField.xmpXmpCreateDate:
|
case MetadataField.xmpXmpCreateDate:
|
||||||
return 'XMP xmp:CreateDate';
|
return 'XMP xmp:CreateDate';
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -356,10 +356,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
);
|
);
|
||||||
if (pattern == null) return;
|
if (pattern == null) return;
|
||||||
|
|
||||||
final entriesToNewName = Map.fromEntries(entries.mapIndexed((index, entry) {
|
final namingFutures = entries.mapIndexed((index, entry) async {
|
||||||
final newName = pattern.apply(entry, index);
|
final newName = await pattern.apply(entry, index);
|
||||||
return MapEntry(entry, '$newName${entry.extension}');
|
return MapEntry(entry, '$newName${entry.extension}');
|
||||||
})).whereNotNullValue();
|
});
|
||||||
|
final entriesToNewName = Map.fromEntries(await Future.wait(namingFutures)).whereNotNullValue();
|
||||||
await rename(context, entriesToNewName: entriesToNewName, persist: true);
|
await rename(context, entriesToNewName: entriesToNewName, persist: true);
|
||||||
|
|
||||||
_browse(context);
|
_browse(context);
|
||||||
|
|
|
@ -55,7 +55,7 @@ mixin EntryEditorMixin {
|
||||||
|
|
||||||
final entry = entries.first;
|
final entry = entries.first;
|
||||||
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
|
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
|
||||||
final fields = await metadataFetchService.getFields(entry, {MetadataSyntheticField.description});
|
final fields = await metadataFetchService.getOverlayMetadata(entry, {MetadataSyntheticField.description});
|
||||||
final initialDescription = fields.description ?? '';
|
final initialDescription = fields.description ?? '';
|
||||||
|
|
||||||
return showDialog<Map<DescriptionField, String?>>(
|
return showDialog<Map<DescriptionField, String?>>(
|
||||||
|
|
|
@ -6,8 +6,10 @@ import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/styles.dart';
|
import 'package:aves/theme/styles.dart';
|
||||||
|
import 'package:aves/view/src/metadata/fields.dart';
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
|
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/popup/expansion_panel.dart';
|
||||||
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -91,10 +93,6 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
child: PopupMenuButton<String>(
|
child: PopupMenuButton<String>(
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
|
||||||
value: DateNamingProcessor.key,
|
|
||||||
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: NameNamingProcessor.key,
|
value: NameNamingProcessor.key,
|
||||||
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
||||||
|
@ -103,6 +101,28 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
value: CounterNamingProcessor.key,
|
value: CounterNamingProcessor.key,
|
||||||
child: MenuRow(text: l10n.renameProcessorCounter, icon: const Icon(AIcons.counter)),
|
child: MenuRow(text: l10n.renameProcessorCounter, icon: const Icon(AIcons.counter)),
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: DateNamingProcessor.key,
|
||||||
|
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: TagsNamingProcessor.key,
|
||||||
|
child: MenuRow(text: l10n.tagPageTitle, icon: const Icon(AIcons.tag)),
|
||||||
|
),
|
||||||
|
PopupMenuExpansionPanel<String>(
|
||||||
|
value: MetadataFieldNamingProcessor.key,
|
||||||
|
icon: AIcons.more,
|
||||||
|
title: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
|
items: [
|
||||||
|
MetadataField.exifMake,
|
||||||
|
MetadataField.exifModel,
|
||||||
|
]
|
||||||
|
.map((field) => PopupMenuItem(
|
||||||
|
value: MetadataFieldNamingProcessor.keyWithField(field),
|
||||||
|
child: MenuRow(text: field.title),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onSelected: (key) async {
|
onSelected: (key) async {
|
||||||
|
@ -159,13 +179,19 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
ValueListenableBuilder<NamingPattern>(
|
ValueListenableBuilder<NamingPattern>(
|
||||||
valueListenable: _namingPatternNotifier,
|
valueListenable: _namingPatternNotifier,
|
||||||
builder: (context, pattern, child) {
|
builder: (context, pattern, child) {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: pattern.apply(entry, index),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final info = snapshot.data;
|
||||||
return Text(
|
return Text(
|
||||||
pattern.apply(entry, index),
|
info ?? '…',
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -74,7 +74,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
if (requestEntry == null) {
|
if (requestEntry == null) {
|
||||||
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
||||||
} else {
|
} else {
|
||||||
_detailLoader = metadataFetchService.getFields(requestEntry, {
|
_detailLoader = metadataFetchService.getOverlayMetadata(requestEntry, {
|
||||||
if (settings.showOverlayShootingDetails) ...{
|
if (settings.showOverlayShootingDetails) ...{
|
||||||
MetadataSyntheticField.aperture,
|
MetadataSyntheticField.aperture,
|
||||||
MetadataSyntheticField.exposureTime,
|
MetadataSyntheticField.exposureTime,
|
||||||
|
|
|
@ -43,6 +43,8 @@ enum MetadataField {
|
||||||
exifGpsTrackRef,
|
exifGpsTrackRef,
|
||||||
exifGpsVersionId,
|
exifGpsVersionId,
|
||||||
exifImageDescription,
|
exifImageDescription,
|
||||||
|
exifMake,
|
||||||
|
exifModel,
|
||||||
exifUserComment,
|
exifUserComment,
|
||||||
mp4GpsCoordinates,
|
mp4GpsCoordinates,
|
||||||
mp4RotationDegrees,
|
mp4RotationDegrees,
|
||||||
|
|
Loading…
Reference in a new issue