Merge branch 'develop'
This commit is contained in:
commit
283a3eba60
31 changed files with 1044 additions and 201 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<*> {
|
||||
|
|
|
@ -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(
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
12
android/app/src/main/res/values-da/strings.xml
Normal file
12
android/app/src/main/res/values-da/strings.xml
Normal 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>
|
5
fastlane/metadata/android/da/full_description.txt
Normal file
5
fastlane/metadata/android/da/full_description.txt
Normal 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>.
|
1
fastlane/metadata/android/da/short_description.txt
Normal file
1
fastlane/metadata/android/da/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Gallery and metadata explorer
|
4
fastlane/metadata/android/en-US/changelogs/113.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/113.txt
Normal 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
|
4
fastlane/metadata/android/en-US/changelogs/11301.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/11301.txt
Normal 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
1
lib/l10n/app_da.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -1332,5 +1332,11 @@
|
|||
"cropAspectRatioSquare": "Τετράγωνο",
|
||||
"@cropAspectRatioSquare": {},
|
||||
"widgetTapUpdateWidget": "Ενημέρωση γραφικού στοιχείου",
|
||||
"@widgetTapUpdateWidget": {}
|
||||
"@widgetTapUpdateWidget": {},
|
||||
"overlayHistogramNone": "Τίποτα",
|
||||
"@overlayHistogramNone": {},
|
||||
"overlayHistogramRGB": "RGB",
|
||||
"@overlayHistogramRGB": {},
|
||||
"overlayHistogramLuminance": "Φωτεινότητα",
|
||||
"@overlayHistogramLuminance": {}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -59,6 +59,7 @@ class AvesApp extends StatefulWidget {
|
|||
static final _unsupportedLocales = {
|
||||
'bn', // Bengali
|
||||
'ckb', // Kurdish (Central)
|
||||
'da', // Danish
|
||||
'fa', // Persian
|
||||
'fi', // Finnish
|
||||
'gl', // Galician
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue