#526 converter: write metadata

This commit is contained in:
Thibault Deckers 2023-02-23 23:20:32 +01:00
parent 64ae4e7614
commit ef6eb53eb2
15 changed files with 285 additions and 84 deletions

View file

@ -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

View file

@ -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 {
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 {
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 {
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()

View file

@ -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) {

View file

@ -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<String, String?>()
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,

View file

@ -422,6 +422,7 @@
"exportEntryDialogFormat": "Format:",
"exportEntryDialogWidth": "Width",
"exportEntryDialogHeight": "Height",
"exportEntryDialogWriteMetadata": "Write metadata",
"renameEntryDialogLabel": "New name",

View file

@ -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;

View file

@ -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:

View file

@ -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<Object?> get props => [mimeType, lengthUnit, width, height];
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height];
const EntryConvertOptions({
required this.mimeType,
required this.writeMetadata,
required this.lengthUnit,
required this.width,
required this.height,

View file

@ -0,0 +1,14 @@
import 'package:flutter/widgets.dart';
class AvesTransitions {
static Widget formTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
);
}
}

View file

@ -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<ConvertEntryDialog> {
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
late String _mimeType;
late bool _sameSized;
late ValueNotifier<String> _mimeTypeNotifier;
late bool _writeMetadata, _sameSized;
late List<LengthUnit> _lengthUnitOptions;
late LengthUnit _lengthUnit;
@ -45,7 +49,8 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
@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<ConvertEntryDialog> {
TextDropdownButton<String>(
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<ConvertEntryDialog> {
],
),
),
const SizedBox(height: 16),
ValueListenableBuilder<String>(
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<DurationsData>().formTransition,
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: AvesTransitions.formTransitionBuilder,
child: child,
);
},
),
],
actions: [
const CancelButton(),
@ -209,12 +239,19 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
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,

View file

@ -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<EditEntryDateDialog> {
duration: context.read<DurationsData>().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<EditEntryDateDialog> {
);
}
Widget _formTransitionBuilder(Widget child, Animation<double> animation) => FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
);
Widget _buildSetCustomContent(BuildContext context) {
final l10n = context.l10n;
final locale = l10n.localeName;

View file

@ -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<EditEntryLocationDialog> {
duration: context.read<DurationsData>().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<EditEntryLocationDialog> {
);
}
Widget _formTransitionBuilder(Widget child, Animation<double> animation) => FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
);
Widget _buildChooseOnMapContent(BuildContext context) {
final l10n = context.l10n;

View file

@ -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<S, G, L> extends State<TileViewDialog<S, G, L>> with
duration: context.read<DurationsData>().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,

View file

@ -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<Histogram> with AutomaticKeepAliveClientMixi
duration: context.read<DurationsData>().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,
);
},

View file

@ -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",