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