diff --git a/CHANGELOG.md b/CHANGELOG.md
index 216ce3367..b2d6762b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.10.4] - 2024-02-07
+
+### Fixed
+
+- motion photo detection for xml variant of google container item
+- HEIF size detection for some corrupted files
+- viewer transition direction & effects for RTL locales
+
## [v1.10.3] - 2024-01-29
### Added
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c9fa9370a..fcdeaacd5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -121,6 +121,7 @@
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
tools:targetApi="tiramisu">
{
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleDeviceContainer.kt
similarity index 82%
rename from android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt
rename to android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleDeviceContainer.kt
index 60b74a507..41d27f9e0 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleDeviceContainer.kt
@@ -1,10 +1,11 @@
-package deckers.thibault.aves.metadata
+package deckers.thibault.aves.metadata.xmp
import android.content.Context
import android.net.Uri
import com.adobe.internal.xmp.XMPMeta
-import deckers.thibault.aves.metadata.XMP.countPropPathArrayItems
-import deckers.thibault.aves.metadata.XMP.getSafeStructField
+import deckers.thibault.aves.metadata.Metadata
+import deckers.thibault.aves.metadata.xmp.XMP.countPropPathArrayItems
+import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
import deckers.thibault.aves.utils.indexOfBytes
import java.io.DataInputStream
@@ -15,12 +16,12 @@ class GoogleDeviceContainer {
private val offsets: MutableList = ArrayList()
fun findItems(xmpMeta: XMPMeta) {
- val containerDirectoryPath = listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME)
+ val containerDirectoryPath = listOf(GoogleXMP.GDEVICE_CONTAINER_PROP_NAME, GoogleXMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME)
val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath)
for (i in 1 until count + 1) {
- val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value
- val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull()
- val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
+ val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value
+ val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull()
+ val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
if (mimeType != null && length != null && dataUri != null) {
items.add(
GoogleDeviceContainerItem(
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleXMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleXMP.kt
new file mode 100644
index 000000000..cf662e1bb
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/GoogleXMP.kt
@@ -0,0 +1,206 @@
+package deckers.thibault.aves.metadata.xmp
+
+import android.util.Log
+import com.adobe.internal.xmp.XMPError
+import com.adobe.internal.xmp.XMPException
+import com.adobe.internal.xmp.XMPMeta
+import deckers.thibault.aves.metadata.xmp.XMP.countPropArrayItems
+import deckers.thibault.aves.metadata.xmp.XMP.doesPropExist
+import deckers.thibault.aves.metadata.xmp.XMP.doesPropPathExist
+import deckers.thibault.aves.metadata.xmp.XMP.getSafeInt
+import deckers.thibault.aves.metadata.xmp.XMP.getSafeLong
+import deckers.thibault.aves.metadata.xmp.XMP.getSafeString
+import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
+import deckers.thibault.aves.model.FieldMap
+import deckers.thibault.aves.utils.LogUtils
+import deckers.thibault.aves.utils.MimeTypes
+
+object GoogleXMP {
+ private val LOG_TAG = LogUtils.createTag()
+
+ // namespaces
+ private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/"
+ private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/"
+ private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
+ private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
+ private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
+ private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/"
+ private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/"
+ private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
+ private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
+ private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
+
+ // embedded media data properties
+ // cf https://developers.google.com/depthmap-metadata
+ // cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
+ private val knownDataProps = listOf(
+ XMPPropName(GAUDIO_NS_URI, "Data"),
+ XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"),
+ XMPPropName(GIMAGE_NS_URI, "Data"),
+ XMPPropName(GDEPTH_NS_URI, "Data"),
+ XMPPropName(GDEPTH_NS_URI, "Confidence"),
+ )
+
+
+ fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it }
+
+ // google portrait
+
+ val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container")
+ val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory")
+ val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI")
+ val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
+ val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
+
+ // container
+
+ private val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
+ private val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
+ private val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
+ private val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
+ private val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
+ private val GCONTAINER_ITEM_SEMANTIC_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Semantic")
+
+ private const val ITEM_SEMANTIC_GAIN_MAP = "GainMap"
+
+ // panorama
+ // cf https://developers.google.com/streetview/spherical-metadata
+
+ private val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels")
+ private val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels")
+ private val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels")
+ private val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels")
+ private val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels")
+ private val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels")
+ private val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType")
+ const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular"
+
+ // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default
+ // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode)
+ private val gpanoRequiredProps = listOf(
+ GPANO_CROPPED_AREA_HEIGHT_PROP_NAME,
+ GPANO_CROPPED_AREA_WIDTH_PROP_NAME,
+ GPANO_CROPPED_AREA_LEFT_PROP_NAME,
+ GPANO_CROPPED_AREA_TOP_PROP_NAME,
+ GPANO_FULL_PANO_WIDTH_PROP_NAME,
+ )
+
+ fun isUltraHdPhoto(meta: XMPMeta): Boolean {
+ if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
+ val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
+ for (i in 1 until count + 1) {
+ val semantic = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_SEMANTIC_PROP_NAME))?.value
+ if (semantic == ITEM_SEMANTIC_GAIN_MAP) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ fun isMotionPhoto(meta: XMPMeta): Boolean {
+ try {
+ // GCamera motion photo
+ if (meta.doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
+
+ // Container motion photo
+ if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
+ val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
+ var hasImage = false
+ var hasVideo = false
+ for (i in 1 until count + 1) {
+ val mime = getContainerItemAttribute(meta, i, GCONTAINER_ITEM_MIME_PROP_NAME)
+ val length = getContainerItemAttribute(meta, i, GCONTAINER_ITEM_LENGTH_PROP_NAME)
+ // `length` is not always provided for the image item
+ hasImage = hasImage || MimeTypes.isImage(mime)
+ hasVideo = hasVideo || (MimeTypes.isVideo(mime) && length != null)
+ }
+ if (hasImage && hasVideo) return true
+ }
+
+ return false
+ } catch (e: XMPException) {
+ if (e.errorCode != XMPError.BADSCHEMA) {
+ // `BADSCHEMA` code is reported when we check a property
+ // from a non standard namespace, and that namespace is not declared in the XMP
+ Log.w(LOG_TAG, "failed to check Google motion photo props from XMP", e)
+ }
+ }
+ return false
+ }
+
+ private fun getContainerItemAttribute(meta: XMPMeta, i: Int, attribute: XMPPropName): String? {
+ // variant of `Container:Item` with ``
+ val mime = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, attribute))?.value
+ // variant of `Container:Item` with ``
+ return mime ?: meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, attribute))?.value
+ }
+
+ fun isPanorama(meta: XMPMeta): Boolean {
+ try {
+ if (gpanoRequiredProps.all { meta.doesPropExist(it) }) return true
+ } catch (e: XMPException) {
+ if (e.errorCode != XMPError.BADSCHEMA) {
+ // `BADSCHEMA` code is reported when we check a property
+ // from a non standard namespace, and that namespace is not declared in the XMP
+ Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e)
+ }
+ }
+ return false
+ }
+
+ fun getPanoramaInfo(meta: XMPMeta): FieldMap {
+ val fields: FieldMap = hashMapOf()
+ try {
+ meta.getSafeInt(GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
+ meta.getSafeInt(GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
+ meta.getSafeInt(GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
+ meta.getSafeInt(GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
+ meta.getSafeInt(GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
+ meta.getSafeInt(GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
+ meta.getSafeString(GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
+ } catch (e: XMPException) {
+ Log.w(LOG_TAG, "failed to read XMP directory", e)
+ }
+ return fields
+ }
+
+ fun getTrailingVideoOffsetFromEnd(meta: XMPMeta): Long? {
+ var offsetFromEnd: Long? = null
+ if (meta.doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
+ // `GCamera` motion photo
+ meta.getSafeLong(GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
+ } else if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
+ // `Container` motion photo
+ val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
+ for (i in 1 until count + 1) {
+ val mime = getContainerItemAttribute(meta, i, GCONTAINER_ITEM_MIME_PROP_NAME)
+ if (MimeTypes.isVideo(mime)) {
+ getContainerItemAttribute(meta, i, GCONTAINER_ITEM_LENGTH_PROP_NAME)?.let { offsetFromEnd = it.toLong() }
+ }
+ }
+ }
+ return offsetFromEnd
+ }
+
+ fun updateTrailingVideoOffset(xmp: String, oldOffset: Int, newOffset: Int): String {
+ return xmp.replace(
+ // GCamera motion photo
+ "${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$oldOffset\"",
+ "${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newOffset\"",
+ ).replace(
+ // Container motion photo
+ "${GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$oldOffset\"",
+ "${GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newOffset\"",
+ )
+ }
+
+
+ fun getDeviceContainer(meta: XMPMeta): GoogleDeviceContainer? {
+ return if (meta.doesPropPathExist(listOf(GDEVICE_CONTAINER_PROP_NAME, GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) {
+ GoogleDeviceContainer().apply { findItems(meta) }
+ } else {
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt
similarity index 67%
rename from android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
rename to android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt
index 2744a70aa..47a26ca7d 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt
@@ -1,4 +1,4 @@
-package deckers.thibault.aves.metadata
+package deckers.thibault.aves.metadata.xmp
import android.content.Context
import android.net.Uri
@@ -11,6 +11,7 @@ import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory
import com.adobe.internal.xmp.properties.XMPProperty
import com.drew.metadata.Directory
+import deckers.thibault.aves.metadata.Mp4ParserHelper
import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
@@ -39,16 +40,6 @@ object XMP {
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
// other namespaces
- private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/"
- private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/"
- private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
- private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
- private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
- private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/"
- private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/"
- private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
- private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
- private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
private const val HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/"
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
@@ -63,66 +54,16 @@ object XMP {
private const val GENERIC_LANG = ""
private const val SPECIFIC_LANG = "en-US"
- // embedded media data properties
- // cf https://developers.google.com/depthmap-metadata
- // cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
- private val knownDataProps = listOf(
- XMPPropName(GAUDIO_NS_URI, "Data"),
- XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"),
- XMPPropName(GIMAGE_NS_URI, "Data"),
- XMPPropName(GDEPTH_NS_URI, "Data"),
- XMPPropName(GDEPTH_NS_URI, "Confidence"),
- )
-
- fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it }
-
- // google portrait
-
- val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container")
- val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory")
- val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI")
- val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
- val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
-
- // container
-
- val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
- val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
- val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
- val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
- val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
- private val GCONTAINER_ITEM_SEMANTIC_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Semantic")
-
- private const val ITEM_SEMANTIC_GAIN_MAP = "GainMap"
+ fun isDataPath(path: String) = GoogleXMP.isDataPath(path)
// HDR gain map
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
// panorama
- // cf https://developers.google.com/streetview/spherical-metadata
-
- val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels")
- val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels")
- val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels")
- val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels")
- val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels")
- val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels")
- val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType")
- const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular"
private val PMTM_IS_PANO360_PROP_NAME = XMPPropName(PMTM_NS_URI, "IsPano360")
- // `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default
- // `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode)
- private val gpanoRequiredProps = listOf(
- GPANO_CROPPED_AREA_HEIGHT_PROP_NAME,
- GPANO_CROPPED_AREA_WIDTH_PROP_NAME,
- GPANO_CROPPED_AREA_LEFT_PROP_NAME,
- GPANO_CROPPED_AREA_TOP_PROP_NAME,
- GPANO_FULL_PANO_WIDTH_PROP_NAME,
- )
-
// as of `metadata-extractor` v2.18.0, XMP is not discovered in HEIC images,
// so we fall back to the native content resolver, if possible
fun checkHeic(
@@ -191,20 +132,10 @@ object XMP {
fun XMPMeta.hasHdrGainMap(): Boolean {
try {
// standard HDR gain map
- if (doesPropExist(HDRGM_VERSION_PROP_NAME)) {
- return true
- }
+ if (doesPropExist(HDRGM_VERSION_PROP_NAME)) return true
// `Ultra HDR`
- if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
- val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
- for (i in 1 until count + 1) {
- val semantic = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_SEMANTIC_PROP_NAME))?.value
- if (semantic == ITEM_SEMANTIC_GAIN_MAP) {
- return true
- }
- }
- }
+ if (GoogleXMP.isUltraHdPhoto(this)) return true
return false
} catch (e: XMPException) {
@@ -217,47 +148,11 @@ object XMP {
return false
}
- fun XMPMeta.isMotionPhoto(): Boolean {
- try {
- // GCamera motion photo
- if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
-
- // Container motion photo
- if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
- val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
- var hasImage = false
- var hasVideo = false
- for (i in 1 until count + 1) {
- val mime = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value
- val length = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
- hasImage = hasImage || MimeTypes.isImage(mime) && length != null
- hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
- }
- if (hasImage && hasVideo) return true
- }
-
- return false
- } catch (e: XMPException) {
- if (e.errorCode != XMPError.BADSCHEMA) {
- // `BADSCHEMA` code is reported when we check a property
- // from a non standard namespace, and that namespace is not declared in the XMP
- Log.w(LOG_TAG, "failed to check Google motion photo props from XMP", e)
- }
- }
- return false
- }
+ fun XMPMeta.isMotionPhoto() = GoogleXMP.isMotionPhoto(this)
fun XMPMeta.isPanorama(): Boolean {
// Google
- try {
- if (gpanoRequiredProps.all { doesPropExist(it) }) return true
- } catch (e: XMPException) {
- if (e.errorCode != XMPError.BADSCHEMA) {
- // `BADSCHEMA` code is reported when we check a property
- // from a non standard namespace, and that namespace is not declared in the XMP
- Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e)
- }
- }
+ if (GoogleXMP.isPanorama(this)) return true
// Photomatix
try {
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 093f2c997..e3c654741 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
@@ -36,8 +36,8 @@ import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
-import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.metadataextractor.Helper
+import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
@@ -982,15 +982,7 @@ abstract class ImageProvider {
)
val newTrailerOffset = trailerOffset + diff
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
- xmp.replace(
- // GCamera motion photo
- "${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$trailerOffset\"",
- "${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newTrailerOffset\"",
- ).replace(
- // Container motion photo
- "${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
- "${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
- )
+ GoogleXMP.updateTrailingVideoOffset(xmp, trailerOffset, newTrailerOffset)
})
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index d921615bd..4754a6e26 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.RecoverableSecurityException
import android.content.*
+import android.graphics.BitmapFactory
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
@@ -31,6 +32,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.FileOutputStream
+import java.io.IOException
import java.io.OutputStream
import java.io.SyncFailedException
import java.util.*
@@ -214,8 +216,8 @@ class MediaStoreImageProvider : ImageProvider() {
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
// in that case we try to use the MIME type provided along the URI
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
- val width = cursor.getInt(widthColumn)
- val height = cursor.getInt(heightColumn)
+ var width = cursor.getInt(widthColumn)
+ var height = cursor.getInt(heightColumn)
val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L
if (mimeType == null) {
@@ -238,6 +240,28 @@ class MediaStoreImageProvider : ImageProvider() {
"contentId" to contentId,
)
+ if (MimeTypes.isHeic(mimeType)) {
+ // The reported size for some HEIC images is simply incorrect.
+ try {
+ StorageUtils.openInputStream(context, itemUri)?.use { input ->
+ val options = BitmapFactory.Options().apply {
+ inJustDecodeBounds = true
+ }
+ BitmapFactory.decodeStream(input, null, options)
+ val outWidth = options.outWidth
+ val outHeight = options.outHeight
+ if (outWidth > 0 && outHeight > 0) {
+ width = outWidth
+ height = outHeight
+ entryMap["width"] = width
+ entryMap["height"] = height
+ }
+ }
+ } catch (e: IOException) {
+ // ignore
+ }
+ }
+
if (MimeTypes.isRaw(mimeType)
|| (width <= 0 || height <= 0) && needSize(mimeType)
|| durationMillis == 0L && needDuration
diff --git a/android/app/src/main/res/values-da/strings.xml b/android/app/src/main/res/values-da/strings.xml
new file mode 100644
index 000000000..4bc58abd4
--- /dev/null
+++ b/android/app/src/main/res/values-da/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Fotoramme
+ Baggrund
+ Videoer
+ Mediascanning
+ Scanner medier
+ Stop
+ Aves
+ Sikker tilstand
+ Søg
+
\ No newline at end of file
diff --git a/fastlane/metadata/android/da/full_description.txt b/fastlane/metadata/android/da/full_description.txt
new file mode 100644
index 000000000..6b96ec3ea
--- /dev/null
+++ b/fastlane/metadata/android/da/full_description.txt
@@ -0,0 +1,5 @@
+Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files.
+
+Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
+
+Aves integrates with Android (from KitKat to Android 14, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker.
diff --git a/fastlane/metadata/android/da/short_description.txt b/fastlane/metadata/android/da/short_description.txt
new file mode 100644
index 000000000..8c9445bd5
--- /dev/null
+++ b/fastlane/metadata/android/da/short_description.txt
@@ -0,0 +1 @@
+Gallery and metadata explorer
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/113.txt b/fastlane/metadata/android/en-US/changelogs/113.txt
new file mode 100644
index 000000000..e08549f81
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/113.txt
@@ -0,0 +1,4 @@
+In v1.10.4:
+- customize your home page
+- analyze your images with the histogram (for real this time)
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/11301.txt b/fastlane/metadata/android/en-US/changelogs/11301.txt
new file mode 100644
index 000000000..e08549f81
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/11301.txt
@@ -0,0 +1,4 @@
+In v1.10.4:
+- customize your home page
+- analyze your images with the histogram (for real this time)
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/lib/l10n/app_da.arb b/lib/l10n/app_da.arb
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/lib/l10n/app_da.arb
@@ -0,0 +1 @@
+{}
diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb
index 0bbcb4ea1..26e285185 100644
--- a/lib/l10n/app_el.arb
+++ b/lib/l10n/app_el.arb
@@ -1332,5 +1332,11 @@
"cropAspectRatioSquare": "Τετράγωνο",
"@cropAspectRatioSquare": {},
"widgetTapUpdateWidget": "Ενημέρωση γραφικού στοιχείου",
- "@widgetTapUpdateWidget": {}
+ "@widgetTapUpdateWidget": {},
+ "overlayHistogramNone": "Τίποτα",
+ "@overlayHistogramNone": {},
+ "overlayHistogramRGB": "RGB",
+ "@overlayHistogramRGB": {},
+ "overlayHistogramLuminance": "Φωτεινότητα",
+ "@overlayHistogramLuminance": {}
}
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
index b4ac9ddab..a2eed236e 100644
--- a/lib/model/app/contributors.dart
+++ b/lib/model/app/contributors.dart
@@ -76,9 +76,11 @@ class Contributors {
Contributor('v1s7', 'v1s7@users.noreply.hosted.weblate.org'),
Contributor('fuzfyy', 'egeozce35@gmail.com'),
Contributor('minh', 'teaminh@skiff.com'),
+ Contributor('luckris25', 'lk1thebestl@gmail.com'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
+ // Contributor('Grooty12', 'Rasmus@rosendahl-kaa.name'), // Danish
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
diff --git a/lib/ref/metadata/xmp.dart b/lib/ref/metadata/xmp.dart
index fa2dbdb13..5a7ae5942 100644
--- a/lib/ref/metadata/xmp.dart
+++ b/lib/ref/metadata/xmp.dart
@@ -1,6 +1,7 @@
class XmpNamespaces {
static const acdsee = 'http://ns.acdsee.com/iptc/1.0/';
static const adsmlat = 'http://adsml.org/xmlns/';
+ static const appleDesktop = 'http://ns.apple.com/namespace/1.0/';
static const avm = 'http://www.communicatingastronomy.org/avm/1.0/';
static const camera = 'http://pix4d.com/camera/1.0/';
static const cc = 'http://creativecommons.org/ns#';
@@ -24,6 +25,7 @@ class XmpNamespaces {
static const gAudio = 'http://ns.google.com/photos/1.0/audio/';
static const gCamera = 'http://ns.google.com/photos/1.0/camera/';
static const gContainer = 'http://ns.google.com/photos/1.0/container/';
+ static const gContainerItem = 'http://ns.google.com/photos/1.0/container/item/';
static const gCreations = 'http://ns.google.com/photos/1.0/creations/';
static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/';
static const gDevice = 'http://ns.google.com/photos/dd/1.0/device/';
diff --git a/lib/view/src/xmp.dart b/lib/view/src/xmp.dart
index 438a5d4fe..cbac7e646 100644
--- a/lib/view/src/xmp.dart
+++ b/lib/view/src/xmp.dart
@@ -7,6 +7,7 @@ class XmpNamespaceView {
XmpNamespaces.adsmlat: 'AdsML',
XmpNamespaces.exifAux: 'Exif Aux',
XmpNamespaces.avm: 'Astronomy Visualization',
+ XmpNamespaces.appleDesktop: 'Apple Desktop',
XmpNamespaces.camera: 'Pix4D Camera',
XmpNamespaces.cc: 'Creative Commons',
XmpNamespaces.crd: 'Camera Raw Defaults',
diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart
index b08b552c1..1839eb521 100644
--- a/lib/widgets/aves_app.dart
+++ b/lib/widgets/aves_app.dart
@@ -59,6 +59,7 @@ class AvesApp extends StatefulWidget {
static final _unsupportedLocales = {
'bn', // Bengali
'ckb', // Kurdish (Central)
+ 'da', // Danish
'fa', // Persian
'fi', // Finnish
'gl', // Galician
diff --git a/lib/widgets/viewer/controls/transitions.dart b/lib/widgets/viewer/controls/transitions.dart
index e76d699b1..0adf9b870 100644
--- a/lib/widgets/viewer/controls/transitions.dart
+++ b/lib/widgets/viewer/controls/transitions.dart
@@ -1,3 +1,4 @@
+import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
class PageTransitionEffects {
@@ -14,7 +15,7 @@ class PageTransitionEffects {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).clamp(0, 1);
- dx = position * width;
+ dx = position * width * (context.isRtl ? -1 : 1);
if (zoomIn) {
scale = 1 + position;
}
@@ -42,7 +43,7 @@ class PageTransitionEffects {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
if (parallax) {
- dx = position * width / 2;
+ dx = position * width / 2 * (context.isRtl ? -1 : 1);
}
}
return ClipRect(
@@ -64,7 +65,7 @@ class PageTransitionEffects {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
- dx = position * width;
+ dx = position * width * (context.isRtl ? -1 : 1);
}
return Opacity(
opacity: opacity,
diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart
index 1a33eb4c0..57ae547d2 100644
--- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart
+++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart
@@ -90,7 +90,11 @@ class XmpNamespace extends Equatable {
List buildNamespaceSection(BuildContext context) {
final props = rawProps.entries
.map((kv) {
- final prop = XmpProp(kv.key, kv.value);
+ final key = kv.key;
+ if (skippedProps.any((pattern) => pattern.allMatches(key).isNotEmpty)) {
+ return null;
+ }
+ final prop = XmpProp(key, kv.value);
var extracted = false;
cards.forEach((card) => extracted |= card.extract(prop));
return extracted ? null : prop;
@@ -134,6 +138,8 @@ class XmpNamespace extends Equatable {
: [];
}
+ Set get skippedProps => {};
+
List get cards => [];
String formatValue(XmpProp prop) => prop.value;
diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart
index e05ab5852..77e740170 100644
--- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart
+++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart
@@ -73,11 +73,26 @@ class XmpGCameraNamespace extends XmpGoogleNamespace {
}
class XmpGContainer extends XmpNamespace {
- XmpGContainer({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gContainer);
+ late final String _gContainerItemNsPrefix;
+ late final String _rdfNsPrefix;
+
+ XmpGContainer({required super.schemaRegistryPrefixes, required super.rawProps}) : super(nsUri: XmpNamespaces.gContainer) {
+ _gContainerItemNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, XmpNamespaces.gContainerItem);
+ _rdfNsPrefix = XmpNamespace.prefixForUri(schemaRegistryPrefixes, XmpNamespaces.rdf);
+ }
+
+ @override
+ late final Set skippedProps = {
+ // variant of `Container:Item` with ``
+ RegExp(nsPrefix + r'Directory\[(\d+)\]/' + _rdfNsPrefix + r'type'),
+ };
@override
late final List cards = [
+ // variant of `Container:Item` with ``
XmpCardData(RegExp(nsPrefix + r'Directory\[(\d+)\]/' + nsPrefix + r'Item/(.*)'), title: 'Directory Item'),
+ // variant of `Container:Item` with ``
+ XmpCardData(RegExp(nsPrefix + r'Directory\[(\d+)\]/(' + _gContainerItemNsPrefix + r'.*)'), title: 'Directory Item'),
];
}
diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart
index b026993d4..287d1d836 100644
--- a/lib/widgets/viewer/visual/entry_page_view.dart
+++ b/lib/widgets/viewer/visual/entry_page_view.dart
@@ -12,6 +12,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/hero.dart';
@@ -420,11 +421,12 @@ class _EntryPageViewState extends State with TickerProviderStateM
}
void _onFling(AxisDirection direction) {
+ const animate = true;
switch (direction) {
case AxisDirection.left:
- const ShowPreviousEntryNotification(animate: true).dispatch(context);
+ (context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
case AxisDirection.right:
- const ShowNextEntryNotification(animate: true).dispatch(context);
+ (context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
case AxisDirection.up:
PopVisualNotification().dispatch(context);
case AxisDirection.down:
@@ -437,11 +439,12 @@ class _EntryPageViewState extends State with TickerProviderStateM
final x = alignment.x;
final sideRatio = _getSideRatio();
if (sideRatio != null) {
+ const animate = false;
if (x < sideRatio) {
- const ShowPreviousEntryNotification(animate: false).dispatch(context);
+ (context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
return;
} else if (x > 1 - sideRatio) {
- const ShowNextEntryNotification(animate: false).dispatch(context);
+ (context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
return;
}
}
diff --git a/plugins/aves_report_crashlytics/lib/aves_report_platform.dart b/plugins/aves_report_crashlytics/lib/aves_report_platform.dart
index 11337eff3..d779a2b97 100644
--- a/plugins/aves_report_crashlytics/lib/aves_report_platform.dart
+++ b/plugins/aves_report_crashlytics/lib/aves_report_platform.dart
@@ -78,6 +78,8 @@ class PlatformReportService extends ReportService {
@override
Future recordFlutterError(FlutterErrorDetails flutterErrorDetails) async {
- return _instance?.recordFlutterError(flutterErrorDetails);
+ if (!flutterErrorDetails.silent) {
+ return _instance?.recordFlutterError(flutterErrorDetails);
+ }
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 7be7adbb7..8c75c8900 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
-version: 1.10.3+112
+version: 1.10.4+113
publish_to: none
environment:
diff --git a/untranslated.json b/untranslated.json
index 0ed85f1bf..a6a7f1646 100644
--- a/untranslated.json
+++ b/untranslated.json
@@ -1204,6 +1204,691 @@
"settingsThumbnailShowHdrIcon"
],
+ "da": [
+ "appName",
+ "welcomeMessage",
+ "welcomeOptional",
+ "welcomeTermsToggle",
+ "itemCount",
+ "columnCount",
+ "timeSeconds",
+ "timeMinutes",
+ "timeDays",
+ "focalLength",
+ "applyButtonLabel",
+ "deleteButtonLabel",
+ "nextButtonLabel",
+ "showButtonLabel",
+ "hideButtonLabel",
+ "continueButtonLabel",
+ "saveCopyButtonLabel",
+ "applyTooltip",
+ "cancelTooltip",
+ "changeTooltip",
+ "clearTooltip",
+ "previousTooltip",
+ "nextTooltip",
+ "showTooltip",
+ "hideTooltip",
+ "actionRemove",
+ "resetTooltip",
+ "saveTooltip",
+ "pickTooltip",
+ "doubleBackExitMessage",
+ "doNotAskAgain",
+ "sourceStateLoading",
+ "sourceStateCataloguing",
+ "sourceStateLocatingCountries",
+ "sourceStateLocatingPlaces",
+ "chipActionDelete",
+ "chipActionGoToAlbumPage",
+ "chipActionGoToCountryPage",
+ "chipActionGoToPlacePage",
+ "chipActionGoToTagPage",
+ "chipActionFilterOut",
+ "chipActionFilterIn",
+ "chipActionHide",
+ "chipActionLock",
+ "chipActionPin",
+ "chipActionUnpin",
+ "chipActionRename",
+ "chipActionSetCover",
+ "chipActionShowCountryStates",
+ "chipActionCreateAlbum",
+ "chipActionCreateVault",
+ "chipActionConfigureVault",
+ "entryActionCopyToClipboard",
+ "entryActionDelete",
+ "entryActionConvert",
+ "entryActionExport",
+ "entryActionInfo",
+ "entryActionRename",
+ "entryActionRestore",
+ "entryActionRotateCCW",
+ "entryActionRotateCW",
+ "entryActionFlip",
+ "entryActionPrint",
+ "entryActionShare",
+ "entryActionShareImageOnly",
+ "entryActionShareVideoOnly",
+ "entryActionViewSource",
+ "entryActionShowGeoTiffOnMap",
+ "entryActionConvertMotionPhotoToStillImage",
+ "entryActionViewMotionPhotoVideo",
+ "entryActionEdit",
+ "entryActionOpen",
+ "entryActionSetAs",
+ "entryActionCast",
+ "entryActionOpenMap",
+ "entryActionRotateScreen",
+ "entryActionAddFavourite",
+ "entryActionRemoveFavourite",
+ "videoActionCaptureFrame",
+ "videoActionMute",
+ "videoActionUnmute",
+ "videoActionPause",
+ "videoActionPlay",
+ "videoActionReplay10",
+ "videoActionSkip10",
+ "videoActionSelectStreams",
+ "videoActionSetSpeed",
+ "viewerActionSettings",
+ "viewerActionLock",
+ "viewerActionUnlock",
+ "slideshowActionResume",
+ "slideshowActionShowInCollection",
+ "entryInfoActionEditDate",
+ "entryInfoActionEditLocation",
+ "entryInfoActionEditTitleDescription",
+ "entryInfoActionEditRating",
+ "entryInfoActionEditTags",
+ "entryInfoActionRemoveMetadata",
+ "entryInfoActionExportMetadata",
+ "entryInfoActionRemoveLocation",
+ "editorActionTransform",
+ "editorTransformCrop",
+ "editorTransformRotate",
+ "cropAspectRatioFree",
+ "cropAspectRatioOriginal",
+ "cropAspectRatioSquare",
+ "filterAspectRatioLandscapeLabel",
+ "filterAspectRatioPortraitLabel",
+ "filterBinLabel",
+ "filterFavouriteLabel",
+ "filterNoDateLabel",
+ "filterNoAddressLabel",
+ "filterLocatedLabel",
+ "filterNoLocationLabel",
+ "filterNoRatingLabel",
+ "filterTaggedLabel",
+ "filterNoTagLabel",
+ "filterNoTitleLabel",
+ "filterOnThisDayLabel",
+ "filterRecentlyAddedLabel",
+ "filterRatingRejectedLabel",
+ "filterTypeAnimatedLabel",
+ "filterTypeMotionPhotoLabel",
+ "filterTypePanoramaLabel",
+ "filterTypeRawLabel",
+ "filterTypeSphericalVideoLabel",
+ "filterTypeGeotiffLabel",
+ "filterMimeImageLabel",
+ "filterMimeVideoLabel",
+ "accessibilityAnimationsRemove",
+ "accessibilityAnimationsKeep",
+ "albumTierNew",
+ "albumTierPinned",
+ "albumTierSpecial",
+ "albumTierApps",
+ "albumTierVaults",
+ "albumTierRegular",
+ "coordinateFormatDms",
+ "coordinateFormatDecimal",
+ "coordinateDms",
+ "coordinateDmsNorth",
+ "coordinateDmsSouth",
+ "coordinateDmsEast",
+ "coordinateDmsWest",
+ "displayRefreshRatePreferHighest",
+ "displayRefreshRatePreferLowest",
+ "keepScreenOnNever",
+ "keepScreenOnVideoPlayback",
+ "keepScreenOnViewerOnly",
+ "keepScreenOnAlways",
+ "lengthUnitPixel",
+ "lengthUnitPercent",
+ "mapStyleGoogleNormal",
+ "mapStyleGoogleHybrid",
+ "mapStyleGoogleTerrain",
+ "mapStyleHuaweiNormal",
+ "mapStyleHuaweiTerrain",
+ "mapStyleOsmHot",
+ "mapStyleStamenWatercolor",
+ "maxBrightnessNever",
+ "maxBrightnessAlways",
+ "nameConflictStrategyRename",
+ "nameConflictStrategyReplace",
+ "nameConflictStrategySkip",
+ "overlayHistogramNone",
+ "overlayHistogramRGB",
+ "overlayHistogramLuminance",
+ "subtitlePositionTop",
+ "subtitlePositionBottom",
+ "themeBrightnessLight",
+ "themeBrightnessDark",
+ "themeBrightnessBlack",
+ "unitSystemMetric",
+ "unitSystemImperial",
+ "vaultLockTypePattern",
+ "vaultLockTypePin",
+ "vaultLockTypePassword",
+ "settingsVideoEnablePip",
+ "videoControlsPlay",
+ "videoControlsPlaySeek",
+ "videoControlsPlayOutside",
+ "videoControlsNone",
+ "videoLoopModeNever",
+ "videoLoopModeShortOnly",
+ "videoLoopModeAlways",
+ "videoPlaybackSkip",
+ "videoPlaybackMuted",
+ "videoPlaybackWithSound",
+ "videoResumptionModeNever",
+ "videoResumptionModeAlways",
+ "viewerTransitionSlide",
+ "viewerTransitionParallax",
+ "viewerTransitionFade",
+ "viewerTransitionZoomIn",
+ "viewerTransitionNone",
+ "wallpaperTargetHome",
+ "wallpaperTargetLock",
+ "wallpaperTargetHomeLock",
+ "widgetDisplayedItemRandom",
+ "widgetDisplayedItemMostRecent",
+ "widgetOpenPageHome",
+ "widgetOpenPageCollection",
+ "widgetOpenPageViewer",
+ "widgetTapUpdateWidget",
+ "storageVolumeDescriptionFallbackPrimary",
+ "storageVolumeDescriptionFallbackNonPrimary",
+ "rootDirectoryDescription",
+ "otherDirectoryDescription",
+ "storageAccessDialogMessage",
+ "restrictedAccessDialogMessage",
+ "notEnoughSpaceDialogMessage",
+ "missingSystemFilePickerDialogMessage",
+ "unsupportedTypeDialogMessage",
+ "nameConflictDialogSingleSourceMessage",
+ "nameConflictDialogMultipleSourceMessage",
+ "addShortcutDialogLabel",
+ "addShortcutButtonLabel",
+ "noMatchingAppDialogMessage",
+ "binEntriesConfirmationDialogMessage",
+ "deleteEntriesConfirmationDialogMessage",
+ "moveUndatedConfirmationDialogMessage",
+ "moveUndatedConfirmationDialogSetDate",
+ "videoResumeDialogMessage",
+ "videoStartOverButtonLabel",
+ "videoResumeButtonLabel",
+ "setCoverDialogLatest",
+ "setCoverDialogAuto",
+ "setCoverDialogCustom",
+ "hideFilterConfirmationDialogMessage",
+ "newAlbumDialogTitle",
+ "newAlbumDialogNameLabel",
+ "newAlbumDialogNameLabelAlreadyExistsHelper",
+ "newAlbumDialogStorageLabel",
+ "newVaultWarningDialogMessage",
+ "newVaultDialogTitle",
+ "configureVaultDialogTitle",
+ "vaultDialogLockModeWhenScreenOff",
+ "vaultDialogLockTypeLabel",
+ "patternDialogEnter",
+ "patternDialogConfirm",
+ "pinDialogEnter",
+ "pinDialogConfirm",
+ "passwordDialogEnter",
+ "passwordDialogConfirm",
+ "authenticateToConfigureVault",
+ "authenticateToUnlockVault",
+ "vaultBinUsageDialogMessage",
+ "renameAlbumDialogLabel",
+ "renameAlbumDialogLabelAlreadyExistsHelper",
+ "renameEntrySetPageTitle",
+ "renameEntrySetPagePatternFieldLabel",
+ "renameEntrySetPageInsertTooltip",
+ "renameEntrySetPagePreviewSectionTitle",
+ "renameProcessorCounter",
+ "renameProcessorName",
+ "deleteSingleAlbumConfirmationDialogMessage",
+ "deleteMultiAlbumConfirmationDialogMessage",
+ "exportEntryDialogFormat",
+ "exportEntryDialogWidth",
+ "exportEntryDialogHeight",
+ "exportEntryDialogQuality",
+ "exportEntryDialogWriteMetadata",
+ "renameEntryDialogLabel",
+ "editEntryDialogCopyFromItem",
+ "editEntryDialogTargetFieldsHeader",
+ "editEntryDateDialogTitle",
+ "editEntryDateDialogSetCustom",
+ "editEntryDateDialogCopyField",
+ "editEntryDateDialogExtractFromTitle",
+ "editEntryDateDialogShift",
+ "editEntryDateDialogSourceFileModifiedDate",
+ "durationDialogHours",
+ "durationDialogMinutes",
+ "durationDialogSeconds",
+ "editEntryLocationDialogTitle",
+ "editEntryLocationDialogSetCustom",
+ "editEntryLocationDialogChooseOnMap",
+ "editEntryLocationDialogLatitude",
+ "editEntryLocationDialogLongitude",
+ "locationPickerUseThisLocationButton",
+ "editEntryRatingDialogTitle",
+ "removeEntryMetadataDialogTitle",
+ "removeEntryMetadataDialogMore",
+ "removeEntryMetadataMotionPhotoXmpWarningDialogMessage",
+ "videoSpeedDialogLabel",
+ "videoStreamSelectionDialogVideo",
+ "videoStreamSelectionDialogAudio",
+ "videoStreamSelectionDialogText",
+ "videoStreamSelectionDialogOff",
+ "videoStreamSelectionDialogTrack",
+ "videoStreamSelectionDialogNoSelection",
+ "genericSuccessFeedback",
+ "genericFailureFeedback",
+ "genericDangerWarningDialogMessage",
+ "tooManyItemsErrorDialogMessage",
+ "menuActionConfigureView",
+ "menuActionSelect",
+ "menuActionSelectAll",
+ "menuActionSelectNone",
+ "menuActionMap",
+ "menuActionSlideshow",
+ "menuActionStats",
+ "viewDialogSortSectionTitle",
+ "viewDialogGroupSectionTitle",
+ "viewDialogLayoutSectionTitle",
+ "viewDialogReverseSortOrder",
+ "tileLayoutMosaic",
+ "tileLayoutGrid",
+ "tileLayoutList",
+ "castDialogTitle",
+ "coverDialogTabCover",
+ "coverDialogTabApp",
+ "coverDialogTabColor",
+ "appPickDialogTitle",
+ "appPickDialogNone",
+ "aboutPageTitle",
+ "aboutLinkLicense",
+ "aboutLinkPolicy",
+ "aboutBugSectionTitle",
+ "aboutBugSaveLogInstruction",
+ "aboutBugCopyInfoInstruction",
+ "aboutBugCopyInfoButton",
+ "aboutBugReportInstruction",
+ "aboutBugReportButton",
+ "aboutDataUsageSectionTitle",
+ "aboutDataUsageData",
+ "aboutDataUsageCache",
+ "aboutDataUsageDatabase",
+ "aboutDataUsageMisc",
+ "aboutDataUsageInternal",
+ "aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
+ "aboutCreditsSectionTitle",
+ "aboutCreditsWorldAtlas1",
+ "aboutCreditsWorldAtlas2",
+ "aboutTranslatorsSectionTitle",
+ "aboutLicensesSectionTitle",
+ "aboutLicensesBanner",
+ "aboutLicensesAndroidLibrariesSectionTitle",
+ "aboutLicensesFlutterPluginsSectionTitle",
+ "aboutLicensesFlutterPackagesSectionTitle",
+ "aboutLicensesDartPackagesSectionTitle",
+ "aboutLicensesShowAllButtonLabel",
+ "policyPageTitle",
+ "collectionPageTitle",
+ "collectionPickPageTitle",
+ "collectionSelectPageTitle",
+ "collectionActionShowTitleSearch",
+ "collectionActionHideTitleSearch",
+ "collectionActionAddShortcut",
+ "collectionActionSetHome",
+ "collectionActionEmptyBin",
+ "collectionActionCopy",
+ "collectionActionMove",
+ "collectionActionRescan",
+ "collectionActionEdit",
+ "collectionSearchTitlesHintText",
+ "collectionGroupAlbum",
+ "collectionGroupMonth",
+ "collectionGroupDay",
+ "collectionGroupNone",
+ "sectionUnknown",
+ "dateToday",
+ "dateYesterday",
+ "dateThisMonth",
+ "collectionDeleteFailureFeedback",
+ "collectionCopyFailureFeedback",
+ "collectionMoveFailureFeedback",
+ "collectionRenameFailureFeedback",
+ "collectionEditFailureFeedback",
+ "collectionExportFailureFeedback",
+ "collectionCopySuccessFeedback",
+ "collectionMoveSuccessFeedback",
+ "collectionRenameSuccessFeedback",
+ "collectionEditSuccessFeedback",
+ "collectionEmptyFavourites",
+ "collectionEmptyVideos",
+ "collectionEmptyImages",
+ "collectionEmptyGrantAccessButtonLabel",
+ "collectionSelectSectionTooltip",
+ "collectionDeselectSectionTooltip",
+ "drawerAboutButton",
+ "drawerSettingsButton",
+ "drawerCollectionAll",
+ "drawerCollectionFavourites",
+ "drawerCollectionImages",
+ "drawerCollectionVideos",
+ "drawerCollectionAnimated",
+ "drawerCollectionMotionPhotos",
+ "drawerCollectionPanoramas",
+ "drawerCollectionRaws",
+ "drawerCollectionSphericalVideos",
+ "drawerAlbumPage",
+ "drawerCountryPage",
+ "drawerPlacePage",
+ "drawerTagPage",
+ "sortByDate",
+ "sortByName",
+ "sortByItemCount",
+ "sortBySize",
+ "sortByAlbumFileName",
+ "sortByRating",
+ "sortOrderNewestFirst",
+ "sortOrderOldestFirst",
+ "sortOrderAtoZ",
+ "sortOrderZtoA",
+ "sortOrderHighestFirst",
+ "sortOrderLowestFirst",
+ "sortOrderLargestFirst",
+ "sortOrderSmallestFirst",
+ "albumGroupTier",
+ "albumGroupType",
+ "albumGroupVolume",
+ "albumGroupNone",
+ "albumMimeTypeMixed",
+ "albumPickPageTitleCopy",
+ "albumPickPageTitleExport",
+ "albumPickPageTitleMove",
+ "albumPickPageTitlePick",
+ "albumCamera",
+ "albumDownload",
+ "albumScreenshots",
+ "albumScreenRecordings",
+ "albumVideoCaptures",
+ "albumPageTitle",
+ "albumEmpty",
+ "createAlbumButtonLabel",
+ "newFilterBanner",
+ "countryPageTitle",
+ "countryEmpty",
+ "statePageTitle",
+ "stateEmpty",
+ "placePageTitle",
+ "placeEmpty",
+ "tagPageTitle",
+ "tagEmpty",
+ "binPageTitle",
+ "searchCollectionFieldHint",
+ "searchRecentSectionTitle",
+ "searchDateSectionTitle",
+ "searchAlbumsSectionTitle",
+ "searchCountriesSectionTitle",
+ "searchStatesSectionTitle",
+ "searchPlacesSectionTitle",
+ "searchTagsSectionTitle",
+ "searchRatingSectionTitle",
+ "searchMetadataSectionTitle",
+ "settingsPageTitle",
+ "settingsSystemDefault",
+ "settingsDefault",
+ "settingsDisabled",
+ "settingsAskEverytime",
+ "settingsModificationWarningDialogMessage",
+ "settingsSearchFieldLabel",
+ "settingsSearchEmpty",
+ "settingsActionExport",
+ "settingsActionExportDialogTitle",
+ "settingsActionImport",
+ "settingsActionImportDialogTitle",
+ "appExportCovers",
+ "appExportFavourites",
+ "appExportSettings",
+ "settingsNavigationSectionTitle",
+ "settingsHomeTile",
+ "settingsHomeDialogTitle",
+ "setHomeCustomCollection",
+ "settingsShowBottomNavigationBar",
+ "settingsKeepScreenOnTile",
+ "settingsKeepScreenOnDialogTitle",
+ "settingsDoubleBackExit",
+ "settingsConfirmationTile",
+ "settingsConfirmationDialogTitle",
+ "settingsConfirmationBeforeDeleteItems",
+ "settingsConfirmationBeforeMoveToBinItems",
+ "settingsConfirmationBeforeMoveUndatedItems",
+ "settingsConfirmationAfterMoveToBinItems",
+ "settingsConfirmationVaultDataLoss",
+ "settingsNavigationDrawerTile",
+ "settingsNavigationDrawerEditorPageTitle",
+ "settingsNavigationDrawerBanner",
+ "settingsNavigationDrawerTabTypes",
+ "settingsNavigationDrawerTabAlbums",
+ "settingsNavigationDrawerTabPages",
+ "settingsNavigationDrawerAddAlbum",
+ "settingsThumbnailSectionTitle",
+ "settingsThumbnailOverlayTile",
+ "settingsThumbnailOverlayPageTitle",
+ "settingsThumbnailShowHdrIcon",
+ "settingsThumbnailShowFavouriteIcon",
+ "settingsThumbnailShowTagIcon",
+ "settingsThumbnailShowLocationIcon",
+ "settingsThumbnailShowMotionPhotoIcon",
+ "settingsThumbnailShowRating",
+ "settingsThumbnailShowRawIcon",
+ "settingsThumbnailShowVideoDuration",
+ "settingsCollectionQuickActionsTile",
+ "settingsCollectionQuickActionEditorPageTitle",
+ "settingsCollectionQuickActionTabBrowsing",
+ "settingsCollectionQuickActionTabSelecting",
+ "settingsCollectionBrowsingQuickActionEditorBanner",
+ "settingsCollectionSelectionQuickActionEditorBanner",
+ "settingsCollectionBurstPatternsTile",
+ "settingsCollectionBurstPatternsNone",
+ "settingsViewerSectionTitle",
+ "settingsViewerGestureSideTapNext",
+ "settingsViewerUseCutout",
+ "settingsViewerMaximumBrightness",
+ "settingsMotionPhotoAutoPlay",
+ "settingsImageBackground",
+ "settingsViewerQuickActionsTile",
+ "settingsViewerQuickActionEditorPageTitle",
+ "settingsViewerQuickActionEditorBanner",
+ "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle",
+ "settingsViewerQuickActionEditorAvailableButtonsSectionTitle",
+ "settingsViewerQuickActionEmpty",
+ "settingsViewerOverlayTile",
+ "settingsViewerOverlayPageTitle",
+ "settingsViewerShowOverlayOnOpening",
+ "settingsViewerShowHistogram",
+ "settingsViewerShowMinimap",
+ "settingsViewerShowInformation",
+ "settingsViewerShowInformationSubtitle",
+ "settingsViewerShowRatingTags",
+ "settingsViewerShowShootingDetails",
+ "settingsViewerShowDescription",
+ "settingsViewerShowOverlayThumbnails",
+ "settingsViewerEnableOverlayBlurEffect",
+ "settingsViewerSlideshowTile",
+ "settingsViewerSlideshowPageTitle",
+ "settingsSlideshowRepeat",
+ "settingsSlideshowShuffle",
+ "settingsSlideshowFillScreen",
+ "settingsSlideshowAnimatedZoomEffect",
+ "settingsSlideshowTransitionTile",
+ "settingsSlideshowIntervalTile",
+ "settingsSlideshowVideoPlaybackTile",
+ "settingsSlideshowVideoPlaybackDialogTitle",
+ "settingsVideoPageTitle",
+ "settingsVideoSectionTitle",
+ "settingsVideoShowVideos",
+ "settingsVideoPlaybackTile",
+ "settingsVideoPlaybackPageTitle",
+ "settingsVideoEnableHardwareAcceleration",
+ "settingsVideoAutoPlay",
+ "settingsVideoLoopModeTile",
+ "settingsVideoLoopModeDialogTitle",
+ "settingsVideoResumptionModeTile",
+ "settingsVideoResumptionModeDialogTitle",
+ "settingsVideoBackgroundMode",
+ "settingsVideoBackgroundModeDialogTitle",
+ "settingsVideoControlsTile",
+ "settingsVideoControlsPageTitle",
+ "settingsVideoButtonsTile",
+ "settingsVideoGestureDoubleTapTogglePlay",
+ "settingsVideoGestureSideDoubleTapSeek",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
+ "settingsSubtitleThemeTile",
+ "settingsSubtitleThemePageTitle",
+ "settingsSubtitleThemeSample",
+ "settingsSubtitleThemeTextAlignmentTile",
+ "settingsSubtitleThemeTextAlignmentDialogTitle",
+ "settingsSubtitleThemeTextPositionTile",
+ "settingsSubtitleThemeTextPositionDialogTitle",
+ "settingsSubtitleThemeTextSize",
+ "settingsSubtitleThemeShowOutline",
+ "settingsSubtitleThemeTextColor",
+ "settingsSubtitleThemeTextOpacity",
+ "settingsSubtitleThemeBackgroundColor",
+ "settingsSubtitleThemeBackgroundOpacity",
+ "settingsSubtitleThemeTextAlignmentLeft",
+ "settingsSubtitleThemeTextAlignmentCenter",
+ "settingsSubtitleThemeTextAlignmentRight",
+ "settingsPrivacySectionTitle",
+ "settingsAllowInstalledAppAccess",
+ "settingsAllowInstalledAppAccessSubtitle",
+ "settingsAllowErrorReporting",
+ "settingsSaveSearchHistory",
+ "settingsEnableBin",
+ "settingsEnableBinSubtitle",
+ "settingsDisablingBinWarningDialogMessage",
+ "settingsAllowMediaManagement",
+ "settingsHiddenItemsTile",
+ "settingsHiddenItemsPageTitle",
+ "settingsHiddenItemsTabFilters",
+ "settingsHiddenFiltersBanner",
+ "settingsHiddenFiltersEmpty",
+ "settingsHiddenItemsTabPaths",
+ "settingsHiddenPathsBanner",
+ "addPathTooltip",
+ "settingsStorageAccessTile",
+ "settingsStorageAccessPageTitle",
+ "settingsStorageAccessBanner",
+ "settingsStorageAccessEmpty",
+ "settingsStorageAccessRevokeTooltip",
+ "settingsAccessibilitySectionTitle",
+ "settingsRemoveAnimationsTile",
+ "settingsRemoveAnimationsDialogTitle",
+ "settingsTimeToTakeActionTile",
+ "settingsAccessibilityShowPinchGestureAlternatives",
+ "settingsDisplaySectionTitle",
+ "settingsThemeBrightnessTile",
+ "settingsThemeBrightnessDialogTitle",
+ "settingsThemeColorHighlights",
+ "settingsThemeEnableDynamicColor",
+ "settingsDisplayRefreshRateModeTile",
+ "settingsDisplayRefreshRateModeDialogTitle",
+ "settingsDisplayUseTvInterface",
+ "settingsLanguageSectionTitle",
+ "settingsLanguageTile",
+ "settingsLanguagePageTitle",
+ "settingsCoordinateFormatTile",
+ "settingsCoordinateFormatDialogTitle",
+ "settingsUnitSystemTile",
+ "settingsUnitSystemDialogTitle",
+ "settingsScreenSaverPageTitle",
+ "settingsWidgetPageTitle",
+ "settingsWidgetShowOutline",
+ "settingsWidgetOpenPage",
+ "settingsWidgetDisplayedItem",
+ "settingsCollectionTile",
+ "statsPageTitle",
+ "statsWithGps",
+ "statsTopCountriesSectionTitle",
+ "statsTopStatesSectionTitle",
+ "statsTopPlacesSectionTitle",
+ "statsTopTagsSectionTitle",
+ "statsTopAlbumsSectionTitle",
+ "viewerOpenPanoramaButtonLabel",
+ "viewerSetWallpaperButtonLabel",
+ "viewerErrorUnknown",
+ "viewerErrorDoesNotExist",
+ "viewerInfoPageTitle",
+ "viewerInfoBackToViewerTooltip",
+ "viewerInfoUnknown",
+ "viewerInfoLabelDescription",
+ "viewerInfoLabelTitle",
+ "viewerInfoLabelDate",
+ "viewerInfoLabelResolution",
+ "viewerInfoLabelSize",
+ "viewerInfoLabelUri",
+ "viewerInfoLabelPath",
+ "viewerInfoLabelDuration",
+ "viewerInfoLabelOwner",
+ "viewerInfoLabelCoordinates",
+ "viewerInfoLabelAddress",
+ "mapStyleDialogTitle",
+ "mapStyleTooltip",
+ "mapZoomInTooltip",
+ "mapZoomOutTooltip",
+ "mapPointNorthUpTooltip",
+ "mapAttributionOsmHot",
+ "mapAttributionStamen",
+ "openMapPageTooltip",
+ "mapEmptyRegion",
+ "viewerInfoOpenEmbeddedFailureFeedback",
+ "viewerInfoOpenLinkText",
+ "viewerInfoViewXmlLinkText",
+ "viewerInfoSearchFieldLabel",
+ "viewerInfoSearchEmpty",
+ "viewerInfoSearchSuggestionDate",
+ "viewerInfoSearchSuggestionDescription",
+ "viewerInfoSearchSuggestionDimensions",
+ "viewerInfoSearchSuggestionResolution",
+ "viewerInfoSearchSuggestionRights",
+ "wallpaperUseScrollEffect",
+ "tagEditorPageTitle",
+ "tagEditorPageNewTagFieldLabel",
+ "tagEditorPageAddTagTooltip",
+ "tagEditorSectionRecent",
+ "tagEditorSectionPlaceholders",
+ "tagEditorDiscardDialogMessage",
+ "tagPlaceholderCountry",
+ "tagPlaceholderState",
+ "tagPlaceholderPlace",
+ "panoramaEnableSensorControl",
+ "panoramaDisableSensorControl",
+ "sourceViewerPageTitle",
+ "filePickerShowHiddenFiles",
+ "filePickerDoNotShowHiddenFiles",
+ "filePickerOpenFrom",
+ "filePickerNoItems",
+ "filePickerUseThisFolder"
+ ],
+
"de": [
"entryActionCast",
"overlayHistogramNone",
@@ -1215,9 +1900,6 @@
"el": [
"entryActionCast",
- "overlayHistogramNone",
- "overlayHistogramRGB",
- "overlayHistogramLuminance",
"castDialogTitle",
"aboutDataUsageSectionTitle",
"aboutDataUsageData",
diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US
index 2060b1138..e08549f81 100644
--- a/whatsnew/whatsnew-en-US
+++ b/whatsnew/whatsnew-en-US
@@ -1,4 +1,4 @@
-In v1.10.3:
+In v1.10.4:
- customize your home page
- analyze your images with the histogram (for real this time)
Full changelog available on GitHub
\ No newline at end of file