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="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
|
## <a id="v1.10.3"></a>[v1.10.3] - 2024-01-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -121,6 +121,7 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|
|
@ -11,14 +11,13 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
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.Metadata
|
||||||
import deckers.thibault.aves.metadata.MultiPage
|
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.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.FieldMap
|
||||||
import deckers.thibault.aves.model.provider.ImageProvider
|
import deckers.thibault.aves.model.provider.ImageProvider
|
||||||
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
|
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
|
// which is returned as a second XMP directory
|
||||||
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
||||||
try {
|
try {
|
||||||
container = xmpDirs.firstNotNullOfOrNull {
|
container = xmpDirs.firstNotNullOfOrNull { GoogleXMP.getDeviceContainer(it.xmpMeta) }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
|
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
|
||||||
return
|
return
|
||||||
|
|
|
@ -50,16 +50,6 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper
|
||||||
import deckers.thibault.aves.metadata.MultiPage
|
import deckers.thibault.aves.metadata.MultiPage
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper
|
import deckers.thibault.aves.metadata.PixyMetaHelper
|
||||||
import deckers.thibault.aves.metadata.QuickTimeMetadata
|
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
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME
|
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_LAST_MODIFICATION_TIME_FORMAT
|
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.Helper.isPngTextDir
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
|
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
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.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
@ -1020,17 +1020,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) {
|
fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) {
|
||||||
if (foundXmp && !allowMultiple) return
|
if (foundXmp && !allowMultiple) return
|
||||||
foundXmp = true
|
foundXmp = true
|
||||||
try {
|
fields.putAll(GoogleXMP.getPanoramaInfo(xmpMeta))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
|
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
|
||||||
|
@ -1062,7 +1052,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
if (fields.isEmpty()) {
|
if (fields.isEmpty()) {
|
||||||
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
|
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
|
||||||
} else {
|
} else {
|
||||||
fields["projectionType"] = fields["projectionType"] ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT
|
fields["projectionType"] = fields["projectionType"] ?: GoogleXMP.GPANO_PROJECTION_TYPE_DEFAULT
|
||||||
result.success(fields)
|
result.success(fields)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.metadata
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
|
|
@ -12,13 +12,11 @@ import android.util.Log
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
import com.drew.imaging.jpeg.JpegSegmentType
|
import com.drew.imaging.jpeg.JpegSegmentType
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
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.Helper
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
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.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
@ -276,20 +274,7 @@ object MultiPage {
|
||||||
var foundXmp = false
|
var foundXmp = false
|
||||||
|
|
||||||
fun processXmp(xmpMeta: XMPMeta) {
|
fun processXmp(xmpMeta: XMPMeta) {
|
||||||
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
|
offsetFromEnd = offsetFromEnd ?: GoogleXMP.getTrailingVideoOffsetFromEnd(xmpMeta)
|
||||||
// `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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import com.drew.imaging.mp4.Mp4Handler
|
||||||
import com.drew.metadata.Metadata
|
import com.drew.metadata.Metadata
|
||||||
import com.drew.metadata.mp4.Mp4Context
|
import com.drew.metadata.mp4.Mp4Context
|
||||||
import com.drew.metadata.mp4.media.Mp4UuidBoxHandler
|
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) {
|
class SafeMp4UuidBoxHandler(metadata: Metadata) : Mp4UuidBoxHandler(metadata) {
|
||||||
override fun processBox(type: String?, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*> {
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
import deckers.thibault.aves.metadata.XMP.countPropPathArrayItems
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
import deckers.thibault.aves.metadata.xmp.XMP.countPropPathArrayItems
|
||||||
|
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
|
||||||
import deckers.thibault.aves.utils.indexOfBytes
|
import deckers.thibault.aves.utils.indexOfBytes
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
|
|
||||||
|
@ -15,12 +16,12 @@ class GoogleDeviceContainer {
|
||||||
private val offsets: MutableList<Int> = ArrayList()
|
private val offsets: MutableList<Int> = ArrayList()
|
||||||
|
|
||||||
fun findItems(xmpMeta: XMPMeta) {
|
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)
|
val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath)
|
||||||
for (i in 1 until count + 1) {
|
for (i in 1 until count + 1) {
|
||||||
val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_MIME_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, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull()
|
val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.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 dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
|
||||||
if (mimeType != null && length != null && dataUri != null) {
|
if (mimeType != null && length != null && dataUri != null) {
|
||||||
items.add(
|
items.add(
|
||||||
GoogleDeviceContainerItem(
|
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.content.Context
|
||||||
import android.net.Uri
|
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.XMPMetaFactory
|
||||||
import com.adobe.internal.xmp.properties.XMPProperty
|
import com.adobe.internal.xmp.properties.XMPProperty
|
||||||
import com.drew.metadata.Directory
|
import com.drew.metadata.Directory
|
||||||
|
import deckers.thibault.aves.metadata.Mp4ParserHelper
|
||||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
|
import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
|
||||||
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
|
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
|
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/"
|
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
|
||||||
|
|
||||||
// other namespaces
|
// 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 HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/"
|
||||||
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
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 GENERIC_LANG = ""
|
||||||
private const val SPECIFIC_LANG = "en-US"
|
private const val SPECIFIC_LANG = "en-US"
|
||||||
|
|
||||||
// embedded media data properties
|
fun isDataPath(path: String) = GoogleXMP.isDataPath(path)
|
||||||
// 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"
|
|
||||||
|
|
||||||
// HDR gain map
|
// HDR gain map
|
||||||
|
|
||||||
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
|
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
|
||||||
|
|
||||||
// panorama
|
// 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")
|
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,
|
// 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
|
// so we fall back to the native content resolver, if possible
|
||||||
fun checkHeic(
|
fun checkHeic(
|
||||||
|
@ -191,20 +132,10 @@ object XMP {
|
||||||
fun XMPMeta.hasHdrGainMap(): Boolean {
|
fun XMPMeta.hasHdrGainMap(): Boolean {
|
||||||
try {
|
try {
|
||||||
// standard HDR gain map
|
// standard HDR gain map
|
||||||
if (doesPropExist(HDRGM_VERSION_PROP_NAME)) {
|
if (doesPropExist(HDRGM_VERSION_PROP_NAME)) return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// `Ultra HDR`
|
// `Ultra HDR`
|
||||||
if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
|
if (GoogleXMP.isUltraHdPhoto(this)) return true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
|
@ -217,47 +148,11 @@ object XMP {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun XMPMeta.isMotionPhoto(): Boolean {
|
fun XMPMeta.isMotionPhoto() = GoogleXMP.isMotionPhoto(this)
|
||||||
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.isPanorama(): Boolean {
|
fun XMPMeta.isPanorama(): Boolean {
|
||||||
// Google
|
// Google
|
||||||
try {
|
if (GoogleXMP.isPanorama(this)) return true
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Photomatix
|
// Photomatix
|
||||||
try {
|
try {
|
|
@ -36,8 +36,8 @@ import deckers.thibault.aves.metadata.MultiPage
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper
|
import deckers.thibault.aves.metadata.PixyMetaHelper
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
||||||
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
|
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.metadataextractor.Helper
|
||||||
|
import deckers.thibault.aves.metadata.xmp.GoogleXMP
|
||||||
import deckers.thibault.aves.model.AvesEntry
|
import deckers.thibault.aves.model.AvesEntry
|
||||||
import deckers.thibault.aves.model.ExifOrientationOp
|
import deckers.thibault.aves.model.ExifOrientationOp
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
|
@ -982,15 +982,7 @@ abstract class ImageProvider {
|
||||||
)
|
)
|
||||||
val newTrailerOffset = trailerOffset + diff
|
val newTrailerOffset = trailerOffset + diff
|
||||||
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
|
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
|
||||||
xmp.replace(
|
GoogleXMP.updateTrailingVideoOffset(xmp, trailerOffset, newTrailerOffset)
|
||||||
// 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\"",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.RecoverableSecurityException
|
import android.app.RecoverableSecurityException
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -31,6 +32,7 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.io.SyncFailedException
|
import java.io.SyncFailedException
|
||||||
import java.util.*
|
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)
|
// `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
|
// in that case we try to use the MIME type provided along the URI
|
||||||
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
|
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
|
||||||
val width = cursor.getInt(widthColumn)
|
var width = cursor.getInt(widthColumn)
|
||||||
val height = cursor.getInt(heightColumn)
|
var height = cursor.getInt(heightColumn)
|
||||||
val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L
|
val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L
|
||||||
|
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
|
@ -238,6 +240,28 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
"contentId" to contentId,
|
"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)
|
if (MimeTypes.isRaw(mimeType)
|
||||||
|| (width <= 0 || height <= 0) && needSize(mimeType)
|
|| (width <= 0 || height <= 0) && needSize(mimeType)
|
||||||
|| durationMillis == 0L && needDuration
|
|| 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": "Τετράγωνο",
|
||||||
"@cropAspectRatioSquare": {},
|
"@cropAspectRatioSquare": {},
|
||||||
"widgetTapUpdateWidget": "Ενημέρωση γραφικού στοιχείου",
|
"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('v1s7', 'v1s7@users.noreply.hosted.weblate.org'),
|
||||||
Contributor('fuzfyy', 'egeozce35@gmail.com'),
|
Contributor('fuzfyy', 'egeozce35@gmail.com'),
|
||||||
Contributor('minh', 'teaminh@skiff.com'),
|
Contributor('minh', 'teaminh@skiff.com'),
|
||||||
|
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
||||||
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
||||||
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
||||||
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // 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('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
|
||||||
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
||||||
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
class XmpNamespaces {
|
class XmpNamespaces {
|
||||||
static const acdsee = 'http://ns.acdsee.com/iptc/1.0/';
|
static const acdsee = 'http://ns.acdsee.com/iptc/1.0/';
|
||||||
static const adsmlat = 'http://adsml.org/xmlns/';
|
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 avm = 'http://www.communicatingastronomy.org/avm/1.0/';
|
||||||
static const camera = 'http://pix4d.com/camera/1.0/';
|
static const camera = 'http://pix4d.com/camera/1.0/';
|
||||||
static const cc = 'http://creativecommons.org/ns#';
|
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 gAudio = 'http://ns.google.com/photos/1.0/audio/';
|
||||||
static const gCamera = 'http://ns.google.com/photos/1.0/camera/';
|
static const gCamera = 'http://ns.google.com/photos/1.0/camera/';
|
||||||
static const gContainer = 'http://ns.google.com/photos/1.0/container/';
|
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 gCreations = 'http://ns.google.com/photos/1.0/creations/';
|
||||||
static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/';
|
static const gDepth = 'http://ns.google.com/photos/1.0/depthmap/';
|
||||||
static const gDevice = 'http://ns.google.com/photos/dd/1.0/device/';
|
static const gDevice = 'http://ns.google.com/photos/dd/1.0/device/';
|
||||||
|
|
|
@ -7,6 +7,7 @@ class XmpNamespaceView {
|
||||||
XmpNamespaces.adsmlat: 'AdsML',
|
XmpNamespaces.adsmlat: 'AdsML',
|
||||||
XmpNamespaces.exifAux: 'Exif Aux',
|
XmpNamespaces.exifAux: 'Exif Aux',
|
||||||
XmpNamespaces.avm: 'Astronomy Visualization',
|
XmpNamespaces.avm: 'Astronomy Visualization',
|
||||||
|
XmpNamespaces.appleDesktop: 'Apple Desktop',
|
||||||
XmpNamespaces.camera: 'Pix4D Camera',
|
XmpNamespaces.camera: 'Pix4D Camera',
|
||||||
XmpNamespaces.cc: 'Creative Commons',
|
XmpNamespaces.cc: 'Creative Commons',
|
||||||
XmpNamespaces.crd: 'Camera Raw Defaults',
|
XmpNamespaces.crd: 'Camera Raw Defaults',
|
||||||
|
|
|
@ -59,6 +59,7 @@ class AvesApp extends StatefulWidget {
|
||||||
static final _unsupportedLocales = {
|
static final _unsupportedLocales = {
|
||||||
'bn', // Bengali
|
'bn', // Bengali
|
||||||
'ckb', // Kurdish (Central)
|
'ckb', // Kurdish (Central)
|
||||||
|
'da', // Danish
|
||||||
'fa', // Persian
|
'fa', // Persian
|
||||||
'fi', // Finnish
|
'fi', // Finnish
|
||||||
'gl', // Galician
|
'gl', // Galician
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class PageTransitionEffects {
|
class PageTransitionEffects {
|
||||||
|
@ -14,7 +15,7 @@ class PageTransitionEffects {
|
||||||
final position = (pageController.page! - index).clamp(-1.0, 1.0);
|
final position = (pageController.page! - index).clamp(-1.0, 1.0);
|
||||||
final width = pageController.position.viewportDimension;
|
final width = pageController.position.viewportDimension;
|
||||||
opacity = (1 - position.abs()).clamp(0, 1);
|
opacity = (1 - position.abs()).clamp(0, 1);
|
||||||
dx = position * width;
|
dx = position * width * (context.isRtl ? -1 : 1);
|
||||||
if (zoomIn) {
|
if (zoomIn) {
|
||||||
scale = 1 + position;
|
scale = 1 + position;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +43,7 @@ class PageTransitionEffects {
|
||||||
final position = (pageController.page! - index).clamp(-1.0, 1.0);
|
final position = (pageController.page! - index).clamp(-1.0, 1.0);
|
||||||
final width = pageController.position.viewportDimension;
|
final width = pageController.position.viewportDimension;
|
||||||
if (parallax) {
|
if (parallax) {
|
||||||
dx = position * width / 2;
|
dx = position * width / 2 * (context.isRtl ? -1 : 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ClipRect(
|
return ClipRect(
|
||||||
|
@ -64,7 +65,7 @@ class PageTransitionEffects {
|
||||||
final position = (pageController.page! - index).clamp(-1.0, 1.0);
|
final position = (pageController.page! - index).clamp(-1.0, 1.0);
|
||||||
final width = pageController.position.viewportDimension;
|
final width = pageController.position.viewportDimension;
|
||||||
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
|
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
|
||||||
dx = position * width;
|
dx = position * width * (context.isRtl ? -1 : 1);
|
||||||
}
|
}
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: opacity,
|
opacity: opacity,
|
||||||
|
|
|
@ -90,7 +90,11 @@ class XmpNamespace extends Equatable {
|
||||||
List<Widget> buildNamespaceSection(BuildContext context) {
|
List<Widget> buildNamespaceSection(BuildContext context) {
|
||||||
final props = rawProps.entries
|
final props = rawProps.entries
|
||||||
.map((kv) {
|
.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;
|
var extracted = false;
|
||||||
cards.forEach((card) => extracted |= card.extract(prop));
|
cards.forEach((card) => extracted |= card.extract(prop));
|
||||||
return extracted ? null : prop;
|
return extracted ? null : prop;
|
||||||
|
@ -134,6 +138,8 @@ class XmpNamespace extends Equatable {
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<RegExp> get skippedProps => {};
|
||||||
|
|
||||||
List<XmpCardData> get cards => [];
|
List<XmpCardData> get cards => [];
|
||||||
|
|
||||||
String formatValue(XmpProp prop) => prop.value;
|
String formatValue(XmpProp prop) => prop.value;
|
||||||
|
|
|
@ -73,11 +73,26 @@ class XmpGCameraNamespace extends XmpGoogleNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpGContainer extends XmpNamespace {
|
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
|
@override
|
||||||
late final List<XmpCardData> cards = [
|
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'),
|
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/view/view.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.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/controller.dart';
|
||||||
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/hero.dart';
|
import 'package:aves/widgets/viewer/hero.dart';
|
||||||
|
@ -420,11 +421,12 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFling(AxisDirection direction) {
|
void _onFling(AxisDirection direction) {
|
||||||
|
const animate = true;
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case AxisDirection.left:
|
case AxisDirection.left:
|
||||||
const ShowPreviousEntryNotification(animate: true).dispatch(context);
|
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
|
||||||
case AxisDirection.right:
|
case AxisDirection.right:
|
||||||
const ShowNextEntryNotification(animate: true).dispatch(context);
|
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
|
||||||
case AxisDirection.up:
|
case AxisDirection.up:
|
||||||
PopVisualNotification().dispatch(context);
|
PopVisualNotification().dispatch(context);
|
||||||
case AxisDirection.down:
|
case AxisDirection.down:
|
||||||
|
@ -437,11 +439,12 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
||||||
final x = alignment.x;
|
final x = alignment.x;
|
||||||
final sideRatio = _getSideRatio();
|
final sideRatio = _getSideRatio();
|
||||||
if (sideRatio != null) {
|
if (sideRatio != null) {
|
||||||
|
const animate = false;
|
||||||
if (x < sideRatio) {
|
if (x < sideRatio) {
|
||||||
const ShowPreviousEntryNotification(animate: false).dispatch(context);
|
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
|
||||||
return;
|
return;
|
||||||
} else if (x > 1 - sideRatio) {
|
} else if (x > 1 - sideRatio) {
|
||||||
const ShowNextEntryNotification(animate: false).dispatch(context);
|
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,8 @@ class PlatformReportService extends ReportService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async {
|
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
|
# - play changelog: /whatsnew/whatsnew-en-US
|
||||||
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
|
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
|
||||||
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
|
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
|
||||||
version: 1.10.3+112
|
version: 1.10.4+113
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -1204,6 +1204,691 @@
|
||||||
"settingsThumbnailShowHdrIcon"
|
"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": [
|
"de": [
|
||||||
"entryActionCast",
|
"entryActionCast",
|
||||||
"overlayHistogramNone",
|
"overlayHistogramNone",
|
||||||
|
@ -1215,9 +1900,6 @@
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
"entryActionCast",
|
"entryActionCast",
|
||||||
"overlayHistogramNone",
|
|
||||||
"overlayHistogramRGB",
|
|
||||||
"overlayHistogramLuminance",
|
|
||||||
"castDialogTitle",
|
"castDialogTitle",
|
||||||
"aboutDataUsageSectionTitle",
|
"aboutDataUsageSectionTitle",
|
||||||
"aboutDataUsageData",
|
"aboutDataUsageData",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
In v1.10.3:
|
In v1.10.4:
|
||||||
- customize your home page
|
- customize your home page
|
||||||
- analyze your images with the histogram (for real this time)
|
- analyze your images with the histogram (for real this time)
|
||||||
Full changelog available on GitHub
|
Full changelog available on GitHub
|
Loading…
Reference in a new issue