Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-05-01 18:03:08 +02:00
commit 645c199b33
134 changed files with 1132 additions and 9814 deletions

@ -1 +1 @@
Subproject commit 300451adae589accbece3490f4396f10bdf15e6e
Subproject commit 54e66469a933b60ddf175f858f82eaeb97e48c8d

View file

@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.0"></a>[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
## <a id="v1.10.9"></a>[v1.10.9] - 2024-04-14
### Fixed

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_flavour">#7B1FA2</color>
<color name="ic_launcher_flavour">#815AFA</color>
</resources>

View file

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

View file

@ -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<String, String>()
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<String, Any>()
if (fields.isEmpty() || isVideo(mimeType)) {
val metadataMap = HashMap<String, Any?>()
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)
return
}
private fun getHashFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
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<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
if (fields.isEmpty() || isVideo(mimeType)) {
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]"
}
}

View file

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

View file

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

View file

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

View file

@ -279,6 +279,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
companion object {
private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>()
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
}
}

View file

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

View file

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

View file

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

View file

@ -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<VideoThumbnail, InputStream> {
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
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<VideoThumbnail, InputStream> {
}
}
internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher<InputStream> {
internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val width: Int, val height: Int) : DataFetcher<InputStream> {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
@ -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 {
retriever.frameAtTime
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 {
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> = 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
}
}

View file

@ -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<MpEntry>? {
fun getJpegMpfEntries(context: Context, uri: Uri, sizeBytes: Long?): List<MpEntry>? {
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<FieldMap> {
fun getJpegMpfPages(context: Context, uri: Uri, sizeBytes: Long): ArrayList<FieldMap> {
val primaryRotation = getJpegMpfPrimaryRotation(context, uri, sizeBytes)
val pages = ArrayList<FieldMap>()
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
}

View file

@ -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() {
@ -11,3 +14,18 @@ object SvgHelper {
setDocumentHeight("100%")
}
}
// 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
}
}

View file

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

View file

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

View file

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

View file

@ -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,10 +138,14 @@ 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) {
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
// according to EXIF orientation when decoding images of known formats

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -4,23 +4,35 @@
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M3.925,16.034 L60.825,72.933a2.421,2.421 0.001,0 0,3.423 0l10.604,-10.603a6.789,6.789 90.001,0 0,0 -9.601L34.066,11.942A8.264,8.264 22.5,0 0,28.222 9.522H6.623A3.815,3.815 112.5,0 0,3.925 16.034Z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m31.78,63.61 l16.47,16.47a2.91,2.91 0,0 1,0 4.12l-7.87,7.87a15.07,15.07 0,0 1,-21.31 0L11.2,84.2a2.91,2.91 0,0 1,0 -4.12l16.47,-16.47a2.91,2.91 0,0 1,4.12 0z"
android:strokeWidth="3.05097"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="m36.36,65.907v28.743a2.557,2.557 22.5,0 0,4.364 1.808L53.817,83.364a6.172,6.172 90,0 0,0 -8.729L42.532,63.35a3.616,3.616 157.5,0 0,-6.172 2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M56.37,39.03L72.84,55.49A2.91,2.91 90,0 1,72.84 59.61L56.37,76.08A2.91,2.91 0,0 1,52.25 76.08L35.78,59.61A2.91,2.91 0,0 1,35.78 55.49L52.25,39.03A2.91,2.91 90,0 1,56.37 39.03z"
android:strokeWidth="3.05095"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M79.653,40.078V11.335A2.557,2.557 22.5,0 0,75.289 9.527L62.195,22.62a6.172,6.172 90,0 0,0 8.729l11.285,11.285a3.616,3.616 157.5,0 0,6.172 -2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m60.37,30.91a2.91,2.91 0,0 0,0 4.12l16.47,16.47a2.91,2.91 0,0 0,4.12 0l16.47,-16.47a2.91,2.91 0,0 0,0 -4.12l-8.17,-8.17a14.64,14.64 0,0 0,-20.7 0zM76.62,30.7a3.21,3.21 0,0 1,4.54 0,3.21 3.21,0 0,1 0,4.54 3.21,3.21 0,0 1,-4.54 0,3.21 3.21,0 0,1 0,-4.54z"
android:strokeWidth="3.05095"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M96.613,16.867 L89.085,9.339a1.917,1.917 157.5,0 0,-3.273 1.356v6.172a4.629,4.629 45,0 0,4.629 4.629h4.255a2.712,2.712 112.5,0 0,1.917 -4.629z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m24.31,6.96 l24.04,24.04a2.91,2.91 0,0 1,0 4.12L31.88,51.59a2.91,2.91 0,0 1,-4.12 0L17.08,40.91a22.74,22.74 0,0 1,0 -32.16L18.87,6.96a3.84,3.84 0,0 1,5.43 0z"
android:strokeWidth="3.05097"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View file

@ -1,30 +1,38 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:fillColor="#ef435a"
android:fillType="evenOdd"
android:pathData="m41.18,56.69 l8.74,8.74a1.54,1.54 0,0 1,0 2.18l-4.18,4.18a7.99,7.99 0,0 1,-11.3 0L30.26,67.61a1.54,1.54 0,0 1,0 -2.18l8.74,-8.74a1.54,1.54 0,0 1,2.18 0z"
android:strokeWidth="1.61863"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#e0e0e0"
android:fillType="evenOdd"
android:pathData="M54.22,43.64L62.96,52.38A1.54,1.54 0,0 1,62.96 54.56L54.22,63.3A1.54,1.54 0,0 1,52.04 63.3L43.3,54.56A1.54,1.54 0,0 1,43.3 52.38L52.04,43.64A1.54,1.54 90,0 1,54.22 43.64z"
android:strokeWidth="1.61862"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffc11f"
android:fillType="evenOdd"
android:pathData="m56.35,39.33a1.54,1.54 0,0 0,0 2.18l8.74,8.74a1.54,1.54 0,0 0,2.18 0l8.74,-8.74a1.54,1.54 0,0 0,0 -2.18l-4.34,-4.34a7.77,7.77 0,0 0,-10.98 0zM64.97,39.22a1.7,1.7 0,0 1,2.41 0,1.7 1.7,0 0,1 0,2.41 1.7,1.7 0,0 1,-2.41 0,1.7 1.7,0 0,1 0,-2.41z"
android:strokeWidth="1.61862"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="@color/ic_launcher_flavour"
android:pathData="M29.77,36.566 L60.448,67.245a1.306,1.306 0,0 0,1.846 0l5.717,-5.717a3.661,3.661 0,0 0,0 -5.177L46.02,34.36A4.456,4.456 0,0 0,42.869 33.055H31.225a2.057,2.057 0,0 0,-1.455 3.511z"
android:strokeWidth="1.345"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="#4caf50"
android:pathData="m47.258,63.456v15.497a1.379,1.379 0,0 0,2.352 0.974l7.059,-7.059a3.328,3.328 0,0 0,0 -4.706l-6.085,-6.086a1.95,1.95 0,0 0,-3.328 1.379z"
android:strokeWidth="1.345"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffc107"
android:pathData="M70.6,49.53V34.032a1.379,1.379 0,0 0,-2.353 -0.974l-7.058,7.059a3.328,3.328 0,0 0,0 4.707l6.085,6.085a1.95,1.95 0,0 0,3.328 -1.379z"
android:strokeWidth="1.345"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="#ff5722"
android:pathData="m79.744,37.015 l-4.059,-4.059a1.035,1.035 0,0 0,-1.765 0.731v3.328a2.496,2.496 0,0 0,2.496 2.496h2.294a1.462,1.462 0,0 0,1.034 -2.496z"
android:strokeWidth="1.345"
android:fillType="evenOdd"
android:pathData="m37.21,26.63 l12.76,12.76a1.54,1.54 0,0 1,0 2.18l-8.74,8.74a1.54,1.54 0,0 1,-2.18 0l-5.67,-5.67a12.06,12.06 0,0 1,0 -17.06l0.95,-0.95a2.04,2.04 0,0 1,2.88 0z"
android:strokeWidth="1.61863"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View file

@ -3,30 +3,36 @@
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<group
android:scaleX=".44"
android:scaleY=".44"
android:translateX="28"
android:translateY="30">
<path
android:pathData="M3.925,16.034 L60.825,72.933a2.421,2.421 0.001,0 0,3.423 0l10.604,-10.603a6.789,6.789 90.001,0 0,0 -9.601L34.066,11.942A8.264,8.264 22.5,0 0,28.222 9.522H6.623A3.815,3.815 112.5,0 0,3.925 16.034Z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m41.18,56.69 l8.74,8.74a1.54,1.54 0,0 1,0 2.18l-4.18,4.18a7.99,7.99 0,0 1,-11.3 0L30.26,67.61a1.54,1.54 0,0 1,0 -2.18l8.74,-8.74a1.54,1.54 0,0 1,2.18 0z"
android:strokeWidth="1.61863"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="m36.36,65.907v28.743a2.557,2.557 22.5,0 0,4.364 1.808L53.817,83.364a6.172,6.172 90,0 0,0 -8.729L42.532,63.35a3.616,3.616 157.5,0 0,-6.172 2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M54.22,43.64L62.96,52.38A1.54,1.54 0,0 1,62.96 54.56L54.22,63.3A1.54,1.54 0,0 1,52.04 63.3L43.3,54.56A1.54,1.54 0,0 1,43.3 52.38L52.04,43.64A1.54,1.54 90,0 1,54.22 43.64z"
android:strokeWidth="1.61862"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M79.653,40.078V11.335A2.557,2.557 22.5,0 0,75.289 9.527L62.195,22.62a6.172,6.172 90,0 0,0 8.729l11.285,11.285a3.616,3.616 157.5,0 0,6.172 -2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m56.35,39.33a1.54,1.54 0,0 0,0 2.18l8.74,8.74a1.54,1.54 0,0 0,2.18 0l8.74,-8.74a1.54,1.54 0,0 0,0 -2.18l-4.34,-4.34a7.77,7.77 0,0 0,-10.98 0zM64.97,39.22a1.7,1.7 0,0 1,2.41 0,1.7 1.7,0 0,1 0,2.41 1.7,1.7 0,0 1,-2.41 0,1.7 1.7,0 0,1 0,-2.41z"
android:strokeWidth="1.61862"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M96.613,16.867 L89.085,9.339a1.917,1.917 157.5,0 0,-3.273 1.356v6.172a4.629,4.629 45,0 0,4.629 4.629h4.255a2.712,2.712 112.5,0 0,1.917 -4.629z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m37.21,26.63 l12.76,12.76a1.54,1.54 0,0 1,0 2.18l-8.74,8.74a1.54,1.54 0,0 1,-2.18 0l-5.67,-5.67a12.06,12.06 0,0 1,0 -17.06l0.95,-0.95a2.04,2.04 0,0 1,2.88 0z"
android:strokeWidth="1.61863"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -3,5 +3,5 @@
<color name="ic_launcher_background">#FFFFFF</color>
<color name="ic_shortcut_background">#FFFFFF</color>
<color name="ic_shortcut_foreground">#455A64</color>
<color name="ic_launcher_flavour">#3f51b5</color>
<color name="ic_launcher_flavour">#1cc8eb</color>
</resources>

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_flavour">#D32F2F</color>
<color name="ic_launcher_flavour">#007A78</color>
</resources>

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M3.925 16.034l56.9 56.9a2.42 2.42.001 003.423 0L74.852 62.33a6.79 6.79 90.001 000-9.601L34.067 11.942a8.264 8.264 22.5 00-5.844-2.42h-21.6a3.815 3.815 112.5 00-2.697 6.512z" fill="#3f51b5" stroke="#000" stroke-width="2.346" stroke-linejoin="round"/><path d="M36.36 65.907V94.65a2.557 2.557 22.5 004.364 1.808l13.093-13.094a6.172 6.172 90 000-8.728L42.532 63.35a3.616 3.616 157.5 00-6.172 2.557z" fill="#4caf50" stroke="#000" stroke-width="2.346" stroke-linejoin="round"/><path d="M79.653 40.078V11.335a2.557 2.557 22.5 00-4.364-1.808L62.195 22.62a6.172 6.172 90 000 8.729l11.286 11.285a3.616 3.616 157.5 006.172-2.556z" fill="#ffc107" stroke="#000" stroke-width="2.346" stroke-linejoin="round"/><path d="M96.613 16.867l-7.528-7.528a1.917 1.917 157.5 00-3.273 1.355v6.173a4.63 4.63 45 004.629 4.629h4.255a2.712 2.712 112.5 001.917-4.63z" fill="#ff5722" stroke="#000" stroke-width="2.346" stroke-linejoin="round"/></svg>
<svg width="100" height="100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m29.04 62.547 16.498 16.497a2.916 2.916 0 0 1 0 4.125l-7.887 7.887a15.094 15.094 0 0 1-21.345 0l-7.887-7.887a2.916 2.916 0 0 1 0-4.124l16.497-16.498a2.916 2.916 0 0 1 4.125 0z" style="fill:#ef435a;fill-opacity:1;fill-rule:evenodd;stroke:#000;stroke-width:3.0565;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"/><path style="fill:#e0e0e0;fill-rule:evenodd;stroke:#000;stroke-width:3.05648;stroke-linecap:round;stroke-linejoin:round" d="m53.669 37.917 16.498 16.498a2.91 2.91 0 0 1 0 4.123L53.669 75.036a2.91 2.91 0 0 1-4.124 0L33.047 58.538a2.91 2.91 0 0 1 0-4.123l16.498-16.498a2.91 2.91 0 0 1 4.124 0z"/><path style="fill:#ffc11f;fill-opacity:1;fill-rule:evenodd;stroke:#000;stroke-width:3.05648;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" d="M57.678 29.784a2.916 2.916 0 0 0 0 4.124l16.498 16.497a2.916 2.916 0 0 0 4.124 0l16.497-16.497a2.916 2.916 0 0 0 0-4.124l-8.188-8.189a14.667 14.667 0 0 0-20.742 0zm16.284-.213a3.219 3.219 0 0 1 4.55 0 3.219 3.219 0 0 1 0 4.55 3.219 3.219 0 0 1-4.55 0 3.219 3.219 0 0 1 0-4.551z"/><path d="M21.548 5.792 45.636 29.88a2.916 2.916 0 0 1 0 4.124L29.138 50.501a2.916 2.916 0 0 1-4.124 0l-10.7-10.7a22.78 22.78 0 0 1 0-32.215l1.792-1.794a3.848 3.848 0 0 1 5.442 0z" style="fill:#1cc8eb;fill-opacity:1;fill-rule:evenodd;stroke:#000;stroke-width:3.0565;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"/></svg>

Before

Width:  |  Height:  |  Size: 989 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/ca/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/cs/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
fastlane/metadata/android/de/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,3 @@
In v1.11.0:
- watch videos with SRT subtitle files
Full changelog available on GitHub

View file

@ -0,0 +1,3 @@
In v1.11.0:
- watch videos with SRT subtitle files
Full changelog available on GitHub

BIN
fastlane/metadata/android/en-US/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
fastlane/metadata/android/es-MX/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
fastlane/metadata/android/eu/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/fr/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/ko/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/pl/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/pt-BR/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/ro/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/ru/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
fastlane/metadata/android/uk/images/featureGraphic.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -8,5 +8,3 @@
preferred-supported-locales:
- en
untranslated-messages-file: untranslated.json

View file

@ -53,13 +53,20 @@ 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) {
switch (type) {
case MetadataType.exif:
return _toExifInterfaceTag();
} else {
case MetadataType.file:
return name;
default:
switch (this) {
case MetadataField.mp4GpsCoordinates:
return 'gpsCoordinates';

View file

@ -23,6 +23,8 @@ extension ExtraMetadataTypeConvert on MetadataType {
return 'photoshop_irb';
case MetadataType.xmp:
return 'xmp';
case MetadataType.file:
return 'file';
}
}
}

View file

@ -1532,5 +1532,9 @@
"stopTooltip": "توقف",
"@stopTooltip": {},
"videoRepeatActionSetStart": "تعيين بداية التشغيل",
"@videoRepeatActionSetStart": {}
"@videoRepeatActionSetStart": {},
"settingsForceWesternArabicNumeralsTile": "فرض الأرقام العربية",
"@settingsForceWesternArabicNumeralsTile": {},
"renameProcessorHash": "تجزئة",
"@renameProcessorHash": {}
}

View file

@ -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": {}
}

View file

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

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -1374,5 +1374,9 @@
"videoRepeatActionSetStart": "Tetapkan awal",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Tetapkan akhir",
"@videoRepeatActionSetEnd": {}
"@videoRepeatActionSetEnd": {},
"settingsForceWesternArabicNumeralsTile": "Paksa angka Arab",
"@settingsForceWesternArabicNumeralsTile": {},
"renameProcessorHash": "Hash",
"@renameProcessorHash": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -1374,5 +1374,9 @@
"stopTooltip": "취소",
"@stopTooltip": {},
"videoActionABRepeat": "A-B 반복",
"@videoActionABRepeat": {}
"@videoActionABRepeat": {},
"renameProcessorHash": "해시",
"@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "아라비아 숫자 항상 사용",
"@settingsForceWesternArabicNumeralsTile": {}
}

View file

@ -1532,5 +1532,9 @@
"videoActionABRepeat": "Powtarzanie A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Ustaw koniec",
"@videoRepeatActionSetEnd": {}
"@videoRepeatActionSetEnd": {},
"settingsForceWesternArabicNumeralsTile": "Wymuszaj cyfry arabskie",
"@settingsForceWesternArabicNumeralsTile": {},
"renameProcessorHash": "Skrót",
"@renameProcessorHash": {}
}

View file

@ -1374,5 +1374,9 @@
"videoActionABRepeat": "Повторить от А до Б",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Установить конец",
"@videoRepeatActionSetEnd": {}
"@videoRepeatActionSetEnd": {},
"renameProcessorHash": "Хэш",
"@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "Принудительные арабские цифры",
"@settingsForceWesternArabicNumeralsTile": {}
}

View file

@ -1374,5 +1374,7 @@
"videoRepeatActionSetStart": "Başlangıç noktası seç",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Bitiş noktası seç",
"@videoRepeatActionSetEnd": {}
"@videoRepeatActionSetEnd": {},
"settingsForceWesternArabicNumeralsTile": "Arap rakamlarını zorla",
"@settingsForceWesternArabicNumeralsTile": {}
}

View file

@ -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": {}
}

View file

@ -1366,5 +1366,13 @@
"collectionActionSetHome": "设置为首页",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "自定义媒体集",
"@setHomeCustomCollection": {}
"@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "设置起点",
"@videoRepeatActionSetStart": {},
"stopTooltip": "停止",
"@stopTooltip": {},
"videoActionABRepeat": "A-B 循环播放",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "设置终点",
"@videoRepeatActionSetEnd": {}
}

View file

@ -1524,5 +1524,13 @@
"entryActionCast": "投放",
"@entryActionCast": {},
"castDialogTitle": "投放裝置",
"@castDialogTitle": {}
"@castDialogTitle": {},
"stopTooltip": "停止",
"@stopTooltip": {},
"videoActionABRepeat": "A-B 重複播放",
"@videoActionABRepeat": {},
"videoRepeatActionSetStart": "設置起點",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "設置終點",
"@videoRepeatActionSetEnd": {}
}

View file

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

View file

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

View file

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

View file

@ -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<MetadataField> getRequiredFields() {
return {field}.whereNotNull().toSet();
}
Set<MetadataField> getRequiredFields() => {field}.whereNotNull().toSet();
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
@ -247,3 +251,27 @@ class CounterNamingProcessor extends NamingProcessor {
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> 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<Object?> get props => [function];
HashNamingProcessor(String function) {
final lowerField = 'hash${function.toLowerCase()}';
this.function = MetadataField.values.firstWhereOrNull((v) => v.name.toLowerCase() == lowerField);
}
@override
Set<MetadataField> getRequiredFields() => {function}.whereNotNull().toSet();
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
return fieldValues[function?.toPlatform]?.toString();
}
}

View file

@ -79,6 +79,10 @@ mixin AppSettings on SettingsAccess {
return _appliedLocale!;
}
bool get forceWesternArabicNumerals => getBool(SettingKeys.forceWesternArabicNumeralsKey) ?? false;
set forceWesternArabicNumerals(bool newValue) => set(SettingKeys.forceWesternArabicNumeralsKey, newValue);
int get catalogTimeZoneRawOffsetMillis => getInt(SettingKeys.catalogTimeZoneRawOffsetMillisKey) ?? 0;
set catalogTimeZoneRawOffsetMillis(int newValue) => set(SettingKeys.catalogTimeZoneRawOffsetMillisKey, newValue);

View file

@ -375,6 +375,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
}
case SettingKeys.isInstalledAppAccessAllowedKey:
case SettingKeys.isErrorReportingAllowedKey:
case SettingKeys.forceWesternArabicNumeralsKey:
case SettingKeys.enableDynamicColorKey:
case SettingKeys.enableBlurEffectKey:
case SettingKeys.enableBottomNavigationBarKey:

View file

@ -1,7 +1,10 @@
class XmpNamespaces {
static const acdsee = 'http://ns.acdsee.com/iptc/1.0/';
static const adsmlat = 'http://adsml.org/xmlns/';
static const appleDepthData = 'http://ns.apple.com/depthData/1.0/';
static const appleDesktop = 'http://ns.apple.com/namespace/1.0/';
static const appleHDRGainMap = 'http://ns.apple.com/HDRGainMap/1.0/';
static const applePixelDataInfo = 'http://ns.apple.com/pixeldatainfo/1.0/';
static const avm = 'http://www.communicatingastronomy.org/avm/1.0/';
static const camera = 'http://pix4d.com/camera/1.0/';
static const cc = 'http://creativecommons.org/ns#';

View file

@ -38,6 +38,7 @@ class PlatformAppService implements AppService {
static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream');
static final _knownAppDirs = {
'com.google.android.apps.photos': {'Google Photos'},
'com.kakao.talk': {'KakaoTalkDownload'},
'com.sony.playmemories.mobile': {'Imaging Edge Mobile'},
'nekox.messenger': {'NekoX'},

View file

@ -60,6 +60,7 @@ class ADurations {
static const highlightJumpDelay = Duration(milliseconds: 400);
static const highlightScrollInitDelay = Duration(milliseconds: 800);
static const motionPhotoAutoPlayDelay = Duration(milliseconds: 700);
static const videoPauseAppInactiveDelay = Duration(milliseconds: 300);
static const videoOverlayHideDelay = Duration(milliseconds: 500);
static const videoProgressTimerInterval = Duration(milliseconds: 300);
static const doubleBackTimerDelay = Duration(milliseconds: 1000);

View file

@ -47,6 +47,7 @@ class AIcons {
static const mainStorage = Icons.smartphone_outlined;
static const mimeType = Icons.code_outlined;
static const opacity = Icons.opacity;
static const palette = Icons.palette_outlined;
static final privacy = MdiIcons.shieldAccountOutline;
static const rating = Icons.star_border_outlined;
static const ratingFull = Icons.star;

View file

@ -61,6 +61,13 @@ class Themes {
// COMPONENT THEMES
checkboxTheme: _checkboxTheme(colors),
floatingActionButtonTheme: _floatingActionButtonTheme(colors),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: colors.background,
selectedIconTheme: IconThemeData(color: colors.primary),
unselectedIconTheme: IconThemeData(color: _unselectedWidgetColor(colors)),
selectedLabelTextStyle: TextStyle(color: colors.primary),
unselectedLabelTextStyle: TextStyle(color: _unselectedWidgetColor(colors)),
),
radioTheme: _radioTheme(colors),
sliderTheme: _sliderTheme(colors),
tooltipTheme: _tooltipTheme,

View file

@ -26,6 +26,12 @@ extension ExtraDateTime on DateTime {
DateTime addDays(int days) => DateTime(year, month, day + days, hour, minute, second, millisecond, microsecond);
}
extension ExtraDuration on Duration {
// using `Duration.inDays` may yield surprising results when crossing DST boundaries
// because a day will not have 24 hours, so we use the following approximation instead
int get inHumanDays => (inMicroseconds / Duration.microsecondsPerDay).round();
}
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
// Overflowing timestamps that are supposed to be in milliseconds

Some files were not shown because too many files have changed in this diff Show more