From ef6eb53eb2fcffb6b2854dc0e36574a41e83b452 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 23 Feb 2023 23:20:32 +0100 Subject: [PATCH] #526 converter: write metadata --- CHANGELOG.md | 3 +- .../channel/streams/ImageOpStreamHandler.kt | 50 +++++-- .../deckers/thibault/aves/model/AvesEntry.kt | 8 +- .../aves/model/provider/ImageProvider.kt | 126 +++++++++++++++--- lib/l10n/app_en.arb | 1 + lib/model/settings/defaults.dart | 8 +- lib/model/settings/settings.dart | 17 +++ lib/services/media/media_edit_service.dart | 5 +- lib/widgets/common/fx/transitions.dart | 14 ++ lib/widgets/dialogs/convert_entry_dialog.dart | 51 ++++++- .../entry_editors/edit_date_dialog.dart | 12 +- .../entry_editors/edit_location_dialog.dart | 12 +- lib/widgets/dialogs/tile_view_dialog.dart | 10 +- lib/widgets/stats/date/histogram.dart | 10 +- untranslated.json | 42 +++++- 15 files changed, 285 insertions(+), 84 deletions(-) create mode 100644 lib/widgets/common/fx/transitions.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc52c163..781422e5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file. ### Added -- Collection: bulk converting +- Export: bulk converting +- Export: write metadata when converting - Places: page & navigation entry ### Changed diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index 6c03cec86..947d7d738 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -132,8 +132,9 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments val lengthUnit = arguments["lengthUnit"] as String? val width = (arguments["width"] as Number?)?.toInt() val height = (arguments["height"] as Number?)?.toInt() + val writeMetadata = arguments["writeMetadata"] as Boolean? val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) - if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || nameConflictStrategy == null) { + if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || writeMetadata == null || nameConflictStrategy == null) { error("convert-args", "missing arguments", null) return } @@ -148,10 +149,21 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments destinationDir = ensureTrailingSeparator(destinationDir) val entries = entryMapList.map(::AvesEntry) - provider.convertMultiple(activity, mimeType, destinationDir, entries, lengthUnit, width, height, nameConflictStrategy, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = success(fields) - override fun onFailure(throwable: Throwable) = error("convert-failure", "failed to convert entries", throwable) - }) + provider.convertMultiple( + activity = activity, + imageExportMimeType = mimeType, + targetDir = destinationDir, + entries = entries, + lengthUnit = lengthUnit, + width = width, + height = height, + writeMetadata = writeMetadata, + nameConflictStrategy = nameConflictStrategy, + callback = object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("convert-failure", "failed to convert entries", throwable) + }, + ) endOfStream() } @@ -183,10 +195,17 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments // always use Media Store (as we move from or to it) val provider = MediaStoreImageProvider() - provider.moveMultiple(activity, copy, nameConflictStrategy, entriesByTargetDir, ::isCancelledOp, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = success(fields) - override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) - }) + provider.moveMultiple( + activity = activity, + copy = copy, + nameConflictStrategy = nameConflictStrategy, + entriesByTargetDir = entriesByTargetDir, + isCancelledOp = ::isCancelledOp, + callback = object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) + }, + ) endOfStream() } @@ -218,10 +237,15 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments } val entryMap = mapOf(*entryList.map { Pair(it.key, it.value) }.toTypedArray()) - provider.renameMultiple(activity, entryMap, ::isCancelledOp, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = success(fields) - override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message) - }) + provider.renameMultiple( + activity = activity, + entriesToNewName = entryMap, + isCancelledOp = ::isCancelledOp, + callback = object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message) + }, + ) } endOfStream() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt index 8d41846ab..71f126f0d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt @@ -15,15 +15,9 @@ class AvesEntry(map: FieldMap) { val trashed = map["trashed"] as Boolean val trashPath = map["trashPath"] as String? - private val isRotated: Boolean + val isRotated: Boolean get() = rotationDegrees % 180 == 90 - val displayWidth: Int - get() = if (isRotated) height else width - - val displayHeight: Int - get() = if (isRotated) width else height - companion object { // convenience method private fun toLong(o: Any?): Long? = when (o) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index 7086f8d17..f00b52ec8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -38,9 +38,13 @@ import deckers.thibault.aves.utils.FileUtils.transferTo import deckers.thibault.aves.utils.MimeTypes.canEditExif import deckers.thibault.aves.utils.MimeTypes.canEditIptc import deckers.thibault.aves.utils.MimeTypes.canEditXmp +import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface +import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata import deckers.thibault.aves.utils.MimeTypes.extensionFor import deckers.thibault.aves.utils.MimeTypes.isVideo +import pixy.meta.meta.Metadata +import pixy.meta.meta.MetadataType import java.io.* import java.nio.channels.Channels import java.util.* @@ -177,6 +181,7 @@ abstract class ImageProvider { lengthUnit: String, width: Int, height: Int, + writeMetadata: Boolean, nameConflictStrategy: NameConflictStrategy, callback: ImageOpCallback, ) { @@ -204,7 +209,7 @@ abstract class ImageProvider { val sourceMimeType = entry.mimeType val exportMimeType = if (isVideo(sourceMimeType)) sourceMimeType else imageExportMimeType try { - val newFields = exportSingle( + val newFields = convertSingle( activity = activity, sourceEntry = entry, targetDir = targetDir, @@ -212,19 +217,20 @@ abstract class ImageProvider { lengthUnit = lengthUnit, width = width, height = height, + writeMetadata = writeMetadata, nameConflictStrategy = nameConflictStrategy, exportMimeType = exportMimeType, ) result["newFields"] = newFields result["success"] = true } catch (e: Exception) { - Log.w(LOG_TAG, "failed to export to targetDir=$targetDir entry with sourcePath=$sourcePath pageId=$pageId", e) + Log.w(LOG_TAG, "failed to convert to targetDir=$targetDir entry with sourcePath=$sourcePath pageId=$pageId", e) } callback.onSuccess(result) } } - private suspend fun exportSingle( + private suspend fun convertSingle( activity: Activity, sourceEntry: AvesEntry, targetDir: String, @@ -232,6 +238,7 @@ abstract class ImageProvider { lengthUnit: String, width: Int, height: Int, + writeMetadata: Boolean, nameConflictStrategy: NameConflictStrategy, exportMimeType: String, ): FieldMap { @@ -269,17 +276,11 @@ abstract class ImageProvider { sourceDocFile.copyTo(output) } } else { - val targetWidthPx: Int - val targetHeightPx: Int - when (lengthUnit) { - LENGTH_UNIT_PERCENT -> { - targetWidthPx = sourceEntry.displayWidth * width / 100 - targetHeightPx = sourceEntry.displayHeight * height / 100 - } - else -> { - targetWidthPx = width - targetHeightPx = height - } + var targetWidthPx: Int = if (sourceEntry.isRotated) height else width + var targetHeightPx: Int = if (sourceEntry.isRotated) width else height + if (lengthUnit == LENGTH_UNIT_PERCENT) { + targetWidthPx = sourceEntry.width * targetWidthPx / 100 + targetHeightPx = sourceEntry.height * targetHeightPx / 100 } val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) { @@ -344,13 +345,108 @@ abstract class ImageProvider { targetNameWithoutExtension = targetNameWithoutExtension, write = write, ) - return scanNewPath(activity, targetPath, exportMimeType) + + val newFields = scanNewPath(activity, targetPath, exportMimeType) + val targetUri = Uri.parse(newFields["uri"] as String) + if (writeMetadata) { + copyMetadata( + context = activity, + sourceMimeType = sourceMimeType, + sourceUri = sourceUri, + targetMimeType = targetMimeType, + targetUri = targetUri, + targetPath = targetPath, + ) + } + + return newFields } finally { // clearing Glide target should happen after effectively writing the bitmap Glide.with(activity).clear(target) } } + private fun copyMetadata( + context: Context, + sourceMimeType: String, + sourceUri: Uri, + targetMimeType: String, + targetUri: Uri, + targetPath: String, + ) { + val editableFile = File.createTempFile("aves", null).apply { + deleteOnExit() + // copy original file to a temporary file for editing + val inputStream = StorageUtils.openInputStream(context, targetUri) + transferFrom(inputStream, File(targetPath).length()) + } + + // copy IPTC / XMP via PixyMeta + + var pixyIptc: pixy.meta.meta.iptc.IPTC? = null + var pixyXmp: pixy.meta.meta.xmp.XMP? = null + if (canReadWithPixyMeta(sourceMimeType)) { + StorageUtils.openInputStream(context, sourceUri)?.use { input -> + val metadata = Metadata.readMetadata(input) + if (canEditIptc(targetMimeType)) { + pixyIptc = metadata[MetadataType.IPTC] as pixy.meta.meta.iptc.IPTC? + } + if (canEditXmp(targetMimeType)) { + pixyXmp = metadata[MetadataType.XMP] as pixy.meta.meta.xmp.XMP? + } + } + } + if (pixyIptc != null || pixyXmp != null) { + editableFile.outputStream().use { output -> + if (pixyIptc != null) { + // reopen input to read from start + StorageUtils.openInputStream(context, targetUri)?.use { input -> + val iptcs = pixyIptc!!.dataSets.flatMap { it.value } + Metadata.insertIPTC(input, output, iptcs) + } + } + if (pixyXmp != null) { + // reopen input to read from start + StorageUtils.openInputStream(context, targetUri)?.use { input -> + val xmpString = pixyXmp!!.xmpDocString() + val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null + PixyMetaHelper.setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null) + } + } + } + } + + // copy Exif via ExifInterface + + val exif = HashMap() + val skippedTags = listOf( + ExifInterface.TAG_IMAGE_LENGTH, + ExifInterface.TAG_IMAGE_WIDTH, + ExifInterface.TAG_ORIENTATION, + ) + if (canReadWithExifInterface(sourceMimeType) && canEditExif(targetMimeType)) { + StorageUtils.openInputStream(context, sourceUri)?.use { input -> + ExifInterface(input).apply { + ExifInterfaceHelper.allTags.keys.filterNot { skippedTags.contains(it) }.filter { hasAttribute(it) }.forEach { tag -> + exif[tag] = getAttribute(tag) + } + } + } + } + if (exif.isNotEmpty()) { + ExifInterface(editableFile).apply { + exif.entries.forEach { (tag, value) -> + setAttribute(tag, value) + } + saveAttributes() + } + } + + // copy the edited temporary file back to the original + editableFile.transferTo(outputStream(context, targetMimeType, targetUri, targetPath)) + editableFile.delete() + } + @Suppress("BlockingMethodInNonBlockingContext") suspend fun captureFrame( contextWrapper: ContextWrapper, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a3765193b..a2bcaa31a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -422,6 +422,7 @@ "exportEntryDialogFormat": "Format:", "exportEntryDialogWidth": "Width", "exportEntryDialogHeight": "Height", + "exportEntryDialogWriteMetadata": "Write metadata", "renameEntryDialogLabel": "New name", diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 72d9a972d..554faccd8 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -4,9 +4,9 @@ import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; -import 'package:aves/widgets/filter_grids/places_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:flutter/material.dart'; @@ -41,7 +41,6 @@ class SettingsDefaults { static const drawerPageBookmarks = [ AlbumListPage.routeName, CountryListPage.routeName, - PlaceListPage.routeName, TagListPage.routeName, ]; @@ -116,6 +115,11 @@ class SettingsDefaults { static const tagEditorCurrentFilterSectionExpanded = true; + // converter + + static const convertMimeType = MimeTypes.jpeg; + static const convertWriteMetadata = true; + // rendering static const imageBackground = EntryBackground.white; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 7e8b20fa7..89d258ce6 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -158,6 +158,11 @@ class Settings extends ChangeNotifier { static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded'; static const tagEditorExpandedSectionKey = 'tag_editor_expanded_section'; + // converter + + static const convertMimeTypeKey = 'convert_mime_type'; + static const convertWriteMetadataKey = 'convert_write_metadata'; + // map static const mapStyleKey = 'info_map_style'; static const mapDefaultCenterKey = 'map_default_center'; @@ -724,6 +729,16 @@ class Settings extends ChangeNotifier { set tagEditorExpandedSection(String? newValue) => _set(tagEditorExpandedSectionKey, newValue); + // converter + + String get convertMimeType => getString(convertMimeTypeKey) ?? SettingsDefaults.convertMimeType; + + set convertMimeType(String newValue) => _set(convertMimeTypeKey, newValue); + + bool get convertWriteMetadata => getBool(convertWriteMetadataKey) ?? SettingsDefaults.convertWriteMetadata; + + set convertWriteMetadata(bool newValue) => _set(convertWriteMetadataKey, newValue); + // map EntryMapStyle? get mapStyle { @@ -1069,6 +1084,7 @@ class Settings extends ChangeNotifier { case videoGestureVerticalDragBrightnessVolumeKey: case subtitleShowOutlineKey: case tagEditorCurrentFilterSectionExpandedKey: + case convertWriteMetadataKey: case saveSearchHistoryKey: case showPinchGestureAlternativesKey: case filePickerShowHiddenFilesKey: @@ -1106,6 +1122,7 @@ class Settings extends ChangeNotifier { case subtitleTextAlignmentKey: case subtitleTextPositionKey: case tagEditorExpandedSectionKey: + case convertMimeTypeKey: case mapStyleKey: case mapDefaultCenterKey: case coordinateFormatKey: diff --git a/lib/services/media/media_edit_service.dart b/lib/services/media/media_edit_service.dart index bc1fdd331..a0d6be076 100644 --- a/lib/services/media/media_edit_service.dart +++ b/lib/services/media/media_edit_service.dart @@ -127,6 +127,7 @@ class PlatformMediaEditService implements MediaEditService { 'lengthUnit': options.lengthUnit.name, 'width': options.width, 'height': options.height, + 'writeMetadata': options.writeMetadata, 'destinationPath': destinationAlbum, 'nameConflictStrategy': nameConflictStrategy.toPlatform(), }) @@ -187,14 +188,16 @@ class PlatformMediaEditService implements MediaEditService { @immutable class EntryConvertOptions extends Equatable { final String mimeType; + final bool writeMetadata; final LengthUnit lengthUnit; final int width, height; @override - List get props => [mimeType, lengthUnit, width, height]; + List get props => [mimeType, writeMetadata, lengthUnit, width, height]; const EntryConvertOptions({ required this.mimeType, + required this.writeMetadata, required this.lengthUnit, required this.width, required this.height, diff --git a/lib/widgets/common/fx/transitions.dart b/lib/widgets/common/fx/transitions.dart new file mode 100644 index 000000000..b4b7f1613 --- /dev/null +++ b/lib/widgets/common/fx/transitions.dart @@ -0,0 +1,14 @@ +import 'package:flutter/widgets.dart'; + +class AvesTransitions { + static Widget formTransitionBuilder(Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: -1, + child: child, + ), + ); + } +} diff --git a/lib/widgets/dialogs/convert_entry_dialog.dart b/lib/widgets/dialogs/convert_entry_dialog.dart index 12ca0f6f6..6fe70a92e 100644 --- a/lib/widgets/dialogs/convert_entry_dialog.dart +++ b/lib/widgets/dialogs/convert_entry_dialog.dart @@ -1,13 +1,17 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/metadata/enums/length_unit.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/media/media_edit_service.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/transitions.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'aves_dialog.dart'; @@ -28,8 +32,8 @@ class ConvertEntryDialog extends StatefulWidget { class _ConvertEntryDialogState extends State { final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); - late String _mimeType; - late bool _sameSized; + late ValueNotifier _mimeTypeNotifier; + late bool _writeMetadata, _sameSized; late List _lengthUnitOptions; late LengthUnit _lengthUnit; @@ -45,7 +49,8 @@ class _ConvertEntryDialogState extends State { @override void initState() { super.initState(); - _mimeType = MimeTypes.jpeg; + _mimeTypeNotifier = ValueNotifier(settings.convertMimeType); + _writeMetadata = settings.convertWriteMetadata; _sameSized = entries.map((entry) => entry.displaySize).toSet().length == 1; _lengthUnitOptions = [ if (_sameSized) LengthUnit.px, @@ -103,10 +108,10 @@ class _ConvertEntryDialogState extends State { TextDropdownButton( values: imageExportFormats, valueText: MimeUtils.displayType, - value: _mimeType, + value: _mimeTypeNotifier.value, onChanged: (selected) { if (selected != null) { - setState(() => _mimeType = selected); + setState(() => _mimeTypeNotifier.value = selected); } }, ), @@ -195,7 +200,32 @@ class _ConvertEntryDialogState extends State { ], ), ), - const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: _mimeTypeNotifier, + builder: (context, mimeType, child) { + Widget child; + if (MimeTypes.canEditExif(mimeType) || MimeTypes.canEditIptc(mimeType) || MimeTypes.canEditXmp(mimeType)) { + child = SwitchListTile( + value: _writeMetadata, + onChanged: (v) => setState(() => _writeMetadata = v), + title: Text(context.l10n.exportEntryDialogWriteMetadata), + contentPadding: const EdgeInsetsDirectional.only( + start: AvesDialog.defaultHorizontalContentPadding, + end: AvesDialog.defaultHorizontalContentPadding - 8, + ), + ); + } else { + child = const SizedBox(height: 16); + } + return AnimatedSwitcher( + duration: context.read().formTransition, + switchInCurve: Curves.easeInOutCubic, + switchOutCurve: Curves.easeInOutCubic, + transitionBuilder: AvesTransitions.formTransitionBuilder, + child: child, + ); + }, + ), ], actions: [ const CancelButton(), @@ -209,12 +239,19 @@ class _ConvertEntryDialogState extends State { final height = int.tryParse(_heightController.text); final options = (width != null && height != null) ? EntryConvertOptions( - mimeType: _mimeType, + mimeType: _mimeTypeNotifier.value, + writeMetadata: _writeMetadata, lengthUnit: _lengthUnit, width: width, height: height, ) : null; + + if (options != null) { + settings.convertMimeType = options.mimeType; + settings.convertWriteMetadata = options.writeMetadata; + } + Navigator.maybeOf(context)?.pop(options); } : null, diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index 56bf224cf..ea1f51447 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -13,6 +13,7 @@ import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/basic/wheel.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/transitions.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/item_picker.dart'; @@ -111,7 +112,7 @@ class _EditEntryDateDialogState extends State { duration: context.read().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, - transitionBuilder: _formTransitionBuilder, + transitionBuilder: AvesTransitions.formTransitionBuilder, child: Column( key: ValueKey(_action), mainAxisSize: MainAxisSize.min, @@ -143,15 +144,6 @@ class _EditEntryDateDialogState extends State { ); } - Widget _formTransitionBuilder(Widget child, Animation animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: child, - ), - ); - Widget _buildSetCustomContent(BuildContext context) { final l10n = context.l10n; final locale = l10n.localeName; diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index b55533e9b..850ff57df 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -11,6 +11,7 @@ import 'package:aves/theme/themes.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/transitions.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/item_picker.dart'; @@ -114,7 +115,7 @@ class _EditEntryLocationDialogState extends State { duration: context.read().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, - transitionBuilder: _formTransitionBuilder, + transitionBuilder: AvesTransitions.formTransitionBuilder, child: Column( key: ValueKey(_action), mainAxisSize: MainAxisSize.min, @@ -145,15 +146,6 @@ class _EditEntryLocationDialogState extends State { ); } - Widget _formTransitionBuilder(Widget child, Animation animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: child, - ), - ); - Widget _buildChooseOnMapContent(BuildContext context) { final l10n = context.l10n; diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 077cedee1..009499b59 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/transitions.dart'; import 'package:aves/widgets/common/identity/aves_caption.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; @@ -98,14 +99,7 @@ class _TileViewDialogState extends State> with duration: context.read().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: child, - ), - ), + transitionBuilder: AvesTransitions.formTransitionBuilder, child: _buildSection( show: canGroup, icon: AIcons.group, diff --git a/lib/widgets/stats/date/histogram.dart b/lib/widgets/stats/date/histogram.dart index 80a73f0b4..bf268a171 100644 --- a/lib/widgets/stats/date/histogram.dart +++ b/lib/widgets/stats/date/histogram.dart @@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/date.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/transitions.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/stats/date/axis.dart'; import 'package:charts_flutter/flutter.dart' as charts; @@ -343,14 +344,7 @@ class _HistogramState extends State with AutomaticKeepAliveClientMixi duration: context.read().formTransition, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: child, - ), - ), + transitionBuilder: AvesTransitions.formTransitionBuilder, child: child, ); }, diff --git a/untranslated.json b/untranslated.json index d6b0d255e..afc36cafc 100644 --- a/untranslated.json +++ b/untranslated.json @@ -215,6 +215,7 @@ "exportEntryDialogFormat", "exportEntryDialogWidth", "exportEntryDialogHeight", + "exportEntryDialogWriteMetadata", "renameEntryDialogLabel", "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", @@ -766,6 +767,7 @@ "exportEntryDialogFormat", "exportEntryDialogWidth", "exportEntryDialogHeight", + "exportEntryDialogWriteMetadata", "renameEntryDialogLabel", "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", @@ -1182,6 +1184,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty", @@ -1211,6 +1214,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty", @@ -1220,11 +1224,16 @@ "el": [ "chipActionGoToPlacePage", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty" ], + "es": [ + "exportEntryDialogWriteMetadata" + ], + "eu": [ "chipActionGoToPlacePage", "chipActionLock", @@ -1247,6 +1256,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty", @@ -1356,6 +1366,7 @@ "renameProcessorName", "deleteSingleAlbumConfirmationDialogMessage", "deleteMultiAlbumConfirmationDialogMessage", + "exportEntryDialogWriteMetadata", "renameEntryDialogLabel", "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", @@ -1721,6 +1732,10 @@ "filePickerUseThisFolder" ], + "fr": [ + "exportEntryDialogWriteMetadata" + ], + "gl": [ "columnCount", "chipActionGoToPlacePage", @@ -1826,6 +1841,7 @@ "exportEntryDialogFormat", "exportEntryDialogWidth", "exportEntryDialogHeight", + "exportEntryDialogWriteMetadata", "renameEntryDialogLabel", "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", @@ -2454,6 +2470,7 @@ "exportEntryDialogFormat", "exportEntryDialogWidth", "exportEntryDialogHeight", + "exportEntryDialogWriteMetadata", "renameEntryDialogLabel", "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", @@ -2850,13 +2867,15 @@ "id": [ "lengthUnitPixel", - "lengthUnitPercent" + "lengthUnitPercent", + "exportEntryDialogWriteMetadata" ], "it": [ "chipActionGoToPlacePage", "lengthUnitPixel", "lengthUnitPercent", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty" @@ -2894,6 +2913,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle", @@ -2908,6 +2928,10 @@ "settingsWidgetDisplayedItem" ], + "ko": [ + "exportEntryDialogWriteMetadata" + ], + "lt": [ "columnCount", "chipActionGoToPlacePage", @@ -2934,6 +2958,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle", @@ -2969,6 +2994,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty", @@ -3013,6 +3039,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle", @@ -3064,6 +3091,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "editEntryDialogTargetFieldsHeader", "editEntryDateDialogSetCustom", "editEntryLocationDialogTitle", @@ -3347,6 +3375,7 @@ "chipActionGoToPlacePage", "lengthUnitPixel", "lengthUnitPercent", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty" @@ -3375,6 +3404,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle", @@ -3388,6 +3418,7 @@ "chipActionGoToPlacePage", "lengthUnitPixel", "lengthUnitPercent", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty" @@ -3417,6 +3448,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle", @@ -3465,6 +3497,7 @@ "vaultBinUsageDialogMessage", "deleteSingleAlbumConfirmationDialogMessage", "deleteMultiAlbumConfirmationDialogMessage", + "exportEntryDialogWriteMetadata", "editEntryLocationDialogLatitude", "editEntryLocationDialogLongitude", "locationPickerUseThisLocationButton", @@ -3873,6 +3906,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "editEntryDateDialogExtractFromTitle", "editEntryDateDialogShift", "removeEntryMetadataDialogTitle", @@ -4217,6 +4251,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "drawerPlacePage", "placePageTitle", "placeEmpty", @@ -4226,7 +4261,8 @@ "uk": [ "lengthUnitPixel", - "lengthUnitPercent" + "lengthUnitPercent", + "exportEntryDialogWriteMetadata" ], "zh": [ @@ -4253,6 +4289,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle", @@ -4291,6 +4328,7 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", "drawerPlacePage", "placePageTitle",