From 119968439a5b892da2f6fc77f42fe5153d0b1f9c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 14 Mar 2023 20:31:34 +0100 Subject: [PATCH] refactor --- lib/model/entry_extensions/geo.dart | 0 lib/model/entry_extensions/images.dart | 79 --- lib/model/entry_extensions/info.dart | 158 ----- .../entry_extensions/metadata_edition.dart | 587 ------------------ lib/model/entry_extensions/multipage.dart | 0 5 files changed, 824 deletions(-) delete mode 100644 lib/model/entry_extensions/geo.dart delete mode 100644 lib/model/entry_extensions/images.dart delete mode 100644 lib/model/entry_extensions/info.dart delete mode 100644 lib/model/entry_extensions/metadata_edition.dart delete mode 100644 lib/model/entry_extensions/multipage.dart diff --git a/lib/model/entry_extensions/geo.dart b/lib/model/entry_extensions/geo.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/model/entry_extensions/images.dart b/lib/model/entry_extensions/images.dart deleted file mode 100644 index 95a3e24a4..000000000 --- a/lib/model/entry_extensions/images.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:math'; - -import 'package:aves/image_providers/region_provider.dart'; -import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_image_provider.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_cache.dart'; -import 'package:aves/utils/math_utils.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; - -extension ExtraAvesEntryImages on AvesEntry { - bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent)); - - ThumbnailProvider getThumbnail({double extent = 0}) { - return ThumbnailProvider(_getThumbnailProviderKey(extent)); - } - - ThumbnailProviderKey _getThumbnailProviderKey(double extent) { - EntryCache.markThumbnailExtent(extent); - return ThumbnailProviderKey( - uri: uri, - mimeType: mimeType, - pageId: pageId, - rotationDegrees: rotationDegrees, - isFlipped: isFlipped, - dateModifiedSecs: dateModifiedSecs ?? -1, - extent: extent, - ); - } - - RegionProvider getRegion({int sampleSize = 1, double scale = 1, required Rectangle region}) { - return RegionProvider(RegionProviderKey( - uri: uri, - mimeType: mimeType, - pageId: pageId, - sizeBytes: sizeBytes, - rotationDegrees: rotationDegrees, - isFlipped: isFlipped, - sampleSize: sampleSize, - region: Rectangle( - (region.left * scale).round(), - (region.top * scale).round(), - (region.width * scale).round(), - (region.height * scale).round(), - ), - imageSize: Size((width * scale).toDouble(), (height * scale).toDouble()), - )); - } - - UriImage get uriImage => UriImage( - uri: uri, - mimeType: mimeType, - pageId: pageId, - rotationDegrees: rotationDegrees, - isFlipped: isFlipped, - sizeBytes: sizeBytes, - ); - - bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; - - List get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList(); - - ThumbnailProvider get bestCachedThumbnail { - final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady); - return sizedThumbnailKey != null ? ThumbnailProvider(sizedThumbnailKey) : getThumbnail(); - } - - // magic number used to derive sample size from scale - static const scaleFactor = 2.0; - - static int sampleSizeForScale(double scale) { - var sample = 0; - if (0 < scale && scale < 1) { - sample = highestPowerOf2((1 / scale) / scaleFactor); - } - return max(1, sample); - } -} diff --git a/lib/model/entry_extensions/info.dart b/lib/model/entry_extensions/info.dart deleted file mode 100644 index 4589b5f6f..000000000 --- a/lib/model/entry_extensions/info.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -import 'package:aves/model/entry.dart'; -import 'package:aves/model/video/keys.dart'; -import 'package:aves/model/video/metadata.dart'; -import 'package:aves/ref/mime_types.dart'; -import 'package:aves/services/common/services.dart'; -import 'package:aves/services/metadata/svg_metadata_service.dart'; -import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -extension ExtraAvesEntryInfo on AvesEntry { - // directory names may contain the name of their parent directory (as prefix + '/') - // directory names may contain an index (as suffix in '[]') - static final directoryNamePattern = RegExp(r'^((?.*?)/)?(?.*?)(\[(?\d+)\])?$'); - - Future>> getMetadataDirectories(BuildContext context) async { - final rawMetadata = await (isSvg ? SvgMetadataService.getAllMetadata(this) : metadataFetchService.getAllMetadata(this)); - final directories = rawMetadata.entries.map((dirKV) { - var directoryName = dirKV.key as String; - - String? parent; - int? index; - final match = directoryNamePattern.firstMatch(directoryName); - if (match != null) { - 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; - return MetadataDirectory( - directoryName, - _toSortedTags(rawTags), - parent: parent, - index: index, - ); - }).toList(); - - if (isVideo || (mimeType == MimeTypes.heif && isMultiPage)) { - directories.addAll(await _getStreamDirectories(context)); - } - - final titledDirectories = directories.map((dir) { - var title = dir.name; - if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) { - title = '${dir.parent}/$title'; - } - if (dir.index != null) { - title += ' ${dir.index}'; - } - return MapEntry(title, dir); - }).toList() - ..sort((a, b) => compareAsciiUpperCase(a.key, b.key)); - - return titledDirectories; - } - - Future> _getStreamDirectories(BuildContext context) async { - final directories = []; - final mediaInfo = await VideoMetadataFormatter.getVideoMetadata(this); - - final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo); - if (formattedMediaTags.isNotEmpty) { - // overwrite generic directory found from the platform side - directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags))); - } - - if (mediaInfo.containsKey(Keys.streams)) { - String getTypeText(Map stream) { - final type = stream[Keys.streamType] ?? StreamTypes.unknown; - switch (type) { - case StreamTypes.attachment: - return 'Attachment'; - case StreamTypes.audio: - return 'Audio'; - case StreamTypes.metadata: - return 'Metadata'; - case StreamTypes.subtitle: - case StreamTypes.timedText: - return 'Text'; - case StreamTypes.video: - return stream.containsKey(Keys.fpsDen) ? 'Video' : 'Image'; - case StreamTypes.unknown: - default: - return 'Unknown'; - } - } - - final allStreams = (mediaInfo[Keys.streams] as List).cast(); - final attachmentStreams = allStreams.where((stream) => stream[Keys.streamType] == StreamTypes.attachment).toList(); - final knownStreams = allStreams.whereNot(attachmentStreams.contains); - - // display known streams as separate directories (e.g. video, audio, subs) - if (knownStreams.isNotEmpty) { - final indexDigits = knownStreams.length.toString().length; - - final colors = context.read(); - for (final stream in knownStreams) { - final index = (stream[Keys.index] ?? 0) + 1; - final typeText = getTypeText(stream); - final dirName = [ - 'Stream ${index.toString().padLeft(indexDigits, '0')}', - typeText, - ].join(Constants.separator); - final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream); - if (formattedStreamTags.isNotEmpty) { - final color = colors.fromString(typeText); - directories.add(MetadataDirectory(dirName, _toSortedTags(formattedStreamTags), color: color)); - } - } - } - - // group attachments by format (e.g. TTF fonts) - if (attachmentStreams.isNotEmpty) { - final formatCount = >{}; - for (final stream in attachmentStreams) { - final codec = (stream[Keys.codecName] as String? ?? 'unknown').toUpperCase(); - if (!formatCount.containsKey(codec)) { - formatCount[codec] = []; - } - formatCount[codec]!.add(stream[Keys.filename]); - } - if (formatCount.isNotEmpty) { - final rawTags = formatCount.map((key, value) { - final count = value.length; - // remove duplicate names, so number of displayed names may not match displayed count - final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase); - return MapEntry(key, '$count items: ${names.join(', ')}'); - }); - directories.add(MetadataDirectory('Attachments', _toSortedTags(rawTags))); - } - } - } - return directories; - } - - SplayTreeMap _toSortedTags(Map rawTags) { - final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { - var value = (tagKV.value as String? ?? '').trim(); - if (value.isEmpty) return null; - final tagName = tagKV.key as String; - return MapEntry(tagName, value); - }).whereNotNull())); - return tags; - } -} diff --git a/lib/model/entry_extensions/metadata_edition.dart b/lib/model/entry_extensions/metadata_edition.dart deleted file mode 100644 index 6a0eb8ff3..000000000 --- a/lib/model/entry_extensions/metadata_edition.dart +++ /dev/null @@ -1,587 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:aves/model/entry.dart'; -import 'package:aves/model/metadata/date_modifier.dart'; -import 'package:aves/model/metadata/enums/date_field_source.dart'; -import 'package:aves/model/metadata/enums/enums.dart'; -import 'package:aves/model/metadata/fields.dart'; -import 'package:aves/ref/exif.dart'; -import 'package:aves/ref/iptc.dart'; -import 'package:aves/ref/mime_types.dart'; -import 'package:aves/services/common/services.dart'; -import 'package:aves/services/metadata/xmp.dart'; -import 'package:aves/utils/time_utils.dart'; -import 'package:aves/utils/xmp_utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:intl/intl.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:xml/xml.dart'; - -extension ExtraAvesEntryMetadataEdition on AvesEntry { - Future> editDate(DateModifier userModifier) async { - final dataTypes = {}; - - final appliedModifier = await _applyDateModifierToEntry(userModifier); - if (appliedModifier == null) { - if (!isMissingAtPath && userModifier.action != DateEditAction.copyField) { - await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); - } - return {}; - } - - if (canEditExif && appliedModifier.fields.any((v) => v.type == MetadataType.exif)) { - final newFields = await metadataEditService.editExifDate(this, appliedModifier); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.basic, - 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.copyItem: - 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; - } - return true; - }), - }; - final newFields = await metadataEditService.editMetadata(this, metadata); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.basic, - EntryDataType.catalog, - }); - } - } - - return dataTypes; - } - - static final removalLocation = LatLng(0, 0); - - Future> editLocation(LatLng? latLng) async { - final dataTypes = {}; - final metadata = {}; - - final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - - if (canEditExif) { - // clear every GPS field - final exifFields = Map.fromEntries(MetadataFields.exifGpsFields.map((k) => MapEntry(k, null))); - // add latitude & longitude, if any - if (latLng != null && latLng != removalLocation) { - final latitude = latLng.latitude; - final longitude = latLng.longitude; - exifFields.addAll({ - MetadataField.exifGpsLatitude: latitude.abs(), - MetadataField.exifGpsLatitudeRef: latitude >= 0 ? Exif.latitudeNorth : Exif.latitudeSouth, - MetadataField.exifGpsLongitude: longitude.abs(), - MetadataField.exifGpsLongitudeRef: longitude >= 0 ? Exif.longitudeEast : Exif.longitudeWest, - }); - } - metadata[MetadataType.exif] = Map.fromEntries(exifFields.entries.map((kv) => MapEntry(kv.key.toPlatform!, kv.value))); - - if (canEditXmp && missingDate != null) { - metadata[MetadataType.xmp] = await _editXmp((descriptions) { - editCreateDateXmp(descriptions, missingDate); - return true; - }); - } - } - - if (mimeType == MimeTypes.mp4) { - final mp4Fields = {}; - - String? iso6709String; - if (latLng != null && latLng != removalLocation) { - final latitude = latLng.latitude; - final longitude = latLng.longitude; - const locale = 'en_US'; - final isoLat = '${latitude >= 0 ? '+' : '-'}${NumberFormat('00.0000', locale).format(latitude.abs())}'; - final isoLon = '${longitude >= 0 ? '+' : '-'}${NumberFormat('000.0000', locale).format(longitude.abs())}'; - iso6709String = '$isoLat$isoLon/'; - } - mp4Fields[MetadataField.mp4GpsCoordinates] = iso6709String; - - if (missingDate != null) { - final xmpParts = await _editXmp((descriptions) { - editCreateDateXmp(descriptions, missingDate); - return true; - }); - mp4Fields[MetadataField.mp4Xmp] = xmpParts[xmpCoreKey]; - } - - metadata[MetadataType.mp4] = Map.fromEntries(mp4Fields.entries.map((kv) => MapEntry(kv.key.toPlatform!, kv.value))); - } - - final newFields = await metadataEditService.editMetadata(this, metadata); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.catalog, - EntryDataType.address, - }); - } - return dataTypes; - } - - Future> _changeExifOrientation(Future> Function() apply) async { - final dataTypes = {}; - - await _missingDateCheckAndExifEdit(dataTypes); - - final newFields = await apply(); - // applying fields is only useful for a smoother visual change, - // as proper refreshing and persistence happens at the caller level - await applyNewFields(newFields, persist: false); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.basic, - EntryDataType.aspectRatio, - EntryDataType.catalog, - }); - } - return dataTypes; - } - - Future> _rotateMp4(int rotationDegrees) async { - final dataTypes = {}; - - final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - - final mp4Fields = { - MetadataField.mp4RotationDegrees: rotationDegrees.toString(), - }; - - if (missingDate != null) { - final xmpParts = await _editXmp((descriptions) { - editCreateDateXmp(descriptions, missingDate); - return true; - }); - mp4Fields[MetadataField.mp4Xmp] = xmpParts[xmpCoreKey]; - } - - final metadata = { - MetadataType.mp4: Map.fromEntries(mp4Fields.entries.map((kv) => MapEntry(kv.key.toPlatform!, kv.value))), - }; - - final newFields = await metadataEditService.editMetadata(this, metadata); - // applying fields is only useful for a smoother visual change, - // as proper refreshing and persistence happens at the caller level - await applyNewFields(newFields, persist: false); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.basic, - EntryDataType.aspectRatio, - EntryDataType.catalog, - }); - } - return dataTypes; - } - - Future> rotate({required bool clockwise}) { - if (mimeType == MimeTypes.mp4) { - return _rotateMp4((rotationDegrees + (clockwise ? 90 : -90) + 360) % 360); - } else { - return _changeExifOrientation(() => metadataEditService.rotate(this, clockwise: clockwise)); - } - } - - Future> flip() { - return _changeExifOrientation(() => metadataEditService.flip(this)); - } - - // write title: - // - IPTC / object-name, if IPTC exists - // - XMP / dc:title - // write description: - // - Exif / ImageDescription - // - IPTC / caption-abstract, if IPTC exists - // - XMP / dc:description - Future> editTitleDescription(Map fields) async { - final dataTypes = {}; - final metadata = {}; - - final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - - final editTitle = fields.keys.contains(DescriptionField.title); - final editDescription = fields.keys.contains(DescriptionField.description); - final title = fields[DescriptionField.title]; - final description = fields[DescriptionField.description]; - - if (canEditExif && editDescription) { - metadata[MetadataType.exif] = { - MetadataField.exifImageDescription.toPlatform!: null, - MetadataField.exifUserComment.toPlatform!: null, - }; - } - - if (canEditIptc) { - final iptc = await metadataFetchService.getIptc(this); - if (iptc != null) { - if (editTitle) { - editIptcValues(iptc, IPTC.applicationRecord, IPTC.objectName, {if (title != null) title}); - } - if (editDescription) { - editIptcValues(iptc, IPTC.applicationRecord, IPTC.captionAbstractTag, {if (description != null) description}); - } - metadata[MetadataType.iptc] = iptc; - } - } - - if (canEditXmp) { - metadata[MetadataType.xmp] = await _editXmp((descriptions) { - var modified = false; - if (editTitle) { - modified |= XMP.setAttribute( - descriptions, - XMP.dcTitle, - title, - namespace: Namespaces.dc, - strat: XmpEditStrategy.always, - ); - } - if (editDescription) { - modified |= XMP.setAttribute( - descriptions, - XMP.dcDescription, - description, - namespace: Namespaces.dc, - strat: XmpEditStrategy.always, - ); - } - if (modified && missingDate != null) { - editCreateDateXmp(descriptions, missingDate); - } - return modified; - }); - } - - final newFields = await metadataEditService.editMetadata(this, metadata); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.basic, - EntryDataType.catalog, - }); - } - - return dataTypes; - } - - // write: - // - IPTC / keywords, if IPTC exists - // - XMP / dc:subject - Future> editTags(Set tags) async { - final dataTypes = {}; - final metadata = {}; - - final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - - if (canEditIptc) { - final iptc = await metadataFetchService.getIptc(this); - if (iptc != null) { - editIptcValues(iptc, IPTC.applicationRecord, IPTC.keywordsTag, tags); - metadata[MetadataType.iptc] = iptc; - } - } - - if (canEditXmp) { - metadata[MetadataType.xmp] = await _editXmp((descriptions) { - final modified = editTagsXmp(descriptions, tags); - if (modified && missingDate != null) { - editCreateDateXmp(descriptions, missingDate); - } - return modified; - }); - } - - final newFields = await metadataEditService.editMetadata(this, metadata); - if (newFields.isNotEmpty) { - dataTypes.add(EntryDataType.catalog); - } - return dataTypes; - } - - // write: - // - XMP / xmp:Rating - // update: - // - XMP / MicrosoftPhoto:Rating - // ignore (Windows tags, not part of Exif 2.32 spec): - // - Exif / Rating - // - Exif / RatingPercent - Future> editRating(int? rating) async { - final dataTypes = {}; - final metadata = {}; - - final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - - if (canEditXmp) { - metadata[MetadataType.xmp] = await _editXmp((descriptions) { - final modified = editRatingXmp(descriptions, rating); - if (modified && missingDate != null) { - editCreateDateXmp(descriptions, missingDate); - } - return modified; - }); - } - - final newFields = await metadataEditService.editMetadata(this, metadata); - if (newFields.isNotEmpty) { - dataTypes.add(EntryDataType.catalog); - } - return dataTypes; - } - - // remove: - // - trailer video - // - XMP / Container:Directory - // - XMP / GCamera:MicroVideo* - // - XMP / GCamera:MotionPhoto* - Future> removeTrailerVideo() async { - final dataTypes = {}; - final metadata = {}; - - if (!canEditXmp) return dataTypes; - - final missingDate = await _missingDateCheckAndExifEdit(dataTypes); - - final newFields = await metadataEditService.removeTrailerVideo(this); - - metadata[MetadataType.xmp] = await _editXmp((descriptions) { - final modified = removeContainerXmp(descriptions); - if (modified && missingDate != null) { - editCreateDateXmp(descriptions, missingDate); - } - return modified; - }); - - newFields.addAll(await metadataEditService.editMetadata(this, metadata, autoCorrectTrailerOffset: false)); - if (newFields.isNotEmpty) { - dataTypes.add(EntryDataType.catalog); - } - return dataTypes; - } - - Future> removeMetadata(Set types) async { - final dataTypes = {}; - - final newFields = await metadataEditService.removeTypes(this, types); - if (newFields.isNotEmpty) { - dataTypes.addAll({ - EntryDataType.basic, - EntryDataType.aspectRatio, - EntryDataType.catalog, - EntryDataType.address, - }); - } - return dataTypes; - } - - static void editIptcValues(List> iptc, int record, int tag, Set values) { - iptc.removeWhere((v) => v['record'] == record && v['tag'] == tag); - iptc.add({ - 'record': record, - 'tag': tag, - 'values': values.map((v) => utf8.encode(v)).toList(), - }); - } - - @visibleForTesting - static bool editCreateDateXmp(List descriptions, DateTime? date) { - return XMP.setAttribute( - descriptions, - XMP.xmpCreateDate, - date != null ? XMP.toXmpDate(date) : null, - namespace: Namespaces.xmp, - strat: XmpEditStrategy.always, - ); - } - - @visibleForTesting - static bool editTagsXmp(List descriptions, Set tags) { - return XMP.setStringBag( - descriptions, - XMP.dcSubject, - tags, - namespace: Namespaces.dc, - strat: XmpEditStrategy.always, - ); - } - - @visibleForTesting - static bool editRatingXmp(List descriptions, int? rating) { - bool modified = false; - - modified |= XMP.setAttribute( - descriptions, - XMP.xmpRating, - (rating ?? 0) == 0 ? null : '$rating', - namespace: Namespaces.xmp, - strat: XmpEditStrategy.always, - ); - - modified |= XMP.setAttribute( - descriptions, - XMP.msPhotoRating, - XMP.toMsPhotoRating(rating), - namespace: Namespaces.microsoftPhoto, - strat: XmpEditStrategy.updateIfPresent, - ); - - return modified; - } - - @visibleForTesting - static bool removeContainerXmp(List descriptions) { - bool modified = false; - - modified |= XMP.removeElements( - descriptions, - XMP.containerDirectory, - Namespaces.gContainer, - ); - - modified |= [ - XMP.gCameraMicroVideo, - XMP.gCameraMicroVideoVersion, - XMP.gCameraMicroVideoOffset, - XMP.gCameraMicroVideoPresentationTimestampUs, - XMP.gCameraMotionPhoto, - XMP.gCameraMotionPhotoVersion, - XMP.gCameraMotionPhotoPresentationTimestampUs, - ].fold(modified, (prev, name) { - return prev |= XMP.removeElements( - descriptions, - name, - Namespaces.gCamera, - ); - }); - - return modified; - } - - // convenience methods - - // 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 _missingDateCheckAndExifEdit(Set dataTypes) async { - if (path == null) return null; - - // make sure entry is catalogued before we check whether is has a metadata date - if (!isCatalogued) { - await catalog(background: false, force: false, persist: true); - } - final dateMillis = catalogMetadata?.dateMillis; - if (dateMillis != null && dateMillis > 0) return null; - - late DateTime date; - try { - 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 _applyDateModifierToEntry(DateModifier modifier) async { - Set mainMetadataDate() => {canEditExif ? MetadataField.exifDateOriginal : MetadataField.xmpXmpCreateDate}; - - switch (modifier.action) { - case DateEditAction.copyField: - DateTime? date; - final source = modifier.copyFieldSource; - if (source != null) { - switch (source) { - case DateFieldSource.fileModifiedDate: - try { - if (path != null) { - final file = File(path!); - if (await file.exists()) { - date = await file.lastModified(); - } - } - } 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: - case DateEditAction.copyItem: - return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!); - case DateEditAction.shift: - case DateEditAction.remove: - return modifier; - } - } - - static const xmpCoreKey = 'xmp'; - static const xmpExtendedKey = 'extendedXmp'; - - Future> _editXmp(bool Function(List descriptions) apply) async { - final xmp = await metadataFetchService.getXmp(this); - if (xmp == null) { - throw Exception('failed to get XMP'); - } - - final xmpString = xmp.xmpString; - final extendedXmpString = xmp.extendedXmpString; - - final editedXmpString = await XMP.edit( - xmpString, - () => PackageInfo.fromPlatform().then((v) => 'Aves v${v.version}'), - apply, - ); - - final editedXmp = AvesXmp(xmpString: editedXmpString, extendedXmpString: extendedXmpString); - return { - xmpCoreKey: editedXmp.xmpString, - xmpExtendedKey: editedXmp.extendedXmpString, - }; - } -} - -enum DescriptionField { title, description } diff --git a/lib/model/entry_extensions/multipage.dart b/lib/model/entry_extensions/multipage.dart deleted file mode 100644 index e69de29bb..000000000