info: edit date for GIF
This commit is contained in:
parent
a3e18d3b3a
commit
b94097bda7
9 changed files with 180 additions and 99 deletions
|
@ -277,7 +277,7 @@
|
||||||
|
|
||||||
"editEntryDateDialogTitle": "Date & Time",
|
"editEntryDateDialogTitle": "Date & Time",
|
||||||
"editEntryDateDialogSetCustom": "Set custom date",
|
"editEntryDateDialogSetCustom": "Set custom date",
|
||||||
"editEntryDateDialogCopyField": "Set from other date",
|
"editEntryDateDialogCopyField": "Copy from other date",
|
||||||
"editEntryDateDialogExtractFromTitle": "Extract from title",
|
"editEntryDateDialogExtractFromTitle": "Extract from title",
|
||||||
"editEntryDateDialogShift": "Shift",
|
"editEntryDateDialogShift": "Shift",
|
||||||
"editEntryDateDialogSourceFileModifiedDate": "File modified date",
|
"editEntryDateDialogSourceFileModifiedDate": "File modified date",
|
||||||
|
|
|
@ -234,7 +234,7 @@ class AvesEntry {
|
||||||
|
|
||||||
bool get canEdit => path != null;
|
bool get canEdit => path != null;
|
||||||
|
|
||||||
bool get canEditDate => canEdit && canEditExif;
|
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||||
|
|
||||||
bool get canEditRating => canEdit && canEditXmp;
|
bool get canEditRating => canEdit && canEditXmp;
|
||||||
|
|
||||||
|
|
|
@ -14,66 +14,69 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
Future<Set<EntryDataType>> editDate(DateModifier modifier) async {
|
Future<Set<EntryDataType>> editDate(DateModifier userModifier) async {
|
||||||
switch (modifier.action) {
|
final Set<EntryDataType> dataTypes = {};
|
||||||
case DateEditAction.copyField:
|
|
||||||
DateTime? date;
|
final appliedModifier = await _applyDateModifierToEntry(userModifier);
|
||||||
final source = modifier.copyFieldSource;
|
if (appliedModifier == null) {
|
||||||
if (source != null) {
|
await reportService.recordError('failed to get date for modifier=$userModifier, uri=$uri', null);
|
||||||
switch (source) {
|
|
||||||
case DateFieldSource.fileModifiedDate:
|
|
||||||
try {
|
|
||||||
date = path != null ? await File(path!).lastModified() : null;
|
|
||||||
} on FileSystemException catch (_) {}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (date != null) {
|
|
||||||
modifier = DateModifier.setCustom(modifier.fields, date);
|
|
||||||
} else {
|
|
||||||
await reportService.recordError('failed to get date for modifier=$modifier, uri=$uri', null);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case DateEditAction.extractFromTitle:
|
if (canEditExif && appliedModifier.fields.any((v) => v.type == MetadataType.exif)) {
|
||||||
final date = parseUnknownDateFormat(bestTitle);
|
final newFields = await metadataEditService.editExifDate(this, appliedModifier);
|
||||||
if (date != null) {
|
if (newFields.isNotEmpty) {
|
||||||
modifier = DateModifier.setCustom(modifier.fields, date);
|
dataTypes.addAll({
|
||||||
} else {
|
|
||||||
await reportService.recordError('failed to get date for modifier=$modifier, uri=$uri', null);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DateEditAction.setCustom:
|
|
||||||
case DateEditAction.shift:
|
|
||||||
case DateEditAction.remove:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
final newFields = await metadataEditService.editDate(this, modifier);
|
|
||||||
return newFields.isEmpty
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
EntryDataType.basic,
|
EntryDataType.basic,
|
||||||
EntryDataType.catalog,
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditXmp && appliedModifier.fields.any((v) => v.type == MetadataType.xmp)) {
|
||||||
|
final metadata = {
|
||||||
|
MetadataType.xmp: await _editXmp((descriptions) {
|
||||||
|
switch (appliedModifier.action) {
|
||||||
|
case DateEditAction.setCustom:
|
||||||
|
case DateEditAction.copyField:
|
||||||
|
case DateEditAction.extractFromTitle:
|
||||||
|
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
||||||
|
break;
|
||||||
|
case DateEditAction.shift:
|
||||||
|
final xmpDate = XMP.getString(descriptions, XMP.xmpCreateDate, namespace: Namespaces.xmp);
|
||||||
|
if (xmpDate != null) {
|
||||||
|
final date = DateTime.tryParse(xmpDate);
|
||||||
|
if (date != null) {
|
||||||
|
// TODO TLAD [date] DateTime.tryParse converts to UTC time, losing the time zone offset
|
||||||
|
final shiftedDate = date.add(Duration(minutes: appliedModifier.shiftMinutes!));
|
||||||
|
editCreateDateXmp(descriptions, shiftedDate);
|
||||||
|
} else {
|
||||||
|
reportService.recordError('failed to parse XMP date=$xmpDate', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DateEditAction.remove:
|
||||||
|
editCreateDateXmp(descriptions, null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Set<EntryDataType>> _changeOrientation(Future<Map<String, dynamic>> Function() apply) async {
|
Future<Set<EntryDataType>> _changeOrientation(Future<Map<String, dynamic>> Function() apply) async {
|
||||||
final Set<EntryDataType> dataTypes = {};
|
final Set<EntryDataType> dataTypes = {};
|
||||||
|
|
||||||
// when editing a file that has no metadata date,
|
await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
// we will set one, using the file modified date, if any
|
|
||||||
var missingDate = await _getMissingMetadataDate();
|
|
||||||
if (missingDate != null && canEditExif) {
|
|
||||||
dataTypes.addAll(await editDate(DateModifier.setCustom(
|
|
||||||
const {MetadataField.exifDateOriginal},
|
|
||||||
missingDate,
|
|
||||||
)));
|
|
||||||
missingDate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final newFields = await apply();
|
final newFields = await apply();
|
||||||
// applying fields is only useful for a smoother visual change,
|
// applying fields is only useful for a smoother visual change,
|
||||||
|
@ -103,16 +106,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
final Set<EntryDataType> dataTypes = {};
|
final Set<EntryDataType> dataTypes = {};
|
||||||
final Map<MetadataType, dynamic> metadata = {};
|
final Map<MetadataType, dynamic> metadata = {};
|
||||||
|
|
||||||
// when editing a file that has no metadata date,
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
// we will set one, using the file modified date, if any
|
|
||||||
var missingDate = await _getMissingMetadataDate();
|
|
||||||
if (missingDate != null && canEditExif) {
|
|
||||||
dataTypes.addAll(await editDate(DateModifier.setCustom(
|
|
||||||
const {MetadataField.exifDateOriginal},
|
|
||||||
missingDate,
|
|
||||||
)));
|
|
||||||
missingDate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canEditIptc) {
|
if (canEditIptc) {
|
||||||
final iptc = await metadataFetchService.getIptc(this);
|
final iptc = await metadataFetchService.getIptc(this);
|
||||||
|
@ -125,8 +119,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
if (canEditXmp) {
|
if (canEditXmp) {
|
||||||
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
if (missingDate != null) {
|
if (missingDate != null) {
|
||||||
editDateXmp(descriptions, missingDate!);
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
missingDate = null;
|
|
||||||
}
|
}
|
||||||
editTagsXmp(descriptions, tags);
|
editTagsXmp(descriptions, tags);
|
||||||
});
|
});
|
||||||
|
@ -150,22 +143,12 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
final Set<EntryDataType> dataTypes = {};
|
final Set<EntryDataType> dataTypes = {};
|
||||||
final Map<MetadataType, dynamic> metadata = {};
|
final Map<MetadataType, dynamic> metadata = {};
|
||||||
|
|
||||||
// when editing a file that has no metadata date,
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
// we will set one, using the file modified date, if any
|
|
||||||
var missingDate = await _getMissingMetadataDate();
|
|
||||||
if (missingDate != null && canEditExif) {
|
|
||||||
dataTypes.addAll(await editDate(DateModifier.setCustom(
|
|
||||||
const {MetadataField.exifDateOriginal},
|
|
||||||
missingDate,
|
|
||||||
)));
|
|
||||||
missingDate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canEditXmp) {
|
if (canEditXmp) {
|
||||||
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
if (missingDate != null) {
|
if (missingDate != null) {
|
||||||
editDateXmp(descriptions, missingDate!);
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
missingDate = null;
|
|
||||||
}
|
}
|
||||||
editRatingXmp(descriptions, rating);
|
editRatingXmp(descriptions, rating);
|
||||||
});
|
});
|
||||||
|
@ -190,11 +173,11 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
static void editDateXmp(List<XmlNode> descriptions, DateTime date) {
|
static void editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
|
||||||
XMP.setAttribute(
|
XMP.setAttribute(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.xmpCreateDate,
|
XMP.xmpCreateDate,
|
||||||
XMP.toXmpDate(date),
|
date != null ? XMP.toXmpDate(date) : null,
|
||||||
namespace: Namespaces.xmp,
|
namespace: Namespaces.xmp,
|
||||||
strat: XmpEditStrategy.always,
|
strat: XmpEditStrategy.always,
|
||||||
);
|
);
|
||||||
|
@ -241,19 +224,69 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
|
|
||||||
// convenience
|
// convenience
|
||||||
|
|
||||||
Future<DateTime?> _getMissingMetadataDate() async {
|
// This method checks whether the item already has a metadata date,
|
||||||
|
// and adds a date (the file modified date) via Exif if possible.
|
||||||
|
// It returns a date if the caller needs to add it via other metadata types (e.g. XMP).
|
||||||
|
Future<DateTime?> _missingDateCheckAndExifEdit(Set<EntryDataType> dataTypes) async {
|
||||||
if (path == null) return null;
|
if (path == null) return null;
|
||||||
|
|
||||||
// make sure entry is catalogued before we check whether is has a metadata date
|
// make sure entry is catalogued before we check whether is has a metadata date
|
||||||
if (!isCatalogued) {
|
if (!isCatalogued) {
|
||||||
await catalog(background: false, force: false, persist: true);
|
await catalog(background: false, force: false, persist: true);
|
||||||
}
|
}
|
||||||
final metadataDate = catalogMetadata?.dateMillis;
|
final dateMillis = catalogMetadata?.dateMillis;
|
||||||
if (metadataDate != null && metadataDate > 0) return null;
|
if (dateMillis != null && dateMillis > 0) return null;
|
||||||
|
|
||||||
|
late DateTime date;
|
||||||
try {
|
try {
|
||||||
return await File(path!).lastModified();
|
date = await File(path!).lastModified();
|
||||||
|
} on FileSystemException catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditExif) {
|
||||||
|
final newFields = await metadataEditService.editExifDate(this, DateModifier.setCustom(const {MetadataField.exifDateOriginal}, date));
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DateModifier?> _applyDateModifierToEntry(DateModifier modifier) async {
|
||||||
|
Set<MetadataField> mainMetadataDate() => {canEditExif ? MetadataField.exifDateOriginal : MetadataField.xmpCreateDate};
|
||||||
|
|
||||||
|
switch (modifier.action) {
|
||||||
|
case DateEditAction.copyField:
|
||||||
|
DateTime? date;
|
||||||
|
final source = modifier.copyFieldSource;
|
||||||
|
if (source != null) {
|
||||||
|
switch (source) {
|
||||||
|
case DateFieldSource.fileModifiedDate:
|
||||||
|
try {
|
||||||
|
date = path != null ? await File(path!).lastModified() : null;
|
||||||
} on FileSystemException catch (_) {}
|
} on FileSystemException catch (_) {}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
||||||
|
case DateEditAction.extractFromTitle:
|
||||||
|
final date = parseUnknownDateFormat(bestTitle);
|
||||||
|
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
||||||
|
case DateEditAction.setCustom:
|
||||||
|
return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!);
|
||||||
|
case DateEditAction.shift:
|
||||||
|
case DateEditAction.remove:
|
||||||
|
return modifier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String?>> _editXmp(void Function(List<XmlNode> descriptions) apply) async {
|
Future<Map<String, String?>> _editXmp(void Function(List<XmlNode> descriptions) apply) async {
|
||||||
|
|
|
@ -9,6 +9,7 @@ class DateModifier {
|
||||||
MetadataField.exifDateOriginal,
|
MetadataField.exifDateOriginal,
|
||||||
MetadataField.exifDateDigitized,
|
MetadataField.exifDateDigitized,
|
||||||
MetadataField.exifGpsDate,
|
MetadataField.exifGpsDate,
|
||||||
|
MetadataField.xmpCreateDate,
|
||||||
];
|
];
|
||||||
|
|
||||||
final DateEditAction action;
|
final DateEditAction action;
|
||||||
|
|
|
@ -3,6 +3,7 @@ enum MetadataField {
|
||||||
exifDateOriginal,
|
exifDateOriginal,
|
||||||
exifDateDigitized,
|
exifDateDigitized,
|
||||||
exifGpsDate,
|
exifGpsDate,
|
||||||
|
xmpCreateDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DateEditAction {
|
enum DateEditAction {
|
||||||
|
@ -91,7 +92,19 @@ extension ExtraMetadataType on MetadataType {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExtraMetadataField on MetadataField {
|
extension ExtraMetadataField on MetadataField {
|
||||||
String toExifInterfaceTag() {
|
MetadataType get type {
|
||||||
|
switch (this) {
|
||||||
|
case MetadataField.exifDate:
|
||||||
|
case MetadataField.exifDateOriginal:
|
||||||
|
case MetadataField.exifDateDigitized:
|
||||||
|
case MetadataField.exifGpsDate:
|
||||||
|
return MetadataType.exif;
|
||||||
|
case MetadataField.xmpCreateDate:
|
||||||
|
return MetadataType.xmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? toExifInterfaceTag() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case MetadataField.exifDate:
|
case MetadataField.exifDate:
|
||||||
return 'DateTime';
|
return 'DateTime';
|
||||||
|
@ -101,6 +114,8 @@ extension ExtraMetadataField on MetadataField {
|
||||||
return 'DateTimeDigitized';
|
return 'DateTimeDigitized';
|
||||||
case MetadataField.exifGpsDate:
|
case MetadataField.exifGpsDate:
|
||||||
return 'GPSDateStamp';
|
return 'GPSDateStamp';
|
||||||
|
case MetadataField.xmpCreateDate:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/metadata/enums.dart';
|
import 'package:aves/model/metadata/enums.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
abstract class MetadataEditService {
|
abstract class MetadataEditService {
|
||||||
|
@ -11,7 +12,7 @@ abstract class MetadataEditService {
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flip(AvesEntry entry);
|
Future<Map<String, dynamic>> flip(AvesEntry entry);
|
||||||
|
|
||||||
Future<Map<String, dynamic>> editDate(AvesEntry entry, DateModifier modifier);
|
Future<Map<String, dynamic>> editExifDate(AvesEntry entry, DateModifier modifier);
|
||||||
|
|
||||||
Future<Map<String, dynamic>> editMetadata(AvesEntry entry, Map<MetadataType, dynamic> modifier);
|
Future<Map<String, dynamic>> editMetadata(AvesEntry entry, Map<MetadataType, dynamic> modifier);
|
||||||
|
|
||||||
|
@ -70,13 +71,13 @@ class PlatformMetadataEditService implements MetadataEditService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> editDate(AvesEntry entry, DateModifier modifier) async {
|
Future<Map<String, dynamic>> editExifDate(AvesEntry entry, DateModifier modifier) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('editDate', <String, dynamic>{
|
final result = await platform.invokeMethod('editDate', <String, dynamic>{
|
||||||
'entry': _toPlatformEntryMap(entry),
|
'entry': _toPlatformEntryMap(entry),
|
||||||
'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch,
|
'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch,
|
||||||
'shiftMinutes': modifier.shiftMinutes,
|
'shiftMinutes': modifier.shiftMinutes,
|
||||||
'fields': modifier.fields.map((v) => v.toExifInterfaceTag()).toList(),
|
'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.toExifInterfaceTag()).whereNotNull().toList(),
|
||||||
});
|
});
|
||||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
|
|
|
@ -79,6 +79,21 @@ class XMP {
|
||||||
|
|
||||||
static String toXmpDate(DateTime date) => '${DateFormat('yyyy-MM-ddTHH:mm:ss').format(date)}${_xmpTimeZoneDesignator(date)}';
|
static String toXmpDate(DateTime date) => '${DateFormat('yyyy-MM-ddTHH:mm:ss').format(date)}${_xmpTimeZoneDesignator(date)}';
|
||||||
|
|
||||||
|
static String? getString(
|
||||||
|
List<XmlNode> nodes,
|
||||||
|
String name, {
|
||||||
|
required String namespace,
|
||||||
|
}) {
|
||||||
|
for (final node in nodes) {
|
||||||
|
final attribute = node.getAttribute(name, namespace: namespace);
|
||||||
|
if (attribute != null) return attribute;
|
||||||
|
|
||||||
|
final element = node.getElement(name, namespace: namespace);
|
||||||
|
if (element != null) return element.innerText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static void _addNamespaces(XmlNode node, Map<String, String> namespaces) => namespaces.forEach((uri, prefix) => node.setAttribute('$xmlnsPrefix:$prefix', uri));
|
static void _addNamespaces(XmlNode node, Map<String, String> namespaces) => namespaces.forEach((uri, prefix) => node.setAttribute('$xmlnsPrefix:$prefix', uri));
|
||||||
|
|
||||||
// remove elements and attributes
|
// remove elements and attributes
|
||||||
|
|
|
@ -30,11 +30,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
late ValueNotifier<int> _shiftHour, _shiftMinute;
|
late ValueNotifier<int> _shiftHour, _shiftMinute;
|
||||||
late ValueNotifier<String> _shiftSign;
|
late ValueNotifier<String> _shiftSign;
|
||||||
bool _showOptions = false;
|
bool _showOptions = false;
|
||||||
final Set<MetadataField> _fields = {
|
final Set<MetadataField> _fields = {...DateModifier.writableDateFields};
|
||||||
MetadataField.exifDate,
|
|
||||||
MetadataField.exifDateDigitized,
|
|
||||||
MetadataField.exifDateOriginal,
|
|
||||||
};
|
|
||||||
|
|
||||||
// use a different shade to avoid having the same background
|
// use a different shade to avoid having the same background
|
||||||
// on the dialog (using the theme `dialogBackgroundColor`)
|
// on the dialog (using the theme `dialogBackgroundColor`)
|
||||||
|
@ -99,10 +95,10 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
if (_action == DateEditAction.setCustom) _buildSetCustomContent(context),
|
if (_action == DateEditAction.setCustom) _buildSetCustomContent(context),
|
||||||
if (_action == DateEditAction.copyField) _buildCopyFieldContent(context),
|
if (_action == DateEditAction.copyField) _buildCopyFieldContent(context),
|
||||||
if (_action == DateEditAction.shift) _buildShiftContent(context),
|
if (_action == DateEditAction.shift) _buildShiftContent(context),
|
||||||
|
(_action == DateEditAction.shift || _action == DateEditAction.remove)? _buildDestinationFields(context): const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildDestinationFields(context),
|
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -135,7 +131,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, top: 4, right: 12),
|
padding: const EdgeInsets.only(left: 16, right: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(formatDateTime(_setDateTime, locale, use24hour))),
|
Expanded(child: Text(formatDateTime(_setDateTime, locale, use24hour))),
|
||||||
|
@ -151,7 +147,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
|
|
||||||
Widget _buildCopyFieldContent(BuildContext context) {
|
Widget _buildCopyFieldContent(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.only(left: 16, top: 0, right: 16),
|
||||||
child: DropdownButton<DateFieldSource>(
|
child: DropdownButton<DateFieldSource>(
|
||||||
items: DateFieldSource.values
|
items: DateFieldSource.values
|
||||||
.map((v) => DropdownMenuItem<DateFieldSource>(
|
.map((v) => DropdownMenuItem<DateFieldSource>(
|
||||||
|
@ -308,6 +304,8 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
return 'Exif digitized date';
|
return 'Exif digitized date';
|
||||||
case MetadataField.exifGpsDate:
|
case MetadataField.exifGpsDate:
|
||||||
return 'Exif GPS date';
|
return 'Exif GPS date';
|
||||||
|
case MetadataField.xmpCreateDate:
|
||||||
|
return 'XMP xmp:CreateDate';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,13 +335,16 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
DateModifier _getModifier() {
|
DateModifier _getModifier() {
|
||||||
|
// fields to modify are only set for the `shift` and `remove` actions,
|
||||||
|
// as the effective fields for the other actions will depend on
|
||||||
|
// whether each item supports Exif edition
|
||||||
switch (_action) {
|
switch (_action) {
|
||||||
case DateEditAction.setCustom:
|
case DateEditAction.setCustom:
|
||||||
return DateModifier.setCustom(_fields, _setDateTime);
|
return DateModifier.setCustom(const {}, _setDateTime);
|
||||||
case DateEditAction.copyField:
|
case DateEditAction.copyField:
|
||||||
return DateModifier.copyField(_fields, _copyFieldSource);
|
return DateModifier.copyField(const {}, _copyFieldSource);
|
||||||
case DateEditAction.extractFromTitle:
|
case DateEditAction.extractFromTitle:
|
||||||
return DateModifier.extractFromTitle(_fields);
|
return DateModifier.extractFromTitle(const {});
|
||||||
case DateEditAction.shift:
|
case DateEditAction.shift:
|
||||||
final shiftTotalMinutes = (_shiftHour.value * 60 + _shiftMinute.value) * (_shiftSign.value == '+' ? 1 : -1);
|
final shiftTotalMinutes = (_shiftHour.value * 60 + _shiftMinute.value) * (_shiftSign.value == '+' ? 1 : -1);
|
||||||
return DateModifier.shift(_fields, shiftTotalMinutes);
|
return DateModifier.shift(_fields, shiftTotalMinutes);
|
||||||
|
|
|
@ -13,6 +13,15 @@ void main() {
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
List<XmlNode> _getDescriptions(String xmpString) {
|
||||||
|
final xmpDoc = XmlDocument.parse(xmpString);
|
||||||
|
final root = xmpDoc.rootElement;
|
||||||
|
final rdf = root.getElement(XMP.rdfRoot, namespace: Namespaces.rdf);
|
||||||
|
return rdf!.children.where((node) {
|
||||||
|
return node is XmlElement && node.name.local == XMP.rdfDescription && node.name.namespaceUri == Namespaces.rdf;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
const inMultiDescriptionRatings = '''
|
const inMultiDescriptionRatings = '''
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
||||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
@ -79,6 +88,12 @@ void main() {
|
||||||
</x:xmpmeta>
|
</x:xmpmeta>
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
test('Get string', () async {
|
||||||
|
expect(XMP.getString(_getDescriptions(inRatingAttribute), XMP.xmpRating, namespace: Namespaces.xmp), '5');
|
||||||
|
expect(XMP.getString(_getDescriptions(inRatingElement), XMP.xmpRating, namespace: Namespaces.xmp), '5');
|
||||||
|
expect(XMP.getString(_getDescriptions(inSubjects), XMP.xmpRating, namespace: Namespaces.xmp), null);
|
||||||
|
});
|
||||||
|
|
||||||
test('Set tags without existing XMP', () async {
|
test('Set tags without existing XMP', () async {
|
||||||
final modifyDate = DateTime.now();
|
final modifyDate = DateTime.now();
|
||||||
final xmpDate = XMP.toXmpDate(modifyDate);
|
final xmpDate = XMP.toXmpDate(modifyDate);
|
||||||
|
|
Loading…
Reference in a new issue