Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-01-29 23:49:00 +01:00
commit 6f7f70babe
108 changed files with 3083 additions and 674 deletions

@ -1 +1 @@
Subproject commit 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9
Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b

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.10.3"></a>[v1.10.3] - 2024-01-29
### Added
- Viewer: optional histogram (for real this time)
- Collection: allow hiding thumbnail overlay HDR icon
- Collection: allow setting any filtered collection as home page
### Changed
- Viewer: lift format control for tiling, allowing large DNG tiling if supported
- Info: strip `unlocated` filter from context collection when editing location via map
- Slideshow: keep playing when losing focus but app is still visible (e.g. split screen)
- upgraded Flutter to stable v3.16.9
### Fixed
- crash when loading some large DNG in viewer
- searching from drawer on mobile
- resizing TIFF during conversion
## <a id="v1.10.2"></a>[v1.10.2] - 2023-12-24
### Changed
@ -16,6 +37,8 @@ All notable changes to this project will be documented in this file.
## <a id="v1.10.1"></a>[v1.10.1] - 2023-12-21
### Added
- Cataloguing: detect/filter `Ultra HDR`
- Viewer: show JPEG MPF dependent images (except thumbnails and HDR gain maps)
- Info: show metadata from JPEG MPF

View file

@ -41,7 +41,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
Aves integrates with Android (from KitKat to Android 14, including Android TV) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
## Screenshots

View file

@ -50,24 +50,27 @@ if (keystorePropertiesFile.exists()) {
android {
namespace 'deckers.thibault.aves'
compileSdk 34
ndkVersion flutter.ndkVersion
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
ndkVersion '25.1.8937393'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
lint {
checkAllWarnings true
warningsAsErrors true
disable 'InvalidPackage'
}
packagingOptions {
jniLibs {
// The Amazon Developer console mistakenly considers the app to not be 64-bit compatible
// if there are some libs in `lib/armeabi-v7a` unmatched by libs in `lib/arm64-v8a`,
// so we exclude the extra `neon` libs bundled by `FFmpegKit`.
exclude 'lib/armeabi-v7a/*_neon.so'
excludes += ['lib/armeabi-v7a/*_neon.so']
}
}
sourceSets {
@ -77,12 +80,9 @@ android {
defaultConfig {
applicationId packageName
// minSdk constraints:
// - Flutter & other plugins: 16
// - Flutter & other plugins: 19 (cf `flutter.minSdkVersion`)
// - google_maps_flutter v2.1.1: 20
// - to build XML documents from XMP data, `metadata-extractor` and `PixyMeta` rely on `DocumentBuilder`,
// which implementation `DocumentBuilderImpl` is provided by the OS and is not customizable on Android,
// but the implementation on API <19 is not robust enough and fails to build XMP documents
minSdk 19
minSdk flutter.minSdkVersion
targetSdk 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -215,7 +215,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
@ -227,7 +227,7 @@ dependencies {
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation 'com.google.android.material:material:1.11.0'
// SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.9'
implementation 'org.slf4j:slf4j-simple:2.0.11'
// forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
@ -236,7 +236,7 @@ dependencies {
implementation 'com.github.deckerst:Android-TiffBitmapFactory:90c06eebf4'
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c'
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c'
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e'
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
// huawei flavor only
huaweiImplementation "com.huawei.agconnect:agconnect-core:$huawei_agconnect_version"

View file

@ -12,6 +12,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
import com.google.android.material.color.DynamicColors
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.MimeTypes
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler

View file

@ -109,7 +109,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
when (call.method) {
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
"getOverlayMetadata" -> ioScope.launch { safe(call, result, ::getOverlayMetadata) }
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
@ -118,7 +118,6 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
else -> result.notImplemented()
}
}
@ -807,17 +806,18 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
private fun getFields(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) {
val fields = call.argument<List<String>>("fields")
if (mimeType == null || uri == null || fields == null) {
result.error("getOverlayMetadata-args", "missing arguments", null)
return
}
val metadataMap = HashMap<String, Any>()
if (isVideo(mimeType)) {
if (fields.isEmpty() || isVideo(mimeType)) {
result.success(metadataMap)
return
}
@ -842,12 +842,23 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val metadata = Helper.safeRead(input)
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
foundExif = true
if (fields.contains(KEY_APERTURE)) {
dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
}
if (fields.contains(KEY_DESCRIPTION)) {
getDescriptionByMetadataExtractor(metadata)?.let { metadataMap[KEY_DESCRIPTION] = it }
}
if (fields.contains(KEY_EXPOSURE_TIME)) {
dir.getSafeRational(ExifDirectoryBase.TAG_EXPOSURE_TIME, saveExposureTime)
}
if (fields.contains(KEY_FOCAL_LENGTH)) {
dir.getSafeRational(ExifDirectoryBase.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
}
if (fields.contains(KEY_ISO)) {
dir.getSafeInt(ExifDirectoryBase.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
}
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) {
@ -862,11 +873,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
if (fields.contains(KEY_APERTURE)) {
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
}
if (fields.contains(KEY_EXPOSURE_TIME)) {
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
}
if (fields.contains(KEY_FOCAL_LENGTH)) {
exif.getSafeDouble(ExifInterface.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
}
if (fields.contains(KEY_ISO)) {
exif.getSafeInt(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) { metadataMap[KEY_ISO] = it }
}
}
} catch (e: Exception) {
// ExifInterface initialization can fail with a RuntimeException
// caused by an internal MediaMetadataRetriever failure
@ -877,6 +896,47 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(metadataMap)
}
// return description from these fields (by precedence):
// - XMP / dc:description
// - IPTC / caption-abstract
// - Exif / UserComment
// - Exif / ImageDescription
private fun getDescriptionByMetadataExtractor(metadata: com.drew.metadata.Metadata): String? {
var description: String? = null
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
val xmpMeta = dir.xmpMeta
try {
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory", e)
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
// user comment field specifies encoding, unlike other string fields
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
if (string.isNotBlank()) {
description = string
}
}
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
}
}
return description
}
private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
@ -1191,70 +1251,6 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(dateMillis)
}
// return description from these fields (by precedence):
// - XMP / dc:description
// - IPTC / caption-abstract
// - Exif / UserComment
// - Exif / ImageDescription
private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) {
result.error("getDescription-args", "missing arguments", null)
return
}
var description: String? = null
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input)
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
val xmpMeta = dir.xmpMeta
try {
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
// user comment field specifies encoding, unlike other string fields
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
if (string.isNotBlank()) {
description = string
}
}
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = 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)
}
}
result.success(description)
}
companion object {
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
@ -1319,6 +1315,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// overlay metadata
private const val KEY_APERTURE = "aperture"
private const val KEY_DESCRIPTION = "description"
private const val KEY_EXPOSURE_TIME = "exposureTime"
private const val KEY_FOCAL_LENGTH = "focalLength"
private const val KEY_ISO = "iso"

View file

@ -18,6 +18,9 @@ import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
import kotlin.math.roundToInt
// As of Android 14 (API 34), `BitmapRegionDecoder` documentation states
// that "only the JPEG, PNG, WebP and HEIF formats are supported"
// but in practice it successfully decodes some others.
class RegionFetcher internal constructor(
private val context: Context,
) {

View file

@ -1,6 +1,7 @@
package deckers.thibault.aves.channel.calls.window
import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Build
import android.view.WindowManager
import deckers.thibault.aves.utils.getDisplayCompat
@ -75,4 +76,21 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
)
)
}
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.getDisplayCompat()?.isHdr ?: false)
}
override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("setHdrColorMode-args", "missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.window.colorMode = if (on) ActivityInfo.COLOR_MODE_HDR else ActivityInfo.COLOR_MODE_DEFAULT
}
result.success(null)
}
}

View file

@ -28,4 +28,12 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
result.success(HashMap<String, Any>())
}
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
}

View file

@ -18,6 +18,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
"supportsHdr" -> Coresult.safe(call, result, ::supportsHdr)
"setHdrColorMode" -> Coresult.safe(call, result, ::setHdrColorMode)
else -> result.notImplemented()
}
}
@ -44,6 +46,10 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
abstract fun supportsHdr(call: MethodCall, result: MethodChannel.Result)
abstract fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result)
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window"

View file

@ -48,7 +48,8 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
val page = model.page ?: 0
var sampleSize = 1
if (width > 0 && height > 0) {
val customSize = width > 0 && height > 0
if (customSize) {
// determine sample size
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
if (fd == null) {
@ -63,7 +64,7 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
val imageWidth = options.outWidth
val imageHeight = options.outHeight
if (imageHeight > height || imageWidth > width) {
while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) {
while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
sampleSize *= 2
}
}
@ -84,6 +85,18 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options)
if (bitmap == null) {
callback.onLoadFailed(Exception("Decoding full TIFF yielded null bitmap"))
} else if (customSize) {
val dstWidth: Int
val dstHeight: Int
val aspectRatio = bitmap.width.toFloat() / bitmap.height
if (aspectRatio > 1) {
dstWidth = (height * aspectRatio).toInt()
dstHeight = height
} else {
dstWidth = width
dstHeight = (width / aspectRatio).toInt()
}
callback.onDataReady(Bitmap.createScaledBitmap(bitmap, dstWidth, dstHeight, true))
} else {
callback.onDataReady(bitmap)
}

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import deckers.thibault.aves.metadata.Metadata.TYPE_COMMENT
import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF
import deckers.thibault.aves.metadata.Metadata.TYPE_ICC_PROFILE
@ -10,6 +12,8 @@ import deckers.thibault.aves.metadata.Metadata.TYPE_JPEG_DUCKY
import deckers.thibault.aves.metadata.Metadata.TYPE_PHOTOSHOP_IRB
import deckers.thibault.aves.metadata.Metadata.TYPE_XMP
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import pixy.meta.meta.Metadata
import pixy.meta.meta.MetadataEntry
import pixy.meta.meta.MetadataType
@ -19,6 +23,7 @@ import pixy.meta.meta.iptc.IPTCRecord
import pixy.meta.meta.jpeg.JPGMeta
import pixy.meta.meta.xmp.XMP
import pixy.meta.string.XMLUtils
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.util.*
@ -105,6 +110,48 @@ object PixyMetaHelper {
fun XMP.extendedXmpDocString(): String = XMLUtils.serializeToString(extendedXmpDocument)
fun copyIptcXmp(
context: Context,
sourceMimeType: String,
sourceUri: Uri,
targetMimeType: String,
targetUri: Uri,
editableFile: File,
) {
var pixyIptc: IPTC? = null
var pixyXmp: XMP? = null
if (MimeTypes.canReadWithPixyMeta(sourceMimeType)) {
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
val metadata = Metadata.readMetadata(input)
if (MimeTypes.canEditIptc(targetMimeType)) {
pixyIptc = metadata[MetadataType.IPTC] as IPTC?
}
if (MimeTypes.canEditXmp(targetMimeType)) {
pixyXmp = metadata[MetadataType.XMP] as XMP?
}
}
}
if (pixyIptc != null || pixyXmp != null) {
editableFile.outputStream().use { output ->
if (pixyIptc != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val iptcs = pixyIptc!!.dataSets.flatMap { it.value }
Metadata.insertIPTC(input, output, iptcs)
}
}
if (pixyXmp != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val xmpString = pixyXmp!!.xmpDocString()
val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null
setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null)
}
}
}
}
}
fun removeMetadata(input: InputStream, output: OutputStream, metadataTypes: Set<String>) {
val types = metadataTypes.map(::toMetadataType).toTypedArray()
Metadata.removeMetadata(input, output, *types)

View file

@ -18,6 +18,12 @@ class AvesEntry(map: FieldMap) {
val isRotated: Boolean
get() = rotationDegrees % 180 == 90
val displayWidth: Int
get() = if (isRotated) height else width
val displayHeight: Int
get() = if (isRotated) width else height
companion object {
// convenience method
private fun toLong(o: Any?): Long? = when (o) {

View file

@ -53,15 +53,12 @@ import deckers.thibault.aves.utils.MimeTypes.canEditExif
import deckers.thibault.aves.utils.MimeTypes.canEditIptc
import deckers.thibault.aves.utils.MimeTypes.canEditXmp
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata
import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pixy.meta.meta.Metadata
import pixy.meta.meta.MetadataType
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
@ -301,11 +298,18 @@ abstract class ImageProvider {
sourceDocFile.copyTo(output)
}
} else {
var targetWidthPx: Int = if (sourceEntry.isRotated) height else width
var targetHeightPx: Int = if (sourceEntry.isRotated) width else height
if (lengthUnit == LENGTH_UNIT_PERCENT) {
targetWidthPx = sourceEntry.width * targetWidthPx / 100
targetHeightPx = sourceEntry.height * targetHeightPx / 100
val targetWidthPx: Int
val targetHeightPx: Int
when (lengthUnit) {
LENGTH_UNIT_PERCENT -> {
targetWidthPx = sourceEntry.displayWidth * width / 100
targetHeightPx = sourceEntry.displayHeight * height / 100
}
else -> {
targetWidthPx = width
targetHeightPx = height
}
}
val model: Any = if (pageId != null && MultiPageImage.isSupported(sourceMimeType)) {
@ -405,39 +409,7 @@ abstract class ImageProvider {
}
// copy IPTC / XMP via PixyMeta
var pixyIptc: pixy.meta.meta.iptc.IPTC? = null
var pixyXmp: pixy.meta.meta.xmp.XMP? = null
if (canReadWithPixyMeta(sourceMimeType)) {
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
val metadata = Metadata.readMetadata(input)
if (canEditIptc(targetMimeType)) {
pixyIptc = metadata[MetadataType.IPTC] as pixy.meta.meta.iptc.IPTC?
}
if (canEditXmp(targetMimeType)) {
pixyXmp = metadata[MetadataType.XMP] as pixy.meta.meta.xmp.XMP?
}
}
}
if (pixyIptc != null || pixyXmp != null) {
editableFile.outputStream().use { output ->
if (pixyIptc != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val iptcs = pixyIptc!!.dataSets.flatMap { it.value }
Metadata.insertIPTC(input, output, iptcs)
}
}
if (pixyXmp != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val xmpString = pixyXmp!!.xmpDocString()
val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null
PixyMetaHelper.setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null)
}
}
}
}
PixyMetaHelper.copyIptcXmp(context, sourceMimeType, sourceUri, targetMimeType, targetUri, editableFile)
// copy Exif via ExifInterface

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ᱮᱣᱥ</string>
<string name="app_widget_label">ᱪᱤᱛᱟᱹᱨ ᱯᱷᱨᱮᱢ</string>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="app_widget_label">Foto Ram</string>
<string name="wallpaper">Bakgrund</string>
<string name="safe_mode_shortcut_short_label">Felsäkert läge</string>
<string name="videos_shortcut_short_label">Videor</string>
<string name="analysis_channel_name">Media scanning</string>
<string name="analysis_notification_default_title">Scannar media</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="search_shortcut_short_label">Sök</string>
</resources>

View file

@ -2,7 +2,7 @@ buildscript {
ext {
kotlin_version = '1.9.21'
ksp_version = "$kotlin_version-1.0.15"
agp_version = '8.1.4'
agp_version = '8.2.2'
glide_version = '4.16.0'
// AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550
huawei_agconnect_version = '1.9.1.300'

View file

@ -0,0 +1,4 @@
In v1.10.3:
- customize your home page
- analyze your images with the histogram (for real this time)
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.10.3:
- customize your home page
- analyze your images with the histogram (for real this time)
Full changelog available on GitHub

View file

@ -1,5 +1,5 @@
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
<i>Aves</i> getur meðhöndlað allar algengar gerðir mynda og myndskeiða, þar með talið JPEG og MP4, en einnig sjaldgæfari skrár á borð við <b>marg-síðna TIFF-myndir, SVG-línuteikningar, eldri gerðir AVI-skráa og margt fleira</b>! Forritið skannar safnið þitt til að greina <b>hreyfiljósmyndir</b>, <b>víðmyndir</b> (t.d. myndahnetti), <b>360° myndskeið</b>, auk <b>GeoTIFF-skráa</b>.
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
<b>Flakk og leit</b> eru mikilvægir hlutar <i>Aves</i>. Markmiðið er að notendur eigi auðvelt með að flæða úr albúmum yfir í ljósmyndir yfir í merki eða landakort, o.s.frv.
<i>Aves</i> integrates with Android (from KitKat to Android 14, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
<i>Aves</i> samtvinnast við Android (frá KitKat til Android 14, að meðtöldu Android TV) með eiginleikum á borð við <b>viðmótshluta</b>, <b>flýtileiðir í forrit</b>, <b>skjáhvílu</b> og <b>víðværa leit</b>. Það virkar einnig sem <b>margmiðlunarskoðari og veljari</b>.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1 +1 @@
Gallery and metadata explorer
Myndasafn og skoðun lýsigagna

View file

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

View file

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

View file

@ -0,0 +1,5 @@
<i>Aves</i> kan hantera alla typer av bilder och videor, inklusive vanliga JPEG- och MP4-filer, men även mer exotiska filer som <b>flersidiga TIFF-filer, SVG-filer, gamla AVI-filer och mycket mer</b>! Den skannar din mediasamling för att identifiera <b>rörelsefoton</b>, <b>panoramor</b> (även kallade fotosfärer), <b>360° videor</b>, samnt <b>GeoTIFF</b> filer.
<b>Navigering och sökhantering</b> är än viktigt del av <i>Aves</i>. Målet är att användarna på ett smidigt sätt ska kunna gå från album till foton till taggar till kartor, osv.
<i>Aves</i> integrerar med Android (från KitKat till Android 14, inklusive Android TV) med funktioner som <b>widgetar</b>, <b>appgenvägar</b>, <b>skärmsläckare</b> och <b>global sökhantering.</b> Den fungerar också som en <b>mediavisare och mediaväljare</b>.

View file

@ -0,0 +1 @@
Galleri och metadatautforskare

View file

@ -1,5 +1,9 @@
import 'package:aves_model/aves_model.dart';
extension ExtraMetadataSyntheticFieldConvert on MetadataSyntheticField {
String? get toPlatform => name;
}
extension ExtraMetadataFieldConvert on MetadataField {
MetadataType get type {
switch (this) {

View file

@ -1,6 +1,7 @@
import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart';
import 'package:aves_report/aves_report.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@ -46,14 +47,14 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
taskKey: key,
);
if (bytes.isEmpty) {
throw StateError('$uri ($mimeType) loading failed');
throw UnreportedStateError('$uri ($mimeType) loading failed');
}
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (error) {
// loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType decoding failed (page $pageId)');
throw UnreportedStateError('$mimeType decoding failed (page $pageId)');
}
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart';
import 'package:aves_report/aves_report.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@ -64,14 +65,14 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
},
);
if (bytes.isEmpty) {
throw StateError('$uri ($mimeType) loading failed');
throw UnreportedStateError('$uri ($mimeType) loading failed');
}
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (error) {
// loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType decoding failed (page $pageId)');
throw UnreportedStateError('$mimeType decoding failed (page $pageId)');
} finally {
unawaited(chunkEvents.close());
}

View file

@ -479,7 +479,7 @@
"@statsTopCountriesSectionTitle": {},
"settingsActionImport": "إستيراد",
"@settingsActionImport": {},
"viewerInfoLabelSize": "المقاس",
"viewerInfoLabelSize": "الحجم",
"@viewerInfoLabelSize": {},
"locationPickerUseThisLocationButton": "استخدم هذا الموقع",
"@locationPickerUseThisLocationButton": {},
@ -1518,5 +1518,11 @@
"filterNoDateLabel": "غير مؤرخ",
"@filterNoDateLabel": {},
"exportEntryDialogWriteMetadata": "كتابة البيانات الوصفية",
"@exportEntryDialogWriteMetadata": {}
"@exportEntryDialogWriteMetadata": {},
"settingsThumbnailShowHdrIcon": "إظهار أيقونة HDR",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "تعيين كخلفية",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "مجموعة مخصصة",
"@setHomeCustomCollection": {}
}

View file

@ -649,7 +649,7 @@
"@viewerTransitionSlide": {},
"overlayHistogramRGB": "RGB",
"@overlayHistogramRGB": {},
"overlayHistogramNone": "Няма",
"overlayHistogramNone": "Не",
"@overlayHistogramNone": {},
"overlayHistogramLuminance": "Яркасць",
"@overlayHistogramLuminance": {},
@ -931,7 +931,7 @@
"@collectionGroupNone": {},
"searchRatingSectionTitle": "Рэйтынгі",
"@searchRatingSectionTitle": {},
"settingsDisabled": "Адключана",
"settingsDisabled": "Адкл.",
"@settingsDisabled": {},
"settingsActionImportDialogTitle": "Імпарт",
"@settingsActionImportDialogTitle": {},
@ -1518,5 +1518,11 @@
"placeholders": {
"minutes": {}
}
}
},
"collectionActionSetHome": "Усталяваць як галоўную",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Уласная калекцыя",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
"@settingsThumbnailShowHdrIcon": {}
}

View file

@ -570,6 +570,7 @@
"collectionActionShowTitleSearch": "Show title filter",
"collectionActionHideTitleSearch": "Hide title filter",
"collectionActionAddShortcut": "Add shortcut",
"collectionActionSetHome": "Set as home",
"collectionActionEmptyBin": "Empty bin",
"collectionActionCopy": "Copy to album",
"collectionActionMove": "Move to album",
@ -757,6 +758,7 @@
"settingsNavigationSectionTitle": "Navigation",
"settingsHomeTile": "Home",
"settingsHomeDialogTitle": "Home",
"setHomeCustomCollection": "Custom collection",
"settingsShowBottomNavigationBar": "Show bottom navigation bar",
"settingsKeepScreenOnTile": "Keep screen on",
"settingsKeepScreenOnDialogTitle": "Keep Screen On",
@ -781,6 +783,7 @@
"settingsThumbnailSectionTitle": "Thumbnails",
"settingsThumbnailOverlayTile": "Overlay",
"settingsThumbnailOverlayPageTitle": "Overlay",
"settingsThumbnailShowHdrIcon": "Show HDR icon",
"settingsThumbnailShowFavouriteIcon": "Show favorite icon",
"settingsThumbnailShowTagIcon": "Show tag icon",
"settingsThumbnailShowLocationIcon": "Show location icon",

View file

@ -1360,5 +1360,11 @@
"entryActionCast": "Echar",
"@entryActionCast": {},
"castDialogTitle": "Dispositivos Cast",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Mostrar el icono del HDR",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Fijar como inicio",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Colección personalizada",
"@setHomeCustomCollection": {}
}

View file

@ -959,15 +959,15 @@
"@settingsNavigationDrawerAddAlbum": {},
"settingsThumbnailSectionTitle": "Miniaturak",
"@settingsThumbnailSectionTitle": {},
"settingsThumbnailShowFavouriteIcon": "Gogokoen ikonoa erakutsi",
"settingsThumbnailShowFavouriteIcon": "Erakutsi gogokoen ikonoa",
"@settingsThumbnailShowFavouriteIcon": {},
"settingsThumbnailShowTagIcon": "Etiketaren ikonoa erakutsi",
"settingsThumbnailShowTagIcon": "Erakutsi etiketaren ikonoa",
"@settingsThumbnailShowTagIcon": {},
"settingsThumbnailShowMotionPhotoIcon": "Mugimendu-argazkiaren ikonoa erakutsi",
"settingsThumbnailShowMotionPhotoIcon": "Erakutsi mugimendu-argazkiaren ikonoa",
"@settingsThumbnailShowMotionPhotoIcon": {},
"settingsThumbnailShowRawIcon": "Raw ikonoa erakutsi",
"settingsThumbnailShowRawIcon": "Erakutsi raw ikonoa",
"@settingsThumbnailShowRawIcon": {},
"settingsThumbnailShowVideoDuration": "Bideoaren iraupena erakutsi",
"settingsThumbnailShowVideoDuration": "Erakutsi bideoaren iraupena",
"@settingsThumbnailShowVideoDuration": {},
"settingsCollectionQuickActionEditorPageTitle": "Ekintza azkarrak",
"@settingsCollectionQuickActionEditorPageTitle": {},
@ -975,13 +975,13 @@
"@settingsCollectionQuickActionTabBrowsing": {},
"settingsThumbnailOverlayPageTitle": "Inkrustazioak",
"@settingsThumbnailOverlayPageTitle": {},
"settingsThumbnailShowLocationIcon": "Kokapenaren ikonoa erakutsi",
"settingsThumbnailShowLocationIcon": "Erakutsi kokalekuaren ikonoa",
"@settingsThumbnailShowLocationIcon": {},
"settingsThumbnailShowRating": "Balorazioa erakutsi",
"settingsThumbnailShowRating": "Erakutsi balorazioa",
"@settingsThumbnailShowRating": {},
"settingsCollectionQuickActionsTile": "Ekintza azkarrak",
"@settingsCollectionQuickActionsTile": {},
"settingsCollectionQuickActionTabSelecting": "Aukeraketa",
"settingsCollectionQuickActionTabSelecting": "Hautapena",
"@settingsCollectionQuickActionTabSelecting": {},
"searchMetadataSectionTitle": "Metadatuak",
"@searchMetadataSectionTitle": {},
@ -1025,7 +1025,7 @@
"@settingsViewerSectionTitle": {},
"settingsViewerGestureSideTapNext": "Pantailaren ertzetan sakatu aurreko/hurrengo elementua bistaratzeko",
"@settingsViewerGestureSideTapNext": {},
"settingsViewerUseCutout": "Moztutako eremua erabili",
"settingsViewerUseCutout": "Erabili moztutako eremua",
"@settingsViewerUseCutout": {},
"settingsImageBackground": "Atzeko irudia",
"@settingsImageBackground": {},
@ -1043,15 +1043,15 @@
"@settingsViewerQuickActionEmpty": {},
"settingsViewerOverlayPageTitle": "Inkrustazioak",
"@settingsViewerOverlayPageTitle": {},
"settingsViewerShowMinimap": "Bistaratu minimapa",
"settingsViewerShowMinimap": "Erakutsi minimapa",
"@settingsViewerShowMinimap": {},
"settingsViewerShowInformation": "Bistaratu informazioa",
"settingsViewerShowInformation": "Erakutsi informazioa",
"@settingsViewerShowInformation": {},
"settingsViewerShowInformationSubtitle": "Bistaratu izenburua, data, kokapena, etab.",
"settingsViewerShowInformationSubtitle": "Erakutsi izenburua, data, kokalekua, etab.",
"@settingsViewerShowInformationSubtitle": {},
"settingsViewerShowDescription": "Bistaratu deskribapena",
"settingsViewerShowDescription": "Erakutsi azalpena",
"@settingsViewerShowDescription": {},
"settingsViewerShowOverlayThumbnails": "Bistaratu miniaturak",
"settingsViewerShowOverlayThumbnails": "Erakutsi miniaturak",
"@settingsViewerShowOverlayThumbnails": {},
"settingsSlideshowTransitionTile": "Trantsizioa",
"@settingsSlideshowTransitionTile": {},
@ -1141,13 +1141,13 @@
"@settingsMotionPhotoAutoPlay": {},
"settingsViewerOverlayTile": "Inkrustazioak",
"@settingsViewerOverlayTile": {},
"settingsViewerShowOverlayOnOpening": "Bistaratu hasieran",
"settingsViewerShowOverlayOnOpening": "Erakutsi irekitzean",
"@settingsViewerShowOverlayOnOpening": {},
"settingsViewerShowRatingTags": "Bistaratu balorazioa eta etiketak",
"settingsViewerShowRatingTags": "Erakutsi balorazioa eta etiketak",
"@settingsViewerShowRatingTags": {},
"settingsViewerEnableOverlayBlurEffect": "Lausotze efektua",
"@settingsViewerEnableOverlayBlurEffect": {},
"settingsViewerShowShootingDetails": "Bistaratu hartualdiaren xehetasunak",
"settingsViewerShowShootingDetails": "Erakutsi hartualdiaren xehetasunak",
"@settingsViewerShowShootingDetails": {},
"settingsViewerSlideshowPageTitle": "Aurkezpena",
"@settingsViewerSlideshowPageTitle": {},
@ -1155,7 +1155,7 @@
"@settingsViewerSlideshowTile": {},
"settingsSlideshowShuffle": "Nahastu",
"@settingsSlideshowShuffle": {},
"settingsSlideshowFillScreen": "Pantaila bete",
"settingsSlideshowFillScreen": "Bete pantaila",
"@settingsSlideshowFillScreen": {},
"settingsSlideshowRepeat": "Errepikatu",
"@settingsSlideshowRepeat": {},
@ -1165,11 +1165,11 @@
"@settingsSlideshowVideoPlaybackTile": {},
"settingsSubtitleThemeTextPositionTile": "Testuaren posizioa",
"@settingsSubtitleThemeTextPositionTile": {},
"settingsVideoShowVideos": "Bideoak erakutsi",
"settingsVideoShowVideos": "Erakutsi bideoak",
"@settingsVideoShowVideos": {},
"settingsVideoPageTitle": "Bideoen ezarpenak",
"settingsVideoPageTitle": "Bideoaren ezarpenak",
"@settingsVideoPageTitle": {},
"settingsVideoSectionTitle": "Bideo",
"settingsVideoSectionTitle": "Bideoa",
"@settingsVideoSectionTitle": {},
"settingsVideoEnableHardwareAcceleration": "Hardwarearen azelerazioa",
"@settingsVideoEnableHardwareAcceleration": {},
@ -1427,13 +1427,13 @@
"@patternDialogConfirm": {},
"settingsVideoEnablePip": "Bideoa leihotxoan",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
"settingsVideoBackgroundMode": "Atzeko planoko modua",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua",
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsNone": "Bat ere ez",
"@settingsCollectionBurstPatternsNone": {},
"settingsCollectionBurstPatternsTile": "Segida moduak",
"settingsCollectionBurstPatternsTile": "Segida ereduak",
"@settingsCollectionBurstPatternsTile": {},
"tagPlaceholderState": "Egoera",
"@tagPlaceholderState": {},
@ -1518,5 +1518,11 @@
"entryActionCast": "Igorri",
"@entryActionCast": {},
"castDialogTitle": "Igortzeko gailuak",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Erakutsi HDR ikonoa",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Ezarri hasiera gisa",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Bilduma pertsonalizatua",
"@setHomeCustomCollection": {}
}

View file

@ -1360,5 +1360,11 @@
"entryActionCast": "Caster",
"@entryActionCast": {},
"castDialogTitle": "Appareils",
"@castDialogTitle": {}
"@castDialogTitle": {},
"collectionActionSetHome": "Définir comme page daccueil",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Collection personnalisée",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afficher licône HDR",
"@settingsThumbnailShowHdrIcon": {}
}

View file

@ -1518,5 +1518,11 @@
"entryActionCast": "Kivetítés",
"@entryActionCast": {},
"castDialogTitle": "Kivetítő eszközök",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "HDR ikon megjelenítése",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Egyéni gyűjtemény",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Kezdőlapnak beállít",
"@collectionActionSetHome": {}
}

View file

@ -1518,5 +1518,11 @@
"settingsStorageAccessBanner": "Sumar möppur krefjast þess að gefin sé sérstök heimild til að breyta skrám í þeim. Þú getur yfirfarið hér þær möppur sem þú hefur gefið aðgangaheimildir fyrir.",
"@settingsStorageAccessBanner": {},
"settingsCollectionBurstPatternsTile": "Mynstur runu",
"@settingsCollectionBurstPatternsTile": {}
"@settingsCollectionBurstPatternsTile": {},
"settingsThumbnailShowHdrIcon": "Birta HDR-táknmynd",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Setja sem upphafsskjá",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Sérsniðið safn",
"@setHomeCustomCollection": {}
}

View file

@ -1360,5 +1360,11 @@
"entryActionCast": "전송",
"@entryActionCast": {},
"castDialogTitle": "전송 대상",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "HDR 아이콘 표시",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "홈으로 설정",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "지정 미디어",
"@setHomeCustomCollection": {}
}

View file

@ -1518,5 +1518,11 @@
"entryActionCast": "Cast",
"@entryActionCast": {},
"castDialogTitle": "Urządzenia Cast",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Własna kolekcja",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Ustaw jako stronę główną",
"@collectionActionSetHome": {}
}

View file

@ -1360,5 +1360,11 @@
"entryActionCast": "Трансляция",
"@entryActionCast": {},
"castDialogTitle": "Устройства трансляции",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Показать значок HDR",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Собственная коллекция",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Установить как главную",
"@collectionActionSetHome": {}
}

7
lib/l10n/app_sat.arb Normal file
View file

@ -0,0 +1,7 @@
{
"@@locale": "sat",
"welcomeMessage": "ᱮᱣᱥ ᱨᱮ ᱥᱟᱹᱜᱩᱱ ᱫᱟᱨᱟᱢ",
"@welcomeMessage": {},
"appName": "ᱮᱣᱥ",
"@appName": {}
}

View file

@ -1518,5 +1518,11 @@
"aboutDataUsageClearCache": "Vymazať cache",
"@aboutDataUsageClearCache": {},
"castDialogTitle": "Cast zariadenia",
"@castDialogTitle": {}
"@castDialogTitle": {},
"collectionActionSetHome": "Nastaviť ako doma",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Kolekcia na mieru",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR",
"@settingsThumbnailShowHdrIcon": {}
}

839
lib/l10n/app_sv.arb Normal file
View file

@ -0,0 +1,839 @@
{
"viewerActionLock": "Lås",
"@viewerActionLock": {},
"entryInfoActionEditTags": "Redigera taggar",
"@entryInfoActionEditTags": {},
"videoActionPlay": "Spela",
"@videoActionPlay": {},
"viewerActionSettings": "Inställningar",
"@viewerActionSettings": {},
"albumTierSpecial": "Vanlig",
"@albumTierSpecial": {},
"displayRefreshRatePreferLowest": "Lägsta intervall",
"@displayRefreshRatePreferLowest": {},
"keepScreenOnViewerOnly": "Visningssidan bara",
"@keepScreenOnViewerOnly": {},
"mapStyleHuaweiTerrain": "Petal Maps (Terrain)",
"@mapStyleHuaweiTerrain": {},
"mapStyleHuaweiNormal": "Petal Maps",
"@mapStyleHuaweiNormal": {},
"mapStyleOsmHot": "Humanitarian OSM",
"@mapStyleOsmHot": {},
"videoResumptionModeAlways": "Alltid",
"@videoResumptionModeAlways": {},
"storageVolumeDescriptionFallbackPrimary": "Intern lagring",
"@storageVolumeDescriptionFallbackPrimary": {},
"widgetOpenPageCollection": "Öppen insamling",
"@widgetOpenPageCollection": {},
"widgetTapUpdateWidget": "Uppdatera widgeten",
"@widgetTapUpdateWidget": {},
"storageVolumeDescriptionFallbackNonPrimary": "SD kort",
"@storageVolumeDescriptionFallbackNonPrimary": {},
"welcomeTermsToggle": "Jag godkäner användar villkoren",
"@welcomeTermsToggle": {},
"focalLength": "{length} mm",
"@focalLength": {
"placeholders": {
"length": {
"type": "String",
"example": "5.4"
}
}
},
"deleteButtonLabel": "Ta bort",
"@deleteButtonLabel": {},
"showButtonLabel": "Visa",
"@showButtonLabel": {},
"hideButtonLabel": "Göm",
"@hideButtonLabel": {},
"continueButtonLabel": "Fortsätt",
"@continueButtonLabel": {},
"saveCopyButtonLabel": "Spara kopia",
"@saveCopyButtonLabel": {},
"changeTooltip": "Ändra",
"@changeTooltip": {},
"clearTooltip": "Rensa",
"@clearTooltip": {},
"previousTooltip": "Föregående",
"@previousTooltip": {},
"showTooltip": "Visa",
"@showTooltip": {},
"hideTooltip": "Göm",
"@hideTooltip": {},
"actionRemove": "Ta bort",
"@actionRemove": {},
"resetTooltip": "Återställ",
"@resetTooltip": {},
"saveTooltip": "Spara",
"@saveTooltip": {},
"pickTooltip": "Välj",
"@pickTooltip": {},
"sourceStateLocatingCountries": "Lokaliserar länder",
"@sourceStateLocatingCountries": {},
"sourceStateLocatingPlaces": "Lokaliserar platser",
"@sourceStateLocatingPlaces": {},
"chipActionDelete": "Ta bort",
"@chipActionDelete": {},
"chipActionGoToAlbumPage": "Visa i album",
"@chipActionGoToAlbumPage": {},
"welcomeOptional": "Valfritt",
"@welcomeOptional": {},
"applyButtonLabel": "TILLÄMPA",
"@applyButtonLabel": {},
"applyTooltip": "Tillämpa",
"@applyTooltip": {},
"cancelTooltip": "Avbryt",
"@cancelTooltip": {},
"doubleBackExitMessage": "Tryck \"bakåt\" igen för att stänga.",
"@doubleBackExitMessage": {},
"chipActionGoToCountryPage": "Visa i länder",
"@chipActionGoToCountryPage": {},
"chipActionGoToPlacePage": "Visa på Platser",
"@chipActionGoToPlacePage": {},
"chipActionGoToTagPage": "Visa i Taggar",
"@chipActionGoToTagPage": {},
"chipActionFilterOut": "Filtrera ut",
"@chipActionFilterOut": {},
"chipActionFilterIn": "Filtrera fram",
"@chipActionFilterIn": {},
"chipActionHide": "Göm",
"@chipActionHide": {},
"chipActionLock": "Lås",
"@chipActionLock": {},
"itemCount": "{count, plural, other{{count} objekt}}",
"@itemCount": {
"placeholders": {
"count": {}
}
},
"columnCount": "{count, plural, =1{1 kolumn} other{{count} kolumner}}",
"@columnCount": {
"placeholders": {
"count": {}
}
},
"timeSeconds": "{seconds, plural, =1{1 sekund} other{{seconds} sekunder}}",
"@timeSeconds": {
"placeholders": {
"seconds": {}
}
},
"timeMinutes": "{minutes, plural, =1{1 minut} other{{minutes} minuter}}",
"@timeMinutes": {
"placeholders": {
"minutes": {}
}
},
"timeDays": "{days, plural, =1{1 dag} other{{days} dagar}}",
"@timeDays": {
"placeholders": {
"days": {}
}
},
"chipActionPin": "Fäst överst",
"@chipActionPin": {},
"chipActionRename": "Byt namn",
"@chipActionRename": {},
"chipActionSetCover": "Välj som omslag",
"@chipActionSetCover": {},
"chipActionShowCountryStates": "Visa delstater",
"@chipActionShowCountryStates": {},
"chipActionCreateAlbum": "Skapa album",
"@chipActionCreateAlbum": {},
"chipActionCreateVault": "Skapa valv",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Konfigurera valv",
"@chipActionConfigureVault": {},
"entryActionConvert": "Konvertera",
"@entryActionConvert": {},
"entryActionExport": "Exportera",
"@entryActionExport": {},
"entryActionInfo": "Info",
"@entryActionInfo": {},
"entryActionRename": "Byt namn",
"@entryActionRename": {},
"entryActionRestore": "Återställ",
"@entryActionRestore": {},
"entryActionRotateCCW": "Rotera moturs",
"@entryActionRotateCCW": {},
"entryActionRotateCW": "Rotera medurs",
"@entryActionRotateCW": {},
"entryActionFlip": "Vrid horisontellt",
"@entryActionFlip": {},
"entryActionPrint": "Skriv ut",
"@entryActionPrint": {},
"entryActionShare": "Dela",
"@entryActionShare": {},
"entryActionShareImageOnly": "Dela endast bild",
"@entryActionShareImageOnly": {},
"entryActionShareVideoOnly": "Dela endast video",
"@entryActionShareVideoOnly": {},
"entryActionViewSource": "Visa källa",
"@entryActionViewSource": {},
"entryActionShowGeoTiffOnMap": "Visa som kartöverlägg",
"@entryActionShowGeoTiffOnMap": {},
"entryActionConvertMotionPhotoToStillImage": "Konvertera till stillbild",
"@entryActionConvertMotionPhotoToStillImage": {},
"entryActionViewMotionPhotoVideo": "Öppna video",
"@entryActionViewMotionPhotoVideo": {},
"entryActionEdit": "Redigera",
"@entryActionEdit": {},
"entryActionOpen": "Öppna med",
"@entryActionOpen": {},
"entryActionSetAs": "Välj som",
"@entryActionSetAs": {},
"entryActionOpenMap": "Visa i kartappen",
"@entryActionOpenMap": {},
"entryActionRotateScreen": "Rotera skärmen",
"@entryActionRotateScreen": {},
"entryActionAddFavourite": "Lägg till i favoritlistan",
"@entryActionAddFavourite": {},
"entryActionRemoveFavourite": "Lägg från favoritlistan",
"@entryActionRemoveFavourite": {},
"videoActionCaptureFrame": "Fånga stillbild",
"@videoActionCaptureFrame": {},
"videoActionMute": "Ljudlös",
"@videoActionMute": {},
"videoActionUnmute": "Ljud",
"@videoActionUnmute": {},
"videoActionPause": "Pausa",
"@videoActionPause": {},
"videoActionReplay10": "Spola tillbaks 10 sekunder",
"@videoActionReplay10": {},
"videoActionSkip10": "Spola framåt 10 sekunder",
"@videoActionSkip10": {},
"videoActionSelectStreams": "Välj spår",
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Uppspelningshastighet",
"@videoActionSetSpeed": {},
"entryActionDelete": "Radera",
"@entryActionDelete": {},
"entryActionCopyToClipboard": "Spara till urklipp",
"@entryActionCopyToClipboard": {},
"viewerActionUnlock": "Öppna",
"@viewerActionUnlock": {},
"slideshowActionResume": "Återuppta",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Visa i samling",
"@slideshowActionShowInCollection": {},
"entryInfoActionEditDate": "Redigera datum & tid",
"@entryInfoActionEditDate": {},
"entryInfoActionEditLocation": "Redigera plats",
"@entryInfoActionEditLocation": {},
"entryInfoActionEditTitleDescription": "Redigera titel och beskrivning",
"@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Redigera betyg",
"@entryInfoActionEditRating": {},
"entryInfoActionRemoveMetadata": "Ta bort metadata",
"@entryInfoActionRemoveMetadata": {},
"entryInfoActionExportMetadata": "Exportera metadata",
"@entryInfoActionExportMetadata": {},
"entryInfoActionRemoveLocation": "Ta bort plats",
"@entryInfoActionRemoveLocation": {},
"editorActionTransform": "Transformera",
"@editorActionTransform": {},
"editorTransformCrop": "Beskär",
"@editorTransformCrop": {},
"editorTransformRotate": "Rotera",
"@editorTransformRotate": {},
"cropAspectRatioFree": "Fri",
"@cropAspectRatioFree": {},
"cropAspectRatioOriginal": "Original",
"@cropAspectRatioOriginal": {},
"cropAspectRatioSquare": "Fyrkant",
"@cropAspectRatioSquare": {},
"filterAspectRatioLandscapeLabel": "Liggande",
"@filterAspectRatioLandscapeLabel": {},
"filterAspectRatioPortraitLabel": "Porträtt",
"@filterAspectRatioPortraitLabel": {},
"filterBinLabel": "Papperskorg",
"@filterBinLabel": {},
"filterFavouriteLabel": "Favorit",
"@filterFavouriteLabel": {},
"filterNoDateLabel": "Odaterat",
"@filterNoDateLabel": {},
"filterNoAddressLabel": "Ingen adress",
"@filterNoAddressLabel": {},
"filterLocatedLabel": "Belägen",
"@filterLocatedLabel": {},
"filterNoLocationLabel": "Ej belägen",
"@filterNoLocationLabel": {},
"filterNoTagLabel": "otaggad",
"@filterNoTagLabel": {},
"filterNoTitleLabel": "Obetitlad",
"@filterNoTitleLabel": {},
"filterOnThisDayLabel": "På den här dagen",
"@filterOnThisDayLabel": {},
"filterRecentlyAddedLabel": "Nyligen tillagd",
"@filterRecentlyAddedLabel": {},
"filterRatingRejectedLabel": "Avisad",
"@filterRatingRejectedLabel": {},
"filterTypeAnimatedLabel": "Animerad",
"@filterTypeAnimatedLabel": {},
"filterTypeMotionPhotoLabel": "Rörelsefoto",
"@filterTypeMotionPhotoLabel": {},
"filterTypePanoramaLabel": "Panorama",
"@filterTypePanoramaLabel": {},
"filterTypeRawLabel": "Raw",
"@filterTypeRawLabel": {},
"filterTypeSphericalVideoLabel": "360° Video",
"@filterTypeSphericalVideoLabel": {},
"filterTypeGeotiffLabel": "GeoTIFF",
"@filterTypeGeotiffLabel": {},
"filterMimeImageLabel": "Bild",
"@filterMimeImageLabel": {},
"filterMimeVideoLabel": "Video",
"@filterMimeVideoLabel": {},
"accessibilityAnimationsRemove": "Förhindra skärmeffekter",
"@accessibilityAnimationsRemove": {},
"accessibilityAnimationsKeep": "Behåll skärmeffekter",
"@accessibilityAnimationsKeep": {},
"albumTierNew": "Ny",
"@albumTierNew": {},
"albumTierPinned": "Fastnålad",
"@albumTierPinned": {},
"albumTierApps": "Appar",
"@albumTierApps": {},
"albumTierVaults": "Valv",
"@albumTierVaults": {},
"albumTierRegular": "Andra",
"@albumTierRegular": {},
"coordinateFormatDms": "DMS",
"@coordinateFormatDms": {},
"coordinateFormatDecimal": "Decimal degrees",
"@coordinateFormatDecimal": {},
"coordinateDms": "{coordinate} {direction}",
"@coordinateDms": {
"placeholders": {
"coordinate": {
"type": "String",
"example": "38° 41 47.72″"
},
"direction": {
"type": "String",
"example": "S"
}
}
},
"coordinateDmsNorth": "N",
"@coordinateDmsNorth": {},
"coordinateDmsSouth": "S",
"@coordinateDmsSouth": {},
"coordinateDmsEast": "Ö",
"@coordinateDmsEast": {},
"coordinateDmsWest": "V",
"@coordinateDmsWest": {},
"displayRefreshRatePreferHighest": "Högsta intervall",
"@displayRefreshRatePreferHighest": {},
"keepScreenOnVideoPlayback": "Under videouppspelning",
"@keepScreenOnVideoPlayback": {},
"keepScreenOnAlways": "Alltid",
"@keepScreenOnAlways": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"@mapStyleGoogleHybrid": {},
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
"@mapStyleGoogleTerrain": {},
"mapStyleStamenWatercolor": "Stamen Watercolor",
"@mapStyleStamenWatercolor": {},
"maxBrightnessNever": "Alldrig",
"@maxBrightnessNever": {},
"maxBrightnessAlways": "Alltid",
"@maxBrightnessAlways": {},
"nameConflictStrategyRename": "Byt namn",
"@nameConflictStrategyRename": {},
"nameConflictStrategyReplace": "Ersätt",
"@nameConflictStrategyReplace": {},
"nameConflictStrategySkip": "Skippa",
"@nameConflictStrategySkip": {},
"overlayHistogramNone": "Igen",
"@overlayHistogramNone": {},
"overlayHistogramRGB": "RGB",
"@overlayHistogramRGB": {},
"overlayHistogramLuminance": "Luminans",
"@overlayHistogramLuminance": {},
"subtitlePositionBottom": "Nederkant",
"@subtitlePositionBottom": {},
"subtitlePositionTop": "Överkant",
"@subtitlePositionTop": {},
"themeBrightnessLight": "Ljus",
"@themeBrightnessLight": {},
"themeBrightnessDark": "Mörk",
"@themeBrightnessDark": {},
"themeBrightnessBlack": "Svart",
"@themeBrightnessBlack": {},
"unitSystemMetric": "Metrisk",
"@unitSystemMetric": {},
"unitSystemImperial": "Brittiskt",
"@unitSystemImperial": {},
"vaultLockTypePattern": "Mönster",
"@vaultLockTypePattern": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "Lösenord",
"@vaultLockTypePassword": {},
"settingsVideoEnablePip": "Bild-i-bild",
"@settingsVideoEnablePip": {},
"videoControlsPlay": "Spela",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Spela & sök bakåt/framåt",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Öppna med annan spelare",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Ingen",
"@videoControlsNone": {},
"videoLoopModeNever": "Aldrig",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Bara korta videor",
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Alltid",
"@videoLoopModeAlways": {},
"videoPlaybackSkip": "Skippa",
"@videoPlaybackSkip": {},
"videoPlaybackMuted": "Spela ljudlös",
"@videoPlaybackMuted": {},
"videoPlaybackWithSound": "Spela med ljud",
"@videoPlaybackWithSound": {},
"videoResumptionModeNever": "Alldrig",
"@videoResumptionModeNever": {},
"viewerTransitionSlide": "Glid",
"@viewerTransitionSlide": {},
"viewerTransitionParallax": "Parallax",
"@viewerTransitionParallax": {},
"viewerTransitionNone": "Igen",
"@viewerTransitionNone": {},
"wallpaperTargetHome": "Hemskärm",
"@wallpaperTargetHome": {},
"widgetDisplayedItemRandom": "Slumpvis",
"@widgetDisplayedItemRandom": {},
"widgetDisplayedItemMostRecent": "Alldra senast",
"@widgetDisplayedItemMostRecent": {},
"widgetOpenPageHome": "Öppna hem",
"@widgetOpenPageHome": {},
"otherDirectoryDescription": "“{name}” map",
"@otherDirectoryDescription": {
"placeholders": {
"name": {
"type": "String",
"example": "Pictures",
"description": "the name of a specific directory"
}
}
},
"storageAccessDialogMessage": "Var snäll och välj {directory} av“{volume}” på nästa skärm för att ge appen åtkomst till den.",
"@storageAccessDialogMessage": {
"placeholders": {
"directory": {
"type": "String",
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"videoStartOverButtonLabel": "BÖRJA OM",
"@videoStartOverButtonLabel": {},
"videoResumeButtonLabel": "Återuppta",
"@videoResumeButtonLabel": {},
"setCoverDialogLatest": "Senaste objektet",
"@setCoverDialogLatest": {},
"setCoverDialogAuto": "Auto",
"@setCoverDialogAuto": {},
"setCoverDialogCustom": "Anpassad",
"@setCoverDialogCustom": {},
"hideFilterConfirmationDialogMessage": "Matchande foton och videor kommer att döljas från din samling. Du kan välja att visa dem igen från \"sekretessinställningarna\".\n\nÄr du säker på att du vill dölja dem?",
"@hideFilterConfirmationDialogMessage": {},
"newAlbumDialogTitle": "Nytt Album",
"@newAlbumDialogTitle": {},
"newAlbumDialogNameLabelAlreadyExistsHelper": "Mappen existerar redan",
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
"newAlbumDialogStorageLabel": "Lagring:",
"@newAlbumDialogStorageLabel": {},
"newVaultDialogTitle": "Nytt Valv",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Konfigurera Valv",
"@configureVaultDialogTitle": {},
"vaultDialogLockTypeLabel": "Lås typ",
"@vaultDialogLockTypeLabel": {},
"pinDialogConfirm": "Bekräfta pinkod",
"@pinDialogConfirm": {},
"pinDialogEnter": "Ange Pinkod",
"@pinDialogEnter": {},
"passwordDialogEnter": "Ange lösenord",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Bekräfta lösenord",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Autentisera för att konfigurera valvet",
"@authenticateToConfigureVault": {},
"appName": "Aves",
"@appName": {},
"welcomeMessage": "Välkommen till Aves",
"@welcomeMessage": {},
"nextButtonLabel": "Nästa",
"@nextButtonLabel": {},
"nextTooltip": "Nästs",
"@nextTooltip": {},
"doNotAskAgain": "Fråga inte igen",
"@doNotAskAgain": {},
"sourceStateLoading": "Laddar",
"@sourceStateLoading": {},
"sourceStateCataloguing": "Katalogiserar",
"@sourceStateCataloguing": {},
"editEntryLocationDialogLongitude": "Longitud",
"@editEntryLocationDialogLongitude": {},
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP behövs för att spela upp videon i ett Rörelsefoto.\n\nÄr du säker att du vill ta bort det?",
"@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
"videoStreamSelectionDialogOff": "Av",
"@videoStreamSelectionDialogOff": {},
"videoStreamSelectionDialogTrack": "Spår",
"@videoStreamSelectionDialogTrack": {},
"videoStreamSelectionDialogNoSelection": "Det finns inga andra spår.",
"@videoStreamSelectionDialogNoSelection": {},
"genericSuccessFeedback": "Klar!",
"@genericSuccessFeedback": {},
"genericFailureFeedback": "Misslyckad",
"@genericFailureFeedback": {},
"genericDangerWarningDialogMessage": "Är du säker?",
"@genericDangerWarningDialogMessage": {},
"tooManyItemsErrorDialogMessage": "Försök ingen med färre objekt.",
"@tooManyItemsErrorDialogMessage": {},
"menuActionConfigureView": "Visa",
"@menuActionConfigureView": {},
"menuActionSelectAll": "Välj alla",
"@menuActionSelectAll": {},
"menuActionSelect": "Välj",
"@menuActionSelect": {},
"menuActionSelectNone": "Välj ingen",
"@menuActionSelectNone": {},
"menuActionMap": "Karta",
"@menuActionMap": {},
"menuActionSlideshow": "Bildspel",
"@menuActionSlideshow": {},
"menuActionStats": "Statistik",
"@menuActionStats": {},
"filterNoRatingLabel": "Ej betygsatt",
"@filterNoRatingLabel": {},
"viewerTransitionZoomIn": "Zooma in",
"@viewerTransitionZoomIn": {},
"nameConflictDialogSingleSourceMessage": "Vissa filer i destinationsmappen har samma namn.",
"@nameConflictDialogSingleSourceMessage": {},
"nameConflictDialogMultipleSourceMessage": "Vissa filer har samma namn.",
"@nameConflictDialogMultipleSourceMessage": {},
"noMatchingAppDialogMessage": "Det finns inga appar som kan hantera detta.",
"@noMatchingAppDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Spara datum",
"@moveUndatedConfirmationDialogSetDate": {},
"videoResumeDialogMessage": "Vill du återuppta uppspelningen vid",
"@videoResumeDialogMessage": {
"placeholders": {
"time": {
"type": "String",
"example": "13:37"
}
}
},
"newAlbumDialogNameLabel": "Album namn",
"@newAlbumDialogNameLabel": {},
"newVaultWarningDialogMessage": "Objekt i valv är endast tillgängliga i denna app och inga andra.\n\nOm du avinstallerar den här appen eller rensar appens data kommer du att förlora alla dessa objekt.",
"@newVaultWarningDialogMessage": {},
"vaultDialogLockModeWhenScreenOff": "Lås när skärmen stängs av",
"@vaultDialogLockModeWhenScreenOff": {},
"patternDialogEnter": "Ange mönster",
"@patternDialogEnter": {},
"patternDialogConfirm": "Bekräfta mönster",
"@patternDialogConfirm": {},
"renameEntrySetPagePatternFieldLabel": "Namnge mönster",
"@renameEntrySetPagePatternFieldLabel": {},
"renameEntrySetPageInsertTooltip": "Infoga ---",
"@renameEntrySetPageInsertTooltip": {},
"renameEntrySetPagePreviewSectionTitle": "Förhandsgranska",
"@renameEntrySetPagePreviewSectionTitle": {},
"renameProcessorName": "Namn",
"@renameProcessorName": {},
"exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {},
"exportEntryDialogWidth": "Bredd",
"@exportEntryDialogWidth": {},
"exportEntryDialogHeight": "Höjd",
"@exportEntryDialogHeight": {},
"exportEntryDialogQuality": "Kvalitet",
"@exportEntryDialogQuality": {},
"exportEntryDialogWriteMetadata": "Skriv metadata",
"@exportEntryDialogWriteMetadata": {},
"renameEntryDialogLabel": "Nytt namn",
"@renameEntryDialogLabel": {},
"editEntryDialogCopyFromItem": "Kopiera från annat objekt",
"@editEntryDialogCopyFromItem": {},
"editEntryDateDialogTitle": "Datum & Tid",
"@editEntryDateDialogTitle": {},
"editEntryDateDialogExtractFromTitle": "Kopiera från titel",
"@editEntryDateDialogExtractFromTitle": {},
"editEntryDateDialogShift": "Skift",
"@editEntryDateDialogShift": {},
"editEntryDateDialogSourceFileModifiedDate": "Filens modifieringsdatum",
"@editEntryDateDialogSourceFileModifiedDate": {},
"durationDialogHours": "Timmar",
"@durationDialogHours": {},
"durationDialogMinutes": "Minuter",
"@durationDialogMinutes": {},
"durationDialogSeconds": "Sekunder",
"@durationDialogSeconds": {},
"editEntryLocationDialogTitle": "Plats",
"@editEntryLocationDialogTitle": {},
"editEntryLocationDialogChooseOnMap": "Välj på karta",
"@editEntryLocationDialogChooseOnMap": {},
"editEntryLocationDialogLatitude": "Latitud",
"@editEntryLocationDialogLatitude": {},
"locationPickerUseThisLocationButton": "Använd den här platsen",
"@locationPickerUseThisLocationButton": {},
"editEntryRatingDialogTitle": "Betyg",
"@editEntryRatingDialogTitle": {},
"removeEntryMetadataDialogTitle": "Borttagning av metadata",
"@removeEntryMetadataDialogTitle": {},
"removeEntryMetadataDialogMore": "Mer",
"@removeEntryMetadataDialogMore": {},
"videoSpeedDialogLabel": "Uppspelningshastighet",
"@videoSpeedDialogLabel": {},
"videoStreamSelectionDialogVideo": "Video",
"@videoStreamSelectionDialogVideo": {},
"videoStreamSelectionDialogAudio": "Ljud",
"@videoStreamSelectionDialogAudio": {},
"videoStreamSelectionDialogText": "Undertexter",
"@videoStreamSelectionDialogText": {},
"aboutBugReportInstruction": "Rapportera på GitHub med loggarna och systeminformation.",
"@aboutBugReportInstruction": {},
"aboutDataUsageInternal": "Internt",
"@aboutDataUsageInternal": {},
"aboutLicensesBanner": "Den här appen använder följande paket och bibliotek under öppen källkod-licens",
"@aboutLicensesBanner": {},
"collectionPickPageTitle": "Välj",
"@collectionPickPageTitle": {},
"aboutBugCopyInfoButton": "Kopiera",
"@aboutBugCopyInfoButton": {},
"aboutBugReportButton": "Rapportera",
"@aboutBugReportButton": {},
"aboutDataUsageData": "Data",
"@aboutDataUsageData": {},
"aboutDataUsageSectionTitle": "DataAnvänding",
"@aboutDataUsageSectionTitle": {},
"aboutDataUsageCache": "Cache",
"@aboutDataUsageCache": {},
"aboutDataUsageDatabase": "Databas",
"@aboutDataUsageDatabase": {},
"aboutDataUsageMisc": "Annat",
"@aboutDataUsageMisc": {},
"aboutDataUsageExternal": "Externt",
"@aboutDataUsageExternal": {},
"aboutDataUsageClearCache": "Rensa Cacheminnet",
"@aboutDataUsageClearCache": {},
"aboutCreditsWorldAtlas1": "Den här appen använder en TopoJSON fil från",
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "under ISC Licens.",
"@aboutCreditsWorldAtlas2": {},
"aboutTranslatorsSectionTitle": "Översättare",
"@aboutTranslatorsSectionTitle": {},
"aboutLicensesSectionTitle": "Öppen-Källkod Licenser",
"@aboutLicensesSectionTitle": {},
"aboutLicensesAndroidLibrariesSectionTitle": "Android Biblotek",
"@aboutLicensesAndroidLibrariesSectionTitle": {},
"aboutLicensesFlutterPluginsSectionTitle": "Flutter Tillägg",
"@aboutLicensesFlutterPluginsSectionTitle": {},
"collectionPageTitle": "Samling",
"@collectionPageTitle": {},
"collectionSelectPageTitle": "Välj objekt",
"@collectionSelectPageTitle": {},
"collectionActionEmptyBin": "Töm papperskorgen",
"@collectionActionEmptyBin": {},
"collectionActionCopy": "kopiera till album",
"@collectionActionCopy": {},
"collectionActionMove": "Flytta till album",
"@collectionActionMove": {},
"viewDialogSortSectionTitle": "Sortera",
"@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grupp",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Layout",
"@viewDialogLayoutSectionTitle": {},
"viewDialogReverseSortOrder": "Omvänd sorteringsordning",
"@viewDialogReverseSortOrder": {},
"tileLayoutMosaic": "Mosaik",
"@tileLayoutMosaic": {},
"tileLayoutGrid": "Nät",
"@tileLayoutGrid": {},
"tileLayoutList": "Lista",
"@tileLayoutList": {},
"coverDialogTabCover": "Omslag",
"@coverDialogTabCover": {},
"coverDialogTabApp": "App",
"@coverDialogTabApp": {},
"coverDialogTabColor": "Färg",
"@coverDialogTabColor": {},
"appPickDialogTitle": "Välj App",
"@appPickDialogTitle": {},
"appPickDialogNone": "Igen",
"@appPickDialogNone": {},
"aboutPageTitle": "Om",
"@aboutPageTitle": {},
"aboutLinkLicense": "Licens",
"@aboutLinkLicense": {},
"aboutLinkPolicy": "IntegritetsPolicy",
"@aboutLinkPolicy": {},
"aboutBugSectionTitle": "FelRapport",
"@aboutBugSectionTitle": {},
"aboutBugSaveLogInstruction": "Spara appens log till en fil",
"@aboutBugSaveLogInstruction": {},
"aboutBugCopyInfoInstruction": "Kopiera systemInformation",
"@aboutBugCopyInfoInstruction": {},
"aboutLicensesShowAllButtonLabel": "Visa alla licenser",
"@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "IntegritetsPolicy",
"@policyPageTitle": {},
"collectionActionHideTitleSearch": "Göm titelfilter",
"@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Lägg till genväg",
"@collectionActionAddShortcut": {},
"collectionActionRescan": "Scanna om",
"@collectionActionRescan": {},
"collectionActionEdit": "Redigera",
"@collectionActionEdit": {},
"collectionSearchTitlesHintText": "Sög titlar",
"@collectionSearchTitlesHintText": {},
"dateYesterday": "Igår",
"@dateYesterday": {},
"dateThisMonth": "Den här månaden",
"@dateThisMonth": {},
"collectionDeleteFailureFeedback": "{count, plural, other{Misslyckades med att ta bort {count} objekt}}",
"@collectionDeleteFailureFeedback": {
"placeholders": {
"count": {}
}
},
"collectionCopyFailureFeedback": "{count, plural, =1{Lyckades inte kopiera 1 objekt} other{Lyckades inte kopiera {count} objekt}}",
"@collectionCopyFailureFeedback": {
"placeholders": {
"count": {}
}
},
"collectionMoveFailureFeedback": "{count, plural, =1{Lyckades inte flytta 1 objekt} other{Lyckades inte flytta {count} objekt}}",
"@collectionMoveFailureFeedback": {
"placeholders": {
"count": {}
}
},
"collectionRenameFailureFeedback": "{count, plural, =1{Lyckades inte byta namn på 1 objekt} other{Lyckades inte byta namn på {count} objekt}}",
"@collectionRenameFailureFeedback": {
"placeholders": {
"count": {}
}
},
"collectionEmptyFavourites": "Inga favoriter",
"@collectionEmptyFavourites": {},
"collectionEmptyGrantAccessButtonLabel": "Ge åtkomst",
"@collectionEmptyGrantAccessButtonLabel": {},
"sortOrderLargestFirst": "Störst först",
"@sortOrderLargestFirst": {},
"sortByDate": "På datum",
"@sortByDate": {},
"sortOrderSmallestFirst": "Minst först",
"@sortOrderSmallestFirst": {},
"wallpaperTargetLock": "Låsskärm",
"@wallpaperTargetLock": {},
"entryActionCast": "Casta",
"@entryActionCast": {},
"filterTaggedLabel": "Taggad",
"@filterTaggedLabel": {},
"keepScreenOnNever": "Alldrig",
"@keepScreenOnNever": {},
"viewerTransitionFade": "Tona ut",
"@viewerTransitionFade": {},
"wallpaperTargetHomeLock": "Hem och låsskärmar",
"@wallpaperTargetHomeLock": {},
"missingSystemFilePickerDialogMessage": "systemets filväljare är borta eller avstängd. Var snäll och sätt på den och försök igen.",
"@missingSystemFilePickerDialogMessage": {},
"renameProcessorCounter": "Räknare",
"@renameProcessorCounter": {},
"editEntryLocationDialogSetCustom": "Ange anpassad plats",
"@editEntryLocationDialogSetCustom": {},
"collectionGroupAlbum": "Om album",
"@collectionGroupAlbum": {},
"collectionGroupMonth": "Om månad",
"@collectionGroupMonth": {},
"collectionGroupDay": "Om dag",
"@collectionGroupDay": {},
"collectionGroupNone": "Gruppera inte",
"@collectionGroupNone": {},
"sectionUnknown": "Okänd",
"@sectionUnknown": {},
"dateToday": "Idag",
"@dateToday": {},
"collectionActionSetHome": "Välj som hem",
"@collectionActionSetHome": {},
"collectionEmptyVideos": "Inga Videor",
"@collectionEmptyVideos": {},
"collectionEmptyImages": "Inga bilder",
"@collectionEmptyImages": {},
"drawerCollectionMotionPhotos": "Rörelsefoton",
"@drawerCollectionMotionPhotos": {},
"drawerAboutButton": "Om",
"@drawerAboutButton": {},
"drawerSettingsButton": "Inställningar",
"@drawerSettingsButton": {},
"drawerCollectionAll": "Hela samlingen",
"@drawerCollectionAll": {},
"drawerCollectionFavourites": "Favoriter",
"@drawerCollectionFavourites": {},
"drawerCollectionImages": "Bilder",
"@drawerCollectionImages": {},
"drawerCollectionVideos": "Videor",
"@drawerCollectionVideos": {},
"drawerCollectionAnimated": "Animerad",
"@drawerCollectionAnimated": {},
"collectionDeselectSectionTooltip": "Avmarkera sektion",
"@collectionDeselectSectionTooltip": {},
"drawerCollectionPanoramas": "Panoraman",
"@drawerCollectionPanoramas": {},
"collectionSelectSectionTooltip": "Markera sektion",
"@collectionSelectSectionTooltip": {},
"drawerCollectionRaws": "Bilder - råformat",
"@drawerCollectionRaws": {},
"drawerCollectionSphericalVideos": "360° Videor",
"@drawerCollectionSphericalVideos": {},
"drawerAlbumPage": "Album",
"@drawerAlbumPage": {},
"drawerCountryPage": "Länder",
"@drawerCountryPage": {},
"drawerPlacePage": "Platser",
"@drawerPlacePage": {},
"drawerTagPage": "Taggar",
"@drawerTagPage": {},
"sortOrderNewestFirst": "Nyast först",
"@sortOrderNewestFirst": {},
"sortByName": "På namn",
"@sortByName": {},
"sortByItemCount": "På antal objekt",
"@sortByItemCount": {},
"sortBySize": "På storlek",
"@sortBySize": {},
"sortByAlbumFileName": "På album & filnamn",
"@sortByAlbumFileName": {},
"sortByRating": "På omdöme",
"@sortByRating": {},
"sortOrderOldestFirst": "Äldst först",
"@sortOrderOldestFirst": {},
"sortOrderAtoZ": "A till Ö",
"@sortOrderAtoZ": {},
"sortOrderZtoA": "Ö till A",
"@sortOrderZtoA": {},
"filePickerUseThisFolder": "Använd den har mappen",
"@filePickerUseThisFolder": {},
"chipActionUnpin": "Släpp från fästet",
"@chipActionUnpin": {}
}

View file

@ -29,7 +29,7 @@
"@hideButtonLabel": {},
"continueButtonLabel": "DEVAM ET",
"@continueButtonLabel": {},
"cancelTooltip": "İptal et",
"cancelTooltip": "İptal",
"@cancelTooltip": {},
"changeTooltip": "Değiştir",
"@changeTooltip": {},
@ -75,7 +75,7 @@
"@chipActionHide": {},
"chipActionPin": "Başa sabitle",
"@chipActionPin": {},
"chipActionUnpin": "Baştan çıkar",
"chipActionUnpin": "Sabitlemeyi kaldır",
"@chipActionUnpin": {},
"chipActionRename": "Yeniden adlandır",
"@chipActionRename": {},
@ -95,7 +95,7 @@
"@entryActionInfo": {},
"entryActionRename": "Yeniden adlandır",
"@entryActionRename": {},
"entryActionRestore": "Dışa aktar",
"entryActionRestore": "Geri getir",
"@entryActionRestore": {},
"entryActionRotateCCW": "Saat yönünün tersine döndür",
"@entryActionRotateCCW": {},
@ -129,11 +129,11 @@
"@entryActionAddFavourite": {},
"entryActionRemoveFavourite": "Favorilerden kaldır",
"@entryActionRemoveFavourite": {},
"videoActionCaptureFrame": "Çerçeve yakala",
"videoActionCaptureFrame": "Kareyi yakala",
"@videoActionCaptureFrame": {},
"videoActionMute": "Sustur",
"videoActionMute": "Sessize al",
"@videoActionMute": {},
"videoActionUnmute": "Susturmayı kaldır",
"videoActionUnmute": "Sesi aç",
"@videoActionUnmute": {},
"videoActionPause": "Duraklat",
"@videoActionPause": {},
@ -143,7 +143,7 @@
"@videoActionReplay10": {},
"videoActionSkip10": "10 saniye ileri git",
"@videoActionSkip10": {},
"videoActionSelectStreams": "Parça seç",
"videoActionSelectStreams": "Ses parçası seç",
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Oynatma hızı",
"@videoActionSetSpeed": {},
@ -241,7 +241,7 @@
"@nameConflictStrategySkip": {},
"keepScreenOnNever": "Asla",
"@keepScreenOnNever": {},
"keepScreenOnViewerOnly": "Yalnızca görüntüleyici sayfası",
"keepScreenOnViewerOnly": "Yalnızca görüntüleyici sayfasında",
"@keepScreenOnViewerOnly": {},
"keepScreenOnAlways": "Her zaman",
"@keepScreenOnAlways": {},
@ -367,7 +367,7 @@
"@editEntryDialogCopyFromItem": {},
"editEntryDateDialogExtractFromTitle": "Başlıktan ayıkla",
"@editEntryDateDialogExtractFromTitle": {},
"editEntryDateDialogShift": "Değişim",
"editEntryDateDialogShift": "Değiştir",
"@editEntryDateDialogShift": {},
"editEntryDateDialogSourceFileModifiedDate": "Dosya değiştirilme tarihi",
"@editEntryDateDialogSourceFileModifiedDate": {},
@ -641,7 +641,7 @@
"@tagEmpty": {},
"binPageTitle": "Geri Dönüşüm Kutusu",
"@binPageTitle": {},
"searchCollectionFieldHint": "Koleksiyonu ara",
"searchCollectionFieldHint": "Koleksiyonda ara",
"@searchCollectionFieldHint": {},
"searchRecentSectionTitle": "Yakın zamanda",
"@searchRecentSectionTitle": {},
@ -1169,7 +1169,7 @@
"@settingsWidgetDisplayedItem": {},
"settingsSubtitleThemeTextPositionDialogTitle": "Metin Konumu",
"@settingsSubtitleThemeTextPositionDialogTitle": {},
"filterNoAddressLabel": "Adres yok",
"filterNoAddressLabel": "Adressiz",
"@filterNoAddressLabel": {},
"filterAspectRatioLandscapeLabel": "Yatay",
"@filterAspectRatioLandscapeLabel": {},
@ -1206,5 +1206,165 @@
"tooManyItemsErrorDialogMessage": "Daha az ögeyle tekrar deneyin.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Parlaklığı/ses seviyesini ayarlamak için yukarı veya aşağı kaydırın",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"patternDialogEnter": "Deseninizi çizin",
"@patternDialogEnter": {},
"saveCopyButtonLabel": "KOPYAYI KAYDET",
"@saveCopyButtonLabel": {},
"applyTooltip": "Uygula",
"@applyTooltip": {},
"chipActionConfigureVault": "Kilitli albüm ayarları",
"@chipActionConfigureVault": {},
"viewerActionLock": "Kontrolleri kilitle",
"@viewerActionLock": {},
"viewerActionUnlock": "Kontrollerin kilidini aç",
"@viewerActionUnlock": {},
"editorTransformRotate": "Döndür",
"@editorTransformRotate": {},
"editorTransformCrop": "Kırp",
"@editorTransformCrop": {},
"editorActionTransform": "Dönüştür",
"@editorActionTransform": {},
"cropAspectRatioFree": "Özgür",
"@cropAspectRatioFree": {},
"cropAspectRatioOriginal": "Orijinal",
"@cropAspectRatioOriginal": {},
"cropAspectRatioSquare": "Kare",
"@cropAspectRatioSquare": {},
"settingsCollectionBurstPatternsTile": "Seri fotoğraf çekme biçimleri",
"@settingsCollectionBurstPatternsTile": {},
"vaultDialogLockTypeLabel": "Kilit türü",
"@vaultDialogLockTypeLabel": {},
"chipActionShowCountryStates": "Eyaletleri göster",
"@chipActionShowCountryStates": {},
"aboutDataUsageSectionTitle": "Kullanılan Alan",
"@aboutDataUsageSectionTitle": {},
"aboutDataUsageData": "Veriler",
"@aboutDataUsageData": {},
"aboutDataUsageInternal": "Dahili",
"@aboutDataUsageInternal": {},
"aboutDataUsageExternal": "Harici",
"@aboutDataUsageExternal": {},
"drawerPlacePage": "Yerler",
"@drawerPlacePage": {},
"settingsAskEverytime": "Her seferinde sor",
"@settingsAskEverytime": {},
"settingsVideoBackgroundModeDialogTitle": "Arkaplan Modu",
"@settingsVideoBackgroundModeDialogTitle": {},
"maxBrightnessNever": "Asla",
"@maxBrightnessNever": {},
"maxBrightnessAlways": "Her zaman",
"@maxBrightnessAlways": {},
"videoResumptionModeNever": "Asla",
"@videoResumptionModeNever": {},
"videoResumptionModeAlways": "Her zaman",
"@videoResumptionModeAlways": {},
"exportEntryDialogQuality": "Kalite",
"@exportEntryDialogQuality": {},
"settingsVideoPlaybackTile": "Oynatma",
"@settingsVideoPlaybackTile": {},
"settingsVideoPlaybackPageTitle": "Oynatma",
"@settingsVideoPlaybackPageTitle": {},
"settingsVideoResumptionModeTile": "Oynatmaya devam et",
"@settingsVideoResumptionModeTile": {},
"settingsVideoResumptionModeDialogTitle": "Oynatmaya Devam Et",
"@settingsVideoResumptionModeDialogTitle": {},
"tagEditorDiscardDialogMessage": "Değişikliklerden vazgeçmek istiyor musunuz?",
"@tagEditorDiscardDialogMessage": {},
"chipActionGoToPlacePage": "Yerler'de Göster",
"@chipActionGoToPlacePage": {},
"chipActionLock": "Kilitle",
"@chipActionLock": {},
"chipActionCreateVault": "Kilitli albüm oluştur",
"@chipActionCreateVault": {},
"albumTierVaults": "Kilitli albümler",
"@albumTierVaults": {},
"vaultLockTypePassword": "Şifre",
"@vaultLockTypePassword": {},
"overlayHistogramNone": "Hiçbiri",
"@overlayHistogramNone": {},
"overlayHistogramRGB": "RGB",
"@overlayHistogramRGB": {},
"widgetTapUpdateWidget": "Widget'i güncelle",
"@widgetTapUpdateWidget": {},
"configureVaultDialogTitle": "Kilitli Albüm Ayarları",
"@configureVaultDialogTitle": {},
"aboutDataUsageCache": "Önbellek",
"@aboutDataUsageCache": {},
"aboutDataUsageDatabase": "Veritabanı",
"@aboutDataUsageDatabase": {},
"aboutDataUsageMisc": "Diğer",
"@aboutDataUsageMisc": {},
"settingsViewerShowHistogram": "Çubuk grafiğini göster",
"@settingsViewerShowHistogram": {},
"authenticateToUnlockVault": "Albümün kilidini açmak için kimliğinizi doğrulayın",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Bazı kilitli albümler çöp kutusunu kullanıyor.",
"@vaultBinUsageDialogMessage": {},
"patternDialogConfirm": "Deseninizi tekrar çizin",
"@patternDialogConfirm": {},
"authenticateToConfigureVault": "Kilitli albümü ayarlamak için kimliğinizi doğrulayın",
"@authenticateToConfigureVault": {},
"statePageTitle": "Eyaletler",
"@statePageTitle": {},
"stateEmpty": "Hiç eyalet bulunamadı",
"@stateEmpty": {},
"searchStatesSectionTitle": "Eyaletler",
"@searchStatesSectionTitle": {},
"settingsConfirmationVaultDataLoss": "Kilitli albüm veri kaybı uyarısını göster",
"@settingsConfirmationVaultDataLoss": {},
"settingsCollectionBurstPatternsNone": "Hiçbiri",
"@settingsCollectionBurstPatternsNone": {},
"settingsDisablingBinWarningDialogMessage": "Çöp kutusundaki ögeler sonsuza dek silinecektir.",
"@settingsDisablingBinWarningDialogMessage": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"newVaultDialogTitle": "Kilitli Albüm Oluştur",
"@newVaultDialogTitle": {},
"passwordDialogConfirm": "Şifrenizi tekrar girin",
"@passwordDialogConfirm": {},
"collectionActionSetHome": "Ana ekran olarak ayarla",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Kişisel koleksiyon",
"@setHomeCustomCollection": {},
"statsTopStatesSectionTitle": "Baş Eyaletler",
"@statsTopStatesSectionTitle": {},
"pinDialogEnter": "PIN girin",
"@pinDialogEnter": {},
"vaultDialogLockModeWhenScreenOff": "Ekran kapatıldığında kilitle",
"@vaultDialogLockModeWhenScreenOff": {},
"pinDialogConfirm": "PIN'inizi tekrar girin",
"@pinDialogConfirm": {},
"exportEntryDialogWriteMetadata": "Metaverileri ekle",
"@exportEntryDialogWriteMetadata": {},
"settingsVideoEnablePip": "Resim içinde resim",
"@settingsVideoEnablePip": {},
"vaultLockTypePattern": "Desen",
"@vaultLockTypePattern": {},
"aboutDataUsageClearCache": "Önbelleği Temizle",
"@aboutDataUsageClearCache": {},
"placeEmpty": "Hiç yer bulunamadı",
"@placeEmpty": {},
"overlayHistogramLuminance": "Parlaklık",
"@overlayHistogramLuminance": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"passwordDialogEnter": "Şifre girin",
"@passwordDialogEnter": {},
"placePageTitle": "Yerler",
"@placePageTitle": {},
"settingsThumbnailShowHdrIcon": "HDR simgesini göster",
"@settingsThumbnailShowHdrIcon": {},
"settingsVideoBackgroundMode": "Arkaplan modu",
"@settingsVideoBackgroundMode": {},
"tagPlaceholderState": "Eyalet",
"@tagPlaceholderState": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"entryActionCast": "Yansıt",
"@entryActionCast": {},
"newVaultWarningDialogMessage": "Kilitli albümlere yalnızca bu uygulama erişebilir, başka herhangi bir uygulama erişemez.\n\nBu uygulamayı kaldırır veya verilerini silerseniz kilitli albümlerdeki bütün ögeleri kaybedersiniz.",
"@newVaultWarningDialogMessage": {},
"castDialogTitle": "Yakındaki Cihazlar",
"@castDialogTitle": {}
}

View file

@ -1518,5 +1518,11 @@
"entryActionCast": "Трансляція",
"@entryActionCast": {},
"castDialogTitle": "Пристрої трансляції",
"@castDialogTitle": {}
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Показати іконку HDR",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Власна колекція",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Встановити як головну",
"@collectionActionSetHome": {}
}

View file

@ -145,7 +145,7 @@
"@filterNoLocationLabel": {},
"videoActionPlay": "Phát",
"@videoActionPlay": {},
"entryActionSetAs": "Thiết lập như",
"entryActionSetAs": "Đặt làm",
"@entryActionSetAs": {},
"filterLocatedLabel": "Đã định vị",
"@filterLocatedLabel": {},
@ -1518,5 +1518,11 @@
"entryActionCast": "Truyền",
"@entryActionCast": {},
"castDialogTitle": "Thiết bị truyền",
"@castDialogTitle": {}
"@castDialogTitle": {},
"setHomeCustomCollection": "Bộ sưu tập tùy chỉnh",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Đặt làm nhà",
"@collectionActionSetHome": {}
}

View file

@ -59,7 +59,7 @@
"@sourceStateLoading": {},
"sourceStateCataloguing": "正在进行编目",
"@sourceStateCataloguing": {},
"sourceStateLocatingCountries": "正在定位国家",
"sourceStateLocatingCountries": "正在定位地区",
"@sourceStateLocatingCountries": {},
"sourceStateLocatingPlaces": "正在定位地点",
"@sourceStateLocatingPlaces": {},
@ -67,7 +67,7 @@
"@chipActionDelete": {},
"chipActionGoToAlbumPage": "在相册中显示",
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "在国家中显示",
"chipActionGoToCountryPage": "在地区中显示",
"@chipActionGoToCountryPage": {},
"chipActionGoToTagPage": "在标签中显示",
"@chipActionGoToTagPage": {},
@ -625,7 +625,7 @@
"@drawerCollectionSphericalVideos": {},
"drawerAlbumPage": "相册",
"@drawerAlbumPage": {},
"drawerCountryPage": "国家",
"drawerCountryPage": "地区",
"@drawerCountryPage": {},
"drawerTagPage": "标签",
"@drawerTagPage": {},
@ -693,9 +693,9 @@
"@createAlbumButtonLabel": {},
"newFilterBanner": "新的",
"@newFilterBanner": {},
"countryPageTitle": "国家",
"countryPageTitle": "地区",
"@countryPageTitle": {},
"countryEmpty": "无国家",
"countryEmpty": "无地区",
"@countryEmpty": {},
"tagPageTitle": "标签",
"@tagPageTitle": {},
@ -711,7 +711,7 @@
"@searchDateSectionTitle": {},
"searchAlbumsSectionTitle": "相册",
"@searchAlbumsSectionTitle": {},
"searchCountriesSectionTitle": "国家",
"searchCountriesSectionTitle": "地区",
"@searchCountriesSectionTitle": {},
"searchPlacesSectionTitle": "地点",
"@searchPlacesSectionTitle": {},
@ -1023,7 +1023,7 @@
"@statsPageTitle": {},
"statsWithGps": "{count, plural, other{{count} 项带位置信息}}",
"@statsWithGps": {},
"statsTopCountriesSectionTitle": "热门国家",
"statsTopCountriesSectionTitle": "热门地区",
"@statsTopCountriesSectionTitle": {},
"statsTopPlacesSectionTitle": "热门地点",
"@statsTopPlacesSectionTitle": {},
@ -1197,7 +1197,7 @@
"@chipActionConfigureVault": {},
"chipActionCreateVault": "创建保险库",
"@chipActionCreateVault": {},
"chipActionShowCountryStates": "显示状态",
"chipActionShowCountryStates": "显示区域",
"@chipActionShowCountryStates": {},
"viewerActionLock": "锁定查看器",
"@viewerActionLock": {},
@ -1255,7 +1255,7 @@
"@vaultLockTypePattern": {},
"albumTierVaults": "保险库",
"@albumTierVaults": {},
"settingsVideoResumptionModeTile": "恢复放",
"settingsVideoResumptionModeTile": "恢复放",
"@settingsVideoResumptionModeTile": {},
"vaultBinUsageDialogMessage": "有些保险库正在使用资源回收站。",
"@vaultBinUsageDialogMessage": {},
@ -1275,9 +1275,9 @@
"@editorTransformCrop": {},
"filterTaggedLabel": "已标记",
"@filterTaggedLabel": {},
"statePageTitle": "区",
"statePageTitle": "区",
"@statePageTitle": {},
"settingsVideoResumptionModeDialogTitle": "恢复放",
"settingsVideoResumptionModeDialogTitle": "恢复放",
"@settingsVideoResumptionModeDialogTitle": {},
"settingsVideoBackgroundMode": "后台模式",
"@settingsVideoBackgroundMode": {},
@ -1305,9 +1305,9 @@
"@newVaultWarningDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "回收站中的项目将被永久删除。",
"@settingsDisablingBinWarningDialogMessage": {},
"statsTopStatesSectionTitle": "最多项的区",
"statsTopStatesSectionTitle": "最多项的区",
"@statsTopStatesSectionTitle": {},
"settingsVideoPlaybackPageTitle": "放",
"settingsVideoPlaybackPageTitle": "放",
"@settingsVideoPlaybackPageTitle": {},
"filterLocatedLabel": "位于",
"@filterLocatedLabel": {},
@ -1337,9 +1337,9 @@
"@cropAspectRatioOriginal": {},
"configureVaultDialogTitle": "设置保险库",
"@configureVaultDialogTitle": {},
"searchStatesSectionTitle": "区",
"searchStatesSectionTitle": "区",
"@searchStatesSectionTitle": {},
"stateEmpty": "没有区",
"stateEmpty": "没有区",
"@stateEmpty": {},
"aboutDataUsageData": "数据",
"@aboutDataUsageData": {},
@ -1353,12 +1353,18 @@
"@newVaultDialogTitle": {},
"settingsVideoBackgroundModeDialogTitle": "后台模式",
"@settingsVideoBackgroundModeDialogTitle": {},
"tagPlaceholderState": "区",
"tagPlaceholderState": "区",
"@tagPlaceholderState": {},
"settingsViewerShowHistogram": "显示直方图",
"@settingsViewerShowHistogram": {},
"settingsVideoPlaybackTile": "放",
"settingsVideoPlaybackTile": "放",
"@settingsVideoPlaybackTile": {},
"exportEntryDialogWriteMetadata": "写入元数据",
"@exportEntryDialogWriteMetadata": {}
"@exportEntryDialogWriteMetadata": {},
"settingsThumbnailShowHdrIcon": "显示 HDR 图标",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "设置为首页",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "自定义媒体集",
"@setHomeCustomCollection": {}
}

View file

@ -34,7 +34,7 @@ class Contributors {
Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'),
Contributor('Piotr K', '1337.kelt@gmail.com'),
Contributor('rehork', 'cooky@e.email'),
Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'),
Contributor('Eric', 'zxmegaxqug@hldrive.com'), // @hamburger2048 /
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
Contributor('kaajjo', 'claymanoff@gmail.com'),
@ -67,11 +67,15 @@ class Contributors {
Contributor('Reza Almanda', 'rezaalmanda27@gmail.com'),
Contributor('Sveinn í Felli', 'sv1@fellsnet.is'),
Contributor('Henning Bunk', 'henningtbunk@gmail.com'),
Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'),
Contributor('Samirah Ail', 'samiratalzahrani@gmail.com'),
Contributor('Salih Ail', 'rrrfff444@gmail.com'),
Contributor('nasreddineloukriz', 'nasreddineloukriz@gmail.com'),
Contributor('Mohamed Zeroug', 'mzeroug19@gmail.com'),
Contributor('ssantos', 'ssantos@web.de'),
Contributor('Сергій', 'sergiy.goncharuk.1@gmail.com'),
Contributor('v1s7', 'v1s7@users.noreply.hosted.weblate.org'),
Contributor('fuzfyy', 'egeozce35@gmail.com'),
Contributor('minh', 'teaminh@skiff.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
@ -86,7 +90,9 @@ class Contributors {
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
// Contributor('slasb37', 'p84haghi@gmail.com'), // Persian
// Contributor('mimvahedi', 'vahedi0vahedi@gmail.com'), // Persian
// Contributor('Prasanta-Hembram', 'Prasantahembram720@gmail.com'), // Santali
// Contributor('mytja', 'mamnju21@gmail.com'), // Slovenian
// Contributor('Shift18', 'bribable.lawyer@posteo.net'), // Swedish
// Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
};
}

View file

@ -74,9 +74,9 @@ class Dependencies {
sourceUrl: 'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
),
Dependency(
name: 'FFmpegKit',
name: 'FFmpegKit (Aves fork)',
license: lgpl3,
sourceUrl: 'https://github.com/arthenica/ffmpeg-kit',
sourceUrl: 'https://github.com/deckerst/ffmpeg-kit',
),
Dependency(
name: 'Floating',
@ -207,9 +207,9 @@ class Dependencies {
static const List<Dependency> flutterPackages = [
Dependency(
name: 'Charts (fzyzcjy fork)',
name: 'Charts (Aves fork)',
license: apache2,
sourceUrl: 'https://github.com/fzyzcjy/charts',
sourceUrl: 'https://github.com/deckerst/flutter_google_charts',
),
Dependency(
name: 'Custom rounded rectangle border',
@ -271,7 +271,7 @@ class Dependencies {
Dependency(
name: 'Panorama (Aves fork)',
license: apache2,
sourceUrl: 'https://github.com/zesage/panorama',
sourceUrl: 'https://github.com/deckerst/aves_panorama',
),
Dependency(
name: 'Pattern Lock',

View file

@ -20,28 +20,6 @@ class AppSupport {
static bool canDecode(String mimeType) => !undecodableImages.contains(mimeType);
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
static bool _supportedByBitmapRegionDecoder(String mimeType) => [
MimeTypes.heic,
MimeTypes.heif,
MimeTypes.jpeg,
MimeTypes.png,
MimeTypes.webp,
MimeTypes.arw,
MimeTypes.cr2,
MimeTypes.nef,
MimeTypes.nrw,
MimeTypes.orf,
MimeTypes.pef,
MimeTypes.raf,
MimeTypes.rw2,
MimeTypes.srw,
].contains(mimeType);
static bool canDecodeRegion(String mimeType) => _supportedByBitmapRegionDecoder(mimeType) || mimeType == MimeTypes.tiff;
// `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes,
// and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files.
static bool canEditExif(String mimeType) {

View file

@ -32,7 +32,7 @@ extension ExtraAvesEntryProps on AvesEntry {
// size
bool get useTiles => canDecodeRegion && (width > 4096 || height > 4096);
bool get useTiles => (width > 4096 || height > 4096) && !isAnimated;
bool get isSized => width > 0 && height > 0;
@ -127,8 +127,6 @@ extension ExtraAvesEntryProps on AvesEntry {
bool get canDecode => AppSupport.canDecode(mimeType);
bool get canDecodeRegion => AppSupport.canDecodeRegion(mimeType) && !isAnimated;
bool get canEditExif => AppSupport.canEditExif(mimeType);
bool get canEditIptc => AppSupport.canEditIptc(mimeType);

View file

@ -15,6 +15,11 @@ class LocationFilter extends CoveredCollectionFilter {
late final String? _code;
late final EntryFilter _test;
static final unlocated = LocationFilter(LocationLevel.place, '');
static final located = unlocated.reverse();
bool get _isUnlocated => _location.isEmpty;
@override
List<Object?> get props => [level, _location, _code, reversed];
@ -23,7 +28,7 @@ class LocationFilter extends CoveredCollectionFilter {
_location = split.isNotEmpty ? split[0] : location;
_code = split.length > 1 ? split[1] : null;
if (_location.isEmpty) {
if (_isUnlocated) {
_test = (entry) => !entry.hasGps;
} else {
switch (level) {
@ -81,11 +86,11 @@ class LocationFilter extends CoveredCollectionFilter {
String get universalLabel => _location;
@override
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterNoLocationLabel : _location;
String getLabel(BuildContext context) => _isUnlocated ? context.l10n.filterNoLocationLabel : _location;
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
if (_location.isEmpty) {
if (_isUnlocated) {
return Icon(AIcons.locationUnlocated, size: size);
}
switch (level) {

View file

@ -4,18 +4,17 @@ import 'package:flutter/foundation.dart';
@immutable
class OverlayMetadata extends Equatable {
final double? aperture, focalLength;
final String? exposureTime;
final String? description, exposureTime;
final int? iso;
@override
List<Object?> get props => [aperture, exposureTime, focalLength, iso];
List<Object?> get props => [aperture, description, exposureTime, focalLength, iso];
bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null;
bool get isNotEmpty => !isEmpty;
bool get hasShootingDetails => aperture != null || exposureTime != null || focalLength != null || iso != null;
const OverlayMetadata({
this.aperture,
this.description,
this.exposureTime,
this.focalLength,
this.iso,
@ -24,6 +23,7 @@ class OverlayMetadata extends Equatable {
factory OverlayMetadata.fromMap(Map map) {
return OverlayMetadata(
aperture: map['aperture'] as double?,
description: map['description'] as String?,
exposureTime: map['exposureTime'] as String?,
focalLength: map['focalLength'] as double?,
iso: map['iso'] as int?,

View file

@ -52,6 +52,7 @@ class SettingsDefaults {
EntrySetAction.delete,
];
static const showThumbnailFavourite = true;
static const showThumbnailHdr = true;
static const thumbnailLocationIcon = ThumbnailOverlayLocationIcon.none;
static const thumbnailTagIcon = ThumbnailOverlayTagIcon.none;
static const showThumbnailMotionPhoto = true;

View file

@ -1,6 +1,6 @@
import 'dart:math';
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves/widgets/viewer/controls/transitions.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';

View file

@ -30,6 +30,10 @@ mixin CollectionSettings on SettingsAccess {
set showThumbnailFavourite(bool newValue) => set(SettingKeys.showThumbnailFavouriteKey, newValue);
bool get showThumbnailHdr => getBool(SettingKeys.showThumbnailHdrKey) ?? SettingsDefaults.showThumbnailHdr;
set showThumbnailHdr(bool newValue) => set(SettingKeys.showThumbnailHdrKey, newValue);
ThumbnailOverlayLocationIcon get thumbnailLocationIcon => getEnumOrDefault(SettingKeys.thumbnailLocationIconKey, SettingsDefaults.thumbnailLocationIcon, ThumbnailOverlayLocationIcon.values);
set thumbnailLocationIcon(ThumbnailOverlayLocationIcon newValue) => set(SettingKeys.thumbnailLocationIconKey, newValue.toString());

View file

@ -1,6 +1,7 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
mixin NavigationSettings on SettingsAccess {
bool get mustBackTwiceToExit => getBool(SettingKeys.mustBackTwiceToExitKey) ?? SettingsDefaults.mustBackTwiceToExit;
@ -15,6 +16,10 @@ mixin NavigationSettings on SettingsAccess {
set homePage(HomePageSetting newValue) => set(SettingKeys.homePageKey, newValue.toString());
Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set homeCustomCollection(Set<CollectionFilter> newValue) => set(SettingKeys.homeCustomCollectionKey, newValue.map((filter) => filter.toJson()).toList());
bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar;
set enableBottomNavigationBar(bool newValue) => set(SettingKeys.enableBottomNavigationBarKey, newValue);

View file

@ -388,6 +388,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
case SettingKeys.setMetadataDateBeforeFileOpKey:
case SettingKeys.collectionSortReverseKey:
case SettingKeys.showThumbnailFavouriteKey:
case SettingKeys.showThumbnailHdrKey:
case SettingKeys.showThumbnailMotionPhotoKey:
case SettingKeys.showThumbnailRatingKey:
case SettingKeys.showThumbnailRawKey:
@ -471,6 +472,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
} else {
debugPrint('failed to import key=$key, value=$newValue is not a string');
}
case SettingKeys.homeCustomCollectionKey:
case SettingKeys.drawerTypeBookmarksKey:
case SettingKeys.drawerAlbumBookmarksKey:
case SettingKeys.drawerPageBookmarksKey:

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/services/common/services.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -22,14 +21,8 @@ class GeocodingService {
'maxResults': 2,
});
return (result as List).cast<Map>().map(Address.fromMap).toList();
} on PlatformException catch (e, stack) {
if (!{
'getAddress-empty',
'getAddress-network',
'getAddress-unavailable',
}.contains(e.code)) {
await reportService.recordError(e, stack);
}
} on PlatformException catch (_) {
// do not report
}
return [];
}

View file

@ -22,7 +22,7 @@ abstract class MetadataFetchService {
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
Future<OverlayMetadata?> getOverlayMetadata(AvesEntry entry);
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields);
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
@ -39,8 +39,6 @@ abstract class MetadataFetchService {
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
Future<String?> getDescription(AvesEntry entry);
}
class PlatformMetadataFetchService implements MetadataFetchService {
@ -112,15 +110,20 @@ class PlatformMetadataFetchService implements MetadataFetchService {
}
@override
Future<OverlayMetadata?> getOverlayMetadata(AvesEntry entry) async {
if (entry.isSvg) return null;
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
if (fields.isNotEmpty && !entry.isSvg) {
try {
// returns map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
// returns fields on demand, with various value types:
// 'aperture' (double),
// 'description' (string)
// 'exposureTime' (string),
// 'focalLength' (double),
// 'iso' (int),
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
'fields': fields.map((v) => v.toPlatform).toList(),
}) as Map;
return OverlayMetadata.fromMap(result);
} on PlatformException catch (e, stack) {
@ -128,7 +131,8 @@ class PlatformMetadataFetchService implements MetadataFetchService {
await reportService.recordError(e, stack);
}
}
return null;
}
return const OverlayMetadata();
}
@override
@ -280,20 +284,4 @@ class PlatformMetadataFetchService implements MetadataFetchService {
}
return null;
}
@override
Future<String?> getDescription(AvesEntry entry) async {
try {
return await _platform.invokeMethod('getDescription', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
});
} on PlatformException catch (e, stack) {
if (entry.isValid) {
await reportService.recordError(e, stack);
}
}
return null;
}
}

View file

@ -17,11 +17,17 @@ abstract class WindowService {
Future<bool> isCutoutAware();
Future<EdgeInsets> getCutoutInsets();
Future<bool> supportsHdr();
Future<void> setHdrColorMode(bool on);
}
class PlatformWindowService implements WindowService {
static const _platform = MethodChannel('deckers.thibault/aves/window');
bool? _isCutoutAware, _supportsHdr;
@override
Future<bool> isActivity() async {
try {
@ -90,8 +96,6 @@ class PlatformWindowService implements WindowService {
}
}
bool? _isCutoutAware;
@override
Future<bool> isCutoutAware() async {
if (_isCutoutAware != null) return SynchronousFuture(_isCutoutAware!);
@ -121,4 +125,28 @@ class PlatformWindowService implements WindowService {
}
return EdgeInsets.zero;
}
@override
Future<bool> supportsHdr() async {
if (_supportsHdr != null) return SynchronousFuture(_supportsHdr!);
try {
final result = await _platform.invokeMethod('supportsHdr');
_supportsHdr = result as bool?;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return _supportsHdr ?? false;
}
@override
Future<void> setHdrColorMode(bool on) async {
// TODO TLAD [hdr] enable when ready
// try {
// await _platform.invokeMethod('setHdrColorMode', <String, dynamic>{
// 'on': on,
// });
// } on PlatformException catch (e, stack) {
// await reportService.recordError(e, stack);
// }
}
}

View file

@ -17,6 +17,7 @@ extension ExtraEntrySetActionView on EntrySetAction {
// different data depending on toggle state
context.l10n.collectionActionShowTitleSearch,
EntrySetAction.addShortcut => context.l10n.collectionActionAddShortcut,
EntrySetAction.setHome => context.l10n.collectionActionSetHome,
EntrySetAction.emptyBin => context.l10n.collectionActionEmptyBin,
// browsing or selecting
EntrySetAction.map => context.l10n.menuActionMap,
@ -61,6 +62,7 @@ extension ExtraEntrySetActionView on EntrySetAction {
// different data depending on toggle state
AIcons.filter,
EntrySetAction.addShortcut => AIcons.addShortcut,
EntrySetAction.setHome => AIcons.home,
EntrySetAction.emptyBin => AIcons.emptyBin,
// browsing or selecting
EntrySetAction.map => AIcons.map,

View file

@ -154,6 +154,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final storageVolumes = await storageService.getStorageVolumes();
final storageGrants = await storageService.getGrantedDirectories();
final supportsHdr = await windowService.supportsHdr();
return [
'Package: ${device.packageName}',
'Installer: ${packageInfo.installerStore}',
@ -162,7 +163,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
'Android version: ${androidInfo.version.release}, API ${androidInfo.version.sdkInt}',
'Android build: ${androidInfo.display}',
'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}',
'Support: dynamic colors=${device.isDynamicColorAvailable}, geocoder=${device.hasGeocoder}, HDR=$supportsHdr',
'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}',
'System locales: ${WidgetsBinding.instance.platformDispatcher.locales.join(', ')}',
'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}',

View file

@ -68,7 +68,9 @@ class AvesApp extends StatefulWidget {
'ml', // Malayalam
'my', // Burmese
'or', // Odia
'sat', // Santali
'sl', // Slovenian
'sv', // Swedish
'th', // Thai
}.map(Locale.new).toSet();
static final List<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList();

View file

@ -618,6 +618,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case EntrySetAction.searchCollection:
case EntrySetAction.toggleTitleSearch:
case EntrySetAction.addShortcut:
case EntrySetAction.setHome:
// browsing or selecting
case EntrySetAction.map:
case EntrySetAction.slideshow:

View file

@ -75,7 +75,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.toggleTitleSearch:
return !useTvLayout && !isSelecting;
case EntrySetAction.addShortcut:
return isMain && !isSelecting && device.canPinShortcut && !isTrash;
return isMain && !isSelecting && !isTrash && device.canPinShortcut;
case EntrySetAction.setHome:
return isMain && !isSelecting && !isTrash && !useTvLayout;
case EntrySetAction.emptyBin:
return canWrite && isMain && isTrash;
// browsing or selecting
@ -131,6 +133,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.searchCollection:
case EntrySetAction.toggleTitleSearch:
case EntrySetAction.addShortcut:
case EntrySetAction.setHome:
return true;
case EntrySetAction.emptyBin:
return !isSelecting && hasItems;
@ -177,6 +180,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
context.read<Query>().toggle();
case EntrySetAction.addShortcut:
_addShortcut(context);
case EntrySetAction.setHome:
_setHome(context);
// browsing or selecting
case EntrySetAction.map:
_goToMap(context);
@ -727,4 +732,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}
void _setHome(BuildContext context) async {
settings.homeCustomCollection = context.read<CollectionLens>().filters;
settings.homePage = HomePageSetting.collection;
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}

View file

@ -55,7 +55,8 @@ mixin EntryEditorMixin {
final entry = entries.first;
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
final initialDescription = await metadataFetchService.getDescription(entry) ?? '';
final fields = await metadataFetchService.getFields(entry, {MetadataSyntheticField.description});
final initialDescription = fields.description ?? '';
return showDialog<Map<DescriptionField, String?>>(
context: context,

View file

@ -40,6 +40,7 @@ class GridTheme extends StatelessWidget {
highlightBorderWidth: highlightBorderWidth,
interactiveDimension: interactiveDimension,
showFavourite: settings.showThumbnailFavourite,
showHdr: settings.showThumbnailHdr,
locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none,
tagIcon: settings.thumbnailTagIcon,
showMotionPhoto: settings.showThumbnailMotionPhoto,
@ -58,7 +59,7 @@ typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesE
class GridThemeData {
final double iconSize, fontSize, highlightBorderWidth, interactiveDimension;
final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
final bool showFavourite, showHdr, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
final bool showLocated, showUnlocated, showTagged, showUntagged;
late final GridThemeIconBuilder iconBuilder;
@ -68,6 +69,7 @@ class GridThemeData {
required this.highlightBorderWidth,
required this.interactiveDimension,
required this.showFavourite,
required this.showHdr,
required ThumbnailOverlayLocationIcon locationIcon,
required ThumbnailOverlayTagIcon tagIcon,
required this.showMotionPhoto,
@ -94,10 +96,10 @@ class GridThemeData {
else if (entry.isAnimated)
const AnimatedImageIcon()
else ...[
if (entry.isHdr && showHdr) const HdrIcon(),
if (entry.isRaw && showRaw) const RawIcon(),
if (entry.is360) const PanoramaIcon(),
],
if (entry.isHdr) const HdrIcon(),
if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(),
if (entry.isMultiPage && !entry.isMotionPhoto) MultiPageIcon(entry: entry),
if (entry.isGeotiff) const GeoTiffIcon(),

View file

@ -19,6 +19,7 @@ import 'package:aves/widgets/debug/app_debug_action.dart';
import 'package:aves/widgets/debug/cache.dart';
import 'package:aves/widgets/debug/colors.dart';
import 'package:aves/widgets/debug/database.dart';
import 'package:aves/widgets/debug/device.dart';
import 'package:aves/widgets/debug/general.dart';
import 'package:aves/widgets/debug/media_store_scan_dialog.dart';
import 'package:aves/widgets/debug/report.dart';
@ -75,6 +76,7 @@ class AppDebugPage extends StatelessWidget {
DebugCacheSection(),
DebugColorSection(),
DebugAppDatabaseSection(),
DebugDeviceSection(),
DebugErrorReportingSection(),
DebugSettingsSection(),
DebugStorageSection(),

View file

@ -0,0 +1,51 @@
import 'package:aves/model/device.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:flutter/material.dart';
class DebugDeviceSection extends StatefulWidget {
const DebugDeviceSection({super.key});
@override
State<DebugDeviceSection> createState() => _DebugDeviceSectionState();
}
class _DebugDeviceSectionState extends State<DebugDeviceSection> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return AvesExpansionTile(
title: 'Device',
children: [
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(
info: {
'packageName': device.packageName,
'packageVersion': device.packageVersion,
'userAgent': device.userAgent,
'canAuthenticateUser': '${device.canAuthenticateUser}',
'canGrantDirectoryAccess': '${device.canGrantDirectoryAccess}',
'canPinShortcut': '${device.canPinShortcut}',
'canRenderFlagEmojis': '${device.canRenderFlagEmojis}',
'canRenderSubdivisionFlagEmojis': '${device.canRenderSubdivisionFlagEmojis}',
'canRequestManageMedia': '${device.canRequestManageMedia}',
'canSetLockScreenWallpaper': '${device.canSetLockScreenWallpaper}',
'canUseCrypto': '${device.canUseCrypto}',
'canUseVaults': '${device.canUseVaults}',
'hasGeocoder': '${device.hasGeocoder}',
'isDynamicColorAvailable': '${device.isDynamicColorAvailable}',
'isTelevision': '${device.isTelevision}',
'showPinShortcutFeedback': '${device.showPinShortcutFeedback}',
'supportEdgeToEdgeUIMode': '${device.supportEdgeToEdgeUIMode}',
'supportPictureInPicture': '${device.supportPictureInPicture}',
},
),
),
],
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/extensions/metadata_edition.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/enums/coordinate_format.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
@ -18,6 +19,7 @@ import 'package:aves/widgets/dialogs/item_picker.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
@ -172,8 +174,10 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
final mapCollection = baseCollection != null
? CollectionLens(
source: baseCollection.source,
filters: baseCollection.filters,
fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).toList(),
filters: {
...baseCollection.filters.whereNot((filter) => filter == LocationFilter.unlocated),
LocationFilter.located,
},
)
: null;
final latLng = await Navigator.maybeOf(context)?.push(

View file

@ -3,6 +3,8 @@ import 'package:aves/widgets/dialogs/selection_dialogs/common.dart';
import 'package:aves/widgets/dialogs/selection_dialogs/radio_list_tile.dart';
import 'package:flutter/material.dart';
// do not use as `T` a record containing a collection
// because radio value comparison will fail without deep equality
class AvesSingleSelectionDialog<T> extends StatefulWidget {
static const routeName = '/dialog/selection';

View file

@ -196,7 +196,7 @@ class _HomePageState extends State<HomePage> {
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection,
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
canAnalyze: !safeMode,
);
case AppMode.screenSaver:
@ -338,7 +338,7 @@ class _HomePageState extends State<HomePage> {
case AppMode.screenSaver:
case AppMode.slideshow:
routeName = _initialRouteName ?? settings.homePage.routeName;
filters = _initialFilters ?? {};
filters = _initialFilters ?? (settings.homePage == HomePageSetting.collection ? settings.homeCustomCollection : {});
}
Route buildRoute(WidgetBuilder builder) => DirectMaterialPageRoute(
settings: RouteSettings(name: routeName),

View file

@ -50,7 +50,7 @@ class PageNavTile extends StatelessWidget {
: null,
onTap: () {
Navigator.maybeOf(context)?.pop();
final route = routeBuilder(context, routeName);
final route = routeBuilder(context, routeName, topLevel);
if (topLevel) {
Navigator.maybeOf(context)?.pushAndRemoveUntil(
route,
@ -65,7 +65,7 @@ class PageNavTile extends StatelessWidget {
);
}
static Route routeBuilder(BuildContext context, String routeName) {
static Route routeBuilder(BuildContext context, String routeName, bool topLevel) {
switch (routeName) {
case SearchPage.routeName:
final currentCollection = context.read<CollectionLens?>();
@ -74,7 +74,7 @@ class PageNavTile extends StatelessWidget {
searchFieldLabel: context.l10n.searchCollectionFieldHint,
searchFieldStyle: Themes.searchFieldStyle(context),
source: context.read<CollectionSource>(),
parentCollection: currentCollection?.copyWith(),
parentCollection: topLevel ? currentCollection?.copyWith() : currentCollection,
),
);
default:

View file

@ -253,7 +253,7 @@ class _TvRailState extends State<TvRail> {
void _goTo(String routeName) {
Navigator.maybeOf(context)?.pushAndRemoveUntil(
PageNavTile.routeBuilder(context, routeName),
PageNavTile.routeBuilder(context, routeName, true),
(route) => false,
);
}

View file

@ -279,7 +279,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
title: context.l10n.searchMetadataSectionTitle,
filters: [
MissingFilter.date,
LocationFilter(LocationLevel.place, ''),
LocationFilter.unlocated,
MissingFilter.fineAddress,
TagFilter(''),
RatingFilter(0),

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
@ -11,6 +12,7 @@ import 'package:aves/widgets/settings/navigation/confirmation_dialogs.dart';
import 'package:aves/widgets/settings/navigation/drawer.dart';
import 'package:aves/widgets/settings/settings_definition.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -38,16 +40,47 @@ class NavigationSection extends SettingsSection {
];
}
class _HomeOption {
final HomePageSetting page;
final Set<CollectionFilter> customCollection;
const _HomeOption(
this.page, {
this.customCollection = const {},
});
String getName(BuildContext context) {
if (page == HomePageSetting.collection && customCollection.isNotEmpty) {
return context.l10n.setHomeCustomCollection;
}
return page.getName(context);
}
@override
bool operator ==(Object other) => identical(this, other) || other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection);
@override
int get hashCode => page.hashCode ^ customCollection.hashCode;
}
class SettingsTileNavigationHomePage extends SettingsTile {
@override
String title(BuildContext context) => context.l10n.settingsHomeTile;
@override
Widget build(BuildContext context) => SettingsSelectionListTile<HomePageSetting>(
values: HomePageSetting.values,
Widget build(BuildContext context) => SettingsSelectionListTile<_HomeOption>(
values: [
const _HomeOption(HomePageSetting.collection),
const _HomeOption(HomePageSetting.albums),
const _HomeOption(HomePageSetting.tags),
if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection),
],
getName: (context, v) => v.getName(context),
selector: (context, s) => s.homePage,
onSelection: (v) => settings.homePage = v,
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection),
onSelection: (v) {
settings.homePage = v.page;
settings.homeCustomCollection = v.customCollection;
},
tileTitle: title(context),
dialogTitle: context.l10n.settingsHomeDialogTitle,
);

View file

@ -66,6 +66,16 @@ class ThumbnailOverlayPage extends StatelessWidget {
color: iconColor,
),
),
SettingsSwitchListTile(
selector: (context, s) => s.showThumbnailHdr,
onChanged: (v) => settings.showThumbnailHdr = v,
title: context.l10n.settingsThumbnailShowHdrIcon,
trailing: Icon(
AIcons.hdr,
size: iconSize,
color: iconColor,
),
),
SettingsSwitchListTile(
selector: (context, s) => s.showThumbnailRaw,
onChanged: (v) => settings.showThumbnailRaw = v,

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:math';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/viewer/controls/cast.dart';
import 'package:aves/widgets/viewer/controls/events.dart';
@ -57,6 +59,7 @@ class ViewerController with CastMixin {
);
}
_initialScale = initialScale;
entryNotifier.addListener(_onEntryChanged);
_autopilotNotifier = ValueNotifier(autopilot);
_autopilotNotifier.addListener(_onAutopilotChanged);
_onAutopilotChanged();
@ -66,12 +69,21 @@ class ViewerController with CastMixin {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
entryNotifier.removeListener(_onEntryChanged);
windowService.setHdrColorMode(false);
_autopilotNotifier.dispose();
_clearAutopilotAnimations();
_stopPlayTimer();
_streamController.close();
}
Future<void> _onEntryChanged() async {
if (await windowService.supportsHdr()) {
final enabled = entryNotifier.value?.isHdr ?? false;
await windowService.setHdrColorMode(enabled);
}
}
void _onAutopilotChanged() {
_clearAutopilotAnimations();
_stopPlayTimer();
@ -115,79 +127,3 @@ class ViewerController with CastMixin {
Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward());
}
}
class PageTransitionEffects {
static TransitionBuilder fade(
PageController pageController,
int index, {
required bool zoomIn,
}) =>
(context, child) {
double opacity = 0;
double dx = 0;
double scale = 1;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).clamp(0, 1);
dx = position * width;
if (zoomIn) {
scale = 1 + position;
}
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: Transform.scale(
scale: scale,
child: child,
),
),
);
};
static TransitionBuilder slide(
PageController pageController,
int index, {
required bool parallax,
}) =>
(context, child) {
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
if (parallax) {
dx = position * width / 2;
}
}
return ClipRect(
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
static TransitionBuilder none(
PageController pageController,
int index,
) =>
(context, child) {
double opacity = 0;
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
dx = position * width;
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
}

View file

@ -0,0 +1,77 @@
import 'package:flutter/widgets.dart';
class PageTransitionEffects {
static TransitionBuilder fade(
PageController pageController,
int index, {
required bool zoomIn,
}) =>
(context, child) {
double opacity = 0;
double dx = 0;
double scale = 1;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).clamp(0, 1);
dx = position * width;
if (zoomIn) {
scale = 1 + position;
}
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: Transform.scale(
scale: scale,
child: child,
),
),
);
};
static TransitionBuilder slide(
PageController pageController,
int index, {
required bool parallax,
}) =>
(context, child) {
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
if (parallax) {
dx = position * width / 2;
}
}
return ClipRect(
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
static TransitionBuilder none(
PageController pageController,
int index,
) =>
(context, child) {
double opacity = 0;
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
dx = position * width;
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
}

View file

@ -290,19 +290,24 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
void _onAppLifecycleStateChanged() {
switch (AvesApp.lifecycleStateNotifier.value) {
case AppLifecycleState.inactive:
// inactive: when losing focus
_onAppInactive();
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
// paused: when switching to another app
// detached: when app is without a view
viewerController.autopilot = false;
pauseVideoControllers();
case AppLifecycleState.resumed:
availability.onResume();
case AppLifecycleState.hidden:
// hidden: transient state between `inactive` and `paused`
break;
}
}
Future<void> _onAppInactive() async {
final playingController = context.read<VideoConductor>().getPlayingController();
viewerController.autopilot = false;
bool enabledPip = false;
if (settings.videoBackgroundMode == VideoBackgroundMode.pip) {
enabledPip |= await _enablePictureInPicture();

View file

@ -15,6 +15,7 @@ import 'package:aves/widgets/viewer/overlay/details/position_title.dart';
import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart';
import 'package:aves/widgets/viewer/overlay/details/shooting.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -44,14 +45,13 @@ class ViewerDetailOverlay extends StatefulWidget {
class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
List<AvesEntry> get entries => widget.entries;
AvesEntry? get entry {
final index = widget.index;
return index < entries.length ? entries[index] : null;
}
AvesEntry? get entry => entryForIndex(widget.index);
late Future<List<dynamic>?> _detailLoader;
AvesEntry? entryForIndex(int index) => index < entries.length ? entries[index] : null;
late Future<OverlayMetadata> _detailLoader;
AvesEntry? _lastEntry;
List<dynamic>? _lastDetails;
OverlayMetadata _lastDetails = const OverlayMetadata();
@override
void initState() {
@ -62,7 +62,8 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
@override
void didUpdateWidget(covariant ViewerDetailOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
if (entry != _lastEntry) {
final newEntry = entryForIndex(widget.index);
if (newEntry != entryForIndex(oldWidget.index) && newEntry != _lastEntry) {
_initDetailLoader();
}
}
@ -70,12 +71,17 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
void _initDetailLoader() {
final requestEntry = entry;
if (requestEntry == null) {
_detailLoader = SynchronousFuture(null);
_detailLoader = SynchronousFuture(const OverlayMetadata());
} else {
_detailLoader = Future.wait([
settings.showOverlayShootingDetails ? metadataFetchService.getOverlayMetadata(requestEntry) : Future.value(null),
settings.showOverlayDescription ? metadataFetchService.getDescription(requestEntry) : Future.value(null),
]);
_detailLoader = metadataFetchService.getFields(requestEntry, {
if (settings.showOverlayShootingDetails) ...{
MetadataSyntheticField.aperture,
MetadataSyntheticField.exposureTime,
MetadataSyntheticField.focalLength,
MetadataSyntheticField.iso,
},
if (settings.showOverlayDescription) MetadataSyntheticField.description,
});
}
}
@ -84,24 +90,20 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
return SafeArea(
top: false,
bottom: false,
child: FutureBuilder<List<dynamic>?>(
child: FutureBuilder<OverlayMetadata>(
future: _detailLoader,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
_lastDetails = snapshot.data;
_lastDetails = snapshot.data!;
_lastEntry = entry;
}
if (_lastEntry == null) return const SizedBox();
final mainEntry = _lastEntry!;
final shootingDetails = _lastDetails![0];
final description = _lastDetails![1];
final multiPageController = widget.multiPageController;
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
pageEntry: pageEntry ?? mainEntry,
shootingDetails: shootingDetails,
description: description,
details: _lastDetails,
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
availableWidth: widget.availableSize.width,
multiPageController: multiPageController,
@ -122,8 +124,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
class ViewerDetailOverlayContent extends StatelessWidget {
final AvesEntry pageEntry;
final OverlayMetadata? shootingDetails;
final String? description;
final OverlayMetadata details;
final String? position;
final double availableWidth;
final MultiPageController? multiPageController;
@ -140,8 +141,7 @@ class ViewerDetailOverlayContent extends StatelessWidget {
const ViewerDetailOverlayContent({
super.key,
required this.pageEntry,
required this.shootingDetails,
required this.description,
required this.details,
required this.position,
required this.availableWidth,
required this.multiPageController,
@ -244,27 +244,27 @@ class ViewerDetailOverlayContent extends StatelessWidget {
Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher(
context: context,
visible: description != null,
visible: details.description != null,
builder: (context) => OverlayRowExpander(
expandedNotifier: expandedNotifier,
child: OverlayDescriptionRow(description: description!),
child: OverlayDescriptionRow(description: details.description!),
),
);
Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher(
context: context,
visible: shootingDetails != null && shootingDetails!.isNotEmpty,
visible: details.hasShootingDetails,
builder: (context) => SizedBox(
width: subRowWidth,
child: OverlayShootingRow(details: shootingDetails!),
child: OverlayShootingRow(details: details),
),
);
Widget _buildShootingSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher(
context: context,
subRowWidth: subRowWidth,
visible: shootingDetails != null && shootingDetails!.isNotEmpty,
builder: (context) => OverlayShootingRow(details: shootingDetails!),
visible: details.hasShootingDetails,
builder: (context) => OverlayShootingRow(details: details),
);
Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher(

View file

@ -22,7 +22,7 @@ class OverlayRowExpander extends StatelessWidget {
textAlign: parent.textAlign,
softWrap: expanded,
overflow: parent.overflow,
maxLines: expanded ? null : 42,
maxLines: expanded ? 16 : 1,
textWidthBasis: parent.textWidthBasis,
child: child!,
);

View file

@ -42,16 +42,13 @@ mixin HistogramMixin {
final blueLevels = List.filled(bins, 0);
final view = Uint8List.view(data.buffer);
final pixelCount = view.length / 4;
for (var i = 0; i < pixelCount; i += 4) {
final viewSize = view.length;
for (var i = 0; i < viewSize; i += 4) {
final a = view[i + 3];
if (a > 0) {
final r = view[i + 0];
final g = view[i + 1];
final b = view[i + 2];
redLevels[r]++;
greenLevels[g]++;
blueLevels[b]++;
redLevels[view[i + 0]]++;
greenLevels[view[i + 1]]++;
blueLevels[view[i + 2]]++;
}
}
@ -75,14 +72,17 @@ mixin HistogramMixin {
const normMax = bins - 1;
final view = Uint8List.view(data.buffer);
final pixelCount = view.length / 4;
for (var i = 0; i < pixelCount; i += 4) {
final viewSize = view.length;
for (var i = 0; i < viewSize; i += 4) {
final a = view[i + 3];
if (a > 0) {
final r = view[i + 0];
final g = view[i + 1];
final b = view[i + 2];
lumLevels[(Color.fromARGB(a, r, g, b).computeLuminance() * normMax).round()]++;
// `Color.computeLuminance()` is more accurate, but slower
// and photo software typically use the simpler formula
final luminance = (r * 0.3 + g * 0.59 + b * 0.11) / 255;
lumLevels[(luminance * normMax).round()]++;
}
}

View file

@ -89,10 +89,10 @@ packages:
dependency: transitive
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:

View file

@ -8,6 +8,7 @@ enum EntrySetAction {
searchCollection,
toggleTitleSearch,
addShortcut,
setHome,
emptyBin,
// browsing or selecting
map,
@ -47,6 +48,7 @@ class EntrySetActions {
EntrySetAction.searchCollection,
EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut,
EntrySetAction.setHome,
null,
EntrySetAction.map,
EntrySetAction.slideshow,
@ -60,11 +62,9 @@ class EntrySetActions {
static const collectionEditorBrowsing = [
EntrySetAction.searchCollection,
EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut,
EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats,
EntrySetAction.rescan,
];
// `null` items are converted to dividers
@ -98,7 +98,6 @@ class EntrySetActions {
EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats,
EntrySetAction.rescan,
// editing actions are in their subsection
];

View file

@ -1,3 +1,11 @@
enum MetadataSyntheticField {
aperture,
description,
exposureTime,
focalLength,
iso,
}
enum MetadataField {
exifDate,
exifDateOriginal,

View file

@ -41,6 +41,7 @@ class SettingKeys {
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page';
static const homeCustomCollectionKey = 'home_custom_collection';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmCreateVaultKey = 'confirm_create_vault';
static const confirmDeleteForeverKey = 'confirm_delete_forever';
@ -60,6 +61,7 @@ class SettingKeys {
static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions';
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
static const showThumbnailFavouriteKey = 'show_thumbnail_favourite';
static const showThumbnailHdrKey = 'show_thumbnail_hdr';
static const thumbnailLocationIconKey = 'thumbnail_location_icon';
static const thumbnailTagIconKey = 'thumbnail_tag_icon';
static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo';

View file

@ -58,10 +58,10 @@ packages:
dependency: "direct main"
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter

View file

@ -35,3 +35,7 @@ abstract class ReportService {
.join('\n'));
}
}
class UnreportedStateError extends StateError {
UnreportedStateError(super.message);
}

View file

@ -71,8 +71,10 @@ class PlatformReportService extends ReportService {
if (exception is PlatformException && stack != null) {
stack = ReportService.buildReportStack(stack, level: 2);
}
if (exception is! UnreportedStateError) {
return _instance?.recordError(exception, stack);
}
}
@override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async {

View file

@ -92,10 +92,10 @@ packages:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "5ccdf05de039f9544d0ba41c5ae2052ca2425985d32229911b09f69981164518"
sha256: "5125b7f3fcef2bfdd7e071afe7edcefd9597968003e44e073456c773d91694ee"
url: "https://pub.dev"
source: hosted
version: "3.4.8"
version: "3.4.9"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
@ -179,10 +179,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter

View file

@ -58,10 +58,10 @@ packages:
dependency: "direct main"
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter

View file

@ -96,10 +96,10 @@ packages:
dependency: transitive
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:

View file

@ -195,42 +195,42 @@ packages:
dependency: "direct main"
description:
name: google_maps_flutter
sha256: d4914cb38b3dcb62c39c085d968d434de0f8050f00f4d9f5ba4a7c7e004934cb
sha256: ae66fef3e71261d7df2eff29b2a119e190b2884325ecaa55321b1e17b5504066
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.5.3"
google_maps_flutter_android:
dependency: "direct main"
description:
name: google_maps_flutter_android
sha256: "4279a338b79288fad5c8b03e5ae6ec30888bff210e0bab10b1f31f31e5a90558"
sha256: "714530f865f13bb3b9505c58821c3baed5d247a871724acf5d2ea5808fbed02c"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.6.2"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: "6ad65362aeeeda44b7c2c807e36bf578ef4b1c163882e085bdb040bf2934b246"
sha256: b644d205c235f85dc60e22f46172a868b1cd642afd5a52b3808c789e461b025a
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.4.1"
google_maps_flutter_platform_interface:
dependency: "direct main"
description:
name: google_maps_flutter_platform_interface
sha256: a3e9e6896501e566d902c6c69f010834d410ef4b7b5c18b90c77e871c86b7907
sha256: "6060779f020638a8eedeb0fb14234818e5fa32ec45a4653d6428ab436e2bbc64"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.3"
google_maps_flutter_web:
dependency: transitive
description:
name: google_maps_flutter_web
sha256: f893d1542c6562bc8299ef768fbbe92ade83c220ab3209b9477ec9f81ad585e4
sha256: "6245721c160d6f531c1ef568cf9bef8d660cd585a982aa75121269030163785a"
url: "https://pub.dev"
source: hosted
version: "0.5.4+2"
version: "0.5.4+3"
html:
dependency: transitive
description:
@ -243,10 +243,10 @@ packages:
dependency: transitive
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@ -355,10 +355,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
polylabel:
dependency: transitive
description:
@ -464,10 +464,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "5.2.0"
win32_registry:
dependency: transitive
description:
@ -485,5 +485,5 @@ packages:
source: hosted
version: "2.0.0"
sdks:
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.13.0"
dart: ">=3.2.3 <4.0.0"
flutter: ">=3.16.6"

View file

@ -117,10 +117,10 @@ packages:
dependency: transitive
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@ -231,10 +231,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
polylabel:
dependency: transitive
description:

View file

@ -103,10 +103,10 @@ packages:
dependency: transitive
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:

View file

@ -51,10 +51,10 @@ packages:
description:
path: "flutter/flutter"
ref: background-lts
resolved-ref: "8eb0534b9f74d7242adf68e22a3578d235911466"
resolved-ref: "3fc7325f89225110b725cfbbb5e2560542a62e2e"
url: "https://github.com/deckerst/ffmpeg-kit.git"
source: git
version: "6.0.2"
version: "6.0.3"
ffmpeg_kit_flutter_platform_interface:
dependency: transitive
description:
@ -104,10 +104,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter

View file

@ -14,12 +14,7 @@ dependencies:
path: ../aves_video
# `video` version is necessary, as some videos make the app crash
# when using only `min` or `https` (the default)
# ffmpeg_kit_flutter_video: 6.0.2-LTS
# ffmpeg_kit_flutter:
# git:
# url: https://github.com/arthenica/ffmpeg-kit.git
# ref: development-flutter
# path: flutter/flutter
# ffmpeg_kit_flutter_video: 6.0.3-LTS
ffmpeg_kit_flutter:
git:
url: https://github.com/deckerst/ffmpeg-kit.git

View file

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
url: "https://pub.dev"
source: hosted
version: "3.4.9"
version: "3.4.10"
args:
dependency: transitive
description:
@ -102,6 +102,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -124,10 +132,10 @@ packages:
dependency: transitive
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@ -140,10 +148,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d"
url: "https://pub.dev"
source: hosted
version: "4.1.3"
version: "4.1.4"
js:
dependency: transitive
description:
@ -244,18 +252,18 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.3"
version: "3.7.4"
safe_local_storage:
dependency: transitive
description:
@ -385,10 +393,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.3.3"
vector_math:
dependency: transitive
description:
@ -433,10 +441,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "5.2.0"
xml:
dependency: transitive
description:

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