](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/1.png)
+[
](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/2.png)
+[
](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/5.png)
+[
](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/3.png)
+[
](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/4.png)
+[
](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/6.png)
+
+
## Changelog
@@ -59,7 +85,7 @@ At this stage this project does *not* accept PRs, except for translations.
### Translations
-If you want to translate this app in your language and share the result, feel free to open a PR or send the translation by [email](mailto:gallery.aves@gmail.com). You can find some localization notes in [pubspec.yaml](https://github.com/deckerst/aves/blob/develop/pubspec.yaml). English, Korean and French are already handled by me. Russian, German and Spanish are handled by generous volunteers.
+If you want to translate this app in your language and share the result, [there is a guide](https://github.com/deckerst/aves/wiki/Contributing-to-Translations). English, Korean and French are already handled by me. Russian, German and Spanish are handled by generous volunteers.
### Donations
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9116411b4..8acc2d333 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -41,15 +41,12 @@ if (keystorePropertiesFile.exists()) {
}
android {
- compileSdkVersion 31
+ compileSdkVersion 32
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
- lintOptions {
- disable 'InvalidPackage'
- }
defaultConfig {
applicationId appId
@@ -60,7 +57,7 @@ android {
// 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
minSdkVersion 19
- targetSdkVersion 31
+ targetSdkVersion 32
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
@@ -129,6 +126,9 @@ android {
}
}
}
+ lint {
+ disable 'InvalidPackage'
+ }
}
flutter {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
index e906ab72c..f56a7f838 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
@@ -143,6 +143,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
.submit(size, size)
try {
+ @Suppress("BlockingMethodInNonBlockingContext")
data = target.get()?.getBytes(canHaveAlpha = true, recycle = false)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
@@ -312,7 +313,16 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
context.startActivity(Intent.createChooser(intent, title))
return true
} catch (e: SecurityException) {
- Log.w(LOG_TAG, "failed to start activity chooser for intent=$intent", e)
+ if (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) {
+ // in some environments, providing the write flag yields a `SecurityException`:
+ // "UID XXXX does not have permission to content://XXXX"
+ // so we retry without it
+ Log.i(LOG_TAG, "retry intent=$intent without FLAG_GRANT_WRITE_URI_PERMISSION")
+ intent.flags = intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
+ return safeStartActivityChooser(title, intent)
+ } else {
+ Log.w(LOG_TAG, "failed to start activity chooser for intent=$intent", e)
+ }
}
return false
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt
index 17ced0c0b..ddb100edd 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt
@@ -9,6 +9,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
+import java.io.IOException
import java.util.*
// as of 2021/03/10, geocoding packages exist but:
@@ -50,6 +51,10 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
val addresses = try {
geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
+ } catch (e: IOException) {
+ // `grpc failed`, etc.
+ result.error("getAddress-network", "failed to get address because of network issues", e.message)
+ return
} catch (e: Exception) {
result.error("getAddress-exception", "failed to get address", e.message)
return
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
index 5c9d40242..68c1c5293 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
@@ -13,7 +13,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.decoder.MultiTrackImage
-import deckers.thibault.aves.decoder.SvgThumbnail
+import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
@@ -128,7 +128,7 @@ class ThumbnailFetcher internal constructor(
.submit(width, height)
} else {
val model: Any = when {
- svgFetch -> SvgThumbnail(context, uri)
+ svgFetch -> SvgImage(context, uri)
tiffFetch -> TiffImage(context, uri, pageId)
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
else -> StorageUtils.getGlideSafeUri(uri, mimeType)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt
index a305180b2..a574fb74b 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt
@@ -134,8 +134,10 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
var destinationDir = arguments["destinationPath"] as String?
val mimeType = arguments["mimeType"] as String?
+ val width = arguments["width"] as Int?
+ val height = arguments["height"] as Int?
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
- if (destinationDir == null || mimeType == null || nameConflictStrategy == null) {
+ if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
error("export-args", "failed because of missing arguments", null)
return
}
@@ -150,7 +152,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
val entries = entryMapList.map(::AvesEntry)
- provider.exportMultiple(activity, mimeType, destinationDir, entries, nameConflictStrategy, object : ImageOpCallback {
+ provider.exportMultiple(activity, mimeType, destinationDir, entries, width, height, nameConflictStrategy, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = success(fields)
override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable)
})
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
index 4a7a048f6..d544ba203 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
@@ -25,27 +25,27 @@ import kotlin.math.ceil
@GlideModule
class SvgGlideModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
- registry.append(SvgThumbnail::class.java, Bitmap::class.java, SvgLoader.Factory())
+ registry.append(SvgImage::class.java, Bitmap::class.java, SvgLoader.Factory())
}
}
-class SvgThumbnail(val context: Context, val uri: Uri)
+class SvgImage(val context: Context, val uri: Uri)
-internal class SvgLoader : ModelLoader
{
- override fun buildLoadData(model: SvgThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData {
+internal class SvgLoader : ModelLoader {
+ override fun buildLoadData(model: SvgImage, width: Int, height: Int, options: Options): ModelLoader.LoadData {
return ModelLoader.LoadData(ObjectKey(model.uri), SvgFetcher(model, width, height))
}
- override fun handles(model: SvgThumbnail): Boolean = true
+ override fun handles(model: SvgImage): Boolean = true
- internal class Factory : ModelLoaderFactory {
- override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = SvgLoader()
+ internal class Factory : ModelLoaderFactory {
+ override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = SvgLoader()
override fun teardown() {}
}
}
-internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: Int) : DataFetcher {
+internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int) : DataFetcher {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) {
val context = model.context
val uri = model.uri
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt
index 102387552..2fc085469 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt
@@ -220,7 +220,7 @@ object ExifInterfaceHelper {
// initialize metadata-extractor directories that we will fill
// by tags converted from the ExifInterface attributes
// so that we can rely on metadata-extractor descriptions
- val dirs = DirType.values().map { Pair(it, it.createDirectory()) }.toMap()
+ val dirs = DirType.values().associate { Pair(it, it.createDirectory()) }
// exclude Exif directory when it only includes image size
val isUselessExif = fun(it: Map): Boolean {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt
index cbb92eee9..cfde73855 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt
@@ -102,8 +102,8 @@ object MediaMetadataRetrieverHelper {
val symbol = "bit/s"
if (size < divider) return "$size $symbol"
- if (size < divider * divider) return "${String.format("%.2f", size.toDouble() / divider)} K$symbol"
- return "${String.format("%.2f", size.toDouble() / divider / divider)} M$symbol"
+ if (size < divider * divider) return "${String.format(Locale.getDefault(), "%.2f", size.toDouble() / divider)} K$symbol"
+ return "${String.format(Locale.getDefault(), "%.2f", size.toDouble() / divider / divider)} M$symbol"
}
fun MediaMetadataRetriever.getSafeDescription(tag: Int, save: (value: String) -> Unit) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
index 84d5959f2..61b17d9c9 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
@@ -6,7 +6,6 @@ import com.drew.lang.Rational
import com.drew.lang.SequentialByteArrayReader
import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase
-import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader
import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 9ef5a9112..0466552bd 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -13,9 +13,11 @@ import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.request.FutureTarget
import com.bumptech.glide.request.RequestOptions
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.decoder.MultiTrackImage
+import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
@@ -35,6 +37,7 @@ import deckers.thibault.aves.utils.MimeTypes.isVideo
import java.io.ByteArrayInputStream
import java.io.File
import java.io.IOException
+import java.io.OutputStream
import java.util.*
import kotlin.collections.HashMap
@@ -82,6 +85,8 @@ abstract class ImageProvider {
imageExportMimeType: String,
targetDir: String,
entries: List,
+ width: Int,
+ height: Int,
nameConflictStrategy: NameConflictStrategy,
callback: ImageOpCallback,
) {
@@ -95,12 +100,6 @@ abstract class ImageProvider {
return
}
- // TODO TLAD [storage] allow inserting by Media Store
- if (targetDirDocFile == null) {
- callback.onFailure(Exception("failed to get tree doc for directory at path=$targetDir"))
- return
- }
-
for (entry in entries) {
val sourceUri = entry.uri
val sourcePath = entry.path
@@ -115,11 +114,13 @@ abstract class ImageProvider {
val sourceMimeType = entry.mimeType
val exportMimeType = if (isVideo(sourceMimeType)) sourceMimeType else imageExportMimeType
try {
- val newFields = exportSingleByTreeDocAndScan(
+ val newFields = exportSingle(
activity = activity,
sourceEntry = entry,
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
+ width = width,
+ height = height,
nameConflictStrategy = nameConflictStrategy,
exportMimeType = exportMimeType,
)
@@ -133,11 +134,13 @@ abstract class ImageProvider {
}
@Suppress("BlockingMethodInNonBlockingContext")
- private suspend fun exportSingleByTreeDocAndScan(
+ private suspend fun exportSingle(
activity: Activity,
sourceEntry: AvesEntry,
targetDir: String,
- targetDirDocFile: DocumentFileCompat,
+ targetDirDocFile: DocumentFileCompat?,
+ width: Int,
+ height: Int,
nameConflictStrategy: NameConflictStrategy,
exportMimeType: String,
): FieldMap {
@@ -163,44 +166,46 @@ abstract class ImageProvider {
conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap
- // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
- // but in order to open an output stream to it, we need to use a `SingleDocumentFile`
- // through a document URI, not a tree URI
- // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
- val targetTreeFile = targetDirDocFile.createFile(exportMimeType, targetNameWithoutExtension)
- val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
-
- if (isVideo(sourceMimeType)) {
- val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
- sourceDocFile.copyTo(targetDocFile)
- } else {
- val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
- MultiTrackImage(activity, sourceUri, pageId)
- } else if (sourceMimeType == MimeTypes.TIFF) {
- TiffImage(activity, sourceUri, pageId)
+ val targetMimeType: String
+ val write: (OutputStream) -> Unit
+ var target: FutureTarget? = null
+ try {
+ if (isVideo(sourceMimeType)) {
+ targetMimeType = sourceMimeType
+ write = { output ->
+ val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
+ sourceDocFile.copyTo(output)
+ }
} else {
- StorageUtils.getGlideSafeUri(sourceUri, sourceMimeType)
- }
+ val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
+ MultiTrackImage(activity, sourceUri, pageId)
+ } else if (sourceMimeType == MimeTypes.TIFF) {
+ TiffImage(activity, sourceUri, pageId)
+ } else if (sourceMimeType == MimeTypes.SVG) {
+ SvgImage(activity, sourceUri)
+ } else {
+ StorageUtils.getGlideSafeUri(sourceUri, sourceMimeType)
+ }
- // request a fresh image with the highest quality format
- val glideOptions = RequestOptions()
- .format(DecodeFormat.PREFER_ARGB_8888)
- .diskCacheStrategy(DiskCacheStrategy.NONE)
- .skipMemoryCache(true)
+ // request a fresh image with the highest quality format
+ val glideOptions = RequestOptions()
+ .format(DecodeFormat.PREFER_ARGB_8888)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .skipMemoryCache(true)
- val target = Glide.with(activity)
- .asBitmap()
- .apply(glideOptions)
- .load(model)
- .submit()
- try {
+ target = Glide.with(activity)
+ .asBitmap()
+ .apply(glideOptions)
+ .load(model)
+ .submit(width, height)
var bitmap = target.get()
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
}
bitmap ?: throw Exception("failed to get image for mimeType=$sourceMimeType uri=$sourceUri page=$pageId")
- targetDocFile.openOutputStream().use { output ->
+ targetMimeType = exportMimeType
+ write = { output ->
if (exportMimeType == MimeTypes.BMP) {
BmpWriter.writeRGB24(bitmap, output)
} else {
@@ -223,21 +228,23 @@ abstract class ImageProvider {
bitmap.compress(format, quality, output)
}
}
- } catch (e: Exception) {
- // remove empty file
- if (targetDocFile.exists()) {
- targetDocFile.delete()
- }
- throw e
- } finally {
- Glide.with(activity).clear(target)
}
+
+ val mediaStoreImageProvider = MediaStoreImageProvider()
+ val targetPath = mediaStoreImageProvider.createSingle(
+ activity = activity,
+ mimeType = targetMimeType,
+ targetDir = targetDir,
+ targetDirDocFile = targetDirDocFile,
+ targetNameWithoutExtension = targetNameWithoutExtension,
+ write = write,
+ )
+ return mediaStoreImageProvider.scanNewPath(activity, targetPath, exportMimeType)
+ } finally {
+ // clearing Glide target should happen after effectively writing the bitmap
+ Glide.with(activity).clear(target)
}
- val fileName = targetDocFile.name
- val targetFullPath = targetDir + fileName
-
- return MediaStoreImageProvider().scanNewPath(activity, targetFullPath, exportMimeType)
}
@Suppress("BlockingMethodInNonBlockingContext")
@@ -808,6 +815,55 @@ abstract class ImageProvider {
modifier: FieldMap,
callback: ImageOpCallback,
) {
+ if (modifier.containsKey("exif")) {
+ val fields = modifier["exif"] as Map<*, *>?
+ if (fields != null && fields.isNotEmpty()) {
+ if (!editExif(context, path, uri, mimeType, callback) { exif ->
+ var setLocation = false
+ fields.forEach { kv ->
+ val tag = kv.key as String?
+ if (tag != null) {
+ val value = kv.value
+ if (value == null) {
+ // remove attribute
+ exif.setAttribute(tag, value)
+ } else {
+ when (tag) {
+ ExifInterface.TAG_GPS_LATITUDE,
+ ExifInterface.TAG_GPS_LATITUDE_REF,
+ ExifInterface.TAG_GPS_LONGITUDE,
+ ExifInterface.TAG_GPS_LONGITUDE_REF -> {
+ setLocation = true
+ }
+ else -> {
+ if (value is String) {
+ exif.setAttribute(tag, value)
+ } else {
+ Log.w(LOG_TAG, "failed to set Exif attribute $tag because value=$value is not a string")
+ }
+ }
+ }
+ }
+ }
+ }
+ if (setLocation) {
+ val latAbs = (fields[ExifInterface.TAG_GPS_LATITUDE] as Number?)?.toDouble()
+ val latRef = fields[ExifInterface.TAG_GPS_LATITUDE_REF] as String?
+ val lngAbs = (fields[ExifInterface.TAG_GPS_LONGITUDE] as Number?)?.toDouble()
+ val lngRef = fields[ExifInterface.TAG_GPS_LONGITUDE_REF] as String?
+ if (latAbs != null && latRef != null && lngAbs != null && lngRef != null) {
+ val latitude = if (latRef == ExifInterface.LATITUDE_SOUTH) -latAbs else latAbs
+ val longitude = if (lngRef == ExifInterface.LONGITUDE_WEST) -lngAbs else lngAbs
+ exif.setLatLong(latitude, longitude)
+ } else {
+ Log.w(LOG_TAG, "failed to set Exif location with latAbs=$latAbs, latRef=$latRef, lngAbs=$lngAbs, lngRef=$lngRef")
+ }
+ }
+ exif.saveAttributes()
+ }) return
+ }
+ }
+
if (modifier.containsKey("iptc")) {
val iptc = (modifier["iptc"] as List<*>?)?.filterIsInstance()
if (!editIptc(
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index 4e4a305d1..4ada34d6f 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -28,6 +28,7 @@ import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.StorageUtils.PathSegments
import deckers.thibault.aves.utils.UriUtils.tryParseId
import java.io.File
+import java.io.OutputStream
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.collections.ArrayList
@@ -414,34 +415,39 @@ class MediaStoreImageProvider : ImageProvider() {
conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap
- return moveSingleByTreeDoc(
+ val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
+ val targetPath = createSingle(
activity = activity,
mimeType = mimeType,
- sourceUri = sourceUri,
- sourcePath = sourcePath,
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
- copy = copy
- )
+ ) { output: OutputStream -> sourceDocFile.copyTo(output) }
+
+ if (!copy) {
+ // delete original entry
+ try {
+ delete(activity, sourceUri, sourcePath, mimeType)
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
+ }
+ }
+
+ return scanNewPath(activity, targetPath, mimeType)
}
- private suspend fun moveSingleByTreeDoc(
+ // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
+ // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
+ // when used with entry URI as `sourceDocumentUri`, and targetDirDocFile URI as `targetParentDocumentUri`
+ fun createSingle(
activity: Activity,
mimeType: String,
- sourceUri: Uri,
- sourcePath: String,
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String,
- copy: Boolean
- ): FieldMap {
- // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
- // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
- // when used with entry URI as `sourceDocumentUri`, and targetDirDocFile URI as `targetParentDocumentUri`
- val source = DocumentFileCompat.fromSingleUri(activity, sourceUri)
-
- val targetPath = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isDownloadDir(activity, targetDir)) {
+ write: (OutputStream) -> Unit,
+ ): String {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isDownloadDir(activity, targetDir)) {
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
@@ -451,10 +457,7 @@ class MediaStoreImageProvider : ImageProvider() {
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
uri?.let {
- @Suppress("BlockingMethodInNonBlockingContext")
- resolver.openOutputStream(uri)?.use { output ->
- source.copyTo(output)
- }
+ resolver.openOutputStream(uri)?.use(write)
values.clear()
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
resolver.update(uri, values, null, null)
@@ -468,12 +471,18 @@ class MediaStoreImageProvider : ImageProvider() {
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
- @Suppress("BlockingMethodInNonBlockingContext")
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
- @Suppress("BlockingMethodInNonBlockingContext")
- source.copyTo(targetDocFile)
+ try {
+ targetDocFile.openOutputStream().use(write)
+ } catch (e: Exception) {
+ // remove empty file
+ if (targetDocFile.exists()) {
+ targetDocFile.delete()
+ }
+ throw e
+ }
// the source file name and the created document file name can be different when:
// - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not*
@@ -481,17 +490,6 @@ class MediaStoreImageProvider : ImageProvider() {
val fileName = targetDocFile.name
targetDir + fileName
}
-
- if (!copy) {
- // delete original entry
- try {
- delete(activity, sourceUri, sourcePath, mimeType)
- } catch (e: Exception) {
- Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
- }
- }
-
- return scanNewPath(activity, targetPath, mimeType)
}
private fun isDownloadDir(context: Context, dirPath: String): Boolean {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
index c801c30ec..bc9f217f2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
@@ -80,77 +80,87 @@ object StorageUtils {
return pathSteps.iterator()
}
+ private fun appSpecificVolumePath(file: File?): String? {
+ file ?: return null
+ val appSpecificPath = file.absolutePath
+ val relativePathStartIndex = appSpecificPath.indexOf("Android/data")
+ if (relativePathStartIndex < 0) return null
+ return appSpecificPath.substring(0, relativePathStartIndex)
+ }
+
private fun findPrimaryVolumePath(context: Context): String? {
- // we want:
- // /storage/emulated/0/
- // `Environment.getExternalStorageDirectory()` (deprecated) yields:
- // /storage/emulated/0
- // `context.getExternalFilesDir(null)` yields:
- // /storage/emulated/0/Android/data/{package_name}/files
- return context.getExternalFilesDir(null)?.let {
- val appSpecificPath = it.absolutePath
- return appSpecificPath.substring(0, appSpecificPath.indexOf("Android/data"))
+ try {
+ // we want:
+ // /storage/emulated/0/
+ // `Environment.getExternalStorageDirectory()` (deprecated) yields:
+ // /storage/emulated/0
+ // `context.getExternalFilesDir(null)` yields:
+ // /storage/emulated/0/Android/data/{package_name}/files
+ return appSpecificVolumePath(context.getExternalFilesDir(null))
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "failed to find primary volume path", e)
}
+ return null
}
private fun findVolumePaths(context: Context): Array {
// Final set of paths
val paths = HashSet()
- // Primary emulated SD-CARD
- val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: ""
- if (TextUtils.isEmpty(rawEmulatedStorageTarget)) {
- // fix of empty raw emulated storage on marshmallow
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- lateinit var files: List
- var validFiles: Boolean
- do {
- // `getExternalFilesDirs` sometimes include `null` when called right after getting read access
- // (e.g. on API 30 emulator) so we retry until the file system is ready
- val externalFilesDirs = context.getExternalFilesDirs(null)
- validFiles = !externalFilesDirs.contains(null)
- if (validFiles) {
- files = externalFilesDirs.filterNotNull()
- } else {
- try {
- Thread.sleep(100)
- } catch (e: InterruptedException) {
- Log.e(LOG_TAG, "insomnia", e)
+ try {
+ // Primary emulated SD-CARD
+ val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: ""
+ if (TextUtils.isEmpty(rawEmulatedStorageTarget)) {
+ // fix of empty raw emulated storage on marshmallow
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ lateinit var files: List
+ var validFiles: Boolean
+ do {
+ // `getExternalFilesDirs` sometimes include `null` when called right after getting read access
+ // (e.g. on API 30 emulator) so we retry until the file system is ready
+ val externalFilesDirs = context.getExternalFilesDirs(null)
+ validFiles = !externalFilesDirs.contains(null)
+ if (validFiles) {
+ files = externalFilesDirs.filterNotNull()
+ } else {
+ try {
+ Thread.sleep(100)
+ } catch (e: InterruptedException) {
+ Log.e(LOG_TAG, "insomnia", e)
+ }
}
- }
- } while (!validFiles)
- for (file in files) {
- val appSpecificAbsolutePath = file.absolutePath
- val emulatedRootPath = appSpecificAbsolutePath.substring(0, appSpecificAbsolutePath.indexOf("Android/data"))
- paths.add(emulatedRootPath)
- }
- } else {
- // Primary physical SD-CARD (not emulated)
- val rawExternalStorage = System.getenv("EXTERNAL_STORAGE") ?: ""
-
- // Device has physical external storage; use plain paths.
- if (TextUtils.isEmpty(rawExternalStorage)) {
- // EXTERNAL_STORAGE undefined; falling back to default.
- paths.addAll(physicalPaths)
+ } while (!validFiles)
+ paths.addAll(files.mapNotNull(::appSpecificVolumePath))
} else {
- paths.add(rawExternalStorage)
+ // Primary physical SD-CARD (not emulated)
+ val rawExternalStorage = System.getenv("EXTERNAL_STORAGE") ?: ""
+
+ // Device has physical external storage; use plain paths.
+ if (TextUtils.isEmpty(rawExternalStorage)) {
+ // EXTERNAL_STORAGE undefined; falling back to default.
+ paths.addAll(physicalPaths)
+ } else {
+ paths.add(rawExternalStorage)
+ }
+ }
+ } else {
+ // Device has emulated storage; external storage paths should have userId burned into them.
+ // /storage/emulated/[0,1,2,...]/
+ val path = getPrimaryVolumePath(context)
+ val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: ""
+ if (rawUserId.isEmpty()) {
+ paths.add(rawEmulatedStorageTarget)
+ } else {
+ paths.add(rawEmulatedStorageTarget + File.separator + rawUserId)
}
}
- } else {
- // Device has emulated storage; external storage paths should have userId burned into them.
- // /storage/emulated/[0,1,2,...]/
- val path = getPrimaryVolumePath(context)
- val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: ""
- if (rawUserId.isEmpty()) {
- paths.add(rawEmulatedStorageTarget)
- } else {
- paths.add(rawEmulatedStorageTarget + File.separator + rawUserId)
- }
- }
- // All Secondary SD-CARDs (all exclude primary) separated by ":"
- System.getenv("SECONDARY_STORAGE")?.let { secondaryStorages ->
- paths.addAll(secondaryStorages.split(File.pathSeparator).filter { it.isNotEmpty() })
+ // All Secondary SD-CARDs (all exclude primary) separated by ":"
+ System.getenv("SECONDARY_STORAGE")?.let { secondaryStorages ->
+ paths.addAll(secondaryStorages.split(File.pathSeparator).filter { it.isNotEmpty() })
+ }
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "failed to find volume paths", e)
}
return paths.map { ensureTrailingSeparator(it) }.toTypedArray()
@@ -272,7 +282,9 @@ object StorageUtils {
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
fun convertTreeUriToDirPath(context: Context, treeUri: Uri): String? {
- val encoded = treeUri.toString().substring(TREE_URI_ROOT.length)
+ val treeUriString = treeUri.toString()
+ if (treeUriString.length <= TREE_URI_ROOT.length) return null
+ val encoded = treeUriString.substring(TREE_URI_ROOT.length)
val matcher = TREE_URI_PATH_PATTERN.matcher(Uri.decode(encoded))
with(matcher) {
if (find()) {
diff --git a/android/app/src/main/res/values-pt/strings.xml b/android/app/src/main/res/values-pt/strings.xml
new file mode 100644
index 000000000..336c769db
--- /dev/null
+++ b/android/app/src/main/res/values-pt/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Aves
+ Procurar
+ Vídeos
+ Digitalização de mídia
+ Digitalizar imagens & vídeos
+ Digitalizando mídia
+ Pare
+
diff --git a/android/build.gradle b/android/build.gradle
index a5d165073..57fd9118c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.0.4'
+ classpath 'com.android.tools.build:gradle:7.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// GMS & Firebase Crashlytics are not actually used by all flavors
classpath 'com.google.gms:google-services:4.3.10'
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index b83b4bcb4..6672c658d 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt
index 8e7929a6f..6f57ace2b 100644
--- a/fastlane/metadata/android/de/full_description.txt
+++ b/fastlane/metadata/android/de/full_description.txt
@@ -2,4 +2,4 @@
Navigation und Suche ist ein wichtiger Bestandteil von Aves . Das Ziel besteht darin, dass Benutzer problemlos von Alben zu Fotos zu Tags zu Karten usw. wechseln können.
-Aves lässt sich mit Android (von API 19 bis 31 , d. h. von KitKat bis S) mit Funktionen wie App-Verknüpfungen und globaler Suche integrieren. Es funktioniert auch als Medienbetrachter und -auswahl .
\ No newline at end of file
+Aves lässt sich mit Android (von API 19 bis 32 , d. h. von KitKat bis Android 12L) mit Funktionen wie App-Verknüpfungen und globaler Suche integrieren. Es funktioniert auch als Medienbetrachter und -auswahl .
\ No newline at end of file
diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/1.png b/fastlane/metadata/android/de/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..7bb979df1
Binary files /dev/null and b/fastlane/metadata/android/de/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/2.png b/fastlane/metadata/android/de/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..b770a7c9e
Binary files /dev/null and b/fastlane/metadata/android/de/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/3.png b/fastlane/metadata/android/de/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..56ef9ded6
Binary files /dev/null and b/fastlane/metadata/android/de/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/4.png b/fastlane/metadata/android/de/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..0a8c2deb7
Binary files /dev/null and b/fastlane/metadata/android/de/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/5.png b/fastlane/metadata/android/de/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..a048d09d4
Binary files /dev/null and b/fastlane/metadata/android/de/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/6.png b/fastlane/metadata/android/de/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..e42e4d034
Binary files /dev/null and b/fastlane/metadata/android/de/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/en-US/changelogs/1064.txt b/fastlane/metadata/android/en-US/changelogs/1064.txt
new file mode 100644
index 000000000..3ce411869
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1064.txt
@@ -0,0 +1,5 @@
+In v1.5.10:
+- show, search and edit ratings
+- add many items to favourites at once
+- enjoy the app in Spanish
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/1065.txt b/fastlane/metadata/android/en-US/changelogs/1065.txt
new file mode 100644
index 000000000..c75dc2346
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1065.txt
@@ -0,0 +1,5 @@
+In v1.5.11:
+- edit locations of images
+- export SVGs to convert and resize them
+- enjoy the app in Portuguese
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 8a74c8d0b..eba12d85e 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -2,4 +2,4 @@
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 API 19 to 31 , i.e. from KitKat to S) with features such as app shortcuts and global search handling. It also works as a media viewer and picker .
\ No newline at end of file
+Aves integrates with Android (from API 19 to 32 , i.e. from KitKat to Android 12L) with features such as app shortcuts and global search handling. It also works as a media viewer and picker .
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index 1b9e0262c..4674df71c 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
index b2e45ff00..d699af62a 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
index f8e7cee24..20457a45f 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
index dc95c05ab..92fec2632 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
index 64a8a9588..95ef69497 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
index 336e6e21d..c2383bd70 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/es-MX/full_description.txt b/fastlane/metadata/android/es-MX/full_description.txt
index 5015acfec..68859bb5d 100644
--- a/fastlane/metadata/android/es-MX/full_description.txt
+++ b/fastlane/metadata/android/es-MX/full_description.txt
@@ -2,4 +2,4 @@
La navegación y búsqueda son partes importantes de Aves . Su propósito es que los usuarios puedan fácimente ir de álbumes a fotos, etiquetas, mapas, etc.
-Aves se integra con Android (desde API 19 a 31 , por ej. desde KitKat hasta S) con características como vínculos de aplicación y manejo de búsqueda global . También funciona como un visor y seleccionador multimedia .
\ No newline at end of file
+Aves se integra con Android (desde API 19 a 32 , por ej. desde KitKat hasta Android 12L) con características como vínculos de aplicación y manejo de búsqueda global . También funciona como un visor y seleccionador multimedia .
\ No newline at end of file
diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..a4ca6bdf7
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..bc1654404
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..f86f0c4a2
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..b5eb49bcc
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..7021c0374
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..7735c2cb3
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/fr/images/featureGraphic.png b/fastlane/metadata/android/fr/images/featureGraphic.png
new file mode 100644
index 000000000..a0b3a3e77
Binary files /dev/null and b/fastlane/metadata/android/fr/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/1.png b/fastlane/metadata/android/fr/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..eb62e1b14
Binary files /dev/null and b/fastlane/metadata/android/fr/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/2.png b/fastlane/metadata/android/fr/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..6aa5def2a
Binary files /dev/null and b/fastlane/metadata/android/fr/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/3.png b/fastlane/metadata/android/fr/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..ad50c09d1
Binary files /dev/null and b/fastlane/metadata/android/fr/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/4.png b/fastlane/metadata/android/fr/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..070238313
Binary files /dev/null and b/fastlane/metadata/android/fr/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/5.png b/fastlane/metadata/android/fr/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..cc70a692c
Binary files /dev/null and b/fastlane/metadata/android/fr/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/6.png b/fastlane/metadata/android/fr/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..7961778de
Binary files /dev/null and b/fastlane/metadata/android/fr/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/ko/images/featureGraphics.png b/fastlane/metadata/android/ko/images/featureGraphics.png
new file mode 100644
index 000000000..cb81a914d
Binary files /dev/null and b/fastlane/metadata/android/ko/images/featureGraphics.png differ
diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/1.png b/fastlane/metadata/android/ko/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..d4fcc34c3
Binary files /dev/null and b/fastlane/metadata/android/ko/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/2.png b/fastlane/metadata/android/ko/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..13a2135ee
Binary files /dev/null and b/fastlane/metadata/android/ko/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/3.png b/fastlane/metadata/android/ko/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..b9593c029
Binary files /dev/null and b/fastlane/metadata/android/ko/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/4.png b/fastlane/metadata/android/ko/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..3bfa8781a
Binary files /dev/null and b/fastlane/metadata/android/ko/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/5.png b/fastlane/metadata/android/ko/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..ad6518250
Binary files /dev/null and b/fastlane/metadata/android/ko/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/6.png b/fastlane/metadata/android/ko/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..6aaa08e7e
Binary files /dev/null and b/fastlane/metadata/android/ko/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt
new file mode 100644
index 000000000..76ce49ba4
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/full_description.txt
@@ -0,0 +1,5 @@
+Aves pode lidar com todos os tipos de imagens e vídeos, incluindo seus típicos JPEGs e MP4s, mas também coisas mais exóticas como páginas múltiplas TIFFs, SVGs, AVIs antigos e muito mais ! Ele verifica sua coleção de mídia para identificar fotos em movimento , panoramas (aka photo spheres), vídeos em 360° , assim como GeoTIFF arquivos.
+
+Navegação e pesquisa é uma parte importante do Aves . O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
+
+Aves integra com Android (de API 19 para 32 , i.e. de KitKat para Android 12L) com recursos como atalhos de apps e pesquisa global manipulação. Também funciona como um visualizador e selecionador de mídia .
diff --git a/fastlane/metadata/android/pt-BR/images/featureGraphics.png b/fastlane/metadata/android/pt-BR/images/featureGraphics.png
new file mode 100644
index 000000000..677f87431
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/featureGraphics.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..8763df15d
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..8c6c9597f
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..200da6361
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..d72ae6282
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..30a9ebd14
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..02b87189c
Binary files /dev/null and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt
new file mode 100644
index 000000000..df48f8c44
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/short_description.txt
@@ -0,0 +1 @@
+Galeria e explorador de metadados
diff --git a/fastlane/metadata/android/ru/images/featureGraphics.png b/fastlane/metadata/android/ru/images/featureGraphics.png
new file mode 100644
index 000000000..5fa1a582a
Binary files /dev/null and b/fastlane/metadata/android/ru/images/featureGraphics.png differ
diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/1.png b/fastlane/metadata/android/ru/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..80af968b3
Binary files /dev/null and b/fastlane/metadata/android/ru/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/2.png b/fastlane/metadata/android/ru/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..1d42a1cb0
Binary files /dev/null and b/fastlane/metadata/android/ru/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/3.png b/fastlane/metadata/android/ru/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..811a42242
Binary files /dev/null and b/fastlane/metadata/android/ru/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/4.png b/fastlane/metadata/android/ru/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..daa6be34e
Binary files /dev/null and b/fastlane/metadata/android/ru/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/5.png b/fastlane/metadata/android/ru/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..b22e9d202
Binary files /dev/null and b/fastlane/metadata/android/ru/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/6.png b/fastlane/metadata/android/ru/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..0a4a2630a
Binary files /dev/null and b/fastlane/metadata/android/ru/images/phoneScreenshots/6.png differ
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 867ecab0d..438c05eca 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -3,10 +3,11 @@
"welcomeMessage": "Willkommen bei Aves",
"welcomeOptional": "Optional",
"welcomeTermsToggle": "Ich stimme den Bedingungen und Konditionen zu",
- "itemCount": " {count, plural, =1{1 Element} other{{count} Elemente}}",
+ "itemCount": "{count, plural, =1{1 Element} other{{count} Elemente}}",
- "timeSeconds": " {seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}",
- "timeMinutes": " {minutes, plural, =1{1 Minute} other{{minutes} Minuten}}",
+ "timeSeconds": "{seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}",
+ "timeMinutes": "{minutes, plural, =1{1 Minute} other{{minutes} Minuten}}",
+ "focalLength": "{length} mm",
"applyButtonLabel": "ANWENDEN",
"deleteButtonLabel": "LÖSCHEN",
@@ -25,7 +26,7 @@
"actionRemove": "Entfernen",
"resetButtonTooltip": "Zurücksetzen",
- "doubleBackExitMessage": "Tippen Sie zum Verlassen erneut auf „Zurück“.",
+ "doubleBackExitMessage": "Zum Verlassen erneut auf „Zurück“ tippen.",
"sourceStateLoading": "Laden",
"sourceStateCataloguing": "Katalogisierung",
@@ -56,7 +57,7 @@
"entryActionViewSource": "Quelle anzeigen",
"entryActionViewMotionPhotoVideo": "Bewegtes Foto öffnen",
"entryActionEdit": "Bearbeiten mit...",
- "entryActionOpen": "Öffnen Sie mit...",
+ "entryActionOpen": "Öffnen mit...",
"entryActionSetAs": "Einstellen als...",
"entryActionOpenMap": "In der Karten-App anzeigen...",
"entryActionRotateScreen": "Bildschirm rotieren",
@@ -73,6 +74,7 @@
"videoActionSettings": "Einstellungen",
"entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten",
+ "entryInfoActionEditLocation": "Standort bearbeiten",
"entryInfoActionEditRating": "Bewertung bearbeiten",
"entryInfoActionEditTags": "Tags bearbeiten",
"entryInfoActionRemoveMetadata": "Metadaten entfernen",
@@ -93,7 +95,7 @@
"coordinateFormatDms": "GMS",
"coordinateFormatDecimal": "Dezimalgrad",
- "coordinateDms": " {coordinate} {direction}",
+ "coordinateDms": "{coordinate} {direction}",
"coordinateDmsNorth": "N",
"coordinateDmsSouth": "s",
"coordinateDmsEast": "O",
@@ -111,10 +113,10 @@
"mapStyleGoogleTerrain": "Google Maps (Gelände)",
"mapStyleOsmHot": "Humanitäres OSM",
"mapStyleStamenToner": "Stamen Toner (SchwarzWeiß)",
- "mapStyleStamenWatercolor": "Stamen Aquarell",
+ "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarell)",
"nameConflictStrategyRename": "Umbenennen",
- "nameConflictStrategyReplace": "Ersetzen Sie",
+ "nameConflictStrategyReplace": "Ersetzen",
"nameConflictStrategySkip": "Überspringen",
"keepScreenOnNever": "Niemals",
@@ -135,16 +137,16 @@
"rootDirectoryDescription": "Hauptverzeichnis",
"otherDirectoryDescription": "„{name}“ Verzeichnis",
"storageAccessDialogTitle": "Speicherzugriff",
- "storageAccessDialogMessage": "Bitte wählen Sie den {directory} von „{volume}“ auf dem nächsten Bildschirm, um dieser App Zugriff darauf zu geben.",
+ "storageAccessDialogMessage": "Bitte den {directory} von „{volume}“ auf dem nächsten Bildschirm auswählen, um dieser App Zugriff darauf zu geben.",
"restrictedAccessDialogTitle": "Eingeschränkter Zugang",
- "restrictedAccessDialogMessage": "Diese Anwendung darf keine Dateien im {directory} von „{volume}“ verändern.\n\nBitte verwenden Sie einen vorinstallierten Dateimanager oder eine Galerie-App, um die Objekte in ein anderes Verzeichnis zu verschieben.",
+ "restrictedAccessDialogMessage": "Diese Anwendung darf keine Dateien im {directory} von „{volume}“ verändern.\n\nBitte einen vorinstallierten Dateimanager verwenden oder eine Galerie-App, um die Objekte in ein anderes Verzeichnis zu verschieben.",
"notEnoughSpaceDialogTitle": "Nicht genug Platz",
"notEnoughSpaceDialogMessage": "Diese Operation benötigt {neededSize} freien Platz auf „{volume}“, um abgeschlossen zu werden, aber es ist nur noch {freeSize} übrig.",
"missingSystemFilePickerDialogTitle": "Fehlender System-Dateiauswahldialog",
- "missingSystemFilePickerDialogMessage": "Der System-Dateiauswahldialog fehlt oder ist deaktiviert. Bitte aktivieren Sie ihn und versuchen Sie es erneut.",
+ "missingSystemFilePickerDialogMessage": "Der System-Dateiauswahldialog fehlt oder ist deaktiviert. Bitte aktivieren und es erneut versuchen.",
"unsupportedTypeDialogTitle": "Nicht unterstützte Typen",
- "unsupportedTypeDialogMessage": " {count, plural, =1{Dieser Vorgang wird für Elemente des folgenden Typs nicht unterstützt: {types}.} other{Dieser Vorgang wird für Elemente der folgenden Typen nicht unterstützt: {types}.}}",
+ "unsupportedTypeDialogMessage": "{count, plural, =1{Dieser Vorgang wird für Elemente des folgenden Typs nicht unterstützt: {types}.} other{Dieser Vorgang wird für Elemente der folgenden Typen nicht unterstützt: {types}.}}",
"nameConflictDialogSingleSourceMessage": "Einige Dateien im Zielordner haben den gleichen Namen.",
"nameConflictDialogMultipleSourceMessage": "Einige Dateien haben denselben Namen.",
@@ -155,9 +157,9 @@
"noMatchingAppDialogTitle": "Keine passende App",
"noMatchingAppDialogMessage": "Es gibt keine Anwendungen, die dies bewältigen können.",
- "deleteEntriesConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie dieses Element löschen möchten?} other{Sind Sie sicher, dass Sie diese {count} Elemente löschen möchten?}}",
+ "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Element gelöscht werden soll?} other{Sicher, dass diese {count} Elemente gelöscht werden sollen?}}",
- "videoResumeDialogMessage": "Möchten Sie bei {time} weiter abspielen?",
+ "videoResumeDialogMessage": "Soll bei {time} weiter abspielt werden?",
"videoStartOverButtonLabel": "NEU BEGINNEN",
"videoResumeButtonLabel": "FORTSETZTEN",
@@ -165,7 +167,7 @@
"setCoverDialogLatest": "Letzter Artikel",
"setCoverDialogCustom": "Benutzerdefiniert",
- "hideFilterConfirmationDialogMessage": "Passende Fotos und Videos werden aus Ihrer Sammlung ausgeblendet. Sie können sie in den „Datenschutz“-Einstellungen wieder einblenden.\n\nSind Sie sicher, dass Sie sie ausblenden möchten?",
+ "hideFilterConfirmationDialogMessage": "Passende Fotos und Videos werden aus Ihrer Sammlung ausgeblendet. Dies kann in den „Datenschutz“-Einstellungen wieder eingeblendet werden.\n\nSicher, dass diese ausblendet werden sollen?",
"newAlbumDialogTitle": "Neues Album",
"newAlbumDialogNameLabel": "Album Name",
@@ -175,10 +177,12 @@
"renameAlbumDialogLabel": "Neuer Name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Verzeichnis existiert bereits",
- "deleteSingleAlbumConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie dieses Album und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie dieses Album und deren {count} Elemente löschen möchten?}}",
- "deleteMultiAlbumConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie diese Alben und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie diese Alben und deren {count} Elemente löschen möchten?}}",
+ "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Album und der Inhalt gelöscht werden soll?} other{Sicher, dass dieses Album und deren {count} Elemente gelöscht werden sollen?}}",
+ "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass diese Alben und deren Inhalt gelöscht werden sollen?} other{Sicher, dass diese Alben und deren {count} Elemente gelöscht werden sollen?}}",
"exportEntryDialogFormat": "Format:",
+ "exportEntryDialogWidth": "Breite",
+ "exportEntryDialogHeight": "Höhe",
"renameEntryDialogLabel": "Neuer Name",
@@ -192,12 +196,19 @@
"editEntryDateDialogHours": "Stunden",
"editEntryDateDialogMinutes": "Minuten",
+ "editEntryLocationDialogTitle": "Standort",
+ "editEntryLocationDialogChooseOnMapTooltip": "Auf Karte wählen",
+ "editEntryLocationDialogLatitude": "Breitengrad",
+ "editEntryLocationDialogLongitude": "Längengrad",
+
+ "locationPickerUseThisLocationButton": "Diesen Standort verwenden",
+
"editEntryRatingDialogTitle": "Bewertung",
"removeEntryMetadataDialogTitle": "Entfernung von Metadaten",
"removeEntryMetadataDialogMore": "Mehr",
- "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP ist erforderlich, um das Video innerhalb eines bewegten Bildes abzuspielen.\n\nSind Sie sicher, dass Sie es entfernen möchten?",
+ "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP ist erforderlich, um das Video innerhalb eines bewegten Bildes abzuspielen.\n\nSicher, dass es entfernt werden soll?",
"videoSpeedDialogLabel": "Wiedergabegeschwindigkeit",
@@ -230,13 +241,6 @@
"aboutLinkLicense": "Lizenz",
"aboutLinkPolicy": "Datenschutzrichtlinie",
- "aboutUpdate": "Neue Version verfügbar",
- "aboutUpdateLinks1": "Eine neue Version von Aves ist verfügbar unter",
- "aboutUpdateLinks2": "und",
- "aboutUpdateLinks3": ".",
- "aboutUpdateGitHub": "github",
- "aboutUpdateGooglePlay": "Google Play",
-
"aboutBug": "Fehlerbericht",
"aboutBugSaveLogInstruction": "Anwendungsprotokolle in einer Datei speichern",
"aboutBugSaveLogButton": "Speichern",
@@ -263,7 +267,7 @@
"collectionPageTitle": "Sammlung",
"collectionPickPageTitle": "Wähle",
- "collectionSelectionPageTitle": " {count, plural, =0{Elemente auswählen} =1{1 Element} other{{count} Elemente}}",
+ "collectionSelectionPageTitle": "{count, plural, =0{Elemente auswählen} =1{1 Element} other{{count} Elemente}}",
"collectionActionShowTitleSearch": "Titelfilter anzeigen",
"collectionActionHideTitleSearch": "Titelfilter ausblenden",
@@ -289,14 +293,14 @@
"dateToday": "Heute",
"dateYesterday": "Gestern",
"dateThisMonth": "Diesen Monat",
- "collectionDeleteFailureFeedback": " {count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}",
- "collectionCopyFailureFeedback": " {count, plural, =1{1 Element konnte nicht kopiert werden} other{{count} Element konnten nicht kopiert werden}}",
- "collectionMoveFailureFeedback": " {count, plural, =1{1 Element konnte nicht verschoben werden} other{{count} Elemente konnten nicht verschoben werden}}",
- "collectionEditFailureFeedback": " {count, plural, =1{1 Element konnte nicht bearbeitet werden} other{{count} 1 Elemente konnten nicht bearbeitet werden}}",
- "collectionExportFailureFeedback": " {count, plural, =1{1 Seite konnte nicht exportiert werden} other{{count} Seiten konnten nicht exportiert werden}}",
- "collectionCopySuccessFeedback": " {count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}",
- "collectionMoveSuccessFeedback": " {count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
- "collectionEditSuccessFeedback": " {count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
+ "collectionDeleteFailureFeedback": "{count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}",
+ "collectionCopyFailureFeedback": "{count, plural, =1{1 Element konnte nicht kopiert werden} other{{count} Element konnten nicht kopiert werden}}",
+ "collectionMoveFailureFeedback": "{count, plural, =1{1 Element konnte nicht verschoben werden} other{{count} Elemente konnten nicht verschoben werden}}",
+ "collectionEditFailureFeedback": "{count, plural, =1{1 Element konnte nicht bearbeitet werden} other{{count} 1 Elemente konnten nicht bearbeitet werden}}",
+ "collectionExportFailureFeedback": "{count, plural, =1{1 Seite konnte nicht exportiert werden} other{{count} Seiten konnten nicht exportiert werden}}",
+ "collectionCopySuccessFeedback": "{count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}",
+ "collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
+ "collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
"collectionEmptyFavourites": "Keine Favoriten",
"collectionEmptyVideos": "Keine Videos",
@@ -305,7 +309,7 @@
"collectionSelectSectionTooltip": "Bereich auswählen",
"collectionDeselectSectionTooltip": "Bereich abwählen",
- "drawerCollectionAll": "Alle Sammlung",
+ "drawerCollectionAll": "Alle Bilder",
"drawerCollectionFavourites": "Favoriten",
"drawerCollectionImages": "Bilder",
"drawerCollectionVideos": "Videos",
@@ -329,7 +333,7 @@
"albumPickPageTitlePick": "Album auswählen",
"albumCamera": "Kamera",
- "albumDownload": "Herunterladen",
+ "albumDownload": "Heruntergeladen",
"albumScreenshots": "Bildschirmfotos",
"albumScreenRecordings": "Bildschirmaufnahmen",
"albumVideoCaptures": "Video-Aufnahmen",
@@ -361,6 +365,10 @@
"settingsActionExport": "Exportieren",
"settingsActionImport": "Importieren",
+ "appExportCovers": "Titelbilder",
+ "appExportFavourites": "Favoriten",
+ "appExportSettings": "Einstellungen",
+
"settingsSectionNavigation": "Navigation",
"settingsHome": "Startseite",
"settingsKeepScreenOnTile": "Bildschirm eingeschaltet lassen",
@@ -369,7 +377,7 @@
"settingsNavigationDrawerTile": "Menü Navigation",
"settingsNavigationDrawerEditorTitle": "Menü Navigation",
- "settingsNavigationDrawerBanner": "Berühren und halten Sie die Taste, um Menüpunkte zu verschieben und neu anzuordnen.",
+ "settingsNavigationDrawerBanner": "Die Taste berühren und halten, um Menüpunkte zu verschieben und neu anzuordnen.",
"settingsNavigationDrawerTabTypes": "Typen",
"settingsNavigationDrawerTabAlbums": "Alben",
"settingsNavigationDrawerTabPages": "Seiten",
@@ -387,8 +395,8 @@
"settingsCollectionQuickActionEditorTitle": "Schnelle Aktionen",
"settingsCollectionQuickActionTabBrowsing": "Durchsuchen",
"settingsCollectionQuickActionTabSelecting": "Auswahl",
- "settingsCollectionBrowsingQuickActionEditorBanner": "Halten Sie die Taste gedrückt, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen beim Durchsuchen von Elementen angezeigt werden.",
- "settingsCollectionSelectionQuickActionEditorBanner": "Halten Sie die Taste gedrückt, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen bei der Auswahl von Elementen angezeigt werden.",
+ "settingsCollectionBrowsingQuickActionEditorBanner": "Die Taste gedrückt halten, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen beim Durchsuchen von Elementen angezeigt werden.",
+ "settingsCollectionSelectionQuickActionEditorBanner": "Die Taste gedrückt halten, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen beim Durchsuchen von Elementen angezeigt werden.",
"settingsSectionViewer": "Anzeige",
"settingsViewerUseCutout": "Ausgeschnittenen Bereich verwenden",
@@ -398,7 +406,7 @@
"settingsViewerQuickActionsTile": "Schnelle Aktionen",
"settingsViewerQuickActionEditorTitle": "Schnelle Aktionen",
- "settingsViewerQuickActionEditorBanner": "Halten Sie die Taste gedrückt, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen im Viewer angezeigt werden sollen.",
+ "settingsViewerQuickActionEditorBanner": "Die Taste gedrückt halten, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen im Viewer angezeigt werden sollen.",
"settingsViewerQuickActionEditorDisplayedButtons": "Angezeigte Schaltflächen",
"settingsViewerQuickActionEditorAvailableButtons": "Verfügbare Schaltflächen",
"settingsViewerQuickActionEmpty": "Keine Tasten",
@@ -444,7 +452,7 @@
"settingsSaveSearchHistory": "Suchverlauf speichern",
"settingsHiddenItemsTile": "Versteckte Elemente",
- "settingsHiddenItemsTitle": "Versteckte Gegenstände",
+ "settingsHiddenItemsTitle": "Versteckte Elemente",
"settingsHiddenFiltersTitle": "Versteckte Filter",
"settingsHiddenFiltersBanner": "Fotos und Videos, die versteckten Filtern entsprechen, werden nicht in Ihrer Sammlung angezeigt.",
@@ -456,7 +464,7 @@
"settingsStorageAccessTile": "Speicherzugriff",
"settingsStorageAccessTitle": "Speicherzugriff",
- "settingsStorageAccessBanner": "Einige Verzeichnisse erfordern eine explizite Zugriffsberechtigung, um Dateien darin zu ändern. Sie können hier Verzeichnisse überprüfen, auf die Sie zuvor Zugriff gewährt haben.",
+ "settingsStorageAccessBanner": "Einige Verzeichnisse erfordern eine explizite Zugriffsberechtigung, um Dateien darin zu ändern. Hier können Verzeichnisse überprüft werden, auf die zuvor Zugriff gewährt wurde.",
"settingsStorageAccessEmpty": "Keine Zugangsberechtigungen",
"settingsStorageAccessRevokeTooltip": "Widerrufen",
@@ -474,7 +482,7 @@
"settingsUnitSystemTitle": "Einheiten",
"statsPageTitle": "Statistiken",
- "statsWithGps": " {count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}",
+ "statsWithGps": "{count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}",
"statsTopCountries": "Top-Länder",
"statsTopPlaces": "Top-Plätze",
"statsTopTags": "Top-Tags",
@@ -509,7 +517,7 @@
"mapEmptyRegion": "Keine Bilder in dieser Region",
"viewerInfoOpenEmbeddedFailureFeedback": "Eingebettete Daten konnten nicht extrahiert werden",
- "viewerInfoOpenLinkText": "Öffnen Sie",
+ "viewerInfoOpenLinkText": "Öffnen",
"viewerInfoViewXmlLinkText": "Ansicht XML",
"viewerInfoSearchFieldLabel": "Metadaten suchen",
@@ -534,5 +542,5 @@
"filePickerDoNotShowHiddenFiles": "Versteckte Dateien nicht anzeigen",
"filePickerOpenFrom": "Öffnen von",
"filePickerNoItems": "Keine Elemente",
- "filePickerUseThisFolder": "Verwenden Sie diesen Ordner"
-}
+ "filePickerUseThisFolder": "Diesen Ordner verwenden"
+}
\ No newline at end of file
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index a4b3ac6a8..0407e89cb 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -22,6 +22,15 @@
"minutes": {}
}
},
+ "focalLength": "{length} mm",
+ "@focalLength": {
+ "placeholders": {
+ "length": {
+ "type": "String",
+ "example": "5.4"
+ }
+ }
+ },
"applyButtonLabel": "APPLY",
"deleteButtonLabel": "DELETE",
@@ -88,6 +97,7 @@
"videoActionSettings": "Settings",
"entryInfoActionEditDate": "Edit date & time",
+ "entryInfoActionEditLocation": "Edit location",
"entryInfoActionEditRating": "Edit rating",
"entryInfoActionEditTags": "Edit tags",
"entryInfoActionRemoveMetadata": "Remove metadata",
@@ -291,6 +301,8 @@
},
"exportEntryDialogFormat": "Format:",
+ "exportEntryDialogWidth": "Width",
+ "exportEntryDialogHeight": "Height",
"renameEntryDialogLabel": "New name",
@@ -304,6 +316,13 @@
"editEntryDateDialogHours": "Hours",
"editEntryDateDialogMinutes": "Minutes",
+ "editEntryLocationDialogTitle": "Location",
+ "editEntryLocationDialogChooseOnMapTooltip": "Choose on map",
+ "editEntryLocationDialogLatitude": "Latitude",
+ "editEntryLocationDialogLongitude": "Longitude",
+
+ "locationPickerUseThisLocationButton": "Use this location",
+
"editEntryRatingDialogTitle": "Rating",
"removeEntryMetadataDialogTitle": "Metadata Removal",
@@ -342,13 +361,6 @@
"aboutLinkLicense": "License",
"aboutLinkPolicy": "Privacy Policy",
- "aboutUpdate": "New Version Available",
- "aboutUpdateLinks1": "A new version of Aves is available on",
- "aboutUpdateLinks2": "and",
- "aboutUpdateLinks3": ".",
- "aboutUpdateGitHub": "GitHub",
- "aboutUpdateGooglePlay": "Google Play",
-
"aboutBug": "Bug Report",
"aboutBugSaveLogInstruction": "Save app logs to a file",
"aboutBugSaveLogButton": "Save",
@@ -530,6 +542,10 @@
"settingsActionExport": "Export",
"settingsActionImport": "Import",
+ "appExportCovers": "Covers",
+ "appExportFavourites": "Favourites",
+ "appExportSettings": "Settings",
+
"settingsSectionNavigation": "Navigation",
"settingsHome": "Home",
"settingsKeepScreenOnTile": "Keep screen on",
@@ -708,6 +724,5 @@
"filePickerDoNotShowHiddenFiles": "Don’t show hidden files",
"filePickerOpenFrom": "Open from",
"filePickerNoItems": "No items",
- "filePickerUseThisFolder": "Use this folder",
- "@filePickerUseThisFolder": {}
+ "filePickerUseThisFolder": "Use this folder"
}
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index f23a0af12..232abb8d3 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -4,10 +4,11 @@
"welcomeOptional": "Opcional",
"welcomeTermsToggle": "Acepto los términos y condiciones",
"itemCount": "{count, plural, =1{1 elemento} other{{count} elementos}}",
-
+
"timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}",
"timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}",
-
+ "focalLength": "{length} mm",
+
"applyButtonLabel": "APLICAR",
"deleteButtonLabel": "BORRAR",
"nextButtonLabel": "SIGUIENTE",
@@ -40,7 +41,7 @@
"chipActionPin": "Fijar",
"chipActionUnpin": "Dejar de fijar",
"chipActionRename": "Renombrar",
- "chipActionSetCover": "Elegir portada",
+ "chipActionSetCover": "Elegir carátula",
"chipActionCreateAlbum": "Crear álbum",
"entryActionCopyToClipboard": "Copiar al portapapeles",
@@ -110,8 +111,8 @@
"mapStyleGoogleHybrid": "Mapas de Google (Híbrido)",
"mapStyleGoogleTerrain": "Mapas de Google (Superficie)",
"mapStyleOsmHot": "OSM Humanitario",
- "mapStyleStamenToner": "Stamen Monocromático (Toner)",
- "mapStyleStamenWatercolor": "Stamen Acuarela (Watercolor)",
+ "mapStyleStamenToner": "Stamen Toner (Monocromático)",
+ "mapStyleStamenWatercolor": "Stamen Watercolor (Acuarela)",
"nameConflictStrategyRename": "Renombrar",
"nameConflictStrategyReplace": "Reemplazar",
@@ -140,13 +141,13 @@
"restrictedAccessDialogMessage": "Esta aplicación no tiene permiso para modificar archivos de {directory} en «{volume}».\n\nPor favor use un gestor de archivos o la aplicación de galería preinstalada para mover los elementos a otro directorio.",
"notEnoughSpaceDialogTitle": "Espacio insuficiente",
"notEnoughSpaceDialogMessage": "Esta operación necesita {neededSize} de espacio libre en «{volume}» para completarse, pero sólo hay {freeSize} disponible.",
-
+
"missingSystemFilePickerDialogTitle": "Selector de archivos del sistema no disponible",
"missingSystemFilePickerDialogMessage": "El selector de archivos del sistema no se encuentra disponible o fue deshabilitado. Por favor habilítelo e intente nuevamente.",
"unsupportedTypeDialogTitle": "Tipos de archivo incompatibles",
"unsupportedTypeDialogMessage": "{count, plural, =1{Esta operación no está disponible para un elemento del siguiente tipo: {types}.} other{Esta operación no está disponible para elementos de los siguientes tipos: {types}.}}",
-
+
"nameConflictDialogSingleSourceMessage": "Algunos archivos en el directorio de destino tienen el mismo nombre.",
"nameConflictDialogMultipleSourceMessage": "Algunos archivos tienen el mismo nombre.",
@@ -231,13 +232,6 @@
"aboutLinkLicense": "Licencia",
"aboutLinkPolicy": "Política de privacidad",
- "aboutUpdate": "Nueva versión disponible",
- "aboutUpdateLinks1": "Una nueva versión de Aves se encuentra disponible en",
- "aboutUpdateLinks2": "y",
- "aboutUpdateLinks3": ".",
- "aboutUpdateGitHub": "GitHub",
- "aboutUpdateGooglePlay": "Google Play",
-
"aboutBug": "Reporte de errores",
"aboutBugSaveLogInstruction": "Guardar registros de la aplicación a un archivo",
"aboutBugSaveLogButton": "Guardar",
@@ -251,7 +245,7 @@
"aboutCreditsWorldAtlas2": "bajo licencia ISC.",
"aboutCreditsTranslators": "Traductores:",
"aboutCreditsTranslatorLine": "{language}: {names}",
-
+
"aboutLicenses": "Licencias de código abierto",
"aboutLicensesBanner": "Esta aplicación usa los siguientes paquetes y librerías de código abierto.",
"aboutLicensesAndroidLibraries": "Librerías de Android",
@@ -362,6 +356,10 @@
"settingsActionExport": "Exportar",
"settingsActionImport": "Importar",
+ "appExportCovers": "Carátulas",
+ "appExportFavourites": "Favoritos",
+ "appExportSettings": "Ajustes",
+
"settingsSectionNavigation": "Navegación",
"settingsHome": "Inicio",
"settingsKeepScreenOnTile": "Mantener pantalla encendida",
@@ -463,9 +461,9 @@
"settingsSectionAccessibility": "Accesibilidad",
"settingsRemoveAnimationsTile": "Remover animaciones",
- "settingsRemoveAnimationsTitle": "Remove animaciones",
- "settingsTimeToTakeActionTile": "Hora de entrar en acción",
- "settingsTimeToTakeActionTitle": "Hora de entrar en acción",
+ "settingsRemoveAnimationsTitle": "Remover animaciones",
+ "settingsTimeToTakeActionTile": "Retraso para ejecutar una acción",
+ "settingsTimeToTakeActionTitle": "Retraso para ejecutar una acción",
"settingsSectionLanguage": "Idioma y formatos",
"settingsLanguage": "Idioma",
@@ -535,6 +533,5 @@
"filePickerDoNotShowHiddenFiles": "No mostrar archivos ocultos",
"filePickerOpenFrom": "Abrir desde",
"filePickerNoItems": "Sin elementos",
- "filePickerUseThisFolder": "Usar esta carpeta",
- "@filePickerUseThisFolder": {}
+ "filePickerUseThisFolder": "Usar esta carpeta"
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 701a808e8..115812126 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, =1{1 seconde} other{{seconds} secondes}}",
"timeMinutes": "{minutes, plural, =1{1 minute} other{{minutes} minutes}}",
+ "focalLength": "{length} mm",
"applyButtonLabel": "ENREGISTRER",
"deleteButtonLabel": "SUPPRIMER",
@@ -73,6 +74,7 @@
"videoActionSettings": "Préférences",
"entryInfoActionEditDate": "Modifier la date",
+ "entryInfoActionEditLocation": "Modifier le lieu",
"entryInfoActionEditRating": "Modifier la notation",
"entryInfoActionEditTags": "Modifier les libellés",
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
@@ -110,8 +112,8 @@
"mapStyleGoogleHybrid": "Google Maps (Satellite)",
"mapStyleGoogleTerrain": "Google Maps (Relief)",
"mapStyleOsmHot": "OSM Humanitaire",
- "mapStyleStamenToner": "Stamen Toner",
- "mapStyleStamenWatercolor": "Stamen Watercolor",
+ "mapStyleStamenToner": "Stamen Toner (Monochrome)",
+ "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarelle)",
"nameConflictStrategyRename": "Renommer",
"nameConflictStrategyReplace": "Remplacer",
@@ -179,6 +181,8 @@
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Voulez-vous vraiment supprimer ces albums et leur élément ?} other{Voulez-vous vraiment supprimer ces albums et leurs {count} éléments ?}}",
"exportEntryDialogFormat": "Format :",
+ "exportEntryDialogWidth": "Largeur",
+ "exportEntryDialogHeight": "Hauteur",
"renameEntryDialogLabel": "Nouveau nom",
@@ -192,6 +196,13 @@
"editEntryDateDialogHours": "Heures",
"editEntryDateDialogMinutes": "Minutes",
+ "editEntryLocationDialogTitle": "Lieu",
+ "editEntryLocationDialogChooseOnMapTooltip": "Sélectionner sur la carte",
+ "editEntryLocationDialogLatitude": "Latitude",
+ "editEntryLocationDialogLongitude": "Longitude",
+
+ "locationPickerUseThisLocationButton": "Utiliser ce lieu",
+
"editEntryRatingDialogTitle": "Notation",
"removeEntryMetadataDialogTitle": "Retrait de métadonnées",
@@ -230,13 +241,6 @@
"aboutLinkLicense": "Licence",
"aboutLinkPolicy": "Politique de confidentialité",
- "aboutUpdate": "Nouvelle Version",
- "aboutUpdateLinks1": "Une nouvelle version d’Aves est disponible sur",
- "aboutUpdateLinks2": "et",
- "aboutUpdateLinks3": ".",
- "aboutUpdateGitHub": "GitHub",
- "aboutUpdateGooglePlay": "Google Play",
-
"aboutBug": "Rapports d’erreur",
"aboutBugSaveLogInstruction": "Sauvegarder les logs de l’app vers un fichier",
"aboutBugSaveLogButton": "Sauvegarder",
@@ -361,6 +365,10 @@
"settingsActionExport": "Exporter",
"settingsActionImport": "Importer",
+ "appExportCovers": "Couvertures",
+ "appExportFavourites": "Favoris",
+ "appExportSettings": "Réglages",
+
"settingsSectionNavigation": "Navigation",
"settingsHome": "Page d’accueil",
"settingsKeepScreenOnTile": "Maintenir l’écran allumé",
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index ac2d83ce4..2edf0589b 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, other{{seconds}초}}",
"timeMinutes": "{minutes, plural, other{{minutes}분}}",
+ "focalLength": "{length} mm",
"applyButtonLabel": "확인",
"deleteButtonLabel": "삭제",
@@ -73,6 +74,7 @@
"videoActionSettings": "설정",
"entryInfoActionEditDate": "날짜 및 시간 수정",
+ "entryInfoActionEditLocation": "위치 수정",
"entryInfoActionEditRating": "별점 수정",
"entryInfoActionEditTags": "태그 수정",
"entryInfoActionRemoveMetadata": "메타데이터 삭제",
@@ -110,8 +112,8 @@
"mapStyleGoogleHybrid": "구글 지도 (위성)",
"mapStyleGoogleTerrain": "구글 지도 (지형)",
"mapStyleOsmHot": "Humanitarian OSM",
- "mapStyleStamenToner": "Stamen 토너",
- "mapStyleStamenWatercolor": "Stamen 수채화",
+ "mapStyleStamenToner": "Stamen Toner (토너)",
+ "mapStyleStamenWatercolor": "Stamen Watercolor (수채화)",
"nameConflictStrategyRename": "이름 변경",
"nameConflictStrategyReplace": "대체",
@@ -179,6 +181,8 @@
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
"exportEntryDialogFormat": "형식:",
+ "exportEntryDialogWidth": "가로",
+ "exportEntryDialogHeight": "세로",
"renameEntryDialogLabel": "이름",
@@ -192,6 +196,13 @@
"editEntryDateDialogHours": "시간",
"editEntryDateDialogMinutes": "분",
+ "editEntryLocationDialogTitle": "위치",
+ "editEntryLocationDialogChooseOnMapTooltip": "지도에서 선택",
+ "editEntryLocationDialogLatitude": "위도",
+ "editEntryLocationDialogLongitude": "경도",
+
+ "locationPickerUseThisLocationButton": "이 위치 사용",
+
"editEntryRatingDialogTitle": "별점",
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
@@ -230,13 +241,6 @@
"aboutLinkLicense": "라이선스",
"aboutLinkPolicy": "개인정보 보호정책",
- "aboutUpdate": "업데이트 사용 가능",
- "aboutUpdateLinks1": "앱의 최신 버전을",
- "aboutUpdateLinks2": "와",
- "aboutUpdateLinks3": "에서 다운로드 사용 가능합니다.",
- "aboutUpdateGitHub": "깃허브",
- "aboutUpdateGooglePlay": "구글 플레이",
-
"aboutBug": "버그 보고",
"aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기",
"aboutBugSaveLogButton": "저장",
@@ -361,6 +365,10 @@
"settingsActionExport": "내보내기",
"settingsActionImport": "가져오기",
+ "appExportCovers": "대표 이미지",
+ "appExportFavourites": "즐겨찾기",
+ "appExportSettings": "설정",
+
"settingsSectionNavigation": "탐색",
"settingsHome": "홈",
"settingsKeepScreenOnTile": "화면 자동 꺼짐 방지",
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
new file mode 100644
index 000000000..b4dfef910
--- /dev/null
+++ b/lib/l10n/app_pt.arb
@@ -0,0 +1,546 @@
+{
+ "appName": "Aves",
+ "welcomeMessage": "Bem-vindo ao Aves",
+ "welcomeOptional": "Opcional",
+ "welcomeTermsToggle": "Eu concordo com os Termos e Condições",
+ "itemCount": "{count, plural, =1{1 item} other{{count} itens}}",
+
+ "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}",
+ "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}",
+ "focalLength": "{length} mm",
+
+ "applyButtonLabel": "APLIQUE",
+ "deleteButtonLabel": "EXCLUIR",
+ "nextButtonLabel": "PROXIMO",
+ "showButtonLabel": "MOSTRAR",
+ "hideButtonLabel": "OCULTAR",
+ "continueButtonLabel": "CONTINUAR",
+
+ "cancelTooltip": "Cancela",
+ "changeTooltip": "Mudar",
+ "clearTooltip": "Claro",
+ "previousTooltip": "Anterior",
+ "nextTooltip": "Proximo",
+ "showTooltip": "Mostrar",
+ "hideTooltip": "Ocultar",
+ "actionRemove": "Remover",
+ "resetButtonTooltip": "Resetar",
+
+ "doubleBackExitMessage": "Toque em “voltar” novamente para sair.",
+
+ "sourceStateLoading": "Carregando",
+ "sourceStateCataloguing": "Catalogação",
+ "sourceStateLocatingCountries": "Localizando países",
+ "sourceStateLocatingPlaces": "Localizando lugares",
+
+ "chipActionDelete": "Deletar",
+ "chipActionGoToAlbumPage": "Mostrar nos Álbuns",
+ "chipActionGoToCountryPage": "Mostrar em Países",
+ "chipActionGoToTagPage": "Mostrar em Etiquetas",
+ "chipActionHide": "Ocultar",
+ "chipActionPin": "Fixar no topo",
+ "chipActionUnpin": "Desafixar do topo",
+ "chipActionRename": "Renomear",
+ "chipActionSetCover": "Definir capa",
+ "chipActionCreateAlbum": "Criar álbum",
+
+ "entryActionCopyToClipboard": "Copiar para área de transferência",
+ "entryActionDelete": "Excluir",
+ "entryActionExport": "Exportar",
+ "entryActionInfo": "Informações",
+ "entryActionRename": "Renomear",
+ "entryActionRotateCCW": "Rotacionar para esquerda",
+ "entryActionRotateCW": "Rotacionar para direita",
+ "entryActionFlip": "Virar horizontalmente",
+ "entryActionPrint": "Imprimir",
+ "entryActionShare": "Compartilhado",
+ "entryActionViewSource": "Ver fonte",
+ "entryActionViewMotionPhotoVideo": "Abrir foto em movimento",
+ "entryActionEdit": "Editar com…",
+ "entryActionOpen": "Abrir com…",
+ "entryActionSetAs": "Definir como…",
+ "entryActionOpenMap": "Mostrar no aplicativo de mapa…",
+ "entryActionRotateScreen": "Girar a tela",
+ "entryActionAddFavourite": "Adicionar aos favoritos",
+ "entryActionRemoveFavourite": "Remova dos favoritos",
+
+ "videoActionCaptureFrame": "Capturar quadro",
+ "videoActionPause": "Pausa",
+ "videoActionPlay": "Toque",
+ "videoActionReplay10": "Retroceda 10 segundos",
+ "videoActionSkip10": "Avançar 10 segundos",
+ "videoActionSelectStreams": "Selecione as faixas",
+ "videoActionSetSpeed": "Velocidade de reprodução",
+ "videoActionSettings": "Configurações",
+
+ "entryInfoActionEditDate": "Editar data e hora",
+ "entryInfoActionEditLocation": "Editar localização",
+ "entryInfoActionEditRating": "Editar classificação",
+ "entryInfoActionEditTags": "Editar etiquetas",
+ "entryInfoActionRemoveMetadata": "Remover metadados",
+
+ "filterFavouriteLabel": "Favorito",
+ "filterLocationEmptyLabel": "Não localizado",
+ "filterTagEmptyLabel": "Sem etiqueta",
+ "filterRatingUnratedLabel": "Sem classificação",
+ "filterRatingRejectedLabel": "Rejeitado",
+ "filterTypeAnimatedLabel": "Animado",
+ "filterTypeMotionPhotoLabel": "Foto em movimento",
+ "filterTypePanoramaLabel": "Panorama",
+ "filterTypeRawLabel": "Raw",
+ "filterTypeSphericalVideoLabel": "360° vídeo",
+ "filterTypeGeotiffLabel": "GeoTIFF",
+ "filterMimeImageLabel": "Imagem",
+ "filterMimeVideoLabel": "Vídeo",
+
+ "coordinateFormatDms": "DMS",
+ "coordinateFormatDecimal": "Graus decimais",
+ "coordinateDms": "{coordinate} {direction}",
+ "coordinateDmsNorth": "N",
+ "coordinateDmsSouth": "S",
+ "coordinateDmsEast": "L",
+ "coordinateDmsWest": "O",
+
+ "unitSystemMetric": "Métrica",
+ "unitSystemImperial": "Imperial",
+
+ "videoLoopModeNever": "Nunca",
+ "videoLoopModeShortOnly": "Apenas vídeos curtos",
+ "videoLoopModeAlways": "Sempre",
+
+ "mapStyleGoogleNormal": "Google Maps",
+ "mapStyleGoogleHybrid": "Google Maps (Híbrido)",
+ "mapStyleGoogleTerrain": "Google Maps (Terreno)",
+ "mapStyleOsmHot": "OSM Humanitário",
+ "mapStyleStamenToner": "Stamen Toner (Monocromático)",
+ "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarela)",
+
+ "nameConflictStrategyRename": "Renomear",
+ "nameConflictStrategyReplace": "Substituir",
+ "nameConflictStrategySkip": "Pular",
+
+ "keepScreenOnNever": "Nunca",
+ "keepScreenOnViewerOnly": "Somente página do visualizador",
+ "keepScreenOnAlways": "Sempre",
+
+ "accessibilityAnimationsRemove": "Prevenir efeitos de tela",
+ "accessibilityAnimationsKeep": "Manter efeitos de tela",
+
+ "albumTierNew": "Novo",
+ "albumTierPinned": "Fixada",
+ "albumTierSpecial": "Comum",
+ "albumTierApps": "Aplicativos",
+ "albumTierRegular": "Outras",
+
+ "storageVolumeDescriptionFallbackPrimary": "Armazenamento interno",
+ "storageVolumeDescriptionFallbackNonPrimary": "cartão SD",
+ "rootDirectoryDescription": "diretório raiz",
+ "otherDirectoryDescription": "diretório “{name}”",
+ "storageAccessDialogTitle": "Acesso de armazenamento",
+ "storageAccessDialogMessage": "Selecione o {directory} de “{volume}” na próxima tela para dar acesso a este aplicativo.",
+ "restrictedAccessDialogTitle": "Acesso restrito",
+ "restrictedAccessDialogMessage": "Este aplicativo não tem permissão para modificar arquivos no {directory} de “{volume}”.\n\nUse um gerenciador de arquivos ou aplicativo de galeria pré-instalado para mover os itens para outro diretório.",
+ "notEnoughSpaceDialogTitle": "Espaço insuficiente",
+ "notEnoughSpaceDialogMessage": "Esta operação precisa {neededSize} de espaço livre em “{volume}” para completar, mas só {freeSize} restantes.",
+ "missingSystemFilePickerDialogTitle": "Seletor de arquivos do sistema ausente",
+ "missingSystemFilePickerDialogMessage": "O seletor de arquivos do sistema está ausente ou desabilitado. Por favor, habilite e tente novamente.",
+
+ "unsupportedTypeDialogTitle": "Tipos não suportados",
+ "unsupportedTypeDialogMessage": "{count, plural, =1{Esta operação não é suportada para itens do seguinte tipo: {types}.} other{Esta operação não é suportada para itens dos seguintes tipos: {types}.}}",
+
+ "nameConflictDialogSingleSourceMessage": "Alguns arquivos na pasta de destino têm o mesmo nome.",
+ "nameConflictDialogMultipleSourceMessage": "Alguns arquivos têm o mesmo nome.",
+
+ "addShortcutDialogLabel": "Rótulo de atalho",
+ "addShortcutButtonLabel": "ADICIONAR",
+
+ "noMatchingAppDialogTitle": "Nenhum aplicativo correspondente",
+ "noMatchingAppDialogMessage": "Não há aplicativos que possam lidar com isso.",
+
+ "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Tem certeza de que deseja excluir este item?} other{Tem certeza de que deseja excluir estes {count} itens?}}",
+
+ "videoResumeDialogMessage": "Deseja continuar jogando em {time}?",
+ "videoStartOverButtonLabel": "RECOMEÇAR",
+ "videoResumeButtonLabel": "RETOMAR",
+
+ "setCoverDialogTitle": "Definir capa",
+ "setCoverDialogLatest": "Último item",
+ "setCoverDialogCustom": "Personalizado",
+
+ "hideFilterConfirmationDialogMessage": "Fotos e vídeos correspondentes serão ocultados da sua coleção. Você pode mostrá-los novamente nas configurações de “Privacidade”.\n\nTem certeza de que deseja ocultá-los?",
+
+ "newAlbumDialogTitle": "Novo álbum",
+ "newAlbumDialogNameLabel": "Nome do álbum",
+ "newAlbumDialogNameLabelAlreadyExistsHelper": "O diretório já existe",
+ "newAlbumDialogStorageLabel": "Armazenar:",
+
+ "renameAlbumDialogLabel": "Novo nome",
+ "renameAlbumDialogLabelAlreadyExistsHelper": "O diretório já existe",
+
+ "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Tem certeza de que deseja excluir este álbum e seu item?} other{Tem certeza de que deseja excluir este álbum e seus {count} itens?}}",
+ "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Tem certeza de que deseja excluir estes álbuns e seus itens?} other{Tem certeza de que deseja excluir estes álbuns e seus {count} itens?}}",
+
+ "exportEntryDialogFormat": "Formato:",
+ "exportEntryDialogWidth": "Largura",
+ "exportEntryDialogHeight": "Altura",
+
+ "renameEntryDialogLabel": "Novo nome",
+
+ "editEntryDateDialogTitle": "Data e hora",
+ "editEntryDateDialogSetCustom": "Definir data personalizada",
+ "editEntryDateDialogCopyField": "Copiar de outra data",
+ "editEntryDateDialogExtractFromTitle": "Extrair do título",
+ "editEntryDateDialogShift": "Mudança",
+ "editEntryDateDialogSourceFileModifiedDate": "Data de modificação do arquivo",
+ "editEntryDateDialogTargetFieldsHeader": "Campos para modificar",
+ "editEntryDateDialogHours": "Horas",
+ "editEntryDateDialogMinutes": "Minutos",
+
+ "editEntryLocationDialogTitle": "Localização",
+ "editEntryLocationDialogChooseOnMapTooltip": "Escolha no mapa",
+ "editEntryLocationDialogLatitude": "Latitude",
+ "editEntryLocationDialogLongitude": "Longitude",
+
+ "locationPickerUseThisLocationButton": "Usar essa localização",
+
+ "editEntryRatingDialogTitle": "Avaliação",
+
+ "removeEntryMetadataDialogTitle": "Remoção de metadados",
+ "removeEntryMetadataDialogMore": "Mais",
+
+ "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP é necessário para reproduzir o vídeo dentro de uma foto em movimento.\n\nTem certeza de que deseja removê-lo?",
+
+ "videoSpeedDialogLabel": "Velocidade de reprodução",
+
+ "videoStreamSelectionDialogVideo": "Video",
+ "videoStreamSelectionDialogAudio": "Áudio",
+ "videoStreamSelectionDialogText": "Legendas",
+ "videoStreamSelectionDialogOff": "Fora",
+ "videoStreamSelectionDialogTrack": "Acompanhar",
+ "videoStreamSelectionDialogNoSelection": "Não há outras faixas.",
+
+ "genericSuccessFeedback": "Feito!",
+ "genericFailureFeedback": "Falhou",
+
+ "menuActionConfigureView": "Visualizar",
+ "menuActionSelect": "Selecionar",
+ "menuActionSelectAll": "Selecionar tudo",
+ "menuActionSelectNone": "Selecione nenhum",
+ "menuActionMap": "Mapa",
+ "menuActionStats": "Estatísticas",
+
+ "viewDialogTabSort": "Organizar",
+ "viewDialogTabGroup": "Grupo",
+ "viewDialogTabLayout": "Layout",
+
+ "tileLayoutGrid": "Grid",
+ "tileLayoutList": "Lista",
+
+ "aboutPageTitle": "Sobre",
+ "aboutLinkSources": "Fontes",
+ "aboutLinkLicense": "Licença",
+ "aboutLinkPolicy": "Política de Privacidade",
+
+ "aboutBug": "Relatório de erro",
+ "aboutBugSaveLogInstruction": "Salvar registros de aplicativos em um arquivo",
+ "aboutBugSaveLogButton": "Salve",
+ "aboutBugCopyInfoInstruction": "Copiar informações do sistema",
+ "aboutBugCopyInfoButton": "Copiar",
+ "aboutBugReportInstruction": "Relatório no GitHub com os logs e informações do sistema",
+ "aboutBugReportButton": "Relatório",
+
+ "aboutCredits": "Créditos",
+ "aboutCreditsWorldAtlas1": "Este aplicativo usa um arquivo de TopoJSON",
+ "aboutCreditsWorldAtlas2": "sob licença ISC.",
+ "aboutCreditsTranslators": "Tradutores:",
+ "aboutCreditsTranslatorLine": "{language}: {names}",
+
+ "aboutLicenses": "Licenças de código aberto",
+ "aboutLicensesBanner": "Este aplicativo usa os seguintes pacotes e bibliotecas de código aberto.",
+ "aboutLicensesAndroidLibraries": "Bibliotecas Android",
+ "aboutLicensesFlutterPlugins": "Plug-ins Flutter",
+ "aboutLicensesFlutterPackages": "Pacotes Flutter",
+ "aboutLicensesDartPackages": "Pacotes Dart",
+ "aboutLicensesShowAllButtonLabel": "Mostrar todas as licenças",
+
+ "policyPageTitle": "Política de Privacidade",
+
+ "collectionPageTitle": "Coleção",
+ "collectionPickPageTitle": "Escolher",
+ "collectionSelectionPageTitle": "{count, plural, =0{Selecionar itens} =1{1 item} other{{count} itens}}",
+
+ "collectionActionShowTitleSearch": "Mostrar filtro de título",
+ "collectionActionHideTitleSearch": "Ocultar filtro de título",
+ "collectionActionAddShortcut": "Adicionar atalho",
+ "collectionActionCopy": "Copiar para o álbum",
+ "collectionActionMove": "Mover para o álbum",
+ "collectionActionRescan": "Reexaminar",
+ "collectionActionEdit": "Editar",
+
+ "collectionSearchTitlesHintText": "Pesquisar títulos",
+
+ "collectionSortDate": "Por data",
+ "collectionSortSize": "Por tamanho",
+ "collectionSortName": "Por álbum e nome de arquivo",
+ "collectionSortRating": "Por classificação",
+
+ "collectionGroupAlbum": "Por álbum",
+ "collectionGroupMonth": "Por mês",
+ "collectionGroupDay": "Por dia",
+ "collectionGroupNone": "Não agrupe",
+
+ "sectionUnknown": "Desconhecido",
+ "dateToday": "Hoje",
+ "dateYesterday": "Ontem",
+ "dateThisMonth": "Este mês",
+ "collectionDeleteFailureFeedback": "{count, plural, =1{Falha ao excluir 1 item} other{Falha ao excluir {count} itens}}",
+ "collectionCopyFailureFeedback": "{count, plural, =1{Falha ao copiar 1 item} other{Falha ao copiar {count} itens}}",
+ "collectionMoveFailureFeedback": "{count, plural, =1{Falha ao mover 1 item} other{Falha ao mover {count} itens}}",
+ "collectionEditFailureFeedback": "{count, plural, =1{Falha ao editar 1 item} other{Falha ao editar {count} itens}}",
+ "collectionExportFailureFeedback": "{count, plural, =1{Falha ao exportar 1 página} other{Falha ao exportar {count} páginas}}",
+ "collectionCopySuccessFeedback": "{count, plural, =1{1 item copiado} other{Copiado {count} itens}}",
+ "collectionMoveSuccessFeedback": "{count, plural, =1{1 item movido} other{Mudou-se {count} itens}}",
+ "collectionEditSuccessFeedback": "{count, plural, =1{Editado 1 item} other{Editado {count} itens}}",
+
+ "collectionEmptyFavourites": "Nenhum favorito",
+ "collectionEmptyVideos": "Nenhum video",
+ "collectionEmptyImages": "Nenhuma image",
+
+ "collectionSelectSectionTooltip": "Selecionar seção",
+ "collectionDeselectSectionTooltip": "Desmarcar seção",
+
+ "drawerCollectionAll": "Toda a coleção",
+ "drawerCollectionFavourites": "Favoritos",
+ "drawerCollectionImages": "Imagens",
+ "drawerCollectionVideos": "Vídeos",
+ "drawerCollectionAnimated": "Animado",
+ "drawerCollectionMotionPhotos": "Fotos em movimento",
+ "drawerCollectionPanoramas": "Panoramas",
+ "drawerCollectionRaws": "Fotos Raw",
+ "drawerCollectionSphericalVideos": "360° Videos",
+
+ "chipSortDate": "Por data",
+ "chipSortName": "Por nome",
+ "chipSortCount": "Por contagem de itens",
+
+ "albumGroupTier": "Por nível",
+ "albumGroupVolume": "Por volume de armazenamento",
+ "albumGroupNone": "Não agrupe",
+
+ "albumPickPageTitleCopy": "Copiar para o álbum",
+ "albumPickPageTitleExport": "Exportar para o álbum",
+ "albumPickPageTitleMove": "Mover para o álbum",
+ "albumPickPageTitlePick": "Escolher álbum",
+
+ "albumCamera": "Câmera",
+ "albumDownload": "Download",
+ "albumScreenshots": "Capturas de tela",
+ "albumScreenRecordings": "Gravações de tela",
+ "albumVideoCaptures": "Capturas de vídeo",
+
+ "albumPageTitle": "Álbuns",
+ "albumEmpty": "Nenhum álbum",
+ "createAlbumTooltip": "Criar álbum",
+ "createAlbumButtonLabel": "CRIA",
+ "newFilterBanner": "novo",
+
+ "countryPageTitle": "Países",
+ "countryEmpty": "Nenhum país",
+
+ "tagPageTitle": "Etiquetas",
+ "tagEmpty": "Sem etiquetas",
+
+ "searchCollectionFieldHint": "Pesquisar coleção",
+ "searchSectionRecent": "Recente",
+ "searchSectionAlbums": "Álbuns",
+ "searchSectionCountries": "Países",
+ "searchSectionPlaces": "Locais",
+ "searchSectionTags": "Etiquetas",
+ "searchSectionRating": "Classificações",
+
+ "settingsPageTitle": "Configurações",
+ "settingsSystemDefault": "Sistema",
+ "settingsDefault": "Padrão",
+
+ "settingsActionExport": "Exportar",
+ "settingsActionImport": "Importar",
+
+ "appExportCovers": "Capas",
+ "appExportFavourites": "Favoritos",
+ "appExportSettings": "Configurações",
+
+ "settingsSectionNavigation": "Navegação",
+ "settingsHome": "Início",
+ "settingsKeepScreenOnTile": "Manter a tela ligada",
+ "settingsKeepScreenOnTitle": "Manter a tela ligada",
+ "settingsDoubleBackExit": "Toque em “voltar” duas vezes para sair",
+
+ "settingsNavigationDrawerTile": "Menu de navegação",
+ "settingsNavigationDrawerEditorTitle": "Menu de navegação",
+ "settingsNavigationDrawerBanner": "Toque e segure para mover e reordenar os itens do menu.",
+ "settingsNavigationDrawerTabTypes": "Tipos",
+ "settingsNavigationDrawerTabAlbums": "Álbuns",
+ "settingsNavigationDrawerTabPages": "Páginas",
+ "settingsNavigationDrawerAddAlbum": "Adicionar álbum",
+
+ "settingsSectionThumbnails": "Miniaturas",
+ "settingsThumbnailShowFavouriteIcon": "Mostrar ícone favorito",
+ "settingsThumbnailShowLocationIcon": "Mostrar ícone de localização",
+ "settingsThumbnailShowMotionPhotoIcon": "Mostrar ícone de foto em movimento",
+ "settingsThumbnailShowRating": "Mostrar classificação",
+ "settingsThumbnailShowRawIcon": "Mostrar ícone raw",
+ "settingsThumbnailShowVideoDuration": "Mostrar duração do vídeo",
+
+ "settingsCollectionQuickActionsTile": "Ações rápidas",
+ "settingsCollectionQuickActionEditorTitle": "Ações rápidas",
+ "settingsCollectionQuickActionTabBrowsing": "Navegando",
+ "settingsCollectionQuickActionTabSelecting": "Selecionando",
+ "settingsCollectionBrowsingQuickActionEditorBanner": "Toque e segure para mover os botões e selecionar quais ações são exibidas ao navegar pelos itens.",
+ "settingsCollectionSelectionQuickActionEditorBanner": "Toque e segure para mover os botões e selecionar quais ações são exibidas ao selecionar itens.",
+
+ "settingsSectionViewer": "Visualizador",
+ "settingsViewerUseCutout": "Usar área de recorte",
+ "settingsViewerMaximumBrightness": "Brilho máximo",
+ "settingsMotionPhotoAutoPlay": "Reprodução automática de fotos em movimento",
+ "settingsImageBackground": "Plano de fundo da imagem",
+
+ "settingsViewerQuickActionsTile": "Ações rápidas",
+ "settingsViewerQuickActionEditorTitle": "Ações rápidas",
+ "settingsViewerQuickActionEditorBanner": "Toque e segure para mover os botões e selecionar quais ações são exibidas no visualizador.",
+ "settingsViewerQuickActionEditorDisplayedButtons": "Botões exibidos",
+ "settingsViewerQuickActionEditorAvailableButtons": "Botões disponíveis",
+ "settingsViewerQuickActionEmpty": "Sem botões",
+
+ "settingsViewerOverlayTile": "Sobreposição",
+ "settingsViewerOverlayTitle": "Sobreposição",
+ "settingsViewerShowOverlayOnOpening": "Mostrar na abertura",
+ "settingsViewerShowMinimap": "Mostrar minimapa",
+ "settingsViewerShowInformation": "Mostrar informações",
+ "settingsViewerShowInformationSubtitle": "Mostrar título, data, local, etc.",
+ "settingsViewerShowShootingDetails": "Mostrar detalhes de disparo",
+ "settingsViewerEnableOverlayBlurEffect": "Efeito de desfoque",
+
+ "settingsVideoPageTitle": "Configurações de vídeo",
+ "settingsSectionVideo": "Vídeo",
+ "settingsVideoShowVideos": "Mostrar vídeos",
+ "settingsVideoEnableHardwareAcceleration": "Aceleraçao do hardware",
+ "settingsVideoEnableAutoPlay": "Reprodução automática",
+ "settingsVideoLoopModeTile": "Modo de loop",
+ "settingsVideoLoopModeTitle": "Modo de loop",
+ "settingsVideoQuickActionsTile": "Ações rápidas para vídeos",
+ "settingsVideoQuickActionEditorTitle": "Ações rápidas",
+
+ "settingsSubtitleThemeTile": "Legendas",
+ "settingsSubtitleThemeTitle": "Legendas",
+ "settingsSubtitleThemeSample": "Esta é uma amostra.",
+ "settingsSubtitleThemeTextAlignmentTile": "Alinhamento de texto",
+ "settingsSubtitleThemeTextAlignmentTitle": "Alinhamento de Texto",
+ "settingsSubtitleThemeTextSize": "Tamanho do texto",
+ "settingsSubtitleThemeShowOutline": "Mostrar contorno e sombra",
+ "settingsSubtitleThemeTextColor": "Cor do texto",
+ "settingsSubtitleThemeTextOpacity": "Opacidade do texto",
+ "settingsSubtitleThemeBackgroundColor": "Cor de fundo",
+ "settingsSubtitleThemeBackgroundOpacity": "Opacidade do plano de fundo",
+ "settingsSubtitleThemeTextAlignmentLeft": "Esquerda",
+ "settingsSubtitleThemeTextAlignmentCenter": "Centro",
+ "settingsSubtitleThemeTextAlignmentRight": "Direita",
+
+ "settingsSectionPrivacy": "Privacidade",
+ "settingsAllowInstalledAppAccess": "Permitir acesso ao inventário de aplicativos",
+ "settingsAllowInstalledAppAccessSubtitle": "Usado para melhorar a exibição do álbum",
+ "settingsAllowErrorReporting": "Permitir relatórios de erros anônimos",
+ "settingsSaveSearchHistory": "Salvar histórico de pesquisa",
+
+ "settingsHiddenItemsTile": "Itens ocultos",
+ "settingsHiddenItemsTitle": "Itens ocultos",
+
+ "settingsHiddenFiltersTitle": "Filtros ocultos",
+ "settingsHiddenFiltersBanner": "Fotos e vídeos que correspondem a filtros ocultos não aparecerão em sua coleção.",
+ "settingsHiddenFiltersEmpty": "Sem filtros ocultos",
+
+ "settingsHiddenPathsTitle": "Caminhos Ocultos",
+ "settingsHiddenPathsBanner": "Fotos e vídeos nessas pastas, ou em qualquer uma de suas subpastas, não aparecerão em sua coleção.",
+ "addPathTooltip": "Adicionar caminho",
+
+ "settingsStorageAccessTile": "Acesso ao armazenamento",
+ "settingsStorageAccessTitle": "Acesso ao armazenamento",
+ "settingsStorageAccessBanner": "Alguns diretórios exigem uma concessão de acesso explícito para modificar arquivos neles. Você pode revisar aqui os diretórios aos quais você deu acesso anteriormente.",
+ "settingsStorageAccessEmpty": "Sem concessões de acesso",
+ "settingsStorageAccessRevokeTooltip": "Revogar",
+
+ "settingsSectionAccessibility": "Acessibilidade",
+ "settingsRemoveAnimationsTile": "Remover animações",
+ "settingsRemoveAnimationsTitle": "Remover Animações",
+ "settingsTimeToTakeActionTile": "Tempo para executar uma ação",
+ "settingsTimeToTakeActionTitle": "Tempo para executar uma ação",
+
+ "settingsSectionLanguage": "Idioma e Formatos",
+ "settingsLanguage": "Língua",
+ "settingsCoordinateFormatTile": "Formato de coordenadas",
+ "settingsCoordinateFormatTitle": "Formato de coordenadas",
+ "settingsUnitSystemTile": "Unidades",
+ "settingsUnitSystemTitle": "Unidades",
+
+ "statsPageTitle": "Estatísticas",
+ "statsWithGps": "{count, plural, =1{1 item com localização} other{{count} itens com localização}}",
+ "statsTopCountries": "Principais Países",
+ "statsTopPlaces": "Principais Lugares",
+ "statsTopTags": "Principais Etiquetas",
+
+ "viewerOpenPanoramaButtonLabel": "ABRIR PANORAMA",
+ "viewerErrorUnknown": "Algo não está certo!",
+ "viewerErrorDoesNotExist": "O arquivo não existe mais.",
+
+ "viewerInfoPageTitle": "Informações",
+ "viewerInfoBackToViewerTooltip": "Voltar ao visualizador",
+
+ "viewerInfoUnknown": "desconhecido",
+ "viewerInfoLabelTitle": "Título",
+ "viewerInfoLabelDate": "Data",
+ "viewerInfoLabelResolution": "Resolução",
+ "viewerInfoLabelSize": "Tamanho",
+ "viewerInfoLabelUri": "URI",
+ "viewerInfoLabelPath": "Caminho",
+ "viewerInfoLabelDuration": "Duração",
+ "viewerInfoLabelOwner": "Propriedade de",
+ "viewerInfoLabelCoordinates": "Coordenadas",
+ "viewerInfoLabelAddress": "Endereço",
+
+ "mapStyleTitle": "Estilo do mapa",
+ "mapStyleTooltip": "Selecione o estilo do mapa",
+ "mapZoomInTooltip": "Mais zoom",
+ "mapZoomOutTooltip": "Reduzir o zoom",
+ "mapPointNorthUpTooltip": "Aponte para o norte para cima",
+ "mapAttributionOsmHot": "Dados do mapa © [OpenStreetMap](https://www.openstreetmap.org/copyright) colaboradores • Blocos por [HOT](https://www.hotosm.org/) • Hospedado por [OSM France](https://openstreetmap.fr/)",
+ "mapAttributionStamen": "Dados do mapa © [OpenStreetMap](https://www.openstreetmap.org/copyright) colaboradores • Blocos por [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
+ "openMapPageTooltip": "Visualizar na página do mapa",
+ "mapEmptyRegion": "Nenhuma imagem nesta região",
+
+ "viewerInfoOpenEmbeddedFailureFeedback": "Falha ao extrair dados incorporados",
+ "viewerInfoOpenLinkText": "Abrir",
+ "viewerInfoViewXmlLinkText": "Visualizar XML",
+
+ "viewerInfoSearchFieldLabel": "Pesquisar metadados",
+ "viewerInfoSearchEmpty": "Nenhuma chave correspondente",
+ "viewerInfoSearchSuggestionDate": "Data e Hora",
+ "viewerInfoSearchSuggestionDescription": "Descrição",
+ "viewerInfoSearchSuggestionDimensions": "Dimensões",
+ "viewerInfoSearchSuggestionResolution": "Resolução",
+ "viewerInfoSearchSuggestionRights": "Direitos",
+
+ "tagEditorPageTitle": "Editar etiquetas",
+ "tagEditorPageNewTagFieldLabel": "Nova etiqueta",
+ "tagEditorPageAddTagTooltip": "Adicionar etiqueta",
+ "tagEditorSectionRecent": "Recente",
+
+ "panoramaEnableSensorControl": "Ativar o controle do sensor",
+ "panoramaDisableSensorControl": "Desabilitar o controle do sensor",
+
+ "sourceViewerPageTitle": "Fonte",
+
+ "filePickerShowHiddenFiles": "Mostrar arquivos ocultos",
+ "filePickerDoNotShowHiddenFiles": "Não mostre arquivos ocultos",
+ "filePickerOpenFrom": "Abrir de",
+ "filePickerNoItems": "Nenhum itens",
+ "filePickerUseThisFolder": "Usar esta pasta"
+}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 1d8649cc9..06d891c34 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -7,6 +7,7 @@
"timeSeconds": "{seconds, plural, =1{1 секунда} few{{seconds} секунды} other{{seconds} секунд}}",
"timeMinutes": "{minutes, plural, =1{1 минута} few{{minutes} минуты} other{{minutes} минут}}",
+ "focalLength": "{length} mm",
"applyButtonLabel": "ПРИМЕНИТЬ",
"deleteButtonLabel": "УДАЛИТЬ",
@@ -33,9 +34,9 @@
"sourceStateLocatingPlaces": "Расположение локаций",
"chipActionDelete": "Удалить",
- "chipActionGoToAlbumPage": "Показывать в Альбомах",
- "chipActionGoToCountryPage": "Показывать в Странах",
- "chipActionGoToTagPage": "Показывать в тегах",
+ "chipActionGoToAlbumPage": "Показать в Альбомах",
+ "chipActionGoToCountryPage": "Показать в Странах",
+ "chipActionGoToTagPage": "Показать в тегах",
"chipActionHide": "Скрыть",
"chipActionPin": "Закрепить",
"chipActionUnpin": "Открепить",
@@ -73,6 +74,7 @@
"videoActionSettings": "Настройки",
"entryInfoActionEditDate": "Изменить дату и время",
+ "entryInfoActionEditLocation": "Изменить местоположение",
"entryInfoActionEditRating": "Изменить рейтинг",
"entryInfoActionEditTags": "Изменить теги",
"entryInfoActionRemoveMetadata": "Удалить метаданные",
@@ -81,7 +83,7 @@
"filterLocationEmptyLabel": "Без местоположения",
"filterTagEmptyLabel": "Без тегов",
"filterRatingUnratedLabel": "Без рейтинга",
- "filterRatingRejectedLabel": "Отклонённые",
+ "filterRatingRejectedLabel": "Отклонённое",
"filterTypeAnimatedLabel": "GIF",
"filterTypeMotionPhotoLabel": "Живое фото",
"filterTypePanoramaLabel": "Панорама",
@@ -179,11 +181,13 @@
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Вы уверены, что хотите удалить эти альбомы и их объекты?} few{Вы уверены, что хотите удалить эти альбомы и их {count} объекта?} other{Вы уверены, что хотите удалить эти альбомы и их {count} объектов?}}",
"exportEntryDialogFormat": "Формат:",
+ "exportEntryDialogWidth": "Ширина",
+ "exportEntryDialogHeight": "Высота",
"renameEntryDialogLabel": "Новое название",
"editEntryDateDialogTitle": "Дата и время",
- "editEntryDateDialogSetCustom": "Задайте дату",
+ "editEntryDateDialogSetCustom": "Установить дату",
"editEntryDateDialogCopyField": "Копировать с другой даты",
"editEntryDateDialogExtractFromTitle": "Извлечь из названия",
"editEntryDateDialogShift": "Сдвиг",
@@ -192,6 +196,13 @@
"editEntryDateDialogHours": "Часов",
"editEntryDateDialogMinutes": "Минут",
+ "editEntryLocationDialogTitle": "Местоположение",
+ "editEntryLocationDialogChooseOnMapTooltip": "Выбрать на карте",
+ "editEntryLocationDialogLatitude": "Широта",
+ "editEntryLocationDialogLongitude": "Долгота",
+
+ "locationPickerUseThisLocationButton": "Использовать это местоположение",
+
"editEntryRatingDialogTitle": "Рейтинг",
"removeEntryMetadataDialogTitle": "Удаление метаданных",
@@ -230,13 +241,6 @@
"aboutLinkLicense": "Лицензия",
"aboutLinkPolicy": "Политика конфиденциальности",
- "aboutUpdate": "Доступна новая версия",
- "aboutUpdateLinks1": "Новая версия Aves доступна на",
- "aboutUpdateLinks2": "и",
- "aboutUpdateLinks3": ".",
- "aboutUpdateGitHub": "GitHub",
- "aboutUpdateGooglePlay": "Play Маркет",
-
"aboutBug": "Отчет об ошибке",
"aboutBugSaveLogInstruction": "Сохраните логи приложения в файл",
"aboutBugSaveLogButton": "Сохранить",
@@ -361,11 +365,15 @@
"settingsActionExport": "Экспорт",
"settingsActionImport": "Импорт",
+ "appExportCovers": "Обложки",
+ "appExportFavourites": "Избранное",
+ "appExportSettings": "Настройки",
+
"settingsSectionNavigation": "Навигация",
"settingsHome": "Домашний каталог",
"settingsKeepScreenOnTile": "Держать экран включенным",
"settingsKeepScreenOnTitle": "Держать экран включенным",
- "settingsDoubleBackExit": "Дважды нажмите «назад», чтобы выйти",
+ "settingsDoubleBackExit": "Дважды нажмите «Назад», чтобы выйти",
"settingsNavigationDrawerTile": "Навигационное меню",
"settingsNavigationDrawerEditorTitle": "Навигационное меню",
@@ -376,11 +384,12 @@
"settingsNavigationDrawerAddAlbum": "Добавить альбом",
"settingsSectionThumbnails": "Эскизы",
+ "settingsThumbnailShowFavouriteIcon": "Показать значок избранного",
"settingsThumbnailShowLocationIcon": "Показать значок местоположения",
- "settingsThumbnailShowMotionPhotoIcon": "Показать значок живого фото",
- "settingsThumbnailShowRating": "Показывать рейтинг",
+ "settingsThumbnailShowMotionPhotoIcon": "Показать значок «живого фото»",
+ "settingsThumbnailShowRating": "Показать рейтинг",
"settingsThumbnailShowRawIcon": "Показать значок RAW-файла",
- "settingsThumbnailShowVideoDuration": "Показывать продолжительность видео",
+ "settingsThumbnailShowVideoDuration": "Показать продолжительность видео",
"settingsCollectionQuickActionsTile": "Быстрые действия",
"settingsCollectionQuickActionEditorTitle": "Быстрые действия",
@@ -392,7 +401,7 @@
"settingsSectionViewer": "Просмотрщик",
"settingsViewerUseCutout": "Использовать область выреза",
"settingsViewerMaximumBrightness": "Максимальная яркость",
- "settingsMotionPhotoAutoPlay": "Автовоспроизведение «Живых фото»",
+ "settingsMotionPhotoAutoPlay": "Автовоспроизведение «живых фото»",
"settingsImageBackground": "Фон изображения",
"settingsViewerQuickActionsTile": "Быстрые действия",
@@ -404,26 +413,26 @@
"settingsViewerOverlayTile": "Наложение",
"settingsViewerOverlayTitle": "Наложение",
- "settingsViewerShowOverlayOnOpening": "Показывать наложение при открытии",
+ "settingsViewerShowOverlayOnOpening": "Показать наложение при открытии",
"settingsViewerShowMinimap": "Показать миникарту",
- "settingsViewerShowInformation": "Показывать информацию",
+ "settingsViewerShowInformation": "Показать информацию",
"settingsViewerShowInformationSubtitle": "Показать название, дату, местоположение и т.д.",
"settingsViewerShowShootingDetails": "Показать детали съёмки",
"settingsViewerEnableOverlayBlurEffect": "Наложение эффекта размытия",
"settingsVideoPageTitle": "Настройки видео",
"settingsSectionVideo": "Видео",
- "settingsVideoShowVideos": "Показывать видео",
+ "settingsVideoShowVideos": "Показать видео",
"settingsVideoEnableHardwareAcceleration": "Аппаратное ускорение",
"settingsVideoEnableAutoPlay": "Автозапуск воспроизведения",
- "settingsVideoLoopModeTile": "Цикличный режим",
+ "settingsVideoLoopModeTile": "Циклический режим",
"settingsVideoLoopModeTitle": "Цикличный режим",
"settingsVideoQuickActionsTile": "Быстрые действия для видео",
"settingsVideoQuickActionEditorTitle": "Быстрые действия",
"settingsSubtitleThemeTile": "Субтитры",
"settingsSubtitleThemeTitle": "Субтитры",
- "settingsSubtitleThemeSample": "Это образец.",
+ "settingsSubtitleThemeSample": "Образец.",
"settingsSubtitleThemeTextAlignmentTile": "Выравнивание текста",
"settingsSubtitleThemeTextAlignmentTitle": "Выравнивание текста",
"settingsSubtitleThemeTextSize": "Размер текста",
@@ -438,7 +447,7 @@
"settingsSectionPrivacy": "Конфиденциальность",
"settingsAllowInstalledAppAccess": "Разрешить доступ к библиотеке приложения",
- "settingsAllowInstalledAppAccessSubtitle": "Используется для улучшения отображения альбома",
+ "settingsAllowInstalledAppAccessSubtitle": "Используется для улучшения отображения альбомов",
"settingsAllowErrorReporting": "Разрешить анонимную отправку логов",
"settingsSaveSearchHistory": "Сохранять историю поиска",
diff --git a/lib/main_common.dart b/lib/main_common.dart
index 98d2790f0..1a1820c9d 100644
--- a/lib/main_common.dart
+++ b/lib/main_common.dart
@@ -28,5 +28,10 @@ void mainCommon(AppFlavor flavor) {
reportService.recordError(errorAndStacktrace.first, errorAndStacktrace.last);
}).sendPort);
+ // Errors during the widget build phase will show by default:
+ // - in debug mode: error on red background
+ // - in release mode: plain grey background
+ // This can be modified via `ErrorWidget.builder`
+
runApp(AvesApp(flavor: flavor));
}
diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart
index ffc6ca198..d23c65e02 100644
--- a/lib/model/actions/entry_info_actions.dart
+++ b/lib/model/actions/entry_info_actions.dart
@@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart';
enum EntryInfoAction {
// general
editDate,
+ editLocation,
editRating,
editTags,
removeMetadata,
@@ -15,6 +16,7 @@ enum EntryInfoAction {
class EntryInfoActions {
static const all = [
EntryInfoAction.editDate,
+ EntryInfoAction.editLocation,
EntryInfoAction.editRating,
EntryInfoAction.editTags,
EntryInfoAction.removeMetadata,
@@ -28,6 +30,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
// general
case EntryInfoAction.editDate:
return context.l10n.entryInfoActionEditDate;
+ case EntryInfoAction.editLocation:
+ return context.l10n.entryInfoActionEditLocation;
case EntryInfoAction.editRating:
return context.l10n.entryInfoActionEditRating;
case EntryInfoAction.editTags:
@@ -49,6 +53,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
// general
case EntryInfoAction.editDate:
return AIcons.date;
+ case EntryInfoAction.editLocation:
+ return AIcons.location;
case EntryInfoAction.editRating:
return AIcons.editRating;
case EntryInfoAction.editTags:
diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart
index f3601560b..5ef1ac427 100644
--- a/lib/model/actions/entry_set_actions.dart
+++ b/lib/model/actions/entry_set_actions.dart
@@ -15,17 +15,18 @@ enum EntrySetAction {
// browsing or selecting
map,
stats,
+ rescan,
// selecting
share,
delete,
copy,
move,
- rescan,
toggleFavourite,
rotateCCW,
rotateCW,
flip,
editDate,
+ editLocation,
editRating,
editTags,
removeMetadata,
@@ -45,6 +46,7 @@ class EntrySetActions {
EntrySetAction.addShortcut,
EntrySetAction.map,
EntrySetAction.stats,
+ EntrySetAction.rescan,
];
static const selection = [
@@ -53,9 +55,9 @@ class EntrySetActions {
EntrySetAction.copy,
EntrySetAction.move,
EntrySetAction.toggleFavourite,
- EntrySetAction.rescan,
EntrySetAction.map,
EntrySetAction.stats,
+ EntrySetAction.rescan,
// editing actions are in their subsection
];
}
@@ -85,6 +87,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.menuActionMap;
case EntrySetAction.stats:
return context.l10n.menuActionStats;
+ case EntrySetAction.rescan:
+ return context.l10n.collectionActionRescan;
// selecting
case EntrySetAction.share:
return context.l10n.entryActionShare;
@@ -94,8 +98,6 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.collectionActionCopy;
case EntrySetAction.move:
return context.l10n.collectionActionMove;
- case EntrySetAction.rescan:
- return context.l10n.collectionActionRescan;
case EntrySetAction.toggleFavourite:
// different data depending on toggle state
return context.l10n.entryActionAddFavourite;
@@ -107,6 +109,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.entryActionFlip;
case EntrySetAction.editDate:
return context.l10n.entryInfoActionEditDate;
+ case EntrySetAction.editLocation:
+ return context.l10n.entryInfoActionEditLocation;
case EntrySetAction.editRating:
return context.l10n.entryInfoActionEditRating;
case EntrySetAction.editTags:
@@ -144,6 +148,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.map;
case EntrySetAction.stats:
return AIcons.stats;
+ case EntrySetAction.rescan:
+ return AIcons.refresh;
// selecting
case EntrySetAction.share:
return AIcons.share;
@@ -153,8 +159,6 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.copy;
case EntrySetAction.move:
return AIcons.move;
- case EntrySetAction.rescan:
- return AIcons.refresh;
case EntrySetAction.toggleFavourite:
// different data depending on toggle state
return AIcons.favourite;
@@ -166,6 +170,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.flip;
case EntrySetAction.editDate:
return AIcons.date;
+ case EntrySetAction.editLocation:
+ return AIcons.location;
case EntrySetAction.editRating:
return AIcons.editRating;
case EntrySetAction.editTags:
diff --git a/lib/model/availability.dart b/lib/model/availability.dart
index 008d0e76c..bdbb691fd 100644
--- a/lib/model/availability.dart
+++ b/lib/model/availability.dart
@@ -1,12 +1,7 @@
import 'package:aves/model/device.dart';
-import 'package:aves/model/settings/settings.dart';
-import 'package:aves/theme/durations.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
-import 'package:github/github.dart';
import 'package:google_api_availability/google_api_availability.dart';
-import 'package:package_info_plus/package_info_plus.dart';
-import 'package:version/version.dart';
abstract class AvesAvailability {
void onResume();
@@ -18,12 +13,10 @@ abstract class AvesAvailability {
Future get canLocatePlaces;
Future get canUseGoogleMaps;
-
- Future get isNewVersionAvailable;
}
class LiveAvesAvailability implements AvesAvailability {
- bool? _isConnected, _hasPlayServices, _isNewVersionAvailable;
+ bool? _isConnected, _hasPlayServices;
LiveAvesAvailability() {
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
@@ -63,30 +56,4 @@ class LiveAvesAvailability implements AvesAvailability {
@override
Future get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices;
-
- @override
- Future get isNewVersionAvailable async {
- if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable!);
-
- final now = DateTime.now();
- final dueDate = settings.lastVersionCheckDate.add(Durations.lastVersionCheckInterval);
- if (now.isBefore(dueDate)) {
- _isNewVersionAvailable = false;
- return SynchronousFuture(_isNewVersionAvailable!);
- }
-
- if (!(await isConnected)) return false;
-
- Version version(String s) => Version.parse(s.replaceFirst('v', ''));
- final currentTag = (await PackageInfo.fromPlatform()).version;
- final latestTag = (await GitHub().repositories.getLatestRelease(RepositorySlug('deckerst', 'aves'))).tagName!;
- _isNewVersionAvailable = version(latestTag) > version(currentTag);
- if (_isNewVersionAvailable!) {
- debugPrint('Aves $latestTag is available on github');
- } else {
- debugPrint('Aves $currentTag is the latest version');
- settings.lastVersionCheckDate = now;
- }
- return _isNewVersionAvailable!;
- }
}
diff --git a/lib/model/covers.dart b/lib/model/covers.dart
index d10e67ca7..2b339889c 100644
--- a/lib/model/covers.dart
+++ b/lib/model/covers.dart
@@ -1,11 +1,12 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
+import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
final Covers covers = Covers._private();
@@ -20,6 +21,8 @@ class Covers with ChangeNotifier {
int get count => _rows.length;
+ Set get all => Set.unmodifiable(_rows);
+
int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId;
Future set(CollectionFilter filter, int? contentId) async {
@@ -75,6 +78,61 @@ class Covers with ChangeNotifier {
notifyListeners();
}
+
+ // import/export
+
+ List>? export(CollectionSource source) {
+ final visibleEntries = source.visibleEntries;
+ final jsonList = covers.all
+ .map((row) {
+ final id = row.contentId;
+ final path = visibleEntries.firstWhereOrNull((entry) => id == entry.contentId)?.path;
+ if (path == null) return null;
+
+ final volume = androidFileUtils.getStorageVolume(path)?.path;
+ if (volume == null) return null;
+
+ final relativePath = path.substring(volume.length);
+ return {
+ 'filter': row.filter.toJson(),
+ 'volume': volume,
+ 'relativePath': relativePath,
+ };
+ })
+ .whereNotNull()
+ .toList();
+ return jsonList.isNotEmpty ? jsonList : null;
+ }
+
+ void import(dynamic jsonList, CollectionSource source) {
+ if (jsonList is! List) {
+ debugPrint('failed to import covers for jsonMap=$jsonList');
+ return;
+ }
+
+ final visibleEntries = source.visibleEntries;
+ jsonList.forEach((row) {
+ final filter = CollectionFilter.fromJson(row['filter']);
+ if (filter == null) {
+ debugPrint('failed to import cover for row=$row');
+ return;
+ }
+
+ final volume = row['volume'];
+ final relativePath = row['relativePath'];
+ if (volume is String && relativePath is String) {
+ final path = pContext.join(volume, relativePath);
+ final entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry));
+ if (entry != null) {
+ covers.set(filter, entry.contentId);
+ } else {
+ debugPrint('failed to import cover for path=$path, filter=$filter');
+ }
+ } else {
+ debugPrint('failed to import cover for volume=$volume, relativePath=$relativePath, filter=$filter');
+ }
+ });
+ }
}
@immutable
diff --git a/lib/model/entry.dart b/lib/model/entry.dart
index be81490c3..20ee4573b 100644
--- a/lib/model/entry.dart
+++ b/lib/model/entry.dart
@@ -16,6 +16,7 @@ import 'package:aves/services/geocoding_service.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/utils/change_notifier.dart';
+import 'package:aves/utils/time_utils.dart';
import 'package:collection/collection.dart';
import 'package:country_code/country_code.dart';
import 'package:flutter/foundation.dart';
@@ -236,6 +237,8 @@ class AvesEntry {
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
+ bool get canEditLocation => canEdit && canEditExif;
+
bool get canEditRating => canEdit && canEditXmp;
bool get canEditTags => canEdit && canEditXmp;
@@ -348,15 +351,7 @@ class AvesEntry {
DateTime? _bestDate;
DateTime? get bestDate {
- if (_bestDate == null) {
- if ((_catalogDateMillis ?? 0) > 0) {
- _bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogDateMillis!);
- } else if ((sourceDateTakenMillis ?? 0) > 0) {
- _bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis!);
- } else if ((dateModifiedSecs ?? 0) > 0) {
- _bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs! * 1000);
- }
- }
+ _bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis((dateModifiedSecs ?? 0) * 1000);
return _bestDate;
}
@@ -504,10 +499,13 @@ class AvesEntry {
}
Future locate({required bool background, required bool force, required Locale geocoderLocale}) async {
- if (!hasGps) return;
- await _locateCountry(force: force);
- if (await availability.canLocatePlaces) {
- await locatePlace(background: background, force: force, geocoderLocale: geocoderLocale);
+ if (hasGps) {
+ await _locateCountry(force: force);
+ if (await availability.canLocatePlaces) {
+ await locatePlace(background: background, force: force, geocoderLocale: geocoderLocale);
+ }
+ } else {
+ addressDetails = null;
}
}
@@ -748,13 +746,11 @@ class AvesEntry {
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
}
- static final _epoch = DateTime.fromMillisecondsSinceEpoch(0);
-
// compare by:
// 1) date descending
// 2) name descending
static int compareByDate(AvesEntry a, AvesEntry b) {
- var c = (b.bestDate ?? _epoch).compareTo(a.bestDate ?? _epoch);
+ var c = (b.bestDate ?? epoch).compareTo(a.bestDate ?? epoch);
if (c != 0) return c;
return compareByName(b, a);
}
diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart
index 57b4684c6..c10467f77 100644
--- a/lib/model/entry_images.dart
+++ b/lib/model/entry_images.dart
@@ -57,7 +57,7 @@ extension ExtraAvesEntryImages on AvesEntry {
bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive;
- List get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map((key) => ThumbnailProvider(key)).toList();
+ List get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList();
ThumbnailProvider get bestCachedThumbnail {
final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady);
diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart
index a69300c39..ad14a3923 100644
--- a/lib/model/entry_metadata_edition.dart
+++ b/lib/model/entry_metadata_edition.dart
@@ -4,12 +4,15 @@ import 'dart:io';
import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/model/metadata/enums.dart';
+import 'package:aves/model/metadata/fields.dart';
+import 'package:aves/ref/exif.dart';
import 'package:aves/ref/iptc.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/xmp.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves/utils/xmp_utils.dart';
import 'package:flutter/foundation.dart';
+import 'package:latlong2/latlong.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:xml/xml.dart';
@@ -73,6 +76,40 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
return dataTypes;
}
+ Future> editLocation(LatLng? latLng) async {
+ final Set dataTypes = {};
+
+ await _missingDateCheckAndExifEdit(dataTypes);
+
+ // clear every GPS field
+ final exifFields = Map.fromEntries(MetadataFields.exifGpsFields.map((k) => MapEntry(k, null)));
+ // add latitude & longitude, if any
+ if (latLng != null) {
+ final latitude = latLng.latitude;
+ final longitude = latLng.longitude;
+ if (latitude != 0 && longitude != 0) {
+ exifFields.addAll({
+ MetadataField.exifGpsLatitude: latitude.abs(),
+ MetadataField.exifGpsLatitudeRef: latitude >= 0 ? Exif.latitudeNorth : Exif.latitudeSouth,
+ MetadataField.exifGpsLongitude: longitude.abs(),
+ MetadataField.exifGpsLongitudeRef: longitude >= 0 ? Exif.longitudeEast : Exif.longitudeWest,
+ });
+ }
+ }
+
+ final metadata = {
+ MetadataType.exif: Map.fromEntries(exifFields.entries.map((kv) => MapEntry(kv.key.exifInterfaceTag!, kv.value))),
+ };
+ final newFields = await metadataEditService.editMetadata(this, metadata);
+ if (newFields.isNotEmpty) {
+ dataTypes.addAll({
+ EntryDataType.catalog,
+ EntryDataType.address,
+ });
+ }
+ return dataTypes;
+ }
+
Future> _changeOrientation(Future> Function() apply) async {
final Set dataTypes = {};
diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart
index aef618695..cc8768f1e 100644
--- a/lib/model/favourites.dart
+++ b/lib/model/favourites.dart
@@ -1,5 +1,7 @@
import 'package:aves/model/entry.dart';
+import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
+import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@@ -17,6 +19,8 @@ class Favourites with ChangeNotifier {
int get count => _rows.length;
+ Set get all => Set.unmodifiable(_rows.map((v) => v.contentId));
+
bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId);
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!);
@@ -59,6 +63,56 @@ class Favourites with ChangeNotifier {
notifyListeners();
}
+
+ // import/export
+
+ Map>? export(CollectionSource source) {
+ final visibleEntries = source.visibleEntries;
+ final ids = favourites.all;
+ final paths = visibleEntries.where((entry) => ids.contains(entry.contentId)).map((entry) => entry.path).whereNotNull().toSet();
+ final byVolume = groupBy(paths, androidFileUtils.getStorageVolume);
+ final jsonMap = Map.fromEntries(byVolume.entries.map((kv) {
+ final volume = kv.key?.path;
+ if (volume == null) return null;
+ final rootLength = volume.length;
+ final relativePaths = kv.value.map((v) => v.substring(rootLength)).toList();
+ return MapEntry(volume, relativePaths);
+ }).whereNotNull());
+ return jsonMap.isNotEmpty ? jsonMap : null;
+ }
+
+ void import(dynamic jsonMap, CollectionSource source) {
+ if (jsonMap is! Map) {
+ debugPrint('failed to import favourites for jsonMap=$jsonMap');
+ return;
+ }
+
+ final visibleEntries = source.visibleEntries;
+ final foundEntries = {};
+ final missedPaths = {};
+ jsonMap.forEach((volume, relativePaths) {
+ if (volume is String && relativePaths is List) {
+ relativePaths.forEach((relativePath) {
+ final path = pContext.join(volume, relativePath);
+ final entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path);
+ if (entry != null) {
+ foundEntries.add(entry);
+ } else {
+ missedPaths.add(path);
+ }
+ });
+ } else {
+ debugPrint('failed to import favourites for volume=$volume, relativePaths=${relativePaths.runtimeType}');
+ }
+
+ if (foundEntries.isNotEmpty) {
+ favourites.add(foundEntries);
+ }
+ if (missedPaths.isNotEmpty) {
+ debugPrint('failed to import favourites with ${missedPaths.length} missed paths');
+ }
+ });
+ }
}
@immutable
diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart
index f2389dce7..058715629 100644
--- a/lib/model/filters/filters.dart
+++ b/lib/model/filters/filters.dart
@@ -21,17 +21,21 @@ import 'package:flutter/widgets.dart';
abstract class CollectionFilter extends Equatable implements Comparable {
static const List categoryOrder = [
QueryFilter.type,
- FavouriteFilter.type,
MimeFilter.type,
- TypeFilter.type,
AlbumFilter.type,
+ TypeFilter.type,
LocationFilter.type,
CoordinateFilter.type,
+ FavouriteFilter.type,
RatingFilter.type,
TagFilter.type,
PathFilter.type,
];
+ final bool not;
+
+ const CollectionFilter({this.not = false});
+
static CollectionFilter? fromJson(String jsonString) {
if (jsonString.isEmpty) return null;
@@ -69,8 +73,6 @@ abstract class CollectionFilter extends Equatable implements Comparable toMap();
String toJson() => jsonEncode(toMap());
diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart
index 1ec3b2a4d..0c6105ba7 100644
--- a/lib/model/filters/tag.dart
+++ b/lib/model/filters/tag.dart
@@ -12,23 +12,25 @@ class TagFilter extends CollectionFilter {
@override
List get props => [tag];
- TagFilter(this.tag) {
+ TagFilter(this.tag, {bool not = false}) : super(not: not) {
if (tag.isEmpty) {
- _test = (entry) => entry.tags.isEmpty;
+ _test = not ? (entry) => entry.tags.isNotEmpty : (entry) => entry.tags.isEmpty;
} else {
- _test = (entry) => entry.tags.contains(tag);
+ _test = not ? (entry) => !entry.tags.contains(tag) : (entry) => entry.tags.contains(tag);
}
}
TagFilter.fromMap(Map json)
: this(
json['tag'],
+ not: json['not'] ?? false,
);
@override
Map toMap() => {
'type': type,
'tag': tag,
+ 'not': not,
};
@override
diff --git a/lib/model/metadata/date_modifier.dart b/lib/model/metadata/date_modifier.dart
index 86c312f62..73d648463 100644
--- a/lib/model/metadata/date_modifier.dart
+++ b/lib/model/metadata/date_modifier.dart
@@ -1,14 +1,16 @@
import 'package:aves/model/metadata/enums.dart';
+import 'package:aves/model/metadata/fields.dart';
+import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@immutable
-class DateModifier {
+class DateModifier extends Equatable {
static const writableDateFields = [
MetadataField.exifDate,
MetadataField.exifDateOriginal,
MetadataField.exifDateDigitized,
- MetadataField.exifGpsDate,
+ MetadataField.exifGpsDatestamp,
MetadataField.xmpCreateDate,
];
@@ -18,6 +20,9 @@ class DateModifier {
final DateFieldSource? copyFieldSource;
final int? shiftMinutes;
+ @override
+ List get props => [action, fields, setDateTime, copyFieldSource, shiftMinutes];
+
const DateModifier._private(
this.action,
this.fields, {
diff --git a/lib/model/metadata/enums.dart b/lib/model/metadata/enums.dart
index 530dc932b..086f35894 100644
--- a/lib/model/metadata/enums.dart
+++ b/lib/model/metadata/enums.dart
@@ -1,10 +1,4 @@
-enum MetadataField {
- exifDate,
- exifDateOriginal,
- exifDateDigitized,
- exifGpsDate,
- xmpCreateDate,
-}
+import 'package:aves/model/metadata/fields.dart';
enum DateEditAction {
setCustom,
@@ -91,35 +85,6 @@ extension ExtraMetadataType on MetadataType {
}
}
-extension ExtraMetadataField on MetadataField {
- MetadataType get type {
- switch (this) {
- case MetadataField.exifDate:
- case MetadataField.exifDateOriginal:
- case MetadataField.exifDateDigitized:
- case MetadataField.exifGpsDate:
- return MetadataType.exif;
- case MetadataField.xmpCreateDate:
- return MetadataType.xmp;
- }
- }
-
- String? toExifInterfaceTag() {
- switch (this) {
- case MetadataField.exifDate:
- return 'DateTime';
- case MetadataField.exifDateOriginal:
- return 'DateTimeOriginal';
- case MetadataField.exifDateDigitized:
- return 'DateTimeDigitized';
- case MetadataField.exifGpsDate:
- return 'GPSDateStamp';
- case MetadataField.xmpCreateDate:
- return null;
- }
- }
-}
-
extension ExtraDateFieldSource on DateFieldSource {
MetadataField? toMetadataField() {
switch (this) {
@@ -132,7 +97,7 @@ extension ExtraDateFieldSource on DateFieldSource {
case DateFieldSource.exifDateDigitized:
return MetadataField.exifDateDigitized;
case DateFieldSource.exifGpsDate:
- return MetadataField.exifGpsDate;
+ return MetadataField.exifGpsDatestamp;
}
}
}
diff --git a/lib/model/metadata/fields.dart b/lib/model/metadata/fields.dart
new file mode 100644
index 000000000..040e5eaaa
--- /dev/null
+++ b/lib/model/metadata/fields.dart
@@ -0,0 +1,199 @@
+import 'package:aves/model/metadata/enums.dart';
+
+enum MetadataField {
+ exifDate,
+ exifDateOriginal,
+ exifDateDigitized,
+ exifGpsAltitude,
+ exifGpsAltitudeRef,
+ exifGpsAreaInformation,
+ exifGpsDatestamp,
+ exifGpsDestBearing,
+ exifGpsDestBearingRef,
+ exifGpsDestDistance,
+ exifGpsDestDistanceRef,
+ exifGpsDestLatitude,
+ exifGpsDestLatitudeRef,
+ exifGpsDestLongitude,
+ exifGpsDestLongitudeRef,
+ exifGpsDifferential,
+ exifGpsDOP,
+ exifGpsHPositioningError,
+ exifGpsImgDirection,
+ exifGpsImgDirectionRef,
+ exifGpsLatitude,
+ exifGpsLatitudeRef,
+ exifGpsLongitude,
+ exifGpsLongitudeRef,
+ exifGpsMapDatum,
+ exifGpsMeasureMode,
+ exifGpsProcessingMethod,
+ exifGpsSatellites,
+ exifGpsSpeed,
+ exifGpsSpeedRef,
+ exifGpsStatus,
+ exifGpsTimestamp,
+ exifGpsTrack,
+ exifGpsTrackRef,
+ exifGpsVersionId,
+ xmpCreateDate,
+}
+
+class MetadataFields {
+ static const Set exifGpsFields = {
+ MetadataField.exifGpsAltitude,
+ MetadataField.exifGpsAltitudeRef,
+ MetadataField.exifGpsAreaInformation,
+ MetadataField.exifGpsDatestamp,
+ MetadataField.exifGpsDestBearing,
+ MetadataField.exifGpsDestBearingRef,
+ MetadataField.exifGpsDestDistance,
+ MetadataField.exifGpsDestDistanceRef,
+ MetadataField.exifGpsDestLatitude,
+ MetadataField.exifGpsDestLatitudeRef,
+ MetadataField.exifGpsDestLongitude,
+ MetadataField.exifGpsDestLongitudeRef,
+ MetadataField.exifGpsDifferential,
+ MetadataField.exifGpsDOP,
+ MetadataField.exifGpsHPositioningError,
+ MetadataField.exifGpsImgDirection,
+ MetadataField.exifGpsImgDirectionRef,
+ MetadataField.exifGpsLatitude,
+ MetadataField.exifGpsLatitudeRef,
+ MetadataField.exifGpsLongitude,
+ MetadataField.exifGpsLongitudeRef,
+ MetadataField.exifGpsMapDatum,
+ MetadataField.exifGpsMeasureMode,
+ MetadataField.exifGpsProcessingMethod,
+ MetadataField.exifGpsSatellites,
+ MetadataField.exifGpsSpeed,
+ MetadataField.exifGpsSpeedRef,
+ MetadataField.exifGpsStatus,
+ MetadataField.exifGpsTimestamp,
+ MetadataField.exifGpsTrack,
+ MetadataField.exifGpsTrackRef,
+ MetadataField.exifGpsVersionId,
+ };
+}
+
+extension ExtraMetadataField on MetadataField {
+ MetadataType get type {
+ switch (this) {
+ case MetadataField.exifDate:
+ case MetadataField.exifDateOriginal:
+ case MetadataField.exifDateDigitized:
+ case MetadataField.exifGpsAltitude:
+ case MetadataField.exifGpsAltitudeRef:
+ case MetadataField.exifGpsAreaInformation:
+ case MetadataField.exifGpsDatestamp:
+ case MetadataField.exifGpsDestBearing:
+ case MetadataField.exifGpsDestBearingRef:
+ case MetadataField.exifGpsDestDistance:
+ case MetadataField.exifGpsDestDistanceRef:
+ case MetadataField.exifGpsDestLatitude:
+ case MetadataField.exifGpsDestLatitudeRef:
+ case MetadataField.exifGpsDestLongitude:
+ case MetadataField.exifGpsDestLongitudeRef:
+ case MetadataField.exifGpsDifferential:
+ case MetadataField.exifGpsDOP:
+ case MetadataField.exifGpsHPositioningError:
+ case MetadataField.exifGpsImgDirection:
+ case MetadataField.exifGpsImgDirectionRef:
+ case MetadataField.exifGpsLatitude:
+ case MetadataField.exifGpsLatitudeRef:
+ case MetadataField.exifGpsLongitude:
+ case MetadataField.exifGpsLongitudeRef:
+ case MetadataField.exifGpsMapDatum:
+ case MetadataField.exifGpsMeasureMode:
+ case MetadataField.exifGpsProcessingMethod:
+ case MetadataField.exifGpsSatellites:
+ case MetadataField.exifGpsSpeed:
+ case MetadataField.exifGpsSpeedRef:
+ case MetadataField.exifGpsStatus:
+ case MetadataField.exifGpsTimestamp:
+ case MetadataField.exifGpsTrack:
+ case MetadataField.exifGpsTrackRef:
+ case MetadataField.exifGpsVersionId:
+ return MetadataType.exif;
+ case MetadataField.xmpCreateDate:
+ return MetadataType.xmp;
+ }
+ }
+
+ String? get exifInterfaceTag {
+ switch (this) {
+ case MetadataField.exifDate:
+ return 'DateTime';
+ case MetadataField.exifDateOriginal:
+ return 'DateTimeOriginal';
+ case MetadataField.exifDateDigitized:
+ return 'DateTimeDigitized';
+ case MetadataField.exifGpsAltitude:
+ return 'GPSAltitude';
+ case MetadataField.exifGpsAltitudeRef:
+ return 'GPSAltitudeRef';
+ case MetadataField.exifGpsAreaInformation:
+ return 'GPSAreaInformation';
+ case MetadataField.exifGpsDatestamp:
+ return 'GPSDateStamp';
+ case MetadataField.exifGpsDestBearing:
+ return 'GPSDestBearing';
+ case MetadataField.exifGpsDestBearingRef:
+ return 'GPSDestBearingRef';
+ case MetadataField.exifGpsDestDistance:
+ return 'GPSDestDistance';
+ case MetadataField.exifGpsDestDistanceRef:
+ return 'GPSDestDistanceRef';
+ case MetadataField.exifGpsDestLatitude:
+ return 'GPSDestLatitude';
+ case MetadataField.exifGpsDestLatitudeRef:
+ return 'GPSDestLatitudeRef';
+ case MetadataField.exifGpsDestLongitude:
+ return 'GPSDestLongitude';
+ case MetadataField.exifGpsDestLongitudeRef:
+ return 'GPSDestLongitudeRef';
+ case MetadataField.exifGpsDifferential:
+ return 'GPSDifferential';
+ case MetadataField.exifGpsDOP:
+ return 'GPSDOP';
+ case MetadataField.exifGpsHPositioningError:
+ return 'GPSHPositioningError';
+ case MetadataField.exifGpsImgDirection:
+ return 'GPSImgDirection';
+ case MetadataField.exifGpsImgDirectionRef:
+ return 'GPSImgDirectionRef';
+ case MetadataField.exifGpsLatitude:
+ return 'GPSLatitude';
+ case MetadataField.exifGpsLatitudeRef:
+ return 'GPSLatitudeRef';
+ case MetadataField.exifGpsLongitude:
+ return 'GPSLongitude';
+ case MetadataField.exifGpsLongitudeRef:
+ return 'GPSLongitudeRef';
+ case MetadataField.exifGpsMapDatum:
+ return 'GPSMapDatum';
+ case MetadataField.exifGpsMeasureMode:
+ return 'GPSMeasureMode';
+ case MetadataField.exifGpsProcessingMethod:
+ return 'GPSProcessingMethod';
+ case MetadataField.exifGpsSatellites:
+ return 'GPSSatellites';
+ case MetadataField.exifGpsSpeed:
+ return 'GPSSpeed';
+ case MetadataField.exifGpsSpeedRef:
+ return 'GPSSpeedRef';
+ case MetadataField.exifGpsStatus:
+ return 'GPSStatus';
+ case MetadataField.exifGpsTimestamp:
+ return 'GPSTimeStamp';
+ case MetadataField.exifGpsTrack:
+ return 'GPSTrack';
+ case MetadataField.exifGpsTrackRef:
+ return 'GPSTrackRef';
+ case MetadataField.exifGpsVersionId:
+ return 'GPSVersionID';
+ case MetadataField.xmpCreateDate:
+ return null;
+ }
+ }
+}
diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart
index ca9f09623..2dfbe54ec 100644
--- a/lib/model/metadata_db.dart
+++ b/lib/model/metadata_db.dart
@@ -231,7 +231,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future> loadAllEntries() async {
final db = await _database;
final maps = await db.query(entryTable);
- final entries = maps.map((map) => AvesEntry.fromMap(map)).toSet();
+ final entries = maps.map(AvesEntry.fromMap).toSet();
return entries;
}
@@ -273,7 +273,7 @@ class SqfliteMetadataDb implements MetadataDb {
orderBy: 'sourceDateTakenMillis DESC',
limit: limit,
);
- return maps.map((map) => AvesEntry.fromMap(map)).toSet();
+ return maps.map(AvesEntry.fromMap).toSet();
}
// date taken
@@ -306,7 +306,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future> loadAllMetadataEntries() async {
final db = await _database;
final maps = await db.query(metadataTable);
- final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList();
+ final metadataEntries = maps.map(CatalogMetadata.fromMap).toList();
return metadataEntries;
}
@@ -367,7 +367,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future> loadAllAddresses() async {
final db = await _database;
final maps = await db.query(addressTable);
- final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList();
+ final addresses = maps.map(AddressDetails.fromMap).toList();
return addresses;
}
@@ -413,7 +413,7 @@ class SqfliteMetadataDb implements MetadataDb {
Future> loadAllFavourites() async {
final db = await _database;
final maps = await db.query(favouriteTable);
- final rows = maps.map((map) => FavouriteRow.fromMap(map)).toSet();
+ final rows = maps.map(FavouriteRow.fromMap).toSet();
return rows;
}
diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart
index 08e3a888c..7593a4c0b 100644
--- a/lib/model/multipage.dart
+++ b/lib/model/multipage.dart
@@ -36,7 +36,7 @@ class MultiPageInfo {
factory MultiPageInfo.fromPageMaps(AvesEntry mainEntry, List pageMaps) {
return MultiPageInfo(
mainEntry: mainEntry,
- pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList(),
+ pages: pageMaps.map(SinglePageInfo.fromMap).toList(),
);
}
diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart
index 1b7f9c872..2740db8fd 100644
--- a/lib/model/settings/settings.dart
+++ b/lib/model/settings/settings.dart
@@ -1,5 +1,4 @@
import 'dart:async';
-import 'dart:convert';
import 'dart:math';
import 'package:aves/l10n/l10n.dart';
@@ -35,7 +34,6 @@ class Settings extends ChangeNotifier {
catalogTimeZoneKey,
videoShowRawTimedTextKey,
searchHistoryKey,
- lastVersionCheckDateKey,
};
// app
@@ -116,9 +114,6 @@ class Settings extends ChangeNotifier {
static const accessibilityAnimationsKey = 'accessibility_animations';
static const timeToTakeActionKey = 'time_to_take_action';
- // version
- static const lastVersionCheckDateKey = 'last_version_check_date';
-
// file picker
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
@@ -478,12 +473,6 @@ class Settings extends ChangeNotifier {
set timeToTakeAction(AccessibilityTimeout newValue) => setAndNotify(timeToTakeActionKey, newValue.toString());
- // version
-
- DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs!.getInt(lastVersionCheckDateKey) ?? 0);
-
- set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch);
-
// file picker
bool get filePickerShowHiddenFiles => getBoolOrDefault(filePickerShowHiddenFilesKey, SettingsDefaults.filePickerShowHiddenFiles);
@@ -580,12 +569,11 @@ class Settings extends ChangeNotifier {
// import/export
- String toJson() => jsonEncode(Map.fromEntries(
+ Map export() => Map.fromEntries(
_prefs!.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, _prefs!.get(k))),
- ));
+ );
- Future fromJson(String jsonString) async {
- final jsonMap = jsonDecode(jsonString);
+ Future import(dynamic jsonMap) async {
if (jsonMap is Map) {
// clear to restore defaults
await reset(includeInternalKeys: false);
diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart
index 64bed85ae..25fe1eb68 100644
--- a/lib/model/source/album.dart
+++ b/lib/model/source/album.dart
@@ -146,9 +146,14 @@ mixin AlbumMixin on SourceBase {
_filterEntryCountMap.clear();
_filterRecentEntryMap.clear();
} else {
- directories ??= entries!.map((entry) => entry.directory).toSet();
- directories.forEach(_filterEntryCountMap.remove);
- directories.forEach(_filterRecentEntryMap.remove);
+ directories ??= {};
+ if (entries != null) {
+ directories.addAll(entries.map((entry) => entry.directory).whereNotNull());
+ }
+ directories.forEach((directory) {
+ _filterEntryCountMap.remove(directory);
+ _filterRecentEntryMap.remove(directory);
+ });
}
eventBus.fire(AlbumSummaryInvalidatedEvent(directories));
}
diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart
index 75ca4ada2..4c1591ba3 100644
--- a/lib/model/source/collection_source.dart
+++ b/lib/model/source/collection_source.dart
@@ -84,8 +84,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
_visibleEntries = null;
_sortedEntriesByDate = null;
invalidateAlbumFilterSummary(entries: entries);
- invalidateCountryFilterSummary(entries);
- invalidateTagFilterSummary(entries);
+ invalidateCountryFilterSummary(entries: entries);
+ invalidateTagFilterSummary(entries: entries);
}
void updateDerivedFilters([Set? entries]) {
@@ -292,6 +292,18 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
Future refreshEntry(AvesEntry entry, Set dataTypes) async {
await entry.refresh(background: false, persist: true, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale);
+
+ // update/delete in DB
+ final contentId = entry.contentId!;
+ if (dataTypes.contains(EntryDataType.catalog)) {
+ await metadataDb.updateMetadataId(contentId, entry.catalogMetadata);
+ onCatalogMetadataChanged();
+ }
+ if (dataTypes.contains(EntryDataType.address)) {
+ await metadataDb.updateAddressId(contentId, entry.addressDetails);
+ onAddressMetadataChanged();
+ }
+
updateDerivedFilters({entry});
eventBus.fire(EntryRefreshedEvent({entry}));
}
diff --git a/lib/model/source/enums.dart b/lib/model/source/enums.dart
index e39413950..6035caa19 100644
--- a/lib/model/source/enums.dart
+++ b/lib/model/source/enums.dart
@@ -8,4 +8,4 @@ enum EntrySortFactor { date, name, rating, size }
enum EntryGroupFactor { none, album, month, day }
-enum TileLayout { grid, list }
\ No newline at end of file
+enum TileLayout { grid, list }
diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart
index e150ceb9a..dbabb7954 100644
--- a/lib/model/source/location.dart
+++ b/lib/model/source/location.dart
@@ -30,6 +30,12 @@ mixin LocationMixin on SourceBase {
Future locateEntries(AnalysisController controller, Set candidateEntries) async {
await _locateCountries(controller, candidateEntries);
await _locatePlaces(controller, candidateEntries);
+
+ final unlocatedIds = candidateEntries.where((entry) => !entry.hasGps).map((entry) => entry.contentId).whereNotNull().toSet();
+ if (unlocatedIds.isNotEmpty) {
+ await metadataDb.removeIds(unlocatedIds, dataTypes: {EntryDataType.address});
+ onAddressMetadataChanged();
+ }
}
static bool locateCountriesTest(AvesEntry entry) => entry.hasGps && !entry.hasAddress;
@@ -176,16 +182,21 @@ mixin LocationMixin on SourceBase {
final Map _filterEntryCountMap = {};
final Map _filterRecentEntryMap = {};
- void invalidateCountryFilterSummary([Set? entries]) {
+ void invalidateCountryFilterSummary({Set? entries, Set? countryCodes}) {
if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
- Set? countryCodes;
- if (entries == null) {
+ if (entries == null && countryCodes == null) {
_filterEntryCountMap.clear();
_filterRecentEntryMap.clear();
} else {
- countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet();
- countryCodes.forEach(_filterEntryCountMap.remove);
+ countryCodes ??= {};
+ if (entries != null) {
+ countryCodes.addAll(entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull());
+ }
+ countryCodes.forEach((countryCode) {
+ _filterEntryCountMap.remove(countryCode);
+ _filterRecentEntryMap.remove(countryCode);
+ });
}
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));
}
diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart
index 121371d63..16583cc2a 100644
--- a/lib/model/source/tag.dart
+++ b/lib/model/source/tag.dart
@@ -77,16 +77,21 @@ mixin TagMixin on SourceBase {
final Map _filterEntryCountMap = {};
final Map _filterRecentEntryMap = {};
- void invalidateTagFilterSummary([Set? entries]) {
+ void invalidateTagFilterSummary({Set? entries, Set? tags}) {
if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
- Set? tags;
- if (entries == null) {
+ if (entries == null && tags == null) {
_filterEntryCountMap.clear();
_filterRecentEntryMap.clear();
} else {
- tags = entries.where((entry) => entry.isCatalogued).expand((entry) => entry.tags).toSet();
- tags.forEach(_filterEntryCountMap.remove);
+ tags ??= {};
+ if (entries != null) {
+ tags.addAll(entries.where((entry) => entry.isCatalogued).expand((entry) => entry.tags));
+ }
+ tags.forEach((tag) {
+ _filterEntryCountMap.remove(tag);
+ _filterRecentEntryMap.remove(tag);
+ });
}
eventBus.fire(TagSummaryInvalidatedEvent(tags));
}
diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart
index 924d45918..c83759bc4 100644
--- a/lib/model/video/metadata.dart
+++ b/lib/model/video/metadata.dart
@@ -15,14 +15,15 @@ import 'package:aves/theme/format.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves/utils/string_utils.dart';
+import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/viewer/video/fijkplayer.dart';
import 'package:collection/collection.dart';
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart';
class VideoMetadataFormatter {
- static final _epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
- static final _anotherDatePattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}):(\d{2}):(\d{2})');
+ static final _dateY4M2D2H2m2s2Pattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}):(\d{2}):(\d{2})');
+ static final _dateY4M2D2H2m2s2APmPattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2})T(\d+):(\d+):(\d+) ([ap]m)Z');
static final _durationPattern = RegExp(r'(\d+):(\d+):(\d+)(.\d+)');
static final _locationPattern = RegExp(r'([+-][.0-9]+)');
static final Map _codecNames = {
@@ -115,9 +116,10 @@ class VideoMetadataFormatter {
// `DateTime` does not recognize these values found in the wild:
// - `UTC 2021-05-30 19:14:21`
// - `2021/10/31 21:23:17`
+ // - `2021-09-10T7:14:49 pmZ`
// - `2021` (not enough to build a date)
- final match = _anotherDatePattern.firstMatch(dateString);
+ var match = _dateY4M2D2H2m2s2Pattern.firstMatch(dateString);
if (match != null) {
final year = int.tryParse(match.group(1)!);
final month = int.tryParse(match.group(2)!);
@@ -132,6 +134,22 @@ class VideoMetadataFormatter {
}
}
+ match = _dateY4M2D2H2m2s2APmPattern.firstMatch(dateString);
+ if (match != null) {
+ final year = int.tryParse(match.group(1)!);
+ final month = int.tryParse(match.group(2)!);
+ final day = int.tryParse(match.group(3)!);
+ final hour = int.tryParse(match.group(4)!);
+ final minute = int.tryParse(match.group(5)!);
+ final second = int.tryParse(match.group(6)!);
+ final pm = match.group(7) == 'pm';
+
+ if (year != null && month != null && day != null && hour != null && minute != null && second != null) {
+ final date = DateTime(year, month, day, hour + (pm ? 12 : 0), minute, second, 0);
+ return date.millisecondsSinceEpoch;
+ }
+ }
+
return null;
}
@@ -349,7 +367,7 @@ class VideoMetadataFormatter {
static String? _formatDate(String value) {
final date = DateTime.tryParse(value);
if (date == null) return value;
- if (date == _epoch) return null;
+ if (date == epoch) return null;
return date.toIso8601String();
}
diff --git a/lib/ref/exif.dart b/lib/ref/exif.dart
index 26fa9bb6d..cd3ecad91 100644
--- a/lib/ref/exif.dart
+++ b/lib/ref/exif.dart
@@ -1,4 +1,11 @@
class Exif {
+ // constants used by GPS related Exif tags
+ // they are locale independent
+ static const String latitudeNorth = 'N';
+ static const String latitudeSouth = 'S';
+ static const String longitudeEast = 'E';
+ static const String longitudeWest = 'W';
+
static String getColorSpaceDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
diff --git a/lib/ref/iptc.dart b/lib/ref/iptc.dart
index 908f18914..8e88eea70 100644
--- a/lib/ref/iptc.dart
+++ b/lib/ref/iptc.dart
@@ -3,4 +3,4 @@ class IPTC {
// ApplicationRecord tags
static const int keywordsTag = 25;
-}
\ No newline at end of file
+}
diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart
index 2b43ebf5e..978668f31 100644
--- a/lib/ref/mime_types.dart
+++ b/lib/ref/mime_types.dart
@@ -103,4 +103,13 @@ class MimeTypes {
return a == b;
}
}
+
+ static String? forExtension(String extension) {
+ switch (extension) {
+ case '.jpg':
+ return jpeg;
+ case '.svg':
+ return svg;
+ }
+ }
}
diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart
index fa47d60ed..ee0366e31 100644
--- a/lib/services/android_app_service.dart
+++ b/lib/services/android_app_service.dart
@@ -38,7 +38,7 @@ class PlatformAndroidAppService implements AndroidAppService {
Future> getPackages() async {
try {
final result = await platform.invokeMethod('getPackages');
- final packages = (result as List).cast().map((map) => Package.fromMap(map)).toSet();
+ final packages = (result as List).cast().map(Package.fromMap).toSet();
// additional info for known directories
final kakaoTalk = packages.firstWhereOrNull((package) => package.packageName == 'com.kakao.talk');
if (kakaoTalk != null) {
diff --git a/lib/services/common/service_policy.dart b/lib/services/common/service_policy.dart
index 0406f4f29..c8fe1f09b 100644
--- a/lib/services/common/service_policy.dart
+++ b/lib/services/common/service_policy.dart
@@ -66,7 +66,7 @@ class ServicePolicy {
}
}
- LinkedHashMap _getQueue(int priority) => _queues.putIfAbsent(priority, () => LinkedHashMap());
+ LinkedHashMap _getQueue(int priority) => _queues.putIfAbsent(priority, LinkedHashMap.new);
void _pickNext() {
_notifyQueueState();
diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart
index 795571bfb..3a877b3d8 100644
--- a/lib/services/common/services.dart
+++ b/lib/services/common/services.dart
@@ -32,18 +32,18 @@ final StorageService storageService = getIt();
final WindowService windowService = getIt();
void initPlatformServices() {
- getIt.registerLazySingleton(() => p.Context());
- getIt.registerLazySingleton(() => LiveAvesAvailability());
- getIt.registerLazySingleton(() => SqfliteMetadataDb());
+ getIt.registerLazySingleton(p.Context.new);
+ getIt.registerLazySingleton(LiveAvesAvailability.new);
+ getIt.registerLazySingleton(SqfliteMetadataDb.new);
- getIt.registerLazySingleton(() => PlatformAndroidAppService());
- getIt.registerLazySingleton(() => PlatformDeviceService());
- getIt.registerLazySingleton(() => PlatformEmbeddedDataService());
- getIt.registerLazySingleton(() => PlatformMediaFileService());
- getIt.registerLazySingleton(() => PlatformMediaStoreService());
- getIt.registerLazySingleton(() => PlatformMetadataEditService());
- getIt.registerLazySingleton(() => PlatformMetadataFetchService());
- getIt.registerLazySingleton(() => PlatformReportService());
- getIt.registerLazySingleton(() => PlatformStorageService());
- getIt.registerLazySingleton(() => PlatformWindowService());
+ getIt.registerLazySingleton(PlatformAndroidAppService.new);
+ getIt.registerLazySingleton(PlatformDeviceService.new);
+ getIt.registerLazySingleton(PlatformEmbeddedDataService.new);
+ getIt.registerLazySingleton(PlatformMediaFileService.new);
+ getIt.registerLazySingleton(PlatformMediaStoreService.new);
+ getIt.registerLazySingleton(PlatformMetadataEditService.new);
+ getIt.registerLazySingleton(PlatformMetadataFetchService.new);
+ getIt.registerLazySingleton(PlatformReportService.new);
+ getIt.registerLazySingleton(PlatformStorageService.new);
+ getIt.registerLazySingleton(PlatformWindowService.new);
}
diff --git a/lib/services/geocoding_service.dart b/lib/services/geocoding_service.dart
index d411dfea2..28f6990d3 100644
--- a/lib/services/geocoding_service.dart
+++ b/lib/services/geocoding_service.dart
@@ -20,9 +20,9 @@ class GeocodingService {
// returns nothing with `maxResults` of 1, but succeeds with `maxResults` of 2+
'maxResults': 2,
});
- return (result as List).cast().map((map) => Address.fromMap(map)).toList();
+ return (result as List).cast().map(Address.fromMap).toList();
} on PlatformException catch (e, stack) {
- if (e.code != 'getAddress-empty') {
+ if (e.code != 'getAddress-empty' && e.code != 'getAddress-network') {
await reportService.recordError(e, stack);
}
}
diff --git a/lib/services/media/media_file_service.dart b/lib/services/media/media_file_service.dart
index 9c65a6c44..a73e74319 100644
--- a/lib/services/media/media_file_service.dart
+++ b/lib/services/media/media_file_service.dart
@@ -10,6 +10,7 @@ import 'package:aves/services/common/output_buffer.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/enums.dart';
+import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart';
@@ -87,7 +88,7 @@ abstract class MediaFileService {
Stream export(
Iterable entries, {
- required String mimeType,
+ required EntryExportOptions options,
required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy,
});
@@ -325,11 +326,14 @@ class PlatformMediaFileService implements MediaFileService {
required Iterable entries,
}) {
try {
- return _opStreamChannel.receiveBroadcastStream({
- 'op': 'delete',
- 'id': opId,
- 'entries': entries.map(_toPlatformEntryMap).toList(),
- }).map((event) => ImageOpEvent.fromMap(event));
+ return _opStreamChannel
+ .receiveBroadcastStream({
+ 'op': 'delete',
+ 'id': opId,
+ 'entries': entries.map(_toPlatformEntryMap).toList(),
+ })
+ .where((event) => event is Map)
+ .map((event) => ImageOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) {
reportService.recordError(e, stack);
return Stream.error(e);
@@ -345,14 +349,17 @@ class PlatformMediaFileService implements MediaFileService {
required NameConflictStrategy nameConflictStrategy,
}) {
try {
- return _opStreamChannel.receiveBroadcastStream({
- 'op': 'move',
- 'id': opId,
- 'entries': entries.map(_toPlatformEntryMap).toList(),
- 'copy': copy,
- 'destinationPath': destinationAlbum,
- 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
- }).map((event) => MoveOpEvent.fromMap(event));
+ return _opStreamChannel
+ .receiveBroadcastStream({
+ 'op': 'move',
+ 'id': opId,
+ 'entries': entries.map(_toPlatformEntryMap).toList(),
+ 'copy': copy,
+ 'destinationPath': destinationAlbum,
+ 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
+ })
+ .where((event) => event is Map)
+ .map((event) => MoveOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) {
reportService.recordError(e, stack);
return Stream.error(e);
@@ -362,18 +369,23 @@ class PlatformMediaFileService implements MediaFileService {
@override
Stream export(
Iterable entries, {
- required String mimeType,
+ required EntryExportOptions options,
required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy,
}) {
try {
- return _opStreamChannel.receiveBroadcastStream({
- 'op': 'export',
- 'entries': entries.map(_toPlatformEntryMap).toList(),
- 'mimeType': mimeType,
- 'destinationPath': destinationAlbum,
- 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
- }).map((event) => ExportOpEvent.fromMap(event));
+ return _opStreamChannel
+ .receiveBroadcastStream({
+ 'op': 'export',
+ 'entries': entries.map(_toPlatformEntryMap).toList(),
+ 'mimeType': options.mimeType,
+ 'width': options.width,
+ 'height': options.height,
+ 'destinationPath': destinationAlbum,
+ 'nameConflictStrategy': nameConflictStrategy.toPlatform(),
+ })
+ .where((event) => event is Map)
+ .map((event) => ExportOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) {
reportService.recordError(e, stack);
return Stream.error(e);
@@ -386,11 +398,14 @@ class PlatformMediaFileService implements MediaFileService {
required String newName,
}) {
try {
- return _opStreamChannel.receiveBroadcastStream({
- 'op': 'rename',
- 'entries': entries.map(_toPlatformEntryMap).toList(),
- 'newName': newName,
- }).map((event) => MoveOpEvent.fromMap(event));
+ return _opStreamChannel
+ .receiveBroadcastStream({
+ 'op': 'rename',
+ 'entries': entries.map(_toPlatformEntryMap).toList(),
+ 'newName': newName,
+ })
+ .where((event) => event is Map)
+ .map((event) => MoveOpEvent.fromMap(event as Map));
} on PlatformException catch (e, stack) {
reportService.recordError(e, stack);
return Stream.error(e);
@@ -422,3 +437,18 @@ class PlatformMediaFileService implements MediaFileService {
return {};
}
}
+
+@immutable
+class EntryExportOptions extends Equatable {
+ final String mimeType;
+ final int width, height;
+
+ @override
+ List get props => [mimeType, width, height];
+
+ const EntryExportOptions({
+ required this.mimeType,
+ required this.width,
+ required this.height,
+ });
+}
diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart
index 67ccb7b5a..9bb493e92 100644
--- a/lib/services/media/media_store_service.dart
+++ b/lib/services/media/media_store_service.dart
@@ -50,9 +50,12 @@ class PlatformMediaStoreService implements MediaStoreService {
@override
Stream getEntries(Map knownEntries) {
try {
- return _streamChannel.receiveBroadcastStream({
- 'knownEntries': knownEntries,
- }).map((event) => AvesEntry.fromMap(event));
+ return _streamChannel
+ .receiveBroadcastStream({
+ 'knownEntries': knownEntries,
+ })
+ .where((event) => event is Map)
+ .map((event) => AvesEntry.fromMap(event as Map));
} on PlatformException catch (e, stack) {
reportService.recordError(e, stack);
return Stream.error(e);
diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart
index 164b5d7e9..1c40637cd 100644
--- a/lib/services/metadata/metadata_edit_service.dart
+++ b/lib/services/metadata/metadata_edit_service.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/model/metadata/enums.dart';
+import 'package:aves/model/metadata/fields.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
@@ -77,7 +78,7 @@ class PlatformMetadataEditService implements MetadataEditService {
'entry': _toPlatformEntryMap(entry),
'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch,
'shiftMinutes': modifier.shiftMinutes,
- 'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.toExifInterfaceTag()).whereNotNull().toList(),
+ 'fields': modifier.fields.where((v) => v.type == MetadataType.exif).map((v) => v.exifInterfaceTag).whereNotNull().toList(),
});
if (result != null) return (result as Map).cast();
} on PlatformException catch (e, stack) {
diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart
index 5a665af3d..40e6143bf 100644
--- a/lib/services/metadata/metadata_fetch_service.dart
+++ b/lib/services/metadata/metadata_fetch_service.dart
@@ -1,12 +1,13 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/catalog.dart';
-import 'package:aves/model/metadata/enums.dart';
+import 'package:aves/model/metadata/fields.dart';
import 'package:aves/model/metadata/overlay.dart';
import 'package:aves/model/multipage.dart';
import 'package:aves/model/panorama.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/xmp.dart';
+import 'package:aves/utils/time_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@@ -235,9 +236,11 @@ class PlatformMetadataFetchService implements MetadataFetchService {
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
- 'field': field.toExifInterfaceTag(),
+ 'field': field.exifInterfaceTag,
});
- if (result is int) return DateTime.fromMillisecondsSinceEpoch(result);
+ if (result is int) {
+ return dateTimeFromMillis(result, isUtc: false);
+ }
} on PlatformException catch (e, stack) {
if (!entry.isMissingAtPath) {
await reportService.recordError(e, stack);
diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart
index a1fc9bd36..003deb434 100644
--- a/lib/services/storage_service.dart
+++ b/lib/services/storage_service.dart
@@ -47,7 +47,7 @@ class PlatformStorageService implements StorageService {
Future> getStorageVolumes() async {
try {
final result = await platform.invokeMethod('getStorageVolumes');
- return (result as List).cast().map((map) => StorageVolume.fromMap(map)).toSet();
+ return (result as List).cast().map(StorageVolume.fromMap).toSet();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart
index 92909675c..bed800993 100644
--- a/lib/theme/durations.dart
+++ b/lib/theme/durations.dart
@@ -20,9 +20,6 @@ class Durations {
static const appBarTitleAnimation = Duration(milliseconds: 300);
static const appBarActionChangeAnimation = Duration(milliseconds: 200);
- // drawer
- static const newsBadgeAnimation = Duration(milliseconds: 200);
-
// filter grids animations
static const chipDecorationAnimation = Duration(milliseconds: 200);
static const highlightScrollAnimationMinMillis = 400;
@@ -68,9 +65,6 @@ class Durations {
static const contentChangeDebounceDelay = Duration(milliseconds: 1000);
static const mapInfoDebounceDelay = Duration(milliseconds: 150);
static const mapIdleDebounceDelay = Duration(milliseconds: 100);
-
- // app life
- static const lastVersionCheckInterval = Duration(days: 7);
}
class DurationsProvider extends StatelessWidget {
diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart
index 3088114a5..9d8266c28 100644
--- a/lib/utils/constants.dart
+++ b/lib/utils/constants.dart
@@ -22,6 +22,13 @@ class Constants {
)
];
+ // Bidi fun, cf https://www.unicode.org/reports/tr9/
+ // First Strong Isolate
+ static const fsi = '\u2068';
+
+ // Pop Directional Isolate
+ static const pdi = '\u2069';
+
static const overlayUnknown = '—'; // em dash
static final pointNemo = LatLng(-48.876667, -123.393333);
@@ -290,11 +297,6 @@ class Constants {
license: 'MIT',
sourceUrl: 'https://github.com/fluttercommunity/get_it',
),
- Dependency(
- name: 'GitHub',
- license: 'MIT',
- sourceUrl: 'https://github.com/SpinlockLabs/github.dart',
- ),
Dependency(
name: 'Intl',
license: 'BSD 3-Clause',
@@ -325,11 +327,6 @@ class Constants {
license: 'BSD 2-Clause',
sourceUrl: 'https://github.com/google/tuple.dart',
),
- Dependency(
- name: 'Version',
- license: 'BSD 3-Clause',
- sourceUrl: 'https://github.com/dartninja/version',
- ),
Dependency(
name: 'XML',
license: 'MIT',
diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart
index 6980430ba..b3699462f 100644
--- a/lib/utils/time_utils.dart
+++ b/lib/utils/time_utils.dart
@@ -1,3 +1,5 @@
+import 'package:flutter/foundation.dart';
+
extension ExtraDateTime on DateTime {
bool isAtSameYearAs(DateTime? other) => year == other?.year;
@@ -14,6 +16,28 @@ extension ExtraDateTime on DateTime {
bool get isThisYear => isAtSameYearAs(DateTime.now());
}
+final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
+
+// Overflowing timestamps that are supposed to be in milliseconds
+// will be retried after stripping extra digits.
+const _millisMaxDigits = 13; // 13 digits can go up to 2286/11/20
+
+DateTime? dateTimeFromMillis(int? millis, {bool isUtc = false}) {
+ if (millis == null || millis == 0) return null;
+ try {
+ return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: isUtc);
+ } catch (e) {
+ // `DateTime`s can represent time values that are at a distance of at most 100,000,000
+ // days from epoch (1970-01-01 UTC): -271821-04-20 to 275760-09-13.
+ debugPrint('failed to build DateTime from timestamp in millis=$millis');
+ }
+ final digits = '$millis'.length;
+ if (digits > _millisMaxDigits) {
+ millis = int.tryParse('$millis'.substring(0, _millisMaxDigits));
+ return dateTimeFromMillis(millis, isUtc: isUtc);
+ }
+}
+
final _unixStampMillisPattern = RegExp(r'\d{13}');
final _unixStampSecPattern = RegExp(r'\d{10}');
final _plainPattern = RegExp(r'(\d{8})([_-\s](\d{6})([_-\s](\d{3}))?)?');
@@ -23,22 +47,22 @@ DateTime? parseUnknownDateFormat(String? s) {
var match = _unixStampMillisPattern.firstMatch(s);
if (match != null) {
- final stampString = match.group(0);
- if (stampString != null) {
- final stampMillis = int.tryParse(stampString);
+ final stampMillisString = match.group(0);
+ if (stampMillisString != null) {
+ final stampMillis = int.tryParse(stampMillisString);
if (stampMillis != null) {
- return DateTime.fromMillisecondsSinceEpoch(stampMillis, isUtc: false);
+ return dateTimeFromMillis(stampMillis, isUtc: false);
}
}
}
match = _unixStampSecPattern.firstMatch(s);
if (match != null) {
- final stampString = match.group(0);
- if (stampString != null) {
- final stampMillis = int.tryParse(stampString);
- if (stampMillis != null) {
- return DateTime.fromMillisecondsSinceEpoch(stampMillis * 1000, isUtc: false);
+ final stampSecString = match.group(0);
+ if (stampSecString != null) {
+ final stampSec = int.tryParse(stampSecString);
+ if (stampSec != null) {
+ return dateTimeFromMillis(stampSec * 1000, isUtc: false);
}
}
}
diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart
index 7e124c114..f87841ab4 100644
--- a/lib/widgets/about/about_page.dart
+++ b/lib/widgets/about/about_page.dart
@@ -2,7 +2,6 @@ import 'package:aves/widgets/about/app_ref.dart';
import 'package:aves/widgets/about/bug_report.dart';
import 'package:aves/widgets/about/credits.dart';
import 'package:aves/widgets/about/licenses.dart';
-import 'package:aves/widgets/about/update.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
@@ -27,7 +26,6 @@ class AboutPage extends StatelessWidget {
const [
AppReference(),
Divider(),
- AboutUpdate(),
BugReport(),
Divider(),
AboutCredits(),
diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart
index db9195c68..978123c10 100644
--- a/lib/widgets/about/bug_report.dart
+++ b/lib/widgets/about/bug_report.dart
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:aves/app_flavor.dart';
import 'package:aves/flutter_version.dart';
+import 'package:aves/model/settings/settings.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
@@ -28,6 +29,7 @@ class BugReport extends StatefulWidget {
}
class _BugReportState extends State with FeedbackMixin {
+ final ScrollController _infoScrollController = ScrollController();
late Future _infoLoader;
bool _showInstructions = false;
@@ -71,16 +73,39 @@ class _BugReportState extends State with FeedbackMixin {
final info = snapshot.data;
if (info == null) return const SizedBox();
return Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- color: Colors.grey.shade800,
- border: Border.all(
- color: Colors.white,
- ),
- borderRadius: const BorderRadius.all(Radius.circular(8)),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade800,
+ border: Border.all(
+ color: Colors.white,
),
- margin: const EdgeInsets.symmetric(vertical: 8),
- child: SelectableText(info));
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
+ ),
+ constraints: const BoxConstraints(maxHeight: 100),
+ margin: const EdgeInsets.symmetric(vertical: 8),
+ child: Theme(
+ data: Theme.of(context).copyWith(
+ scrollbarTheme: const ScrollbarThemeData(
+ isAlwaysShown: true,
+ radius: Radius.circular(16),
+ crossAxisMargin: 6,
+ mainAxisMargin: 6,
+ interactive: true,
+ ),
+ ),
+ child: Scrollbar(
+ // when using `Scrollbar.isAlwaysShown`, a controller must be provided
+ // and used by both the `Scrollbar` and the `Scrollable`, but
+ // as of Flutter v2.8.1, `SelectableText` does not allow passing the `scrollController`
+ // so we wrap it in a `SingleChildScrollView`
+ controller: _infoScrollController,
+ child: SingleChildScrollView(
+ padding: const EdgeInsetsDirectional.only(start: 8, top: 4, end: 16, bottom: 4),
+ controller: _infoScrollController,
+ child: SelectableText(info),
+ ),
+ ),
+ ),
+ );
},
),
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
@@ -118,7 +143,7 @@ class _BugReportState extends State with FeedbackMixin {
AvesOutlinedButton(
label: buttonText,
onPressed: onPressed,
- )
+ ),
],
),
);
@@ -136,6 +161,8 @@ class _BugReportState extends State with FeedbackMixin {
'Android build: ${androidInfo.display}',
'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}',
+ 'System locales: ${WidgetsBinding.instance!.window.locales.join(', ')}',
+ 'Aves locale: ${settings.locale} -> ${settings.appliedLocale}',
].join('\n');
}
@@ -162,6 +189,6 @@ class _BugReportState extends State with FeedbackMixin {
}
Future _goToGithub() async {
- await launch('${Constants.avesGithub}/issues/new');
+ await launch('${Constants.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md');
}
}
diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart
index 35251d760..67ef9be7c 100644
--- a/lib/widgets/about/credits.dart
+++ b/lib/widgets/about/credits.dart
@@ -9,6 +9,7 @@ class AboutCredits extends StatelessWidget {
static const translators = {
'Deutsch': 'JanWaldhorn',
'Español (México)': 'n-berenice',
+ 'Português (Brasil)': 'Jonatas De Almeida Barros',
'Русский': 'D3ZOXY',
};
diff --git a/lib/widgets/about/news_badge.dart b/lib/widgets/about/news_badge.dart
deleted file mode 100644
index c0557a82e..000000000
--- a/lib/widgets/about/news_badge.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import 'package:flutter/material.dart';
-
-class AboutNewsBadge extends StatelessWidget {
- const AboutNewsBadge({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return const Icon(
- Icons.circle,
- size: 12,
- color: Colors.red,
- );
- }
-}
diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart
index fbfa3adcc..67c12597c 100644
--- a/lib/widgets/about/policy_page.dart
+++ b/lib/widgets/about/policy_page.dart
@@ -17,10 +17,13 @@ class PolicyPage extends StatefulWidget {
class _PolicyPageState extends State {
late Future _termsLoader;
+ static const termsPath = 'assets/terms.md';
+ static const termsDirection = TextDirection.ltr;
+
@override
void initState() {
super.initState();
- _termsLoader = rootBundle.loadString('assets/terms.md');
+ _termsLoader = rootBundle.loadString(termsPath);
}
@override
@@ -38,7 +41,10 @@ class _PolicyPageState extends State {
final terms = snapshot.data!;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
- child: MarkdownContainer(data: terms),
+ child: MarkdownContainer(
+ data: terms,
+ textDirection: termsDirection,
+ ),
);
},
),
diff --git a/lib/widgets/about/update.dart b/lib/widgets/about/update.dart
deleted file mode 100644
index 8401c170d..000000000
--- a/lib/widgets/about/update.dart
+++ /dev/null
@@ -1,93 +0,0 @@
-import 'package:aves/services/common/services.dart';
-import 'package:aves/utils/constants.dart';
-import 'package:aves/widgets/about/news_badge.dart';
-import 'package:aves/widgets/common/basic/link_chip.dart';
-import 'package:aves/widgets/common/extensions/build_context.dart';
-import 'package:flutter/material.dart';
-
-class AboutUpdate extends StatefulWidget {
- const AboutUpdate({Key? key}) : super(key: key);
-
- @override
- _AboutUpdateState createState() => _AboutUpdateState();
-}
-
-class _AboutUpdateState extends State {
- late Future _updateChecker;
-
- @override
- void initState() {
- super.initState();
- _updateChecker = availability.isNewVersionAvailable;
- }
-
- @override
- Widget build(BuildContext context) {
- return FutureBuilder(
- future: _updateChecker,
- builder: (context, snapshot) {
- final newVersionAvailable = snapshot.data == true;
- if (!newVersionAvailable) return const SizedBox();
- return Column(
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- ConstrainedBox(
- constraints: const BoxConstraints(minHeight: 48),
- child: Align(
- alignment: AlignmentDirectional.centerStart,
- child: Text.rich(
- TextSpan(
- children: [
- const WidgetSpan(
- child: Padding(
- padding: EdgeInsetsDirectional.only(end: 8),
- child: AboutNewsBadge(),
- ),
- alignment: PlaceholderAlignment.middle,
- ),
- TextSpan(text: context.l10n.aboutUpdate, style: Constants.titleTextStyle),
- ],
- ),
- ),
- ),
- ),
- Text.rich(
- TextSpan(
- children: [
- TextSpan(text: context.l10n.aboutUpdateLinks1),
- WidgetSpan(
- child: LinkChip(
- text: context.l10n.aboutUpdateGitHub,
- url: '${Constants.avesGithub}/releases',
- textStyle: const TextStyle(fontWeight: FontWeight.bold),
- ),
- alignment: PlaceholderAlignment.middle,
- ),
- TextSpan(text: context.l10n.aboutUpdateLinks2),
- WidgetSpan(
- child: LinkChip(
- text: context.l10n.aboutUpdateGooglePlay,
- url: 'https://play.google.com/store/apps/details?id=deckers.thibault.aves',
- textStyle: const TextStyle(fontWeight: FontWeight.bold),
- ),
- alignment: PlaceholderAlignment.middle,
- ),
- TextSpan(text: context.l10n.aboutUpdateLinks3),
- ],
- ),
- ),
- const SizedBox(height: 16),
- ],
- ),
- ),
- const Divider(),
- ],
- );
- },
- );
- }
-}
diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart
index ef35eb8fb..995ad2040 100644
--- a/lib/widgets/aves_app.dart
+++ b/lib/widgets/aves_app.dart
@@ -129,8 +129,6 @@ class _AvesAppState extends State {
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
- // checkerboardRasterCacheImages: true,
- // checkerboardOffscreenLayers: true,
);
},
);
diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart
index e8934726f..7ba7144ff 100644
--- a/lib/widgets/collection/app_bar.dart
+++ b/lib/widgets/collection/app_bar.dart
@@ -16,6 +16,7 @@ import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/collection/query_bar.dart';
+import 'package:aves/widgets/common/animated_icons_fix.dart';
import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart';
@@ -164,8 +165,9 @@ class _CollectionAppBarState extends State with SingleTickerPr
return IconButton(
// key is expected by test driver
key: const Key('appbar-leading-button'),
- icon: AnimatedIcon(
- icon: AnimatedIcons.menu_arrow,
+ // TODO TLAD [rtl] replace to regular `AnimatedIcon` when this is fixed: https://github.com/flutter/flutter/issues/60521
+ icon: AnimatedIconFixIssue60521(
+ icon: AnimatedIconsFixIssue60521.menu_arrow,
progress: _browseToSelectAnimation,
),
onPressed: onPressed,
@@ -252,6 +254,7 @@ class _CollectionAppBarState extends State with SingleTickerPr
_buildRotateAndFlipMenuItems(context, canApply: canApply),
...[
EntrySetAction.editDate,
+ EntrySetAction.editLocation,
EntrySetAction.editRating,
EntrySetAction.editTags,
EntrySetAction.removeMetadata,
@@ -426,17 +429,18 @@ class _CollectionAppBarState extends State with SingleTickerPr
// browsing or selecting
case EntrySetAction.map:
case EntrySetAction.stats:
+ case EntrySetAction.rescan:
// selecting
case EntrySetAction.share:
case EntrySetAction.delete:
case EntrySetAction.copy:
case EntrySetAction.move:
- case EntrySetAction.rescan:
case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW:
case EntrySetAction.flip:
case EntrySetAction.editDate:
+ case EntrySetAction.editLocation:
case EntrySetAction.editRating:
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:
diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart
index b1d647b86..4041019b6 100644
--- a/lib/widgets/collection/collection_grid.dart
+++ b/lib/widgets/collection/collection_grid.dart
@@ -192,6 +192,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main);
final selector = GridSelectionGestureDetector(
+ scrollableKey: scrollableKey,
selectable: isMainMode,
items: collection.sortedEntries,
scrollController: scrollController,
@@ -238,6 +239,7 @@ class _CollectionScaler extends StatelessWidget {
borderWidth: DecoratedThumbnail.borderWidth,
borderRadius: Radius.zero,
color: DecoratedThumbnail.borderColor,
+ textDirection: Directionality.of(context),
),
child: child,
),
diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart
index dc5b75c6f..db0e0d242 100644
--- a/lib/widgets/collection/entry_set_action_delegate.dart
+++ b/lib/widgets/collection/entry_set_action_delegate.dart
@@ -67,18 +67,19 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
// browsing or selecting
case EntrySetAction.map:
case EntrySetAction.stats:
+ case EntrySetAction.rescan:
return appMode == AppMode.main;
// selecting
case EntrySetAction.share:
case EntrySetAction.delete:
case EntrySetAction.copy:
case EntrySetAction.move:
- case EntrySetAction.rescan:
case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW:
case EntrySetAction.flip:
case EntrySetAction.editDate:
+ case EntrySetAction.editLocation:
case EntrySetAction.editRating:
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:
@@ -110,18 +111,19 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
return true;
case EntrySetAction.map:
case EntrySetAction.stats:
+ case EntrySetAction.rescan:
return (!isSelecting && hasItems) || (isSelecting && hasSelection);
// selecting
case EntrySetAction.share:
case EntrySetAction.delete:
case EntrySetAction.copy:
case EntrySetAction.move:
- case EntrySetAction.rescan:
case EntrySetAction.toggleFavourite:
case EntrySetAction.rotateCCW:
case EntrySetAction.rotateCW:
case EntrySetAction.flip:
case EntrySetAction.editDate:
+ case EntrySetAction.editLocation:
case EntrySetAction.editRating:
case EntrySetAction.editTags:
case EntrySetAction.removeMetadata:
@@ -154,6 +156,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.stats:
_goToStats(context);
break;
+ case EntrySetAction.rescan:
+ _rescan(context);
+ break;
// selecting
case EntrySetAction.share:
_share(context);
@@ -167,9 +172,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.move:
_move(context, moveType: MoveType.move);
break;
- case EntrySetAction.rescan:
- _rescan(context);
- break;
case EntrySetAction.toggleFavourite:
_toggleFavourite(context);
break;
@@ -185,6 +187,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.editDate:
_editDate(context);
break;
+ case EntrySetAction.editLocation:
+ _editLocation(context);
+ break;
case EntrySetAction.editRating:
_editRating(context);
break;
@@ -210,12 +215,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
}
void _rescan(BuildContext context) {
- final source = context.read();
final selection = context.read>();
- final selectedItems = _getExpandedSelectedItems(selection);
+ final collection = context.read();
+ final entries = (selection.isSelecting ? _getExpandedSelectedItems(selection) : collection.sortedEntries.toSet());
final controller = AnalysisController(canStartService: true, force: true);
- source.analyze(controller, entries: selectedItems);
+ collection.source.analyze(controller, entries: entries);
selection.browse();
}
@@ -428,6 +433,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: todoItems)) return;
+ Set obsoleteTags = todoItems.expand((entry) => entry.tags).toSet();
+ Set obsoleteCountryCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet();
+
final source = context.read();
source.pauseMonitoring();
var cancelled = false;
@@ -448,7 +456,18 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final editedOps = successOps.where((e) => !e.skipped).toSet();
selection.browse();
source.resumeMonitoring();
- unawaited(source.refreshUris(editedOps.map((v) => v.uri).toSet()));
+
+ unawaited(source.refreshUris(editedOps.map((v) => v.uri).toSet()).then((_) {
+ // invalidate filters derived from values before edition
+ // this invalidation must happen after the source is refreshed,
+ // otherwise filter chips may eagerly rebuild in between with the old state
+ if (obsoleteCountryCodes.isNotEmpty) {
+ source.invalidateCountryFilterSummary(countryCodes: obsoleteCountryCodes);
+ }
+ if (obsoleteTags.isNotEmpty) {
+ source.invalidateTagFilterSummary(tags: obsoleteTags);
+ }
+ }));
final l10n = context.l10n;
final successCount = successOps.length;
@@ -536,6 +555,20 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
await _edit(context, selection, todoItems, (entry) => entry.editDate(modifier));
}
+ Future _editLocation(BuildContext context) async {
+ final collection = context.read();
+ final selection = context.read>();
+ final selectedItems = _getExpandedSelectedItems(selection);
+
+ final todoItems = await _getEditableItems(context, selectedItems: selectedItems, canEdit: (entry) => entry.canEditLocation);
+ if (todoItems == null || todoItems.isEmpty) return;
+
+ final location = await selectLocation(context, todoItems, collection);
+ if (location == null) return;
+
+ await _edit(context, selection, todoItems, (entry) => entry.editLocation(location));
+ }
+
Future _editRating(BuildContext context) async {
final selection = context.read>();
final selectedItems = _getExpandedSelectedItems(selection);
diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart
index d73656a2f..76f265e2a 100644
--- a/lib/widgets/collection/filter_bar.dart
+++ b/lib/widgets/collection/filter_bar.dart
@@ -90,7 +90,7 @@ class _FilterBarState extends State {
initialItemCount: widget.filters.length,
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
- padding: const EdgeInsets.only(left: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 4),
itemBuilder: (context, index, animation) {
if (index >= widget.filters.length) return const SizedBox();
return _buildChip(widget.filters.toList()[index]);
@@ -102,7 +102,7 @@ class _FilterBarState extends State {
Padding _buildChip(CollectionFilter filter) {
return Padding(
- padding: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 4),
child: Center(
child: AvesFilterChip(
key: ValueKey(filter),
diff --git a/lib/widgets/collection/grid/headers/date.dart b/lib/widgets/collection/grid/headers/date.dart
index adc11af73..abce6c9dd 100644
--- a/lib/widgets/collection/grid/headers/date.dart
+++ b/lib/widgets/collection/grid/headers/date.dart
@@ -65,7 +65,7 @@ class MonthSectionHeader extends StatelessWidget {
if (date == null) return l10n.sectionUnknown;
if (date.isThisMonth) return l10n.dateThisMonth;
final locale = l10n.localeName;
- final localized = date.isThisYear? DateFormat.MMMM(locale).format(date) : DateFormat.yMMMM(locale).format(date);
+ final localized = date.isThisYear ? DateFormat.MMMM(locale).format(date) : DateFormat.yMMMM(locale).format(date);
return '${localized.substring(0, 1).toUpperCase()}${localized.substring(1)}';
}
diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart
index 149ffe83a..5170b5327 100644
--- a/lib/widgets/collection/grid/list_details.dart
+++ b/lib/widgets/collection/grid/list_details.dart
@@ -25,7 +25,7 @@ class EntryListDetails extends StatelessWidget {
return Container(
padding: EntryListDetailsTheme.contentPadding,
foregroundDecoration: BoxDecoration(
- border: Border(top: AvesBorder.side),
+ border: Border(top: AvesBorder.straightSide),
),
margin: EntryListDetailsTheme.contentMargin,
child: IconTheme.merge(
diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart
index 9b84e7d32..6e67e4865 100644
--- a/lib/widgets/common/action_mixins/entry_editor.dart
+++ b/lib/widgets/common/action_mixins/entry_editor.dart
@@ -1,14 +1,17 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/model/metadata/enums.dart';
+import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
-import 'package:aves/widgets/dialogs/entry_editors/edit_entry_date_dialog.dart';
-import 'package:aves/widgets/dialogs/entry_editors/edit_entry_rating_dialog.dart';
-import 'package:aves/widgets/dialogs/entry_editors/edit_entry_tags_dialog.dart';
-import 'package:aves/widgets/dialogs/entry_editors/remove_entry_metadata_dialog.dart';
+import 'package:aves/widgets/dialogs/entry_editors/edit_date_dialog.dart';
+import 'package:aves/widgets/dialogs/entry_editors/edit_location_dialog.dart';
+import 'package:aves/widgets/dialogs/entry_editors/edit_rating_dialog.dart';
+import 'package:aves/widgets/dialogs/entry_editors/edit_tags_dialog.dart';
+import 'package:aves/widgets/dialogs/entry_editors/remove_metadata_dialog.dart';
import 'package:flutter/material.dart';
+import 'package:latlong2/latlong.dart';
mixin EntryEditorMixin {
Future selectDateModifier(BuildContext context, Set entries) async {
@@ -23,10 +26,23 @@ mixin EntryEditorMixin {
return modifier;
}
+ Future selectLocation(BuildContext context, Set entries, CollectionLens? collection) async {
+ if (entries.isEmpty) return null;
+
+ final location = await showDialog(
+ context: context,
+ builder: (context) => EditEntryLocationDialog(
+ entry: entries.first,
+ collection: collection,
+ ),
+ );
+ return location;
+ }
+
Future selectRating(BuildContext context, Set entries) async {
if (entries.isEmpty) return null;
- final rating = await showDialog(
+ final rating = await showDialog(
context: context,
builder: (context) => EditEntryRatingDialog(
entry: entries.first,
diff --git a/lib/widgets/common/animated_icons_fix.dart b/lib/widgets/common/animated_icons_fix.dart
new file mode 100644
index 000000000..505364bc2
--- /dev/null
+++ b/lib/widgets/common/animated_icons_fix.dart
@@ -0,0 +1,1315 @@
+// TODO TLAD [rtl] remove the whole file when this is fixed: https://github.com/flutter/flutter/issues/60521
+// as of Flutter v2.8.1, mirrored animated icon is misplaced
+// cf PR https://github.com/flutter/flutter/pull/93312
+
+// ignore_for_file: constant_identifier_names, curly_braces_in_flow_control_structures, unnecessary_null_comparison
+import 'dart:math' as math show pi;
+import 'dart:ui' as ui show Paint, Path, Canvas;
+import 'dart:ui' show lerpDouble;
+
+import 'package:flutter/widgets.dart';
+
+abstract class AnimatedIconData {
+ /// Abstract const constructor. This constructor enables subclasses to provide
+ /// const constructors so that they can be used in const expressions.
+ const AnimatedIconData();
+
+ /// Whether this icon should be mirrored horizontally when text direction is
+ /// right-to-left.
+ ///
+ /// See also:
+ ///
+ /// * [TextDirection], which discusses concerns regarding reading direction
+ /// in Flutter.
+ /// * [Directionality], a widget which determines the ambient directionality.
+ bool get matchTextDirection;
+}
+
+class _AnimatedIconData extends AnimatedIconData {
+ const _AnimatedIconData(this.size, this.paths, {this.matchTextDirection = false});
+
+ final Size size;
+ final List<_PathFrames> paths;
+
+ @override
+ final bool matchTextDirection;
+}
+
+class AnimatedIconFixIssue60521 extends StatelessWidget {
+ /// Creates an AnimatedIcon.
+ ///
+ /// The [progress] and [icon] arguments must not be null.
+ /// The [size] and [color] default to the value given by the current [IconTheme].
+ const AnimatedIconFixIssue60521({
+ Key? key,
+ required this.icon,
+ required this.progress,
+ this.color,
+ this.size,
+ this.semanticLabel,
+ this.textDirection,
+ }) : assert(progress != null),
+ assert(icon != null),
+ super(key: key);
+
+ /// The animation progress for the animated icon.
+ ///
+ /// The value is clamped to be between 0 and 1.
+ ///
+ /// This determines the actual frame that is displayed.
+ final Animation progress;
+
+ /// The color to use when drawing the icon.
+ ///
+ /// Defaults to the current [IconTheme] color, if any.
+ ///
+ /// The given color will be adjusted by the opacity of the current
+ /// [IconTheme], if any.
+ ///
+ /// In material apps, if there is a [Theme] without any [IconTheme]s
+ /// specified, icon colors default to white if the theme is dark
+ /// and black if the theme is light.
+ ///
+ /// If no [IconTheme] and no [Theme] is specified, icons will default to black.
+ ///
+ /// See [Theme] to set the current theme and [ThemeData.brightness]
+ /// for setting the current theme's brightness.
+ final Color? color;
+
+ /// The size of the icon in logical pixels.
+ ///
+ /// Icons occupy a square with width and height equal to size.
+ ///
+ /// Defaults to the current [IconTheme] size.
+ final double? size;
+
+ /// The icon to display. Available icons are listed in [AnimatedIcons].
+ final AnimatedIconData icon;
+
+ /// Semantic label for the icon.
+ ///
+ /// Announced in accessibility modes (e.g TalkBack/VoiceOver).
+ /// This label does not show in the UI.
+ ///
+ /// See also:
+ ///
+ /// * [SemanticsProperties.label], which is set to [semanticLabel] in the
+ /// underlying [Semantics] widget.
+ final String? semanticLabel;
+
+ /// The text direction to use for rendering the icon.
+ ///
+ /// If this is null, the ambient [Directionality] is used instead.
+ ///
+ /// If the text direction is [TextDirection.rtl], the icon will be mirrored
+ /// horizontally (e.g back arrow will point right).
+ final TextDirection? textDirection;
+
+ static ui.Path _pathFactory() => ui.Path();
+
+ @override
+ Widget build(BuildContext context) {
+ assert(debugCheckHasDirectionality(context));
+ final _AnimatedIconData iconData = icon as _AnimatedIconData;
+ final IconThemeData iconTheme = IconTheme.of(context);
+ assert(iconTheme.isConcrete);
+ final double iconSize = size ?? iconTheme.size!;
+ final TextDirection textDirection = this.textDirection ?? Directionality.of(context);
+ final double iconOpacity = iconTheme.opacity!;
+ Color iconColor = color ?? iconTheme.color!;
+ if (iconOpacity != 1.0) iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity);
+ return Semantics(
+ label: semanticLabel,
+ child: CustomPaint(
+ size: Size(iconSize, iconSize),
+ painter: _AnimatedIconPainter(
+ paths: iconData.paths,
+ progress: progress,
+ color: iconColor,
+ scale: iconSize / iconData.size.width,
+ shouldMirror: textDirection == TextDirection.rtl && iconData.matchTextDirection,
+ uiPathFactory: _pathFactory,
+ ),
+ ),
+ );
+ }
+}
+
+typedef _UiPathFactory = ui.Path Function();
+
+class _AnimatedIconPainter extends CustomPainter {
+ _AnimatedIconPainter({
+ required this.paths,
+ required this.progress,
+ required this.color,
+ required this.scale,
+ required this.shouldMirror,
+ required this.uiPathFactory,
+ }) : super(repaint: progress);
+
+ // This list is assumed to be immutable, changes to the contents of the list
+ // will not trigger a redraw as shouldRepaint will keep returning false.
+ final List<_PathFrames> paths;
+ final Animation progress;
+ final Color color;
+ final double scale;
+
+ /// If this is true the image will be mirrored horizontally.
+ final bool shouldMirror;
+ final _UiPathFactory uiPathFactory;
+
+ @override
+ void paint(ui.Canvas canvas, Size size) {
+ // The RenderCustomPaint render object performs canvas.save before invoking
+ // this and canvas.restore after, so we don't need to do it here.
+ if (shouldMirror) {
+ canvas.rotate(math.pi);
+ canvas.translate(-size.width, -size.height);
+ }
+ canvas.scale(scale, scale);
+
+ final double clampedProgress = progress.value.clamp(0.0, 1.0);
+ for (final _PathFrames path in paths) path.paint(canvas, color, uiPathFactory, clampedProgress);
+ }
+
+ @override
+ bool shouldRepaint(_AnimatedIconPainter oldDelegate) {
+ return oldDelegate.progress.value != progress.value ||
+ oldDelegate.color != color
+ // We are comparing the paths list by reference, assuming the list is
+ // treated as immutable to be more efficient.
+ ||
+ oldDelegate.paths != paths ||
+ oldDelegate.scale != scale ||
+ oldDelegate.uiPathFactory != uiPathFactory;
+ }
+
+ @override
+ bool? hitTest(Offset position) => null;
+
+ @override
+ bool shouldRebuildSemantics(CustomPainter oldDelegate) => false;
+
+ @override
+ SemanticsBuilderCallback? get semanticsBuilder => null;
+}
+
+class _PathFrames {
+ const _PathFrames({
+ required this.commands,
+ required this.opacities,
+ });
+
+ final List<_PathCommand> commands;
+ final List opacities;
+
+ void paint(ui.Canvas canvas, Color color, _UiPathFactory uiPathFactory, double progress) {
+ final double opacity = _interpolate(opacities, progress, lerpDouble)!;
+ final ui.Paint paint = ui.Paint()
+ ..style = PaintingStyle.fill
+ ..color = color.withOpacity(color.opacity * opacity);
+ final ui.Path path = uiPathFactory();
+ for (final _PathCommand command in commands) command.apply(path, progress);
+ canvas.drawPath(path, paint);
+ }
+}
+
+abstract class _PathCommand {
+ const _PathCommand();
+
+ /// Applies the path command to [path].
+ ///
+ /// For example if the object is a [_PathMoveTo] command it will invoke
+ /// [Path.moveTo] on [path].
+ void apply(ui.Path path, double progress);
+}
+
+class _PathMoveTo extends _PathCommand {
+ const _PathMoveTo(this.points);
+
+ final List points;
+
+ @override
+ void apply(Path path, double progress) {
+ final Offset offset = _interpolate(points, progress, Offset.lerp)!;
+ path.moveTo(offset.dx, offset.dy);
+ }
+}
+
+class _PathCubicTo extends _PathCommand {
+ const _PathCubicTo(this.controlPoints1, this.controlPoints2, this.targetPoints);
+
+ final List controlPoints2;
+ final List controlPoints1;
+ final List targetPoints;
+
+ @override
+ void apply(Path path, double progress) {
+ final Offset controlPoint1 = _interpolate(controlPoints1, progress, Offset.lerp)!;
+ final Offset controlPoint2 = _interpolate(controlPoints2, progress, Offset.lerp)!;
+ final Offset targetPoint = _interpolate(targetPoints, progress, Offset.lerp)!;
+ path.cubicTo(
+ controlPoint1.dx,
+ controlPoint1.dy,
+ controlPoint2.dx,
+ controlPoint2.dy,
+ targetPoint.dx,
+ targetPoint.dy,
+ );
+ }
+}
+
+// ignore: unused_element
+class _PathLineTo extends _PathCommand {
+ const _PathLineTo(this.points);
+
+ final List points;
+
+ @override
+ void apply(Path path, double progress) {
+ final Offset point = _interpolate(points, progress, Offset.lerp)!;
+ path.lineTo(point.dx, point.dy);
+ }
+}
+
+class _PathClose extends _PathCommand {
+ const _PathClose();
+
+ @override
+ void apply(Path path, double progress) {
+ path.close();
+ }
+}
+
+T _interpolate(List values, double progress, _Interpolator interpolator) {
+ assert(progress <= 1.0);
+ assert(progress >= 0.0);
+ if (values.length == 1) return values[0];
+ final double targetIdx = lerpDouble(0, values.length - 1, progress)!;
+ final int lowIdx = targetIdx.floor();
+ final int highIdx = targetIdx.ceil();
+ final double t = targetIdx - lowIdx;
+ return interpolator(values[lowIdx], values[highIdx], t);
+}
+
+typedef _Interpolator = T Function(T a, T b, double progress);
+
+abstract class AnimatedIconsFixIssue60521 {
+ static const AnimatedIconData menu_arrow = _AnimatedIconData(
+ Size(48.0, 48.0),
+ <_PathFrames>[
+ _PathFrames(
+ opacities: [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ ],
+ commands: <_PathCommand>[
+ _PathMoveTo(
+ [
+ Offset(6.0, 26.0),
+ Offset(5.976562557689849, 25.638185989482512),
+ Offset(5.951781669661045, 24.367972149512962),
+ Offset(6.172793116155802, 21.823631861702058),
+ Offset(7.363587976838016, 17.665129222832853),
+ Offset(11.400806749308899, 11.800457098273661),
+ Offset(17.41878573585796, 8.03287301910486),
+ Offset(24.257523532175192, 6.996159828679087),
+ Offset(29.90338248135665, 8.291042849526),
+ Offset(33.76252909490214, 10.56619705548221),
+ Offset(36.23501636298456, 12.973675163618006),
+ Offset(37.77053540180521, 15.158665125787222),
+ Offset(38.70420448893307, 17.008159945496722),
+ Offset(39.260392038988186, 18.5104805430827),
+ Offset(39.58393261852967, 19.691668944482075),
+ Offset(39.766765502294305, 20.58840471665747),
+ Offset(39.866421084642994, 21.237322746452932),
+ Offset(39.91802804639694, 21.671102155152063),
+ Offset(39.94204075298555, 21.917555098992118),
+ Offset(39.94920417650143, 21.999827480806236),
+ Offset(39.94921875, 22.0),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(6.0, 26.0),
+ Offset(5.976562557689849, 25.638185989482512),
+ Offset(5.951781669661045, 24.367972149512962),
+ Offset(6.172793116155802, 21.823631861702058),
+ Offset(7.363587976838016, 17.665129222832853),
+ Offset(11.400806749308899, 11.800457098273661),
+ Offset(17.41878573585796, 8.03287301910486),
+ Offset(24.257523532175192, 6.996159828679087),
+ Offset(29.90338248135665, 8.291042849526),
+ Offset(33.76252909490214, 10.56619705548221),
+ Offset(36.23501636298456, 12.973675163618006),
+ Offset(37.77053540180521, 15.158665125787222),
+ Offset(38.70420448893307, 17.008159945496722),
+ Offset(39.260392038988186, 18.5104805430827),
+ Offset(39.58393261852967, 19.691668944482075),
+ Offset(39.766765502294305, 20.58840471665747),
+ Offset(39.866421084642994, 21.237322746452932),
+ Offset(39.91802804639694, 21.671102155152063),
+ Offset(39.94204075298555, 21.917555098992118),
+ Offset(39.94920417650143, 21.999827480806236),
+ Offset(39.94921875, 22.0),
+ ],
+ [
+ Offset(42.0, 26.0),
+ Offset(41.91421333157091, 26.360426629492423),
+ Offset(41.55655262500356, 27.60382930516768),
+ Offset(40.57766190556539, 29.99090297157744),
+ Offset(38.19401046368096, 33.57567286235671),
+ Offset(32.70215654116029, 37.756226919427284),
+ Offset(26.22621984436523, 39.26167875408963),
+ Offset(20.102351173097617, 38.04803275423973),
+ Offset(15.903199608216863, 35.25316524725598),
+ Offset(13.57741782841064, 32.27000071222682),
+ Offset(12.442030802775209, 29.665215617986277),
+ Offset(11.981806515947115, 27.560177578292762),
+ Offset(11.879421136842055, 25.918712565594948),
+ Offset(11.95091483982305, 24.66543021784112),
+ Offset(12.092167805674123, 23.72603017548901),
+ Offset(12.245452640806768, 23.03857447590349),
+ Offset(12.379956070248545, 22.554583229506296),
+ Offset(12.480582865035407, 22.237279988168645),
+ Offset(12.541514124262473, 22.059212079933666),
+ Offset(12.562455771803593, 22.000123717314214),
+ Offset(12.562499999999996, 22.000000000000004),
+ ],
+ [
+ Offset(42.0, 26.0),
+ Offset(41.91421333157091, 26.360426629492423),
+ Offset(41.55655262500356, 27.60382930516768),
+ Offset(40.57766190556539, 29.99090297157744),
+ Offset(38.19401046368096, 33.57567286235671),
+ Offset(32.70215654116029, 37.756226919427284),
+ Offset(26.22621984436523, 39.26167875408963),
+ Offset(20.102351173097617, 38.04803275423973),
+ Offset(15.903199608216863, 35.25316524725598),
+ Offset(13.57741782841064, 32.27000071222682),
+ Offset(12.442030802775209, 29.665215617986277),
+ Offset(11.981806515947115, 27.560177578292762),
+ Offset(11.879421136842055, 25.918712565594948),
+ Offset(11.95091483982305, 24.66543021784112),
+ Offset(12.092167805674123, 23.72603017548901),
+ Offset(12.245452640806768, 23.03857447590349),
+ Offset(12.379956070248545, 22.554583229506296),
+ Offset(12.480582865035407, 22.237279988168645),
+ Offset(12.541514124262473, 22.059212079933666),
+ Offset(12.562455771803593, 22.000123717314214),
+ Offset(12.562499999999996, 22.000000000000004),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(42.0, 26.0),
+ Offset(41.91421333157091, 26.360426629492423),
+ Offset(41.55655262500356, 27.60382930516768),
+ Offset(40.57766190556539, 29.99090297157744),
+ Offset(38.19401046368096, 33.57567286235671),
+ Offset(32.70215654116029, 37.756226919427284),
+ Offset(26.22621984436523, 39.26167875408963),
+ Offset(20.102351173097617, 38.04803275423973),
+ Offset(15.903199608216863, 35.25316524725598),
+ Offset(13.57741782841064, 32.27000071222682),
+ Offset(12.442030802775209, 29.665215617986277),
+ Offset(11.981806515947115, 27.560177578292762),
+ Offset(11.879421136842055, 25.918712565594948),
+ Offset(11.95091483982305, 24.66543021784112),
+ Offset(12.092167805674123, 23.72603017548901),
+ Offset(12.245452640806768, 23.03857447590349),
+ Offset(12.379956070248545, 22.554583229506296),
+ Offset(12.480582865035407, 22.237279988168645),
+ Offset(12.541514124262473, 22.059212079933666),
+ Offset(12.562455771803593, 22.000123717314214),
+ Offset(12.562499999999996, 22.000000000000004),
+ ],
+ [
+ Offset(42.0, 22.0),
+ Offset(41.99458528858859, 22.361234167441474),
+ Offset(41.91859127809106, 23.620246996030513),
+ Offset(41.501535596836376, 26.09905798461081),
+ Offset(40.02840620381446, 30.021099432452637),
+ Offset(35.79419835461124, 35.2186537827727),
+ Offset(30.076040790179817, 38.175916954629336),
+ Offset(24.067012730992623, 38.57855959743385),
+ Offset(19.453150566288006, 37.096490556388844),
+ Offset(16.506465839286186, 34.99409280868502),
+ Offset(14.73924581501028, 32.939784778587686),
+ Offset(13.715334530064114, 31.165018854170466),
+ Offset(13.140377980959201, 29.714761542791386),
+ Offset(12.83036672005031, 28.56755327976071),
+ Offset(12.672939622830032, 27.683643609921106),
+ Offset(12.600162038813565, 27.02281609043513),
+ Offset(12.571432188039635, 26.54999771317575),
+ Offset(12.56310619400641, 26.23642863509033),
+ Offset(12.562193301685781, 26.059158626029138),
+ Offset(12.562499038934627, 26.000123717080207),
+ Offset(12.562499999999996, 26.000000000000004),
+ ],
+ [
+ Offset(42.0, 22.0),
+ Offset(41.99458528858859, 22.361234167441474),
+ Offset(41.91859127809106, 23.620246996030513),
+ Offset(41.501535596836376, 26.09905798461081),
+ Offset(40.02840620381446, 30.021099432452637),
+ Offset(35.79419835461124, 35.2186537827727),
+ Offset(30.076040790179817, 38.175916954629336),
+ Offset(24.067012730992623, 38.57855959743385),
+ Offset(19.453150566288006, 37.096490556388844),
+ Offset(16.506465839286186, 34.99409280868502),
+ Offset(14.73924581501028, 32.939784778587686),
+ Offset(13.715334530064114, 31.165018854170466),
+ Offset(13.140377980959201, 29.714761542791386),
+ Offset(12.83036672005031, 28.56755327976071),
+ Offset(12.672939622830032, 27.683643609921106),
+ Offset(12.600162038813565, 27.02281609043513),
+ Offset(12.571432188039635, 26.54999771317575),
+ Offset(12.56310619400641, 26.23642863509033),
+ Offset(12.562193301685781, 26.059158626029138),
+ Offset(12.562499038934627, 26.000123717080207),
+ Offset(12.562499999999996, 26.000000000000004),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(42.0, 22.0),
+ Offset(41.99458528858859, 22.361234167441474),
+ Offset(41.91859127809106, 23.620246996030513),
+ Offset(41.501535596836376, 26.09905798461081),
+ Offset(40.02840620381446, 30.021099432452637),
+ Offset(35.79419835461124, 35.2186537827727),
+ Offset(30.076040790179817, 38.175916954629336),
+ Offset(24.067012730992623, 38.57855959743385),
+ Offset(19.453150566288006, 37.096490556388844),
+ Offset(16.506465839286186, 34.99409280868502),
+ Offset(14.73924581501028, 32.939784778587686),
+ Offset(13.715334530064114, 31.165018854170466),
+ Offset(13.140377980959201, 29.714761542791386),
+ Offset(12.83036672005031, 28.56755327976071),
+ Offset(12.672939622830032, 27.683643609921106),
+ Offset(12.600162038813565, 27.02281609043513),
+ Offset(12.571432188039635, 26.54999771317575),
+ Offset(12.56310619400641, 26.23642863509033),
+ Offset(12.562193301685781, 26.059158626029138),
+ Offset(12.562499038934627, 26.000123717080207),
+ Offset(12.562499999999996, 26.000000000000004),
+ ],
+ [
+ Offset(6.0, 22.0),
+ Offset(6.056934514707525, 21.63899352743156),
+ Offset(6.3138203227485405, 20.384389840375796),
+ Offset(7.096666807426793, 17.931786874735423),
+ Offset(9.197983716971518, 14.110555792928775),
+ Offset(14.492848562759846, 9.262883961619078),
+ Offset(21.26860668167255, 6.947111219644562),
+ Offset(28.222185090070198, 7.526686671873211),
+ Offset(33.453333439427794, 10.134368158658866),
+ Offset(36.69157710577769, 13.290289151940406),
+ Offset(38.53223137521963, 16.248244324219414),
+ Offset(39.50406341592221, 18.763506401664923),
+ Offset(39.965161333050226, 20.80420892269316),
+ Offset(40.139843919215444, 22.41260360500229),
+ Offset(40.164704435685586, 23.649282378914172),
+ Offset(40.1214749003011, 24.572646331189105),
+ Offset(40.057897202434084, 25.232737230122385),
+ Offset(40.00055137536795, 25.670250802073745),
+ Offset(39.96271993040885, 25.917501645087587),
+ Offset(39.949247443632466, 25.99982748057223),
+ Offset(39.94921875, 26.0),
+ ],
+ [
+ Offset(6.0, 22.0),
+ Offset(6.056934514707525, 21.63899352743156),
+ Offset(6.3138203227485405, 20.384389840375796),
+ Offset(7.096666807426793, 17.931786874735423),
+ Offset(9.197983716971518, 14.110555792928775),
+ Offset(14.492848562759846, 9.262883961619078),
+ Offset(21.26860668167255, 6.947111219644562),
+ Offset(28.222185090070198, 7.526686671873211),
+ Offset(33.453333439427794, 10.134368158658866),
+ Offset(36.69157710577769, 13.290289151940406),
+ Offset(38.53223137521963, 16.248244324219414),
+ Offset(39.50406341592221, 18.763506401664923),
+ Offset(39.965161333050226, 20.80420892269316),
+ Offset(40.139843919215444, 22.41260360500229),
+ Offset(40.164704435685586, 23.649282378914172),
+ Offset(40.1214749003011, 24.572646331189105),
+ Offset(40.057897202434084, 25.232737230122385),
+ Offset(40.00055137536795, 25.670250802073745),
+ Offset(39.96271993040885, 25.917501645087587),
+ Offset(39.949247443632466, 25.99982748057223),
+ Offset(39.94921875, 26.0),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(6.0, 22.0),
+ Offset(6.056934514707525, 21.63899352743156),
+ Offset(6.3138203227485405, 20.384389840375796),
+ Offset(7.096666807426793, 17.931786874735423),
+ Offset(9.197983716971518, 14.110555792928775),
+ Offset(14.492848562759846, 9.262883961619078),
+ Offset(21.26860668167255, 6.947111219644562),
+ Offset(28.222185090070198, 7.526686671873211),
+ Offset(33.453333439427794, 10.134368158658866),
+ Offset(36.69157710577769, 13.290289151940406),
+ Offset(38.53223137521963, 16.248244324219414),
+ Offset(39.50406341592221, 18.763506401664923),
+ Offset(39.965161333050226, 20.80420892269316),
+ Offset(40.139843919215444, 22.41260360500229),
+ Offset(40.164704435685586, 23.649282378914172),
+ Offset(40.1214749003011, 24.572646331189105),
+ Offset(40.057897202434084, 25.232737230122385),
+ Offset(40.00055137536795, 25.670250802073745),
+ Offset(39.96271993040885, 25.917501645087587),
+ Offset(39.949247443632466, 25.99982748057223),
+ Offset(39.94921875, 26.0),
+ ],
+ [
+ Offset(6.0, 26.0),
+ Offset(5.976562557689849, 25.638185989482512),
+ Offset(5.951781669661045, 24.367972149512962),
+ Offset(6.172793116155802, 21.823631861702058),
+ Offset(7.363587976838016, 17.665129222832853),
+ Offset(11.400806749308899, 11.800457098273661),
+ Offset(17.41878573585796, 8.03287301910486),
+ Offset(24.257523532175192, 6.996159828679087),
+ Offset(29.90338248135665, 8.291042849526),
+ Offset(33.76252909490214, 10.56619705548221),
+ Offset(36.23501636298456, 12.973675163618006),
+ Offset(37.77053540180521, 15.158665125787222),
+ Offset(38.70420448893307, 17.008159945496722),
+ Offset(39.260392038988186, 18.5104805430827),
+ Offset(39.58393261852967, 19.691668944482075),
+ Offset(39.766765502294305, 20.58840471665747),
+ Offset(39.866421084642994, 21.237322746452932),
+ Offset(39.91802804639694, 21.671102155152063),
+ Offset(39.94204075298555, 21.917555098992118),
+ Offset(39.94920417650143, 21.999827480806236),
+ Offset(39.94921875, 22.0),
+ ],
+ [
+ Offset(6.0, 26.0),
+ Offset(5.976562557689849, 25.638185989482512),
+ Offset(5.951781669661045, 24.367972149512962),
+ Offset(6.172793116155802, 21.823631861702058),
+ Offset(7.363587976838016, 17.665129222832853),
+ Offset(11.400806749308899, 11.800457098273661),
+ Offset(17.41878573585796, 8.03287301910486),
+ Offset(24.257523532175192, 6.996159828679087),
+ Offset(29.90338248135665, 8.291042849526),
+ Offset(33.76252909490214, 10.56619705548221),
+ Offset(36.23501636298456, 12.973675163618006),
+ Offset(37.77053540180521, 15.158665125787222),
+ Offset(38.70420448893307, 17.008159945496722),
+ Offset(39.260392038988186, 18.5104805430827),
+ Offset(39.58393261852967, 19.691668944482075),
+ Offset(39.766765502294305, 20.58840471665747),
+ Offset(39.866421084642994, 21.237322746452932),
+ Offset(39.91802804639694, 21.671102155152063),
+ Offset(39.94204075298555, 21.917555098992118),
+ Offset(39.94920417650143, 21.999827480806236),
+ Offset(39.94921875, 22.0),
+ ],
+ ),
+ _PathClose(),
+ ],
+ ),
+ _PathFrames(
+ opacities: [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ ],
+ commands: <_PathCommand>[
+ _PathMoveTo(
+ [
+ Offset(6.0, 36.0),
+ Offset(5.8396336833594695, 35.66398057820908),
+ Offset(5.329309336374063, 34.47365089829387),
+ Offset(4.546341863759643, 32.03857491308836),
+ Offset(3.9472816617934896, 27.893335303194206),
+ Offset(4.788314785722232, 21.470485758169694),
+ Offset(7.406922551234356, 16.186721598040453),
+ Offset(10.987511722222681, 12.449414121983239),
+ Offset(14.290737577882037, 10.382465570533384),
+ Offset(16.84152025666389, 9.340052761292668),
+ Offset(18.753361861843203, 8.79207829497377),
+ Offset(20.19495897321279, 8.483469022255434),
+ Offset(21.293826339887335, 8.297708512391797),
+ Offset(22.135385178177998, 8.180000583359465),
+ Offset(22.776244370552647, 8.102975309903787),
+ Offset(23.25488929254563, 8.051973096906334),
+ Offset(23.598629725699347, 8.018606137477462),
+ Offset(23.827700643867974, 7.99783596371886),
+ Offset(23.95771797811348, 7.986559676107813),
+ Offset(24.001111438945117, 7.982878122631195),
+ Offset(24.001202429357242, 7.98287044589657),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(6.0, 36.0),
+ Offset(5.8396336833594695, 35.66398057820908),
+ Offset(5.329309336374063, 34.47365089829387),
+ Offset(4.546341863759643, 32.03857491308836),
+ Offset(3.9472816617934896, 27.893335303194206),
+ Offset(4.788314785722232, 21.470485758169694),
+ Offset(7.406922551234356, 16.186721598040453),
+ Offset(10.987511722222681, 12.449414121983239),
+ Offset(14.290737577882037, 10.382465570533384),
+ Offset(16.84152025666389, 9.340052761292668),
+ Offset(18.753361861843203, 8.79207829497377),
+ Offset(20.19495897321279, 8.483469022255434),
+ Offset(21.293826339887335, 8.297708512391797),
+ Offset(22.135385178177998, 8.180000583359465),
+ Offset(22.776244370552647, 8.102975309903787),
+ Offset(23.25488929254563, 8.051973096906334),
+ Offset(23.598629725699347, 8.018606137477462),
+ Offset(23.827700643867974, 7.99783596371886),
+ Offset(23.95771797811348, 7.986559676107813),
+ Offset(24.001111438945117, 7.982878122631195),
+ Offset(24.001202429357242, 7.98287044589657),
+ ],
+ [
+ Offset(42.0, 36.0),
+ Offset(41.7493389152824, 36.20520796529164),
+ Offset(40.85819701033384, 36.89246335931071),
+ Offset(39.01294315759756, 38.1256246432051),
+ Offset(35.758514239960064, 39.76970128020763),
+ Offset(30.180134511403956, 41.28645636464381),
+ Offset(24.56603417073137, 41.32925393403815),
+ Offset(19.271926095830622, 39.91690773672663),
+ Offset(15.201959304751512, 37.5726832793895),
+ Offset(12.456295622648877, 35.01429311055303),
+ Offset(10.686459838185314, 32.608514843335385),
+ Offset(9.579921816288039, 30.502293804851334),
+ Offset(8.90802993167501, 28.734147272525124),
+ Offset(8.513791284564158, 27.294928344333726),
+ Offset(8.292240475325507, 26.156988797411067),
+ Offset(8.174465865426919, 25.287693028463128),
+ Offset(8.11616441641861, 24.655137447505503),
+ Offset(8.089821190085125, 24.230473791307258),
+ Offset(8.079382709319852, 23.988506993748523),
+ Offset(8.076631388780909, 23.907616552409003),
+ Offset(8.076626005900048, 23.907446869353766),
+ ],
+ [
+ Offset(42.0, 36.0),
+ Offset(41.7493389152824, 36.20520796529164),
+ Offset(40.85819701033384, 36.89246335931071),
+ Offset(39.01294315759756, 38.1256246432051),
+ Offset(35.758514239960064, 39.76970128020763),
+ Offset(30.180134511403956, 41.28645636464381),
+ Offset(24.56603417073137, 41.32925393403815),
+ Offset(19.271926095830622, 39.91690773672663),
+ Offset(15.201959304751512, 37.5726832793895),
+ Offset(12.456295622648877, 35.01429311055303),
+ Offset(10.686459838185314, 32.608514843335385),
+ Offset(9.579921816288039, 30.502293804851334),
+ Offset(8.90802993167501, 28.734147272525124),
+ Offset(8.513791284564158, 27.294928344333726),
+ Offset(8.292240475325507, 26.156988797411067),
+ Offset(8.174465865426919, 25.287693028463128),
+ Offset(8.11616441641861, 24.655137447505503),
+ Offset(8.089821190085125, 24.230473791307258),
+ Offset(8.079382709319852, 23.988506993748523),
+ Offset(8.076631388780909, 23.907616552409003),
+ Offset(8.076626005900048, 23.907446869353766),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(42.0, 36.0),
+ Offset(41.7493389152824, 36.20520796529164),
+ Offset(40.85819701033384, 36.89246335931071),
+ Offset(39.01294315759756, 38.1256246432051),
+ Offset(35.758514239960064, 39.76970128020763),
+ Offset(30.180134511403956, 41.28645636464381),
+ Offset(24.56603417073137, 41.32925393403815),
+ Offset(19.271926095830622, 39.91690773672663),
+ Offset(15.201959304751512, 37.5726832793895),
+ Offset(12.456295622648877, 35.01429311055303),
+ Offset(10.686459838185314, 32.608514843335385),
+ Offset(9.579921816288039, 30.502293804851334),
+ Offset(8.90802993167501, 28.734147272525124),
+ Offset(8.513791284564158, 27.294928344333726),
+ Offset(8.292240475325507, 26.156988797411067),
+ Offset(8.174465865426919, 25.287693028463128),
+ Offset(8.11616441641861, 24.655137447505503),
+ Offset(8.089821190085125, 24.230473791307258),
+ Offset(8.079382709319852, 23.988506993748523),
+ Offset(8.076631388780909, 23.907616552409003),
+ Offset(8.076626005900048, 23.907446869353766),
+ ],
+ [
+ Offset(42.0, 32.0),
+ Offset(41.803966700752746, 32.205577011286266),
+ Offset(41.104447603276626, 32.89996903899956),
+ Offset(39.64402995767152, 34.17517788052204),
+ Offset(37.031973302731046, 35.97545970343111),
+ Offset(32.44508133022271, 37.98012671725157),
+ Offset(27.6644042246058, 38.77327245743646),
+ Offset(22.963108117227325, 38.302914175295534),
+ Offset(19.18039906547299, 36.862333955479784),
+ Offset(16.509090720567585, 35.04434211490934),
+ Offset(14.703380298498667, 33.21759365821649),
+ Offset(13.512146444284534, 31.556733263561572),
+ Offset(12.740174664860898, 30.12862517729895),
+ Offset(12.248059307884624, 28.947244716051806),
+ Offset(11.939734974297815, 28.002595790430043),
+ Offset(11.750425410476474, 27.27521551305395),
+ Offset(11.637314290474384, 26.742992599694542),
+ Offset(11.572897732210654, 26.384358993735816),
+ Offset(11.54031155133882, 26.17955109507089),
+ Offset(11.530083003283234, 26.111009046369567),
+ Offset(11.530061897030713, 26.110865227715482),
+ ],
+ [
+ Offset(42.0, 32.0),
+ Offset(41.803966700752746, 32.205577011286266),
+ Offset(41.104447603276626, 32.89996903899956),
+ Offset(39.64402995767152, 34.17517788052204),
+ Offset(37.031973302731046, 35.97545970343111),
+ Offset(32.44508133022271, 37.98012671725157),
+ Offset(27.6644042246058, 38.77327245743646),
+ Offset(22.963108117227325, 38.302914175295534),
+ Offset(19.18039906547299, 36.862333955479784),
+ Offset(16.509090720567585, 35.04434211490934),
+ Offset(14.703380298498667, 33.21759365821649),
+ Offset(13.512146444284534, 31.556733263561572),
+ Offset(12.740174664860898, 30.12862517729895),
+ Offset(12.248059307884624, 28.947244716051806),
+ Offset(11.939734974297815, 28.002595790430043),
+ Offset(11.750425410476474, 27.27521551305395),
+ Offset(11.637314290474384, 26.742992599694542),
+ Offset(11.572897732210654, 26.384358993735816),
+ Offset(11.54031155133882, 26.17955109507089),
+ Offset(11.530083003283234, 26.111009046369567),
+ Offset(11.530061897030713, 26.110865227715482),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(42.0, 32.0),
+ Offset(41.803966700752746, 32.205577011286266),
+ Offset(41.104447603276626, 32.89996903899956),
+ Offset(39.64402995767152, 34.17517788052204),
+ Offset(37.031973302731046, 35.97545970343111),
+ Offset(32.44508133022271, 37.98012671725157),
+ Offset(27.6644042246058, 38.77327245743646),
+ Offset(22.963108117227325, 38.302914175295534),
+ Offset(19.18039906547299, 36.862333955479784),
+ Offset(16.509090720567585, 35.04434211490934),
+ Offset(14.703380298498667, 33.21759365821649),
+ Offset(13.512146444284534, 31.556733263561572),
+ Offset(12.740174664860898, 30.12862517729895),
+ Offset(12.248059307884624, 28.947244716051806),
+ Offset(11.939734974297815, 28.002595790430043),
+ Offset(11.750425410476474, 27.27521551305395),
+ Offset(11.637314290474384, 26.742992599694542),
+ Offset(11.572897732210654, 26.384358993735816),
+ Offset(11.54031155133882, 26.17955109507089),
+ Offset(11.530083003283234, 26.111009046369567),
+ Offset(11.530061897030713, 26.110865227715482),
+ ],
+ [
+ Offset(6.0, 32.0),
+ Offset(5.899914425897517, 31.66443482499171),
+ Offset(5.601001082666045, 30.482888615847468),
+ Offset(5.242005036683729, 28.09953280239226),
+ Offset(5.346316156571252, 24.145975901906155),
+ Offset(7.249241148069178, 18.317100047682345),
+ Offset(10.710823881370487, 13.931896549234073),
+ Offset(14.817117889097364, 11.294374466111893),
+ Offset(18.288493245756, 10.248489378687303),
+ Offset(20.784419638077317, 10.013509863155594),
+ Offset(22.541938014255397, 10.075312777589325),
+ Offset(23.798109358346892, 10.220508832423288),
+ Offset(24.71461203122786, 10.370924674281323),
+ Offset(25.392890381083, 10.501349297587215),
+ Offset(25.896277759611298, 10.60605174724228),
+ Offset(26.265268043339944, 10.685909272436422),
+ Offset(26.526795349038366, 10.74364670273436),
+ Offset(26.699555102368272, 10.782158496973931),
+ Offset(26.79709065296033, 10.80399872839147),
+ Offset(26.829561509459538, 10.811282301423006),
+ Offset(26.829629554119695, 10.811297570626497),
+ ],
+ [
+ Offset(6.0, 32.0),
+ Offset(5.899914425897517, 31.66443482499171),
+ Offset(5.601001082666045, 30.482888615847468),
+ Offset(5.242005036683729, 28.09953280239226),
+ Offset(5.346316156571252, 24.145975901906155),
+ Offset(7.249241148069178, 18.317100047682345),
+ Offset(10.710823881370487, 13.931896549234073),
+ Offset(14.817117889097364, 11.294374466111893),
+ Offset(18.288493245756, 10.248489378687303),
+ Offset(20.784419638077317, 10.013509863155594),
+ Offset(22.541938014255397, 10.075312777589325),
+ Offset(23.798109358346892, 10.220508832423288),
+ Offset(24.71461203122786, 10.370924674281323),
+ Offset(25.392890381083, 10.501349297587215),
+ Offset(25.896277759611298, 10.60605174724228),
+ Offset(26.265268043339944, 10.685909272436422),
+ Offset(26.526795349038366, 10.74364670273436),
+ Offset(26.699555102368272, 10.782158496973931),
+ Offset(26.79709065296033, 10.80399872839147),
+ Offset(26.829561509459538, 10.811282301423006),
+ Offset(26.829629554119695, 10.811297570626497),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(6.0, 32.0),
+ Offset(5.899914425897517, 31.66443482499171),
+ Offset(5.601001082666045, 30.482888615847468),
+ Offset(5.242005036683729, 28.09953280239226),
+ Offset(5.346316156571252, 24.145975901906155),
+ Offset(7.249241148069178, 18.317100047682345),
+ Offset(10.710823881370487, 13.931896549234073),
+ Offset(14.817117889097364, 11.294374466111893),
+ Offset(18.288493245756, 10.248489378687303),
+ Offset(20.784419638077317, 10.013509863155594),
+ Offset(22.541938014255397, 10.075312777589325),
+ Offset(23.798109358346892, 10.220508832423288),
+ Offset(24.71461203122786, 10.370924674281323),
+ Offset(25.392890381083, 10.501349297587215),
+ Offset(25.896277759611298, 10.60605174724228),
+ Offset(26.265268043339944, 10.685909272436422),
+ Offset(26.526795349038366, 10.74364670273436),
+ Offset(26.699555102368272, 10.782158496973931),
+ Offset(26.79709065296033, 10.80399872839147),
+ Offset(26.829561509459538, 10.811282301423006),
+ Offset(26.829629554119695, 10.811297570626497),
+ ],
+ [
+ Offset(6.0, 36.0),
+ Offset(5.839633683308566, 35.66398057820831),
+ Offset(5.329309336323984, 34.47365089829046),
+ Offset(4.546341863735712, 32.03857491308413),
+ Offset(3.947281661825336, 27.893335303206097),
+ Offset(4.788314785746671, 21.47048575818877),
+ Offset(7.406922551270995, 16.18672159809414),
+ Offset(10.98751172223972, 12.449414122039723),
+ Offset(14.290737577881032, 10.382465570503403),
+ Offset(16.841520256655304, 9.340052761342939),
+ Offset(18.753361861827802, 8.792078295019234),
+ Offset(20.194958973207576, 8.483469022266245),
+ Offset(21.293826339889407, 8.297708512388375),
+ Offset(22.13538517817335, 8.180000583365981),
+ Offset(22.776244370563283, 8.102975309890528),
+ Offset(23.25488929251534, 8.051973096940955),
+ Offset(23.598629725644848, 8.018606137536025),
+ Offset(23.82770064384222, 7.997835963745423),
+ Offset(23.957717978081078, 7.986559676140466),
+ Offset(24.001111438940168, 7.982878122636148),
+ Offset(24.001202429373503, 7.982870445880305),
+ ],
+ [
+ Offset(6.0, 36.0),
+ Offset(5.839633683308566, 35.66398057820831),
+ Offset(5.329309336323984, 34.47365089829046),
+ Offset(4.546341863735712, 32.03857491308413),
+ Offset(3.947281661825336, 27.893335303206097),
+ Offset(4.788314785746671, 21.47048575818877),
+ Offset(7.406922551270995, 16.18672159809414),
+ Offset(10.98751172223972, 12.449414122039723),
+ Offset(14.290737577881032, 10.382465570503403),
+ Offset(16.841520256655304, 9.340052761342939),
+ Offset(18.753361861827802, 8.792078295019234),
+ Offset(20.194958973207576, 8.483469022266245),
+ Offset(21.293826339889407, 8.297708512388375),
+ Offset(22.13538517817335, 8.180000583365981),
+ Offset(22.776244370563283, 8.102975309890528),
+ Offset(23.25488929251534, 8.051973096940955),
+ Offset(23.598629725644848, 8.018606137536025),
+ Offset(23.82770064384222, 7.997835963745423),
+ Offset(23.957717978081078, 7.986559676140466),
+ Offset(24.001111438940168, 7.982878122636148),
+ Offset(24.001202429373503, 7.982870445880305),
+ ],
+ ),
+ _PathClose(),
+ ],
+ ),
+ _PathFrames(
+ opacities: [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ ],
+ commands: <_PathCommand>[
+ _PathMoveTo(
+ [
+ Offset(6.0, 16.0),
+ Offset(6.222470088677106, 15.614531066984553),
+ Offset(7.071161725316092, 14.306422712262563),
+ Offset(9.085869786142727, 11.907139949336411),
+ Offset(13.311519331212619, 8.711520321213257),
+ Offset(21.694206315186374, 6.462423500731354),
+ Offset(30.07031570748504, 8.471955170698632),
+ Offset(36.20036889900587, 14.155750775196541),
+ Offset(38.533897479983715, 20.76099122996903),
+ Offset(38.182626701431914, 26.194302454359914),
+ Offset(36.59711302702814, 30.110286603895076),
+ Offset(34.63761335058528, 32.76106836363335),
+ Offset(32.7272901891386, 34.4927008221791),
+ Offset(31.04869117038896, 35.596105690451935),
+ Offset(29.664526028757855, 36.28441549314729),
+ Offset(28.581655311555835, 36.70452225851578),
+ Offset(27.782897949107628, 36.95396775456513),
+ Offset(27.242531133855476, 37.09522522130338),
+ Offset(26.933380541033216, 37.166375518103024),
+ Offset(26.82984682779076, 37.188656481991416),
+ Offset(26.829629554103434, 37.18870242935725),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(6.0, 16.0),
+ Offset(6.222470088677106, 15.614531066984553),
+ Offset(7.071161725316092, 14.306422712262563),
+ Offset(9.085869786142727, 11.907139949336411),
+ Offset(13.311519331212619, 8.711520321213257),
+ Offset(21.694206315186374, 6.462423500731354),
+ Offset(30.07031570748504, 8.471955170698632),
+ Offset(36.20036889900587, 14.155750775196541),
+ Offset(38.533897479983715, 20.76099122996903),
+ Offset(38.182626701431914, 26.194302454359914),
+ Offset(36.59711302702814, 30.110286603895076),
+ Offset(34.63761335058528, 32.76106836363335),
+ Offset(32.7272901891386, 34.4927008221791),
+ Offset(31.04869117038896, 35.596105690451935),
+ Offset(29.664526028757855, 36.28441549314729),
+ Offset(28.581655311555835, 36.70452225851578),
+ Offset(27.782897949107628, 36.95396775456513),
+ Offset(27.242531133855476, 37.09522522130338),
+ Offset(26.933380541033216, 37.166375518103024),
+ Offset(26.82984682779076, 37.188656481991416),
+ Offset(26.829629554103434, 37.18870242935725),
+ ],
+ [
+ Offset(42.0, 16.0),
+ Offset(42.119273441095075, 16.516374018071716),
+ Offset(42.428662704565184, 18.32937541467259),
+ Offset(42.54812490043565, 21.94159775950881),
+ Offset(41.3111285319893, 27.683594454682137),
+ Offset(36.06395079582478, 35.01020271691918),
+ Offset(28.59459512599702, 38.51093769070532),
+ Offset(21.239886122259133, 38.07233071493643),
+ Offset(16.251628495692138, 35.34156866251391),
+ Offset(13.527101819238178, 32.27103394597236),
+ Offset(12.16858814546228, 29.604397296366464),
+ Offset(11.548946515009288, 27.474331231158473),
+ Offset(11.311114637013635, 25.826563435488687),
+ Offset(11.262012546535352, 24.572239162454554),
+ Offset(11.298221100690522, 23.63118177535833),
+ Offset(11.364474416879979, 22.940254245947138),
+ Offset(11.431638843687892, 22.451805922237554),
+ Offset(11.485090012547001, 22.130328573710905),
+ Offset(11.518417313485447, 21.949395273355513),
+ Offset(11.530012405933167, 21.889264075838188),
+ Offset(11.53003696527787, 21.889138124802937),
+ ],
+ [
+ Offset(42.0, 16.0),
+ Offset(42.119273441095075, 16.516374018071716),
+ Offset(42.428662704565184, 18.32937541467259),
+ Offset(42.54812490043565, 21.94159775950881),
+ Offset(41.3111285319893, 27.683594454682137),
+ Offset(36.06395079582478, 35.01020271691918),
+ Offset(28.59459512599702, 38.51093769070532),
+ Offset(21.239886122259133, 38.07233071493643),
+ Offset(16.251628495692138, 35.34156866251391),
+ Offset(13.527101819238178, 32.27103394597236),
+ Offset(12.16858814546228, 29.604397296366464),
+ Offset(11.548946515009288, 27.474331231158473),
+ Offset(11.311114637013635, 25.826563435488687),
+ Offset(11.262012546535352, 24.572239162454554),
+ Offset(11.298221100690522, 23.63118177535833),
+ Offset(11.364474416879979, 22.940254245947138),
+ Offset(11.431638843687892, 22.451805922237554),
+ Offset(11.485090012547001, 22.130328573710905),
+ Offset(11.518417313485447, 21.949395273355513),
+ Offset(11.530012405933167, 21.889264075838188),
+ Offset(11.53003696527787, 21.889138124802937),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(42.0, 16.0),
+ Offset(42.119273441095075, 16.516374018071716),
+ Offset(42.428662704565184, 18.32937541467259),
+ Offset(42.54812490043565, 21.94159775950881),
+ Offset(41.3111285319893, 27.683594454682137),
+ Offset(36.06395079582478, 35.01020271691918),
+ Offset(28.59459512599702, 38.51093769070532),
+ Offset(21.239886122259133, 38.07233071493643),
+ Offset(16.251628495692138, 35.34156866251391),
+ Offset(13.527101819238178, 32.27103394597236),
+ Offset(12.16858814546228, 29.604397296366464),
+ Offset(11.548946515009288, 27.474331231158473),
+ Offset(11.311114637013635, 25.826563435488687),
+ Offset(11.262012546535352, 24.572239162454554),
+ Offset(11.298221100690522, 23.63118177535833),
+ Offset(11.364474416879979, 22.940254245947138),
+ Offset(11.431638843687892, 22.451805922237554),
+ Offset(11.485090012547001, 22.130328573710905),
+ Offset(11.518417313485447, 21.949395273355513),
+ Offset(11.530012405933167, 21.889264075838188),
+ Offset(11.53003696527787, 21.889138124802937),
+ ],
+ [
+ Offset(42.0, 12.0),
+ Offset(42.22538630246601, 12.517777761542249),
+ Offset(42.90619853384615, 14.357900907446863),
+ Offset(43.759884509852945, 18.128995147835514),
+ Offset(43.66585885175813, 24.44736028078141),
+ Offset(39.74861752085834, 33.43380529842439),
+ Offset(32.57188683977151, 39.07136996422343),
+ Offset(24.376857043988256, 40.600018479197814),
+ Offset(17.959269400168804, 39.004426856660785),
+ Offset(13.850567169499653, 36.311009998593796),
+ Offset(11.374155956344177, 33.58880277176081),
+ Offset(9.917496515696001, 31.204288894581083),
+ Offset(9.07498759074148, 29.236785710939074),
+ Offset(8.597571742452605, 27.666692096657314),
+ Offset(8.334783321442917, 26.44693980672826),
+ Offset(8.195874559699876, 25.52824222288586),
+ Offset(8.126295299747222, 24.866824239052814),
+ Offset(8.093843447379264, 24.426077640310794),
+ Offset(8.080338503727083, 24.17611706018137),
+ Offset(8.076619249177135, 24.092742069165425),
+ Offset(8.07661186374038, 24.09256727275783),
+ ],
+ [
+ Offset(42.0, 12.0),
+ Offset(42.22538630246601, 12.517777761542249),
+ Offset(42.90619853384615, 14.357900907446863),
+ Offset(43.759884509852945, 18.128995147835514),
+ Offset(43.66585885175813, 24.44736028078141),
+ Offset(39.74861752085834, 33.43380529842439),
+ Offset(32.57188683977151, 39.07136996422343),
+ Offset(24.376857043988256, 40.600018479197814),
+ Offset(17.959269400168804, 39.004426856660785),
+ Offset(13.850567169499653, 36.311009998593796),
+ Offset(11.374155956344177, 33.58880277176081),
+ Offset(9.917496515696001, 31.204288894581083),
+ Offset(9.07498759074148, 29.236785710939074),
+ Offset(8.597571742452605, 27.666692096657314),
+ Offset(8.334783321442917, 26.44693980672826),
+ Offset(8.195874559699876, 25.52824222288586),
+ Offset(8.126295299747222, 24.866824239052814),
+ Offset(8.093843447379264, 24.426077640310794),
+ Offset(8.080338503727083, 24.17611706018137),
+ Offset(8.076619249177135, 24.092742069165425),
+ Offset(8.07661186374038, 24.09256727275783),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(42.0, 12.0),
+ Offset(42.22538630246601, 12.517777761542249),
+ Offset(42.90619853384615, 14.357900907446863),
+ Offset(43.759884509852945, 18.128995147835514),
+ Offset(43.66585885175813, 24.44736028078141),
+ Offset(39.74861752085834, 33.43380529842439),
+ Offset(32.57188683977151, 39.07136996422343),
+ Offset(24.376857043988256, 40.600018479197814),
+ Offset(17.959269400168804, 39.004426856660785),
+ Offset(13.850567169499653, 36.311009998593796),
+ Offset(11.374155956344177, 33.58880277176081),
+ Offset(9.917496515696001, 31.204288894581083),
+ Offset(9.07498759074148, 29.236785710939074),
+ Offset(8.597571742452605, 27.666692096657314),
+ Offset(8.334783321442917, 26.44693980672826),
+ Offset(8.195874559699876, 25.52824222288586),
+ Offset(8.126295299747222, 24.866824239052814),
+ Offset(8.093843447379264, 24.426077640310794),
+ Offset(8.080338503727083, 24.17611706018137),
+ Offset(8.076619249177135, 24.092742069165425),
+ Offset(8.07661186374038, 24.09256727275783),
+ ],
+ [
+ Offset(6.0, 12.0),
+ Offset(6.3229312318803075, 11.61579282114921),
+ Offset(7.523361420980265, 10.332065476778915),
+ Offset(10.234818160108134, 8.075701885898315),
+ Offset(15.555284551985588, 5.400098023461183),
+ Offset(25.267103519984172, 4.663978182144188),
+ Offset(34.065497532306516, 8.668225867992323),
+ Offset(39.59155761731576, 16.27703318845691),
+ Offset(40.72409454498984, 24.108085016590273),
+ Offset(39.139841854472834, 30.0780814324673),
+ Offset(36.514293313228855, 34.10942912386185),
+ Offset(33.744815583253256, 36.6601595585975),
+ Offset(31.226861893018718, 38.20062678263231),
+ Offset(29.10189988007002, 39.09038725780428),
+ Offset(27.3951953205187, 39.57837027981981),
+ Offset(26.083922435637483, 39.82883505984612),
+ Offset(25.128742795932077, 39.94653528477588),
+ Offset(24.487982707377697, 39.99564983955995),
+ Offset(24.123290412440365, 40.013021521592925),
+ Offset(24.001457946431486, 40.017121849607435),
+ Offset(24.001202429333205, 40.017129554079396),
+ ],
+ [
+ Offset(6.0, 12.0),
+ Offset(6.3229312318803075, 11.61579282114921),
+ Offset(7.523361420980265, 10.332065476778915),
+ Offset(10.234818160108134, 8.075701885898315),
+ Offset(15.555284551985588, 5.400098023461183),
+ Offset(25.267103519984172, 4.663978182144188),
+ Offset(34.065497532306516, 8.668225867992323),
+ Offset(39.59155761731576, 16.27703318845691),
+ Offset(40.72409454498984, 24.108085016590273),
+ Offset(39.139841854472834, 30.0780814324673),
+ Offset(36.514293313228855, 34.10942912386185),
+ Offset(33.744815583253256, 36.6601595585975),
+ Offset(31.226861893018718, 38.20062678263231),
+ Offset(29.10189988007002, 39.09038725780428),
+ Offset(27.3951953205187, 39.57837027981981),
+ Offset(26.083922435637483, 39.82883505984612),
+ Offset(25.128742795932077, 39.94653528477588),
+ Offset(24.487982707377697, 39.99564983955995),
+ Offset(24.123290412440365, 40.013021521592925),
+ Offset(24.001457946431486, 40.017121849607435),
+ Offset(24.001202429333205, 40.017129554079396),
+ ],
+ ),
+ _PathCubicTo(
+ [
+ Offset(6.0, 12.0),
+ Offset(6.3229312318803075, 11.61579282114921),
+ Offset(7.523361420980265, 10.332065476778915),
+ Offset(10.234818160108134, 8.075701885898315),
+ Offset(15.555284551985588, 5.400098023461183),
+ Offset(25.267103519984172, 4.663978182144188),
+ Offset(34.065497532306516, 8.668225867992323),
+ Offset(39.59155761731576, 16.27703318845691),
+ Offset(40.72409454498984, 24.108085016590273),
+ Offset(39.139841854472834, 30.0780814324673),
+ Offset(36.514293313228855, 34.10942912386185),
+ Offset(33.744815583253256, 36.6601595585975),
+ Offset(31.226861893018718, 38.20062678263231),
+ Offset(29.10189988007002, 39.09038725780428),
+ Offset(27.3951953205187, 39.57837027981981),
+ Offset(26.083922435637483, 39.82883505984612),
+ Offset(25.128742795932077, 39.94653528477588),
+ Offset(24.487982707377697, 39.99564983955995),
+ Offset(24.123290412440365, 40.013021521592925),
+ Offset(24.001457946431486, 40.017121849607435),
+ Offset(24.001202429333205, 40.017129554079396),
+ ],
+ [
+ Offset(6.0, 16.0),
+ Offset(6.22247008872931, 15.614531066985863),
+ Offset(7.071161725356028, 14.306422712267109),
+ Offset(9.085869786222908, 11.907139949360454),
+ Offset(13.311519331206826, 8.711520321209331),
+ Offset(21.69420631520211, 6.462423500762615),
+ Offset(30.070315707485825, 8.471955170682651),
+ Offset(36.20036889903345, 14.155750775152455),
+ Offset(38.53389748002304, 20.760991229943293),
+ Offset(38.18262670145813, 26.194302454353455),
+ Offset(36.597113027065134, 30.110286603895844),
+ Offset(34.63761335066132, 32.761068363650764),
+ Offset(32.72729018913396, 34.49270082217723),
+ Offset(31.048691170407302, 35.59610569046216),
+ Offset(29.66452602881138, 36.28441549318417),
+ Offset(28.58165531160348, 36.70452225855387),
+ Offset(27.78289794916673, 36.95396775461755),
+ Offset(27.24253113386635, 37.09522522131371),
+ Offset(26.933380541051008, 37.16637551812059),
+ Offset(26.829846827821875, 37.18865648202253),
+ Offset(26.829629554079393, 37.188702429333205),
+ ],
+ [
+ Offset(6.0, 16.0),
+ Offset(6.22247008872931, 15.614531066985863),
+ Offset(7.071161725356028, 14.306422712267109),
+ Offset(9.085869786222908, 11.907139949360454),
+ Offset(13.311519331206826, 8.711520321209331),
+ Offset(21.69420631520211, 6.462423500762615),
+ Offset(30.070315707485825, 8.471955170682651),
+ Offset(36.20036889903345, 14.155750775152455),
+ Offset(38.53389748002304, 20.760991229943293),
+ Offset(38.18262670145813, 26.194302454353455),
+ Offset(36.597113027065134, 30.110286603895844),
+ Offset(34.63761335066132, 32.761068363650764),
+ Offset(32.72729018913396, 34.49270082217723),
+ Offset(31.048691170407302, 35.59610569046216),
+ Offset(29.66452602881138, 36.28441549318417),
+ Offset(28.58165531160348, 36.70452225855387),
+ Offset(27.78289794916673, 36.95396775461755),
+ Offset(27.24253113386635, 37.09522522131371),
+ Offset(26.933380541051008, 37.16637551812059),
+ Offset(26.829846827821875, 37.18865648202253),
+ Offset(26.829629554079393, 37.188702429333205),
+ ],
+ ),
+ _PathClose(),
+ ],
+ ),
+ ],
+ matchTextDirection: true,
+ );
+}
diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart
index c5acbaa60..fb7bd7b11 100644
--- a/lib/widgets/common/basic/draggable_scrollbar.dart
+++ b/lib/widgets/common/basic/draggable_scrollbar.dart
@@ -1,5 +1,6 @@
import 'dart:async';
+import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
/*
@@ -9,6 +10,8 @@ import 'package:flutter/material.dart';
- allow any `Widget` as label content
- moved out constraints responsibility
- various extent & thumb positioning fixes
+ - null safety
+ - directionality aware
*/
/// Build the Scroll Thumb and label using the current configuration
@@ -116,7 +119,7 @@ class ScrollLabel extends StatelessWidget {
return FadeTransition(
opacity: animation,
child: Container(
- margin: const EdgeInsets.only(right: 12.0),
+ margin: const EdgeInsetsDirectional.only(end: 12.0),
child: Material(
elevation: 4.0,
color: backgroundColor,
@@ -350,8 +353,8 @@ class SlideFadeTransition extends StatelessWidget {
builder: (context, child) => animation.value == 0.0 ? Container() : child!,
child: SlideTransition(
position: Tween(
- begin: const Offset(0.3, 0.0),
- end: const Offset(0.0, 0.0),
+ begin: Offset((context.isRtl ? -1 : 1) * .3, 0),
+ end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart
index 2727565e6..4915aea22 100644
--- a/lib/widgets/common/basic/insets.dart
+++ b/lib/widgets/common/basic/insets.dart
@@ -33,6 +33,8 @@ class SideGestureAreaProtector extends StatelessWidget {
Widget build(BuildContext context) {
return Positioned.fill(
child: Row(
+ // `systemGestureInsets` are not directional
+ textDirection: TextDirection.ltr,
children: [
SizedBox(
width: context.select((mq) => mq.systemGestureInsets.left),
diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart
index 47e28bc4a..76110e64f 100644
--- a/lib/widgets/common/basic/markdown_container.dart
+++ b/lib/widgets/common/basic/markdown_container.dart
@@ -4,10 +4,12 @@ import 'package:url_launcher/url_launcher.dart';
class MarkdownContainer extends StatelessWidget {
final String data;
+ final TextDirection? textDirection;
const MarkdownContainer({
Key? key,
required this.data,
+ this.textDirection,
}) : super(key: key);
static const double maxWidth = 460;
@@ -34,15 +36,18 @@ class MarkdownContainer extends StatelessWidget {
),
),
child: Scrollbar(
- child: Markdown(
- data: data,
- selectable: true,
- onTapLink: (text, href, title) async {
- if (href != null && await canLaunch(href)) {
- await launch(href);
- }
- },
- shrinkWrap: true,
+ child: Directionality(
+ textDirection: textDirection ?? Directionality.of(context),
+ child: Markdown(
+ data: data,
+ selectable: true,
+ onTapLink: (text, href, title) async {
+ if (href != null && await canLaunch(href)) {
+ await launch(href);
+ }
+ },
+ shrinkWrap: true,
+ ),
),
),
),
diff --git a/lib/widgets/common/basic/menu.dart b/lib/widgets/common/basic/menu.dart
index ad96aa9ef..58a5dcc4f 100644
--- a/lib/widgets/common/basic/menu.dart
+++ b/lib/widgets/common/basic/menu.dart
@@ -18,7 +18,7 @@ class MenuRow extends StatelessWidget {
children: [
if (icon != null)
Padding(
- padding: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsetsDirectional.only(end: 8),
child: icon,
),
Expanded(child: Text(text)),
diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart
index cf3a515ee..6a4a86af0 100644
--- a/lib/widgets/common/extensions/build_context.dart
+++ b/lib/widgets/common/extensions/build_context.dart
@@ -5,4 +5,6 @@ extension ExtraContext on BuildContext {
String? get currentRouteName => ModalRoute.of(this)?.settings.name;
AppLocalizations get l10n => AppLocalizations.of(this)!;
+
+ bool get isRtl => Directionality.of(this) == TextDirection.rtl;
}
diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart
index 830cdd4b5..766cd8671 100644
--- a/lib/widgets/common/fx/borders.dart
+++ b/lib/widgets/common/fx/borders.dart
@@ -6,12 +6,22 @@ class AvesBorder {
static const borderColor = Colors.white30;
// directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery`
- static double get borderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0;
- static BorderSide get side => BorderSide(
+ // 1 device pixel for straight lines is fine
+ static double get straightBorderWidth => 1 / window.devicePixelRatio;
+
+ // 1 device pixel for curves is too thin
+ static double get curvedBorderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0;
+
+ static BorderSide get straightSide => BorderSide(
color: borderColor,
- width: borderWidth,
+ width: straightBorderWidth,
);
- static Border get border => Border.fromBorderSide(side);
+ static BorderSide get curvedSide => BorderSide(
+ color: borderColor,
+ width: curvedBorderWidth,
+ );
+
+ static Border get border => Border.fromBorderSide(curvedSide);
}
diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart
index 9d1b9c356..bcb0e40e0 100644
--- a/lib/widgets/common/grid/header.dart
+++ b/lib/widgets/common/grid/header.dart
@@ -25,8 +25,6 @@ class SectionHeader extends StatelessWidget {
}) : super(key: key);
static const leadingDimension = 32.0;
- static const leadingPadding = EdgeInsets.only(right: 8, bottom: 4);
- static const trailingPadding = EdgeInsets.only(left: 8, bottom: 2);
static const padding = EdgeInsets.all(16);
static const widgetSpanAlignment = PlaceholderAlignment.middle;
@@ -48,7 +46,7 @@ class SectionHeader extends StatelessWidget {
sectionKey: sectionKey,
browsingBuilder: leading != null
? (context) => Container(
- padding: leadingPadding,
+ padding: const EdgeInsetsDirectional.only(end: 8, bottom: 4),
width: leadingDimension,
height: leadingDimension,
child: leading,
@@ -65,7 +63,7 @@ class SectionHeader extends StatelessWidget {
WidgetSpan(
alignment: widgetSpanAlignment,
child: Container(
- padding: trailingPadding,
+ padding: const EdgeInsetsDirectional.only(start: 8, bottom: 2),
child: trailing,
),
),
@@ -100,7 +98,7 @@ class SectionHeader extends StatelessWidget {
final para = RenderParagraph(
TextSpan(
children: [
- // as of Flutter v1.22.3, `RenderParagraph` fails to lay out `WidgetSpan` offscreen
+ // as of Flutter v2.8.1, `RenderParagraph` fails to lay out `WidgetSpan` offscreen
// so we use a hair space times a magic number to match width
TextSpan(
// 23 hair spaces match a width of 40.0
diff --git a/lib/widgets/common/grid/overlay.dart b/lib/widgets/common/grid/overlay.dart
index dafa4fd7d..c9c111df4 100644
--- a/lib/widgets/common/grid/overlay.dart
+++ b/lib/widgets/common/grid/overlay.dart
@@ -30,8 +30,9 @@ class GridItemSelectionOverlay extends StatelessWidget {
? OverlayIcon(
key: ValueKey(isSelected),
icon: isSelected ? AIcons.selected : AIcons.unselected,
+ margin: EdgeInsets.zero,
)
- : const SizedBox.shrink();
+ : const SizedBox();
child = AnimatedSwitcher(
duration: duration,
switchInCurve: Curves.easeOutBack,
diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart
index 58aacf147..15097ba46 100644
--- a/lib/widgets/common/grid/scaling.dart
+++ b/lib/widgets/common/grid/scaling.dart
@@ -4,6 +4,7 @@ import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
@@ -304,7 +305,7 @@ class _ScaleOverlayState extends State<_ScaleOverlay> {
gradientCenter = center;
break;
case TileLayout.list:
- gradientCenter = Offset(0, center.dy);
+ gradientCenter = Offset(context.isRtl ? gridWidth : 0, center.dy);
break;
}
@@ -338,6 +339,7 @@ class GridPainter extends CustomPainter {
final double spacing, borderWidth;
final Radius borderRadius;
final Color color;
+ final TextDirection textDirection;
const GridPainter({
required this.tileLayout,
@@ -347,6 +349,7 @@ class GridPainter extends CustomPainter {
required this.borderWidth,
required this.borderRadius,
required this.color,
+ required this.textDirection,
});
@override
@@ -375,7 +378,8 @@ class GridPainter extends CustomPainter {
break;
case TileLayout.list:
chipSize = Size.square(tileSize.shortestSide);
- chipCenter = Offset(chipSize.width / 2, tileCenter.dy);
+ final chipCenterToEdge = chipSize.width / 2;
+ chipCenter = Offset(textDirection == TextDirection.rtl ? size.width - chipCenterToEdge : chipCenterToEdge, tileCenter.dy);
deltaColumn = 0;
strokeShader = ui.Gradient.linear(
tileCenter - Offset(0, chipSize.shortestSide * 3),
diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart
index c2d9999bc..c30ac4c7d 100644
--- a/lib/widgets/common/grid/section_layout.dart
+++ b/lib/widgets/common/grid/section_layout.dart
@@ -133,6 +133,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget {
width: tileWidth,
height: tileHeight,
spacing: spacing,
+ textDirection: Directionality.of(context),
children: children,
);
}
@@ -190,6 +191,7 @@ class SectionedListLayout {
required this.sectionLayouts,
});
+ // return tile rectangle in layout space, i.e. x=0 is start
Rect? getTileRect(T item) {
final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item));
if (section == null) return null;
@@ -210,6 +212,7 @@ class SectionedListLayout {
SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset);
+ // `position` in layout space, i.e. x=0 is start
T? getItemAt(Offset position) {
var dy = position.dy;
final sectionLayout = getSectionAt(dy);
@@ -283,12 +286,14 @@ class SectionLayout extends Equatable {
class _GridRow extends MultiChildRenderObjectWidget {
final double width, height, spacing;
+ final TextDirection textDirection;
_GridRow({
Key? key,
required this.width,
required this.height,
required this.spacing,
+ required this.textDirection,
required List children,
}) : super(key: key, children: children);
@@ -298,6 +303,7 @@ class _GridRow extends MultiChildRenderObjectWidget {
width: width,
height: height,
spacing: spacing,
+ textDirection: textDirection,
);
}
@@ -306,6 +312,7 @@ class _GridRow extends MultiChildRenderObjectWidget {
renderObject.width = width;
renderObject.height = height;
renderObject.spacing = spacing;
+ renderObject.textDirection = textDirection;
}
@override
@@ -314,6 +321,7 @@ class _GridRow extends MultiChildRenderObjectWidget {
properties.add(DoubleProperty('width', width));
properties.add(DoubleProperty('height', height));
properties.add(DoubleProperty('spacing', spacing));
+ properties.add(EnumProperty('textDirection', textDirection));
}
}
@@ -325,9 +333,11 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin _textDirection;
+ TextDirection _textDirection;
+
+ set textDirection(TextDirection value) {
+ if (_textDirection == value) return;
+ _textDirection = value;
+ markNeedsLayout();
+ }
+
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _GridRowParentData) {
@@ -388,12 +407,14 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin('textDirection', textDirection));
}
}
diff --git a/lib/widgets/common/grid/selector.dart b/lib/widgets/common/grid/selector.dart
index 92604fbf4..05f41d58c 100644
--- a/lib/widgets/common/grid/selector.dart
+++ b/lib/widgets/common/grid/selector.dart
@@ -3,12 +3,14 @@ import 'dart:math';
import 'package:aves/model/selection.dart';
import 'package:aves/utils/math_utils.dart';
+import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class GridSelectionGestureDetector extends StatefulWidget {
+ final GlobalKey scrollableKey;
final bool selectable;
final List items;
final ScrollController scrollController;
@@ -17,6 +19,7 @@ class GridSelectionGestureDetector extends StatefulWidget {
const GridSelectionGestureDetector({
Key? key,
+ required this.scrollableKey,
this.selectable = true,
required this.items,
required this.scrollController,
@@ -42,6 +45,13 @@ class _GridSelectionGestureDetectorState extends State