diff --git a/.flutter b/.flutter
index 300451ada..54e66469a 160000
--- a/.flutter
+++ b/.flutter
@@ -1 +1 @@
-Subproject commit 300451adae589accbece3490f4396f10bdf15e6e
+Subproject commit 54e66469a933b60ddf175f858f82eaeb97e48c8d
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bda00713..76754fc1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.11.0] - 2024-05-01
+
+### Added
+
+- Cataloguing: identify Apple variant of HDR images
+- Collection: allow using hash (md5/sha1/sha256) when bulk renaming
+- Info: color palette
+- Video: external subtitle support (SRT)
+- option to force using western arabic numerals for dates
+
+### Changed
+
+- logo
+- upgraded Flutter to stable v3.19.6
+
+### Fixed
+
+- rendering of SVG with large header
+- stopping video playback when changing device orientation on Android >=13
+- printing content orientation according to page format
+
## [v1.10.9] - 2024-04-14
### Fixed
diff --git a/android/app/src/debug/res/values/colors.xml b/android/app/src/debug/res/values/colors.xml
index 206921163..023cc2034 100644
--- a/android/app/src/debug/res/values/colors.xml
+++ b/android/app/src/debug/res/values/colors.xml
@@ -1,4 +1,4 @@
- #7B1FA2
+ #815AFA
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
index 8cafc76b3..e46d31a46 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
@@ -153,13 +153,13 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
}
val pageIndex = id - 1
- val mpEntries = MultiPage.getJpegMpfEntries(context, uri)
+ val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex]
mpEntry.mimeType?.let { embedMimeType ->
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
- val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri)
+ val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
if (baseOffset != null) {
dataOffset += baseOffset
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
index 91170c56b..8819f24ff 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
@@ -20,6 +20,7 @@ import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.exif.GpsDirectory
+import com.drew.metadata.exif.makernotes.AppleMakernoteDirectory
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory
import com.drew.metadata.iptc.IptcDirectory
@@ -69,6 +70,8 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
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.SafeXmpReader
+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
@@ -82,6 +85,7 @@ 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.HashUtils
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
@@ -101,6 +105,7 @@ import org.json.JSONObject
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import java.text.ParseException
+import java.util.Locale
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@@ -392,6 +397,21 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// do not overwrite XMP parsed by metadata-extractor
// with raw XMP found by ExifInterface
allTags.remove(Metadata.DIR_XMP)
+ } else {
+ val xmpTags = allTags[Metadata.DIR_XMP]
+ if (xmpTags != null) {
+ val xmpRaw = xmpTags[ExifInterface.TAG_XMP]
+ if (xmpRaw != null) {
+ val metadata = com.drew.metadata.Metadata()
+ val xmpBytes = xmpRaw.toByteArray(Charsets.UTF_8)
+ SafeXmpReader().extract(xmpBytes, 0, xmpBytes.size, metadata, null)
+ metadata.getFirstDirectoryOfType(XmpDirectory::class.java)?.let { xmpDir ->
+ val dirMap = HashMap()
+ processXmp(xmpDir.xmpMeta, dirMap, allowMultiple = true)
+ allTags[Metadata.DIR_XMP] = dirMap
+ }
+ }
+ }
}
metadataMap.putAll(allTags.mapValues { it.value.toMutableMap() })
}
@@ -639,6 +659,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// JPEG Multi-Picture Format
if (metadata.getDirectoriesOfType(MpEntryDirectory::class.java).count { !it.entry.isThumbnail } > 1) {
flags = flags or MASK_IS_MULTIPAGE
+
+ if (hasAppleHdrGainMap(uri, sizeBytes, metadata)) {
+ flags = flags or MASK_IS_HDR
+ }
}
// XMP
@@ -765,6 +789,29 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
metadataMap[KEY_FLAGS] = flags
}
+ private fun hasAppleHdrGainMap(uri: Uri, sizeBytes: Long?, primaryMetadata: com.drew.metadata.Metadata): Boolean {
+ if (!primaryMetadata.containsDirectoryOfType(AppleMakernoteDirectory::class.java)) return false
+
+ val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes) ?: return false
+ mpEntries.filter { it.type == MpEntry.TYPE_UNDEFINED }.forEach { mpEntry ->
+ var dataOffset = mpEntry.dataOffset
+ if (dataOffset > 0) {
+ val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
+ if (baseOffset != null) {
+ dataOffset += baseOffset
+ }
+ }
+ StorageUtils.openInputStream(context, uri)?.let { input ->
+ input.skip(dataOffset)
+ val pageMetadata = Helper.safeRead(input)
+ if (pageMetadata.getDirectoriesOfType(XmpDirectory::class.java).any { it.xmpMeta.hasHdrGainMap() }) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
private fun getMultimediaCatalogMetadataByMediaMetadataRetriever(
mimeType: String,
uri: Uri,
@@ -1004,7 +1051,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} else {
when (mimeType) {
MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri)
- MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri)
+ MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri, sizeBytes)
MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri)
else -> null
}
@@ -1262,10 +1309,36 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
return
}
- val metadataMap = HashMap()
+ val metadataMap = HashMap()
+
+ val hashFields = fields.filter { it.startsWith(HASH_FIELD_PREFIX) }.toSet()
+ metadataMap.putAll(getHashFields(uri, mimeType, sizeBytes, hashFields))
+
+ val exifFields = fields.filterNot { hashFields.contains(it) }.toSet()
+ metadataMap.putAll(getExifFields(uri, mimeType, sizeBytes, exifFields))
+
+ result.success(metadataMap)
+ }
+
+ private fun getHashFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set): FieldMap {
+ val metadataMap = HashMap()
+ fields.forEach { field ->
+ val function = field.substringAfter(HASH_FIELD_PREFIX).lowercase(Locale.ROOT)
+ try {
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
+ metadataMap[field] = HashUtils.getHash(input, function)
+ }
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to get hash for mimeType=$mimeType uri=$uri function=$function", e)
+ }
+ }
+ return metadataMap
+ }
+
+ private fun getExifFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set): FieldMap {
+ val metadataMap = HashMap()
if (fields.isEmpty() || isVideo(mimeType)) {
- result.success(metadataMap)
- return
+ return metadataMap
}
var foundExif = false
@@ -1314,7 +1387,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
- result.success(metadataMap)
+ return metadataMap
}
companion object {
@@ -1389,6 +1462,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// additional media key
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture"
+ private const val HASH_FIELD_PREFIX = "hash"
private const val VALUE_SKIPPED_DATA = "[skipped]"
}
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
index 762e0e091..81da9f0b0 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
@@ -134,7 +134,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
return
}
- val trashDirs = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }
+ val trashDirs = context.getExternalFilesDirs(null).filterNotNull().mapNotNull { StorageUtils.trashDirFor(context, it.path) }
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.filterNotNull()?.mapNotNull { file -> file.path } ?: listOf() }
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt
index 32eaf7e25..6b3d7f9aa 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt
@@ -10,6 +10,7 @@ import com.caverock.androidsvg.PreserveAspectRatio
import com.caverock.androidsvg.RenderOptions
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
+import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
import deckers.thibault.aves.utils.BitmapUtils.getBytes
@@ -47,7 +48,7 @@ class SvgRegionFetcher internal constructor(
if (currentSvgRef == null) {
val newSvg = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
- SVG.getFromInputStream(input)
+ SVG.getFromInputStream(SVGParserBufferedInputStream(input))
} catch (ex: SVGParseException) {
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
return
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
index 98e44e135..7b5cdc2d1 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
@@ -146,7 +146,7 @@ class ThumbnailFetcher internal constructor(
return try {
var bitmap = target.get()
- if (needRotationAfterGlide(mimeType)) {
+ if (needRotationAfterGlide(mimeType, pageId)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
bitmap
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
index 9fd0d45a0..7d442e98b 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
@@ -279,6 +279,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
companion object {
private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/activity_result_stream"
- private const val BUFFER_SIZE = 2 shl 17 // 256kB
+ private const val BUFFER_SIZE = 1 shl 18 // 256kB
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
index 54fc04f9d..2cb61ad99 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
@@ -145,7 +145,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
.submit()
try {
var bitmap = withContext(Dispatchers.IO) { target.get() }
- if (needRotationAfterGlide(mimeType)) {
+ if (needRotationAfterGlide(mimeType, pageId)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
if (bitmap != null) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
index d544ba203..0a8cef472 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
@@ -18,6 +18,7 @@ import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
+import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.StorageUtils
import kotlin.math.ceil
@@ -52,7 +53,7 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)
val bitmap: Bitmap? = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
- SVG.getFromInputStream(input)?.let { svg ->
+ SVG.getFromInputStream(SVGParserBufferedInputStream(input))?.let { svg ->
svg.normalizeSize()
val viewBox = svg.documentViewBox
val svgWidth = viewBox.width()
@@ -60,7 +61,7 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)
val bitmapWidth: Int
val bitmapHeight: Int
- if (width / height > svgWidth / svgHeight) {
+ if (width / height.toFloat() > svgWidth / svgHeight) {
bitmapWidth = ceil(svgWidth * height / svgHeight).toInt()
bitmapHeight = height
} else {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt
index c96631bf9..0671237a8 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/TiffGlideModule.kt
@@ -63,7 +63,7 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
TiffBitmapFactory.decodeFileDescriptor(fd, options)
val imageWidth = options.outWidth
val imageHeight = options.outHeight
- if (imageHeight > height || imageWidth > width) {
+ if (imageWidth > width || imageHeight > height) {
while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
sampleSize *= 2
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt
index f13a85f8e..ddb1e7096 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt
@@ -1,8 +1,11 @@
package deckers.thibault.aves.decoder
import android.content.Context
+import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.Registry
@@ -16,7 +19,9 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey
+import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
+import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -24,6 +29,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.io.InputStream
+import kotlin.math.ceil
+import kotlin.math.roundToInt
@GlideModule
class VideoThumbnailGlideModule : LibraryGlideModule() {
@@ -36,7 +43,7 @@ class VideoThumbnail(val context: Context, val uri: Uri)
internal class VideoThumbnailLoader : ModelLoader {
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData {
- return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
+ return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model, width, height))
}
override fun handles(model: VideoThumbnail): Boolean = true
@@ -48,7 +55,7 @@ internal class VideoThumbnailLoader : ModelLoader {
}
}
-internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher {
+internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val width: Int, val height: Int) : DataFetcher {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun loadData(priority: Priority, callback: DataCallback) {
@@ -68,10 +75,62 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
if (durationMillis != null) {
timeMillis = if (durationMillis < 15000) 0 else 15000
}
- val frame = if (timeMillis != null) {
- retriever.getFrameAtTime(timeMillis * 1000)
+ val timeMicros = if (timeMillis != null) timeMillis * 1000 else -1
+ val option = MediaMetadataRetriever.OPTION_CLOSEST_SYNC
+
+ var videoWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toFloatOrNull()
+ var videoHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toFloatOrNull()
+ if (videoWidth == null || videoHeight == null) {
+ throw Exception("failed to get video dimensions")
+ }
+
+ var dstWidth = 0
+ var dstHeight = 0
+ if (width > 0 && height > 0) {
+ val rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull()
+ if (rotationDegrees != null) {
+ val isRotated = rotationDegrees % 180 == 90
+ if (isRotated) {
+ val temp = videoWidth
+ videoWidth = videoHeight
+ videoHeight = temp
+ }
+
+ // cover fit
+ val videoAspectRatio = videoWidth / videoHeight
+ if (videoWidth > width || videoHeight > height) {
+ if (width / height.toFloat() > videoAspectRatio) {
+ dstHeight = ceil(videoHeight * width / videoWidth).toInt()
+ dstWidth = (dstHeight * videoAspectRatio).roundToInt()
+ } else {
+ dstWidth = ceil(videoWidth * height / videoHeight).toInt()
+ dstHeight = (dstWidth / videoAspectRatio).roundToInt()
+ }
+ }
+ }
+ }
+
+ // the returned frame is already rotated according to the video metadata
+ val frame = if (dstWidth > 0 && dstHeight > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ val targetBitmapSizeBytes: Long = FORMAT_BYTE_SIZE.toLong() * dstWidth * dstHeight
+ if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
+ throw Exception("not enough memory to allocate $targetBitmapSizeBytes bytes for the scaled frame at $dstWidth x $dstHeight")
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight, getBitmapParams())
+ } else {
+ retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight)
+ }
} else {
- retriever.frameAtTime
+ val targetBitmapSizeBytes: Long = (FORMAT_BYTE_SIZE.toLong() * videoWidth * videoHeight).toLong()
+ if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
+ throw Exception("not enough memory to allocate $targetBitmapSizeBytes bytes for the full frame at $videoWidth x $videoHeight")
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ retriever.getFrameAtTime(timeMicros, option, getBitmapParams())
+ } else {
+ retriever.getFrameAtTime(timeMicros, option)
+ }
}
bytes = frame?.getBytes(canHaveAlpha = false, recycle = false)
}
@@ -91,6 +150,17 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
}
}
+ @RequiresApi(Build.VERSION_CODES.P)
+ private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
+ // for wide-gamut and HDR content which does not require alpha blending
+ setPreferredConfig(Bitmap.Config.RGBA_1010102)
+ } else {
+ setPreferredConfig(Bitmap.Config.ARGB_8888)
+ }
+ }
+
// already cleaned up in loadData and ByteArrayInputStream will be GC'd
override fun cleanup() {}
@@ -100,4 +170,9 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
override fun getDataClass(): Class = InputStream::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
+
+ companion object {
+ // same for either `ARGB_8888` or `RGBA_1010102`
+ private const val FORMAT_BYTE_SIZE = BitmapUtils.ARGB_8888_BYTE_SIZE
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
index 65876c72c..2a7d679ac 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
@@ -9,10 +9,15 @@ import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.util.Log
+import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType
+import com.drew.metadata.exif.ExifDirectoryBase
+import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.xmp.XmpDirectory
+import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt
import deckers.thibault.aves.metadata.metadataextractor.Helper
+import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP
@@ -20,6 +25,7 @@ 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
+import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.indexOfBytes
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
@@ -83,13 +89,58 @@ object MultiPage {
return tracks
}
+ private fun getJpegMpfPrimaryRotation(context: Context, uri: Uri, sizeBytes: Long): Int {
+ val mimeType = MimeTypes.JPEG
+ var rotationDegrees = 0
+
+ var foundExif = false
+ if (canReadWithMetadataExtractor(mimeType)) {
+ try {
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
+ val metadata = Helper.safeRead(input)
+ foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
+ for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
+ dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) {
+ rotationDegrees = Metadata.getRotationDegreesForExifCode(it)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
+ } catch (e: NoClassDefFoundError) {
+ Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
+ } catch (e: AssertionError) {
+ Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
+ }
+ }
+
+ if (!foundExif) {
+ // fallback to read EXIF via ExifInterface
+ try {
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
+ val exif = ExifInterface(input)
+ exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) {
+ rotationDegrees = exif.rotationDegrees
+ }
+ }
+ } catch (e: Exception) {
+ // ExifInterface initialization can fail with a RuntimeException
+ // caused by an internal MediaMetadataRetriever failure
+ Log.w(LOG_TAG, "failed to get metadata by ExifInterface for mimeType=$mimeType uri=$uri", e)
+ }
+ }
+
+ return rotationDegrees
+ }
+
// starts after `[APP2 marker (1 byte)] [segment size (2 bytes)] [MPF marker (4 bytes)]`
- fun getJpegMpfBaseOffset(context: Context, uri: Uri): Int? {
+ fun getJpegMpfBaseOffset(context: Context, uri: Uri, sizeBytes: Long?): Int? {
+ val mimeType = MimeTypes.JPEG
val app2Marker = JpegSegmentType.APP2.byteValue
val mpfMarker = "MPF".toByteArray() + 0x00
try {
- Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input ->
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
var offset = 0
while (true) {
do {
@@ -113,9 +164,10 @@ object MultiPage {
return null
}
- fun getJpegMpfEntries(context: Context, uri: Uri): List? {
+ fun getJpegMpfEntries(context: Context, uri: Uri, sizeBytes: Long?): List? {
+ val mimeType = MimeTypes.JPEG
try {
- Metadata.openSafeInputStream(context, uri, MimeTypes.JPEG, null)?.use { input ->
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input)
return metadata.getDirectoriesOfType(MpEntryDirectory::class.java).map { it.entry }
}
@@ -129,10 +181,12 @@ object MultiPage {
return null
}
- fun getJpegMpfPages(context: Context, uri: Uri): ArrayList {
+ fun getJpegMpfPages(context: Context, uri: Uri, sizeBytes: Long): ArrayList {
+ val primaryRotation = getJpegMpfPrimaryRotation(context, uri, sizeBytes)
+
val pages = ArrayList()
- val baseOffset = getJpegMpfBaseOffset(context, uri)
- val mpEntries = getJpegMpfEntries(context, uri)
+ val baseOffset = getJpegMpfBaseOffset(context, uri, sizeBytes)
+ val mpEntries = getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && baseOffset != null) {
for ((pageIndex, mpEntry) in mpEntries.withIndex()) {
mpEntry.mimeType?.let { embedMimeType ->
@@ -140,8 +194,7 @@ object MultiPage {
KEY_PAGE to pageIndex,
KEY_MIME_TYPE to embedMimeType,
KEY_IS_DEFAULT to (pageIndex == 0),
- // TODO TLAD [MPF] page[KEY_ROTATION_DEGREES] = same as primary
- KEY_ROTATION_DEGREES to 0,
+ KEY_ROTATION_DEGREES to primaryRotation,
)
var dataOffset = mpEntry.dataOffset
@@ -167,12 +220,12 @@ object MultiPage {
}
fun getJpegMpfBitmap(context: Context, uri: Uri, pageIndex: Int): Bitmap? {
- val mpEntries = getJpegMpfEntries(context, uri)
+ val mpEntries = getJpegMpfEntries(context, uri, null)
if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex]
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
- val baseOffset = getJpegMpfBaseOffset(context, uri)
+ val baseOffset = getJpegMpfBaseOffset(context, uri, null)
if (baseOffset != null) {
dataOffset += baseOffset
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SvgHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SvgHelper.kt
index 08efc7bb5..da90f0c0e 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SvgHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SvgHelper.kt
@@ -1,6 +1,9 @@
package deckers.thibault.aves.metadata
import com.caverock.androidsvg.SVG
+import java.io.BufferedInputStream
+import java.io.InputStream
+import kotlin.math.max
object SvgHelper {
fun SVG.normalizeSize() {
@@ -10,4 +13,19 @@ object SvgHelper {
setDocumentWidth("100%")
setDocumentHeight("100%")
}
-}
\ No newline at end of file
+}
+
+// As of AndroidSVG v1.4, SVGParser.ENTITY_WATCH_BUFFER_SIZE is set at 4096.
+// This constant is not configurable and used for the internal buffer mark read limit.
+// Parsing will fail if the SVG header is larger than this value.
+// So we define and apply a minimum read limit.
+class SVGParserBufferedInputStream(input: InputStream) : BufferedInputStream(input) {
+ @Synchronized
+ override fun mark(readlimit: Int) {
+ super.mark(max(MINIMUM_READ_LIMIT, readlimit))
+ }
+
+ companion object {
+ private const val MINIMUM_READ_LIMIT = 1 shl 14 // 16kB
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt
index 47a26ca7d..b7d49f9f9 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/xmp/XMP.kt
@@ -40,6 +40,7 @@ object XMP {
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
// other namespaces
+ private const val APPLE_HDRGM_NS_URI = "http://ns.apple.com/HDRGainMap/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"
@@ -59,6 +60,7 @@ object XMP {
// HDR gain map
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
+ private val APPLE_HDRGM_VERSION_PROP_NAME = XMPPropName(APPLE_HDRGM_NS_URI, "HDRGainMapVersion")
// panorama
@@ -137,6 +139,9 @@ object XMP {
// `Ultra HDR`
if (GoogleXMP.isUltraHdPhoto(this)) return true
+ // Apple HDR gain map
+ if (doesPropExist(APPLE_HDRGM_VERSION_PROP_NAME)) return true
+
return false
} catch (e: XMPException) {
if (e.errorCode != XMPError.BADSCHEMA) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index e3c654741..cc54bd459 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -334,7 +334,7 @@ abstract class ImageProvider {
.load(model)
.submit(targetWidthPx, targetHeightPx)
var bitmap = withContext(Dispatchers.IO) { target.get() }
- if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
+ if (MimeTypes.needRotationAfterGlide(sourceMimeType, pageId)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
}
bitmap ?: throw Exception("failed to get image for mimeType=$sourceMimeType uri=$sourceUri page=$pageId")
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/HashUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/HashUtils.kt
new file mode 100644
index 000000000..c90e317fa
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/HashUtils.kt
@@ -0,0 +1,35 @@
+package deckers.thibault.aves.utils
+
+import java.io.InputStream
+import java.math.BigInteger
+import java.security.MessageDigest
+
+object HashUtils {
+ fun getHash(input: InputStream, algorithmKey: String): String {
+ val algorithm = toMessageDigestAlgorithm(algorithmKey)
+ val digest = MessageDigest.getInstance(algorithm)
+ val buffer = ByteArray(1 shl 14)
+ var read: Int
+ while ((input.read(buffer).also { read = it }) > 0) {
+ digest.update(buffer, 0, read)
+ }
+ val md5sum = digest.digest()
+ val output = BigInteger(1, md5sum).toString(16)
+
+ return when (algorithm) {
+ "MD5" -> output.padStart(32, '0') // 128 bits = 32 hex digits
+ "SHA-1" -> output.padStart(40, '0') // 160 bits = 40 hex digits
+ "SHA-256" -> output.padStart(64, '0') // 256 bits = 64 hex digits
+ else -> throw IllegalArgumentException("unsupported hash algorithm: $algorithmKey")
+ }
+ }
+
+ private fun toMessageDigestAlgorithm(algorithmKey: String): String {
+ return when (algorithmKey) {
+ "md5" -> "MD5"
+ "sha1" -> "SHA-1"
+ "sha256" -> "SHA-256"
+ else -> throw IllegalArgumentException("unsupported hash algorithm: $algorithmKey")
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
index 9d219a7a2..645fa47a1 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
@@ -2,6 +2,7 @@ package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface
+import deckers.thibault.aves.decoder.MultiPageImage
object MimeTypes {
const val ANY = "*/*"
@@ -137,9 +138,13 @@ object MimeTypes {
// but we need to rotate the decoded bitmap for the other formats
// maybe related to ExifInterface version used by Glide:
// https://github.com/bumptech/glide/blob/master/gradle.properties#L21
- fun needRotationAfterGlide(mimeType: String) = when (mimeType) {
- DNG, HEIC, HEIF, PNG, WEBP -> true
- else -> false
+ fun needRotationAfterGlide(mimeType: String, pageId: Int?): Boolean {
+ return if (pageId != null && MultiPageImage.isSupported(mimeType)) {
+ true
+ } else when (mimeType) {
+ DNG, HEIC, HEIF, PNG, WEBP -> true
+ else -> false
+ }
}
// Thumbnails obtained from the Media Store are automatically rotated
diff --git a/android/app/src/main/res/drawable-nodpi/banner.png b/android/app/src/main/res/drawable-nodpi/banner.png
index c33433d6c..ac4ec599c 100644
Binary files a/android/app/src/main/res/drawable-nodpi/banner.png and b/android/app/src/main/res/drawable-nodpi/banner.png differ
diff --git a/android/app/src/main/res/drawable-v21/ic_notification.xml b/android/app/src/main/res/drawable-v21/ic_notification.xml
index a9fdcd46f..480be0b2c 100644
--- a/android/app/src/main/res/drawable-v21/ic_notification.xml
+++ b/android/app/src/main/res/drawable-v21/ic_notification.xml
@@ -4,23 +4,35 @@
android:viewportWidth="100"
android:viewportHeight="100">
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
index 0933093c4..aed5a0c30 100644
--- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,30 +1,38 @@
-
-
-
-
+ android:width="100dp"
+ android:height="100dp"
+ android:viewportWidth="100"
+ android:viewportHeight="100">
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_launcher_mono.xml b/android/app/src/main/res/drawable/ic_launcher_mono.xml
index 738ecc287..553be0af1 100644
--- a/android/app/src/main/res/drawable/ic_launcher_mono.xml
+++ b/android/app/src/main/res/drawable/ic_launcher_mono.xml
@@ -3,30 +3,36 @@
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
-
-
-
-
-
-
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 18c76a697..7c532b798 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 644a73bb4..740704177 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 2764beb47..f9bbc54f3 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 6cccc47ff..c31695eae 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 7e9003c34..e3491dad8 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index c70ef8f6b..8e5ee2bb1 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 70801fc46..51adec063 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 335d15d58..b11aeb86f 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4f856ac51..8da583ba4 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 0552c72b5..8b7999bd8 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 6e3fbd50f..a77f5f66a 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -3,5 +3,5 @@
#FFFFFF
#FFFFFF
#455A64
- #3f51b5
+ #1cc8eb
\ No newline at end of file
diff --git a/android/app/src/profile/res/values/colors.xml b/android/app/src/profile/res/values/colors.xml
index f5bfedf7c..9981f6cd3 100644
--- a/android/app/src/profile/res/values/colors.xml
+++ b/android/app/src/profile/res/values/colors.xml
@@ -1,4 +1,4 @@
- #D32F2F
+ #007A78
\ No newline at end of file
diff --git a/aves_logo.svg b/aves_logo.svg
index a3c89f30d..327ddbe1d 100644
--- a/aves_logo.svg
+++ b/aves_logo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/fastlane/metadata/android/ar/images/featureGraphic.png b/fastlane/metadata/android/ar/images/featureGraphic.png
index 7204bd0d6..9bc5c9971 100644
Binary files a/fastlane/metadata/android/ar/images/featureGraphic.png and b/fastlane/metadata/android/ar/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/be/images/featureGraphic.png b/fastlane/metadata/android/be/images/featureGraphic.png
index c1455009b..24e0a97c3 100644
Binary files a/fastlane/metadata/android/be/images/featureGraphic.png and b/fastlane/metadata/android/be/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/ca/images/featureGraphic.png b/fastlane/metadata/android/ca/images/featureGraphic.png
old mode 100644
new mode 100755
index ad29dfe39..405897a43
Binary files a/fastlane/metadata/android/ca/images/featureGraphic.png and b/fastlane/metadata/android/ca/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/cs/images/featureGraphic.png b/fastlane/metadata/android/cs/images/featureGraphic.png
old mode 100644
new mode 100755
index a0b3a3e77..c18ccc0a5
Binary files a/fastlane/metadata/android/cs/images/featureGraphic.png and b/fastlane/metadata/android/cs/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/de/images/featureGraphic.png b/fastlane/metadata/android/de/images/featureGraphic.png
old mode 100644
new mode 100755
index a0b3a3e77..c18ccc0a5
Binary files a/fastlane/metadata/android/de/images/featureGraphic.png and b/fastlane/metadata/android/de/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/el/images/featureGraphic.png b/fastlane/metadata/android/el/images/featureGraphic.png
index eaadbbef5..e5664dd9d 100644
Binary files a/fastlane/metadata/android/el/images/featureGraphic.png and b/fastlane/metadata/android/el/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/changelogs/119.txt b/fastlane/metadata/android/en-US/changelogs/119.txt
new file mode 100644
index 000000000..d0a2a5a14
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/119.txt
@@ -0,0 +1,3 @@
+In v1.11.0:
+- watch videos with SRT subtitle files
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/11901.txt b/fastlane/metadata/android/en-US/changelogs/11901.txt
new file mode 100644
index 000000000..d0a2a5a14
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/11901.txt
@@ -0,0 +1,3 @@
+In v1.11.0:
+- watch videos with SRT subtitle files
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
old mode 100644
new mode 100755
index c3eeb06dc..1ccb76900
Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
index 3b7a936cc..2e75b0042 100644
Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/es-MX/images/featureGraphic.png b/fastlane/metadata/android/es-MX/images/featureGraphic.png
old mode 100644
new mode 100755
index 6bcf1d781..be1a1f7eb
Binary files a/fastlane/metadata/android/es-MX/images/featureGraphic.png and b/fastlane/metadata/android/es-MX/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/eu/images/featureGraphic.png b/fastlane/metadata/android/eu/images/featureGraphic.png
old mode 100644
new mode 100755
index ad29dfe39..405897a43
Binary files a/fastlane/metadata/android/eu/images/featureGraphic.png and b/fastlane/metadata/android/eu/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/fr/images/featureGraphic.png b/fastlane/metadata/android/fr/images/featureGraphic.png
old mode 100644
new mode 100755
index a0b3a3e77..c18ccc0a5
Binary files a/fastlane/metadata/android/fr/images/featureGraphic.png and b/fastlane/metadata/android/fr/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/hu/images/featureGraphic.png b/fastlane/metadata/android/hu/images/featureGraphic.png
index 00071b077..f3a5ff209 100644
Binary files a/fastlane/metadata/android/hu/images/featureGraphic.png and b/fastlane/metadata/android/hu/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/id/images/featureGraphic.png b/fastlane/metadata/android/id/images/featureGraphic.png
index 11d0ca7af..293632231 100644
Binary files a/fastlane/metadata/android/id/images/featureGraphic.png and b/fastlane/metadata/android/id/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/is/images/featureGraphic.png b/fastlane/metadata/android/is/images/featureGraphic.png
index d86ecedbc..2b65e244b 100644
Binary files a/fastlane/metadata/android/is/images/featureGraphic.png and b/fastlane/metadata/android/is/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/it/images/featureGraphic.png b/fastlane/metadata/android/it/images/featureGraphic.png
index 3d0cd2b2e..6afae6d6e 100644
Binary files a/fastlane/metadata/android/it/images/featureGraphic.png and b/fastlane/metadata/android/it/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/ja/images/featureGraphic.png b/fastlane/metadata/android/ja/images/featureGraphic.png
index 0b3824d0e..1d72a07d0 100644
Binary files a/fastlane/metadata/android/ja/images/featureGraphic.png and b/fastlane/metadata/android/ja/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/ko/images/featureGraphic.png b/fastlane/metadata/android/ko/images/featureGraphic.png
old mode 100644
new mode 100755
index cb81a914d..a926db4f9
Binary files a/fastlane/metadata/android/ko/images/featureGraphic.png and b/fastlane/metadata/android/ko/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/lt/images/featureGraphic.png b/fastlane/metadata/android/lt/images/featureGraphic.png
index eb0819532..d14be1c9e 100644
Binary files a/fastlane/metadata/android/lt/images/featureGraphic.png and b/fastlane/metadata/android/lt/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/nb-NO/images/featureGraphic.png b/fastlane/metadata/android/nb-NO/images/featureGraphic.png
index e9e800f6b..2d8ec0710 100644
Binary files a/fastlane/metadata/android/nb-NO/images/featureGraphic.png and b/fastlane/metadata/android/nb-NO/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/nl/images/featureGraphic.png b/fastlane/metadata/android/nl/images/featureGraphic.png
index ec6a7efdc..e692101ad 100644
Binary files a/fastlane/metadata/android/nl/images/featureGraphic.png and b/fastlane/metadata/android/nl/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/nn/images/featureGraphic.png b/fastlane/metadata/android/nn/images/featureGraphic.png
index e9e800f6b..2d8ec0710 100644
Binary files a/fastlane/metadata/android/nn/images/featureGraphic.png and b/fastlane/metadata/android/nn/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/pl/images/featureGraphic.png b/fastlane/metadata/android/pl/images/featureGraphic.png
old mode 100644
new mode 100755
index ad29dfe39..405897a43
Binary files a/fastlane/metadata/android/pl/images/featureGraphic.png and b/fastlane/metadata/android/pl/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/featureGraphic.png b/fastlane/metadata/android/pt-BR/images/featureGraphic.png
old mode 100644
new mode 100755
index 677f87431..405897a43
Binary files a/fastlane/metadata/android/pt-BR/images/featureGraphic.png and b/fastlane/metadata/android/pt-BR/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/ro/images/featureGraphic.png b/fastlane/metadata/android/ro/images/featureGraphic.png
old mode 100644
new mode 100755
index ad29dfe39..405897a43
Binary files a/fastlane/metadata/android/ro/images/featureGraphic.png and b/fastlane/metadata/android/ro/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/ru/images/featureGraphic.png b/fastlane/metadata/android/ru/images/featureGraphic.png
old mode 100644
new mode 100755
index 5fa1a582a..ada0f6087
Binary files a/fastlane/metadata/android/ru/images/featureGraphic.png and b/fastlane/metadata/android/ru/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/sk/images/featureGraphic.png b/fastlane/metadata/android/sk/images/featureGraphic.png
index 00071b077..f3a5ff209 100644
Binary files a/fastlane/metadata/android/sk/images/featureGraphic.png and b/fastlane/metadata/android/sk/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/tr/images/featureGraphic.png b/fastlane/metadata/android/tr/images/featureGraphic.png
index 11d0ca7af..293632231 100644
Binary files a/fastlane/metadata/android/tr/images/featureGraphic.png and b/fastlane/metadata/android/tr/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/uk/images/featureGraphic.png b/fastlane/metadata/android/uk/images/featureGraphic.png
old mode 100644
new mode 100755
index 5fa1a582a..ada0f6087
Binary files a/fastlane/metadata/android/uk/images/featureGraphic.png and b/fastlane/metadata/android/uk/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/vi/images/featureGraphic.png b/fastlane/metadata/android/vi/images/featureGraphic.png
index e63150519..8529af752 100644
Binary files a/fastlane/metadata/android/vi/images/featureGraphic.png and b/fastlane/metadata/android/vi/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/zh-CN/images/featureGraphic.png b/fastlane/metadata/android/zh-CN/images/featureGraphic.png
index 9b6d5510b..2194bc4e0 100644
Binary files a/fastlane/metadata/android/zh-CN/images/featureGraphic.png and b/fastlane/metadata/android/zh-CN/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/zh-Hant/images/featureGraphic.png b/fastlane/metadata/android/zh-Hant/images/featureGraphic.png
index 9b0a090c9..e5a223fd6 100644
Binary files a/fastlane/metadata/android/zh-Hant/images/featureGraphic.png and b/fastlane/metadata/android/zh-Hant/images/featureGraphic.png differ
diff --git a/l10n.yaml b/l10n.yaml
index e01189e57..1a10e8fda 100644
--- a/l10n.yaml
+++ b/l10n.yaml
@@ -8,5 +8,3 @@
preferred-supported-locales:
- en
-
-untranslated-messages-file: untranslated.json
diff --git a/lib/convert/metadata/fields.dart b/lib/convert/metadata/fields.dart
index a0509b563..659f6bcca 100644
--- a/lib/convert/metadata/fields.dart
+++ b/lib/convert/metadata/fields.dart
@@ -53,23 +53,30 @@ extension ExtraMetadataFieldConvert on MetadataField {
return MetadataType.mp4;
case MetadataField.xmpXmpCreateDate:
return MetadataType.xmp;
+ case MetadataField.hashMd5:
+ case MetadataField.hashSha1:
+ case MetadataField.hashSha256:
+ return MetadataType.file;
}
}
String? get toPlatform {
- if (type == MetadataType.exif) {
- return _toExifInterfaceTag();
- } else {
- switch (this) {
- case MetadataField.mp4GpsCoordinates:
- return 'gpsCoordinates';
- case MetadataField.mp4RotationDegrees:
- return 'rotationDegrees';
- case MetadataField.mp4Xmp:
- return 'xmp';
- default:
- return null;
- }
+ switch (type) {
+ case MetadataType.exif:
+ return _toExifInterfaceTag();
+ case MetadataType.file:
+ return name;
+ default:
+ switch (this) {
+ case MetadataField.mp4GpsCoordinates:
+ return 'gpsCoordinates';
+ case MetadataField.mp4RotationDegrees:
+ return 'rotationDegrees';
+ case MetadataField.mp4Xmp:
+ return 'xmp';
+ default:
+ return null;
+ }
}
}
diff --git a/lib/convert/metadata/metadata_type.dart b/lib/convert/metadata/metadata_type.dart
index 63259e7ba..5c7f40851 100644
--- a/lib/convert/metadata/metadata_type.dart
+++ b/lib/convert/metadata/metadata_type.dart
@@ -23,6 +23,8 @@ extension ExtraMetadataTypeConvert on MetadataType {
return 'photoshop_irb';
case MetadataType.xmp:
return 'xmp';
+ case MetadataType.file:
+ return 'file';
}
}
}
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 472c39041..5774273ee 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -1532,5 +1532,9 @@
"stopTooltip": "توقف",
"@stopTooltip": {},
"videoRepeatActionSetStart": "تعيين بداية التشغيل",
- "@videoRepeatActionSetStart": {}
+ "@videoRepeatActionSetStart": {},
+ "settingsForceWesternArabicNumeralsTile": "فرض الأرقام العربية",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "renameProcessorHash": "تجزئة",
+ "@renameProcessorHash": {}
}
diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb
index cbe9d7fc7..42d9c2f31 100644
--- a/lib/l10n/app_cs.arb
+++ b/lib/l10n/app_cs.arb
@@ -1518,5 +1518,19 @@
"entryActionCast": "Promítat",
"@entryActionCast": {},
"castDialogTitle": "Zařízení pro promítání",
- "@castDialogTitle": {}
+ "@castDialogTitle": {},
+ "setHomeCustomCollection": "Vlastní sbírka",
+ "@setHomeCustomCollection": {},
+ "settingsThumbnailShowHdrIcon": "Zobrazit ikonu HDR",
+ "@settingsThumbnailShowHdrIcon": {},
+ "settingsForceWesternArabicNumeralsTile": "Vynutit arabské číslice",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "stopTooltip": "Zastavit",
+ "@stopTooltip": {},
+ "videoRepeatActionSetStart": "Nastavit začátek",
+ "@videoRepeatActionSetStart": {},
+ "videoRepeatActionSetEnd": "Nastavit konec",
+ "@videoRepeatActionSetEnd": {},
+ "videoActionABRepeat": "Opakování A-B",
+ "@videoActionABRepeat": {}
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 935fe211b..d803fb63e 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -440,6 +440,7 @@
"renameEntrySetPagePreviewSectionTitle": "Preview",
"renameProcessorCounter": "Counter",
+ "renameProcessorHash": "Hash",
"renameProcessorName": "Name",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and the item in it?} other{Delete this album and the {count} items in it?}}",
@@ -933,6 +934,7 @@
"settingsCoordinateFormatDialogTitle": "Coordinate Format",
"settingsUnitSystemTile": "Units",
"settingsUnitSystemDialogTitle": "Units",
+ "settingsForceWesternArabicNumeralsTile": "Force Arabic numerals",
"settingsScreenSaverPageTitle": "Screen Saver",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index d4ac1b778..249ee65da 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -717,9 +717,9 @@
"@settingsHomeDialogTitle": {},
"settingsShowBottomNavigationBar": "Mostrar barra de navegación inferior",
"@settingsShowBottomNavigationBar": {},
- "settingsKeepScreenOnTile": "Mantener pantalla encendida",
+ "settingsKeepScreenOnTile": "Mantener la pantalla encendida",
"@settingsKeepScreenOnTile": {},
- "settingsKeepScreenOnDialogTitle": "Mantener pantalla encendida",
+ "settingsKeepScreenOnDialogTitle": "Mantener la pantalla encendida",
"@settingsKeepScreenOnDialogTitle": {},
"settingsDoubleBackExit": "Presione «atrás» dos veces para salir",
"@settingsDoubleBackExit": {},
@@ -1374,5 +1374,9 @@
"videoActionABRepeat": "Repetir de A a B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Fijar el fin",
- "@videoRepeatActionSetEnd": {}
+ "@videoRepeatActionSetEnd": {},
+ "settingsForceWesternArabicNumeralsTile": "Forzar números arábigos",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "renameProcessorHash": "Hash",
+ "@renameProcessorHash": {}
}
diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb
index 856613cd6..ac714af6b 100644
--- a/lib/l10n/app_eu.arb
+++ b/lib/l10n/app_eu.arb
@@ -1524,5 +1524,17 @@
"collectionActionSetHome": "Ezarri hasiera gisa",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Bilduma pertsonalizatua",
- "@setHomeCustomCollection": {}
+ "@setHomeCustomCollection": {},
+ "renameProcessorHash": "Hash-a",
+ "@renameProcessorHash": {},
+ "settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "videoRepeatActionSetStart": "Ezarri hasiera",
+ "@videoRepeatActionSetStart": {},
+ "stopTooltip": "Gelditu",
+ "@stopTooltip": {},
+ "videoActionABRepeat": "Atik Brako errepikapena",
+ "@videoActionABRepeat": {},
+ "videoRepeatActionSetEnd": "Ezarri amaiera",
+ "@videoRepeatActionSetEnd": {}
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 5132a33c0..3dc8fece3 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1374,5 +1374,9 @@
"videoActionABRepeat": "Lecture répétée A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetStart": "Définir le début",
- "@videoRepeatActionSetStart": {}
+ "@videoRepeatActionSetStart": {},
+ "renameProcessorHash": "Hash",
+ "@renameProcessorHash": {},
+ "settingsForceWesternArabicNumeralsTile": "Toujours utiliser les chiffres arabes",
+ "@settingsForceWesternArabicNumeralsTile": {}
}
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index 3512284f5..8ef064d2e 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -1374,5 +1374,9 @@
"videoRepeatActionSetStart": "Tetapkan awal",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Tetapkan akhir",
- "@videoRepeatActionSetEnd": {}
+ "@videoRepeatActionSetEnd": {},
+ "settingsForceWesternArabicNumeralsTile": "Paksa angka Arab",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "renameProcessorHash": "Hash",
+ "@renameProcessorHash": {}
}
diff --git a/lib/l10n/app_is.arb b/lib/l10n/app_is.arb
index bbd9d5e08..15ece4e21 100644
--- a/lib/l10n/app_is.arb
+++ b/lib/l10n/app_is.arb
@@ -1524,5 +1524,17 @@
"collectionActionSetHome": "Setja sem upphafsskjá",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Sérsniðið safn",
- "@setHomeCustomCollection": {}
+ "@setHomeCustomCollection": {},
+ "renameProcessorHash": "Tætigildi",
+ "@renameProcessorHash": {},
+ "videoRepeatActionSetStart": "Stilla byrjun",
+ "@videoRepeatActionSetStart": {},
+ "stopTooltip": "Stöðva",
+ "@stopTooltip": {},
+ "settingsForceWesternArabicNumeralsTile": "Þvinga arabískar tölur",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "videoActionABRepeat": "Endurtekning A-B",
+ "@videoActionABRepeat": {},
+ "videoRepeatActionSetEnd": "Stilla endi",
+ "@videoRepeatActionSetEnd": {}
}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index d71aa99b5..7157352ab 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -1360,5 +1360,19 @@
"aboutDataUsageClearCache": "Pulisci Cache",
"@aboutDataUsageClearCache": {},
"castDialogTitle": "Dispositivi per Cast",
- "@castDialogTitle": {}
+ "@castDialogTitle": {},
+ "stopTooltip": "Ferma",
+ "@stopTooltip": {},
+ "videoActionABRepeat": "Ripeti A-B",
+ "@videoActionABRepeat": {},
+ "videoRepeatActionSetStart": "Imposta inizio",
+ "@videoRepeatActionSetStart": {},
+ "videoRepeatActionSetEnd": "Imposta fine",
+ "@videoRepeatActionSetEnd": {},
+ "settingsThumbnailShowHdrIcon": "Mostra icona HDR",
+ "@settingsThumbnailShowHdrIcon": {},
+ "collectionActionSetHome": "Imposta come pagina iniziale",
+ "@collectionActionSetHome": {},
+ "setHomeCustomCollection": "Collezione personalizzata",
+ "@setHomeCustomCollection": {}
}
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index 669fbb575..db3489eb0 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -1374,5 +1374,9 @@
"stopTooltip": "취소",
"@stopTooltip": {},
"videoActionABRepeat": "A-B 반복",
- "@videoActionABRepeat": {}
+ "@videoActionABRepeat": {},
+ "renameProcessorHash": "해시",
+ "@renameProcessorHash": {},
+ "settingsForceWesternArabicNumeralsTile": "아라비아 숫자 항상 사용",
+ "@settingsForceWesternArabicNumeralsTile": {}
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index 7b861ad7e..1f898ac01 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -1532,5 +1532,9 @@
"videoActionABRepeat": "Powtarzanie A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Ustaw koniec",
- "@videoRepeatActionSetEnd": {}
+ "@videoRepeatActionSetEnd": {},
+ "settingsForceWesternArabicNumeralsTile": "Wymuszaj cyfry arabskie",
+ "@settingsForceWesternArabicNumeralsTile": {},
+ "renameProcessorHash": "Skrót",
+ "@renameProcessorHash": {}
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 2de63a7f7..25e3f10f4 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1374,5 +1374,9 @@
"videoActionABRepeat": "Повторить от А до Б",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Установить конец",
- "@videoRepeatActionSetEnd": {}
+ "@videoRepeatActionSetEnd": {},
+ "renameProcessorHash": "Хэш",
+ "@renameProcessorHash": {},
+ "settingsForceWesternArabicNumeralsTile": "Принудительные арабские цифры",
+ "@settingsForceWesternArabicNumeralsTile": {}
}
diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb
index 69c72eac7..22b236829 100644
--- a/lib/l10n/app_tr.arb
+++ b/lib/l10n/app_tr.arb
@@ -1374,5 +1374,7 @@
"videoRepeatActionSetStart": "Başlangıç noktası seç",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Bitiş noktası seç",
- "@videoRepeatActionSetEnd": {}
+ "@videoRepeatActionSetEnd": {},
+ "settingsForceWesternArabicNumeralsTile": "Arap rakamlarını zorla",
+ "@settingsForceWesternArabicNumeralsTile": {}
}
diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb
index f897239c5..a79f07b43 100644
--- a/lib/l10n/app_vi.arb
+++ b/lib/l10n/app_vi.arb
@@ -1524,5 +1524,17 @@
"settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Đặt làm nhà",
- "@collectionActionSetHome": {}
+ "@collectionActionSetHome": {},
+ "stopTooltip": "Dừng",
+ "@stopTooltip": {},
+ "videoActionABRepeat": "Lặp lại A-B",
+ "@videoActionABRepeat": {},
+ "videoRepeatActionSetEnd": "Đặt kết thúc",
+ "@videoRepeatActionSetEnd": {},
+ "videoRepeatActionSetStart": "Đặt bắt đầu",
+ "@videoRepeatActionSetStart": {},
+ "renameProcessorHash": "Băm",
+ "@renameProcessorHash": {},
+ "settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập",
+ "@settingsForceWesternArabicNumeralsTile": {}
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 2e99628e7..fd5cdb2bd 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -1366,5 +1366,13 @@
"collectionActionSetHome": "设置为首页",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "自定义媒体集",
- "@setHomeCustomCollection": {}
+ "@setHomeCustomCollection": {},
+ "videoRepeatActionSetStart": "设置起点",
+ "@videoRepeatActionSetStart": {},
+ "stopTooltip": "停止",
+ "@stopTooltip": {},
+ "videoActionABRepeat": "A-B 循环播放",
+ "@videoActionABRepeat": {},
+ "videoRepeatActionSetEnd": "设置终点",
+ "@videoRepeatActionSetEnd": {}
}
diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb
index dbd7c3a7e..de8052229 100644
--- a/lib/l10n/app_zh_Hant.arb
+++ b/lib/l10n/app_zh_Hant.arb
@@ -1524,5 +1524,13 @@
"entryActionCast": "投放",
"@entryActionCast": {},
"castDialogTitle": "投放裝置",
- "@castDialogTitle": {}
+ "@castDialogTitle": {},
+ "stopTooltip": "停止",
+ "@stopTooltip": {},
+ "videoActionABRepeat": "A-B 重複播放",
+ "@videoActionABRepeat": {},
+ "videoRepeatActionSetStart": "設置起點",
+ "@videoRepeatActionSetStart": {},
+ "videoRepeatActionSetEnd": "設置終點",
+ "@videoRepeatActionSetEnd": {}
}
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
index d21b15155..771241595 100644
--- a/lib/model/app/contributors.dart
+++ b/lib/model/app/contributors.dart
@@ -82,6 +82,7 @@ class Contributors {
Contributor('しいたけ', 'Shiitake@users.noreply.hosted.weblate.org'),
Contributor('wanzh', 'wanzh66666@gmail.com'),
Contributor('ID J', 'tabby4442@gmail.com'),
+ Contributor('randint', 'lancameb@hotmail.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
diff --git a/lib/model/entry/extensions/images.dart b/lib/model/entry/extensions/images.dart
index ee9dbafd1..bac9d14bf 100644
--- a/lib/model/entry/extensions/images.dart
+++ b/lib/model/entry/extensions/images.dart
@@ -17,7 +17,8 @@ extension ExtraAvesEntryImages on AvesEntry {
}
ThumbnailProviderKey _getThumbnailProviderKey(double extent) {
- EntryCache.markThumbnailExtent(extent);
+ final requestExtent = extent.roundToDouble();
+ EntryCache.markThumbnailExtent(requestExtent);
return ThumbnailProviderKey(
uri: uri,
mimeType: mimeType,
@@ -25,7 +26,7 @@ extension ExtraAvesEntryImages on AvesEntry {
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
dateModifiedSecs: dateModifiedSecs ?? -1,
- extent: extent,
+ extent: requestExtent,
);
}
diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart
index 8953e04c7..1b231ea59 100644
--- a/lib/model/entry/extensions/props.dart
+++ b/lib/model/entry/extensions/props.dart
@@ -10,6 +10,7 @@ import 'package:aves/ref/unicode.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/text.dart';
import 'package:aves/utils/android_file_utils.dart';
+import 'package:aves/utils/time_utils.dart';
extension ExtraAvesEntryProps on AvesEntry {
bool get isValid => !isMissingAtPath && sizeBytes != 0 && width > 0 && height > 0;
@@ -85,7 +86,7 @@ extension ExtraAvesEntryProps on AvesEntry {
int? get trashDaysLeft {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return null;
- return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
+ return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inHumanDays;
}
// storage
diff --git a/lib/model/naming_pattern.dart b/lib/model/naming_pattern.dart
index ab988fef5..c31c16143 100644
--- a/lib/model/naming_pattern.dart
+++ b/lib/model/naming_pattern.dart
@@ -39,18 +39,6 @@ class NamingPattern {
final processorKey = match.group(1);
final processorOptions = match.group(3);
switch (processorKey) {
- case DateNamingProcessor.key:
- if (processorOptions != null) {
- processors.add(DateNamingProcessor(processorOptions.trim(), locale));
- }
- case TagsNamingProcessor.key:
- processors.add(TagsNamingProcessor(processorOptions?.trim() ?? ''));
- case MetadataFieldNamingProcessor.key:
- if (processorOptions != null) {
- processors.add(MetadataFieldNamingProcessor(processorOptions.trim()));
- }
- case NameNamingProcessor.key:
- processors.add(const NameNamingProcessor());
case CounterNamingProcessor.key:
int? start, padding;
_applyProcessorOptions(processorOptions, (key, value) {
@@ -65,6 +53,22 @@ class NamingPattern {
}
});
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
+ case DateNamingProcessor.key:
+ if (processorOptions != null) {
+ processors.add(DateNamingProcessor(processorOptions.trim(), locale));
+ }
+ case HashNamingProcessor.key:
+ if (processorOptions != null) {
+ processors.add(HashNamingProcessor(processorOptions.trim()));
+ }
+ case MetadataFieldNamingProcessor.key:
+ if (processorOptions != null) {
+ processors.add(MetadataFieldNamingProcessor(processorOptions.trim()));
+ }
+ case NameNamingProcessor.key:
+ processors.add(const NameNamingProcessor());
+ case TagsNamingProcessor.key:
+ processors.add(TagsNamingProcessor(processorOptions?.trim() ?? ''));
default:
debugPrint('unsupported naming processor: ${match.group(0)}');
}
@@ -106,6 +110,8 @@ class NamingPattern {
switch (processorKey) {
case DateNamingProcessor.key:
return '<$processorKey, yyyyMMdd-HHmmss>';
+ case HashNamingProcessor.key:
+ return '<$processorKey, md5>';
case TagsNamingProcessor.key:
return '<$processorKey, ->';
case CounterNamingProcessor.key:
@@ -204,9 +210,7 @@ class MetadataFieldNamingProcessor extends NamingProcessor {
}
@override
- Set getRequiredFields() {
- return {field}.whereNotNull().toSet();
- }
+ Set getRequiredFields() => {field}.whereNotNull().toSet();
@override
String? process(AvesEntry entry, int index, Map fieldValues) {
@@ -247,3 +251,27 @@ class CounterNamingProcessor extends NamingProcessor {
@override
String? process(AvesEntry entry, int index, Map fieldValues) => '${index + start}'.padLeft(padding, '0');
}
+
+@immutable
+class HashNamingProcessor extends NamingProcessor {
+ static const key = 'hash';
+ static const optionFunction = 'function';
+
+ late final MetadataField? function;
+
+ @override
+ List