Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-02-07 22:30:36 +01:00
commit 283a3eba60
31 changed files with 1044 additions and 201 deletions

View file

@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.10.4"></a>[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
## <a id="v1.10.3"></a>[v1.10.3] - 2024-01-29
### Added

View file

@ -121,6 +121,7 @@
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
tools:targetApi="tiramisu">
<activity
android:name=".MainActivity"

View file

@ -11,14 +11,13 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.GoogleDeviceContainer
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.doesPropPathExist
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.XMPPropName
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.xmp.GoogleDeviceContainer
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
import deckers.thibault.aves.metadata.xmp.XMPPropName
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
@ -108,14 +107,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
// which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
try {
container = xmpDirs.firstNotNullOfOrNull {
val xmpMeta = it.xmpMeta
if (xmpMeta.doesPropPathExist(listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) {
GoogleDeviceContainer().apply { findItems(xmpMeta) }
} else {
null
}
}
container = xmpDirs.firstNotNullOfOrNull { GoogleXMP.getDeviceContainer(it.xmpMeta) }
} catch (e: XMPException) {
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
return

View file

@ -50,16 +50,6 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.metadata.QuickTimeMetadata
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.doesPropExist
import deckers.thibault.aves.metadata.XMP.getPropArrayItemValues
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeInt
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.getSafeString
import deckers.thibault.aves.metadata.XMP.hasHdrGainMap
import deckers.thibault.aves.metadata.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_LAST_MODIFICATION_TIME_FORMAT
@ -78,6 +68,16 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP
import deckers.thibault.aves.metadata.xmp.XMP.doesPropExist
import deckers.thibault.aves.metadata.xmp.XMP.getPropArrayItemValues
import deckers.thibault.aves.metadata.xmp.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.xmp.XMP.getSafeInt
import deckers.thibault.aves.metadata.xmp.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.xmp.XMP.hasHdrGainMap
import deckers.thibault.aves.metadata.xmp.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.xmp.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.LogUtils
@ -1020,17 +1020,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) {
if (foundXmp && !allowMultiple) return
foundXmp = true
try {
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
xmpMeta.getSafeString(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
fields.putAll(GoogleXMP.getPanoramaInfo(xmpMeta))
}
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
@ -1062,7 +1052,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (fields.isEmpty()) {
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
} else {
fields["projectionType"] = fields["projectionType"] ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT
fields["projectionType"] = fields["projectionType"] ?: GoogleXMP.GPANO_PROJECTION_TYPE_DEFAULT
result.success(fields)
}
}

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import android.util.Log
import deckers.thibault.aves.metadata.xmp.XMP
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils

View file

@ -12,13 +12,11 @@ import android.util.Log
import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.metadata.XMP.countPropArrayItems
import deckers.thibault.aves.metadata.XMP.doesPropExist
import deckers.thibault.aves.metadata.XMP.getSafeLong
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
@ -276,20 +274,7 @@ object MultiPage {
var foundXmp = false
fun processXmp(xmpMeta: XMPMeta) {
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
// `GCamera` motion photo
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
} else if (xmpMeta.doesPropExist(XMP.GCONTAINER_DIRECTORY_PROP_NAME)) {
// `Container` motion photo
val count = xmpMeta.countPropArrayItems(XMP.GCONTAINER_DIRECTORY_PROP_NAME)
for (i in 1 until count + 1) {
val mime = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
if (MimeTypes.isVideo(mime) && length != null) {
offsetFromEnd = length.toLong()
}
}
}
offsetFromEnd = offsetFromEnd ?: GoogleXMP.getTrailingVideoOffsetFromEnd(xmpMeta)
}
try {

View file

@ -4,7 +4,7 @@ import com.drew.imaging.mp4.Mp4Handler
import com.drew.metadata.Metadata
import com.drew.metadata.mp4.Mp4Context
import com.drew.metadata.mp4.media.Mp4UuidBoxHandler
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.xmp.XMP
class SafeMp4UuidBoxHandler(metadata: Metadata) : Mp4UuidBoxHandler(metadata) {
override fun processBox(type: String?, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*> {

View file

@ -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<Int> = 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(

View file

@ -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<GoogleXMP>()
// 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 `<rdf:li rdf:parseType="Resource">`
val mime = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, attribute))?.value
// variant of `Container:Item` with `<rdf:li>`
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
}
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_widget_label">Fotoramme</string>
<string name="wallpaper">Baggrund</string>
<string name="videos_shortcut_short_label">Videoer</string>
<string name="analysis_channel_name">Mediascanning</string>
<string name="analysis_notification_default_title">Scanner medier</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="app_name">Aves</string>
<string name="safe_mode_shortcut_short_label">Sikker tilstand</string>
<string name="search_shortcut_short_label">Søg</string>
</resources>

View file

@ -0,0 +1,5 @@
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
<i>Aves</i> integrates with Android (from KitKat to Android 14, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.

View file

@ -0,0 +1 @@
Gallery and metadata explorer

View file

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

View file

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

1
lib/l10n/app_da.arb Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1332,5 +1332,11 @@
"cropAspectRatioSquare": "Τετράγωνο",
"@cropAspectRatioSquare": {},
"widgetTapUpdateWidget": "Ενημέρωση γραφικού στοιχείου",
"@widgetTapUpdateWidget": {}
"@widgetTapUpdateWidget": {},
"overlayHistogramNone": "Τίποτα",
"@overlayHistogramNone": {},
"overlayHistogramRGB": "RGB",
"@overlayHistogramRGB": {},
"overlayHistogramLuminance": "Φωτεινότητα",
"@overlayHistogramLuminance": {}
}

View file

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

View file

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

View file

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

View file

@ -59,6 +59,7 @@ class AvesApp extends StatefulWidget {
static final _unsupportedLocales = {
'bn', // Bengali
'ckb', // Kurdish (Central)
'da', // Danish
'fa', // Persian
'fi', // Finnish
'gl', // Galician

View file

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

View file

@ -90,7 +90,11 @@ class XmpNamespace extends Equatable {
List<Widget> 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<RegExp> get skippedProps => {};
List<XmpCardData> get cards => [];
String formatValue(XmpProp prop) => prop.value;

View file

@ -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<RegExp> skippedProps = {
// variant of `Container:Item` with `<rdf:li>`
RegExp(nsPrefix + r'Directory\[(\d+)\]/' + _rdfNsPrefix + r'type'),
};
@override
late final List<XmpCardData> cards = [
// variant of `Container:Item` with `<rdf:li rdf:parseType="Resource">`
XmpCardData(RegExp(nsPrefix + r'Directory\[(\d+)\]/' + nsPrefix + r'Item/(.*)'), title: 'Directory Item'),
// variant of `Container:Item` with `<rdf:li>`
XmpCardData(RegExp(nsPrefix + r'Directory\[(\d+)\]/(' + _gContainerItemNsPrefix + r'.*)'), title: 'Directory Item'),
];
}

View file

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

View file

@ -78,6 +78,8 @@ class PlatformReportService extends ReportService {
@override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async {
return _instance?.recordFlutterError(flutterErrorDetails);
if (!flutterErrorDetails.silent) {
return _instance?.recordFlutterError(flutterErrorDetails);
}
}
}

View file

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

View file

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

View file

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