Merge branch 'develop'
4
.gitignore
vendored
|
@ -44,3 +44,7 @@ app.*.map.json
|
|||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# screenshot generation
|
||||
/test_driver/assets/screenshots/
|
||||
/screenshots/
|
||||
|
|
23
CHANGELOG.md
|
@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="v1.5.11"></a>[v1.5.11] - 2022-01-30
|
||||
|
||||
### Added
|
||||
|
||||
- Collection / Info: edit location of JPG/PNG/WEBP/DNG images via Exif
|
||||
- Viewer: resize option when exporting
|
||||
- Settings: export/import covers & favourites along with settings
|
||||
- Collection: allow rescan when browsing
|
||||
- support Android 12L (API 32)
|
||||
- Portuguese translation (thanks Jonatas De Almeida Barros)
|
||||
|
||||
### Removed
|
||||
|
||||
- new version check
|
||||
|
||||
### Fixed
|
||||
|
||||
- loading when system locale uses non-western arabic numerals
|
||||
- handling timestamps provided in 10^-8 s (18 digits)
|
||||
- Viewer: SVG export
|
||||
- Viewer: sending to editing app on some environments
|
||||
- Map: projected center anchoring
|
||||
|
||||
## <a id="v1.5.10"></a>[v1.5.10] - 2022-01-07
|
||||
|
||||
### Added
|
||||
|
|
32
README.md
|
@ -29,11 +29,37 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
|
|||
|
||||
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
Aves integrates with Android (from **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**.
|
||||
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**.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/2-S10-viewer.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/5-S10-stats.png" alt='Stats screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/3-S10-info__basic_.png" alt='Info (basic) screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/4-S10-info__metadata_.png" alt='Info (metadata) screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/6-S10-countries.png" alt='Countries screenshot' height="400" />
|
||||
<div align="center">
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/readme/en/1.png"
|
||||
alt='Collection screenshot'
|
||||
width="130" />](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/1.png)
|
||||
[<img
|
||||
src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/readme/en/2.png"
|
||||
alt='Image screenshot'
|
||||
width="130" />](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/2.png)
|
||||
[<img
|
||||
src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/readme/en/5.png"
|
||||
alt='Stats screenshot'
|
||||
width="130" />](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/5.png)
|
||||
[<img
|
||||
src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/readme/en/3.png"
|
||||
alt='Info (basic) screenshot'
|
||||
width="130" />](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/3.png)
|
||||
[<img
|
||||
src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/readme/en/4.png"
|
||||
alt='Info (metadata) screenshot'
|
||||
width="130" />](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/4.png)
|
||||
[<img
|
||||
src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/readme/en/6.png"
|
||||
alt='Countries screenshot'
|
||||
width="130" />](https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/play/en/6.png)
|
||||
|
||||
<div align="left">
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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<SvgThumbnail, Bitmap> {
|
||||
override fun buildLoadData(model: SvgThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||
internal class SvgLoader : ModelLoader<SvgImage, Bitmap> {
|
||||
override fun buildLoadData(model: SvgImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
|
||||
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<SvgThumbnail, Bitmap> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<SvgThumbnail, Bitmap> = SvgLoader()
|
||||
internal class Factory : ModelLoaderFactory<SvgImage, Bitmap> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<SvgImage, Bitmap> = SvgLoader()
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: Int) : DataFetcher<Bitmap> {
|
||||
internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int) : DataFetcher<Bitmap> {
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
|
||||
val context = model.context
|
||||
val uri = model.uri
|
||||
|
|
|
@ -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<String, String>): Boolean {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AvesEntry>,
|
||||
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<Bitmap>? = 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<FieldMap>()
|
||||
if (!editIptc(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<String> {
|
||||
// Final set of paths
|
||||
val paths = HashSet<String>()
|
||||
|
||||
// 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<File>
|
||||
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<File>
|
||||
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()) {
|
||||
|
|
10
android/app/src/main/res/values-pt/strings.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="search_shortcut_short_label">Procurar</string>
|
||||
<string name="videos_shortcut_short_label">Vídeos</string>
|
||||
<string name="analysis_channel_name">Digitalização de mídia</string>
|
||||
<string name="analysis_service_description">Digitalizar imagens & vídeos</string>
|
||||
<string name="analysis_notification_default_title">Digitalizando mídia</string>
|
||||
<string name="analysis_notification_action_stop">Pare</string>
|
||||
</resources>
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
<b>Navigation und Suche</b> ist ein wichtiger Bestandteil von <i>Aves</i>. Das Ziel besteht darin, dass Benutzer problemlos von Alben zu Fotos zu Tags zu Karten usw. wechseln können.
|
||||
|
||||
<i>Aves</i> lässt sich mit Android (von <b>API 19 bis 31</b>, d. h. von KitKat bis S) mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.
|
||||
<i>Aves</i> lässt sich mit Android (von <b>API 19 bis 32</b>, d. h. von KitKat bis Android 12L) mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.
|
BIN
fastlane/metadata/android/de/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
fastlane/metadata/android/de/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
fastlane/metadata/android/de/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
fastlane/metadata/android/de/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
fastlane/metadata/android/de/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
fastlane/metadata/android/de/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 366 KiB |
5
fastlane/metadata/android/en-US/changelogs/1064.txt
Normal file
|
@ -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
|
5
fastlane/metadata/android/en-US/changelogs/1065.txt
Normal file
|
@ -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
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
<i>Aves</i> integrates with Android (from <b>API 19 to 31</b>, i.e. from KitKat to S) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
||||
<i>Aves</i> integrates with Android (from <b>API 19 to 32</b>, i.e. from KitKat to Android 12L) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 500 KiB |
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 367 KiB |
|
@ -2,4 +2,4 @@
|
|||
|
||||
La <b>navegación y búsqueda</b> son partes importantes de <i>Aves</i>. Su propósito es que los usuarios puedan fácimente ir de álbumes a fotos, etiquetas, mapas, etc.
|
||||
|
||||
<i>Aves</i> se integra con Android (desde <b>API 19 a 31</b>, por ej. desde KitKat hasta S) con características como <b>vínculos de aplicación</b> y manejo de <b>búsqueda global</b>. También funciona como un <b>visor y seleccionador multimedia</b>.
|
||||
<i>Aves</i> se integra con Android (desde <b>API 19 a 32</b>, por ej. desde KitKat hasta Android 12L) con características como <b>vínculos de aplicación</b> y manejo de <b>búsqueda global</b>. También funciona como un <b>visor y seleccionador multimedia</b>.
|
BIN
fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 277 KiB |
BIN
fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 212 KiB |
BIN
fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 366 KiB |
BIN
fastlane/metadata/android/fr/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
fastlane/metadata/android/fr/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
fastlane/metadata/android/fr/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
fastlane/metadata/android/fr/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
fastlane/metadata/android/fr/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
fastlane/metadata/android/fr/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
fastlane/metadata/android/fr/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 366 KiB |
BIN
fastlane/metadata/android/ko/images/featureGraphics.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
fastlane/metadata/android/ko/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 274 KiB |
BIN
fastlane/metadata/android/ko/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 501 KiB |
BIN
fastlane/metadata/android/ko/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
fastlane/metadata/android/ko/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
fastlane/metadata/android/ko/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
fastlane/metadata/android/ko/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 365 KiB |
5
fastlane/metadata/android/pt-BR/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> 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 <b>páginas múltiplas TIFFs, SVGs, AVIs antigos e muito mais</b>! Ele verifica sua coleção de mídia para identificar <b>fotos em movimento</b>, <b>panoramas</b> (aka photo spheres), <b>vídeos em 360°</b>, assim como <b>GeoTIFF</b> arquivos.
|
||||
|
||||
<b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
|
||||
|
||||
<i>Aves</i> integra com Android (de <b>API 19 para 32</b>, i.e. de KitKat para Android 12L) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.
|
BIN
fastlane/metadata/android/pt-BR/images/featureGraphics.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 277 KiB |
BIN
fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 366 KiB |
1
fastlane/metadata/android/pt-BR/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Galeria e explorador de metadados
|
BIN
fastlane/metadata/android/ru/images/featureGraphics.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
fastlane/metadata/android/ru/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 276 KiB |
BIN
fastlane/metadata/android/ru/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
fastlane/metadata/android/ru/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 215 KiB |
BIN
fastlane/metadata/android/ru/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
fastlane/metadata/android/ru/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
fastlane/metadata/android/ru/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 366 KiB |
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
"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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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é",
|
||||
|
|
|
@ -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": "화면 자동 꺼짐 방지",
|
||||
|
|
546
lib/l10n/app_pt.arb
Normal file
|
@ -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"
|
||||
}
|
|
@ -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": "Сохранять историю поиска",
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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<bool> get canLocatePlaces;
|
||||
|
||||
Future<bool> get canUseGoogleMaps;
|
||||
|
||||
Future<bool> 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<bool> get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices;
|
||||
|
||||
@override
|
||||
Future<bool> 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!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CoverRow> get all => Set.unmodifiable(_rows);
|
||||
|
||||
int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId;
|
||||
|
||||
Future<void> set(CollectionFilter filter, int? contentId) async {
|
||||
|
@ -75,6 +78,61 @@ class Covers with ChangeNotifier {
|
|||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// import/export
|
||||
|
||||
List<Map<String, dynamic>>? 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
|
||||
|
|
|
@ -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<void> 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);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ extension ExtraAvesEntryImages on AvesEntry {
|
|||
|
||||
bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive;
|
||||
|
||||
List<ThumbnailProvider> get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map((key) => ThumbnailProvider(key)).toList();
|
||||
List<ThumbnailProvider> get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList();
|
||||
|
||||
ThumbnailProvider get bestCachedThumbnail {
|
||||
final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady);
|
||||
|
|
|
@ -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<Set<EntryDataType>> editLocation(LatLng? latLng) async {
|
||||
final Set<EntryDataType> dataTypes = {};
|
||||
|
||||
await _missingDateCheckAndExifEdit(dataTypes);
|
||||
|
||||
// clear every GPS field
|
||||
final exifFields = Map<MetadataField, dynamic>.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<String, dynamic>.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<Set<EntryDataType>> _changeOrientation(Future<Map<String, dynamic>> Function() apply) async {
|
||||
final Set<EntryDataType> dataTypes = {};
|
||||
|
||||
|
|
|
@ -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<int> 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<String, List<String>>? 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<String, StorageVolume?>(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 = <AvesEntry>{};
|
||||
final missedPaths = <String>{};
|
||||
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
|
||||
|
|
|
@ -21,17 +21,21 @@ import 'package:flutter/widgets.dart';
|
|||
abstract class CollectionFilter extends Equatable implements Comparable<CollectionFilter> {
|
||||
static const List<String> 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<Collecti
|
|||
return null;
|
||||
}
|
||||
|
||||
const CollectionFilter();
|
||||
|
||||
Map<String, dynamic> toMap();
|
||||
|
||||
String toJson() => jsonEncode(toMap());
|
||||
|
|
|
@ -12,23 +12,25 @@ class TagFilter extends CollectionFilter {
|
|||
@override
|
||||
List<Object?> 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<String, dynamic> json)
|
||||
: this(
|
||||
json['tag'],
|
||||
not: json['not'] ?? false,
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'tag': tag,
|
||||
'not': not,
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
|
@ -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<Object?> get props => [action, fields, setDateTime, copyFieldSource, shiftMinutes];
|
||||
|
||||
const DateModifier._private(
|
||||
this.action,
|
||||
this.fields, {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
199
lib/model/metadata/fields.dart
Normal file
|
@ -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<MetadataField> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -231,7 +231,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
Future<Set<AvesEntry>> 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<List<CatalogMetadata>> 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<List<AddressDetails>> 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<Set<FavouriteRow>> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class MultiPageInfo {
|
|||
factory MultiPageInfo.fromPageMaps(AvesEntry mainEntry, List<Map> pageMaps) {
|
||||
return MultiPageInfo(
|
||||
mainEntry: mainEntry,
|
||||
pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList(),
|
||||
pages: pageMaps.map(SinglePageInfo.fromMap).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, dynamic> export() => Map.fromEntries(
|
||||
_prefs!.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, _prefs!.get(k))),
|
||||
));
|
||||
);
|
||||
|
||||
Future<void> fromJson(String jsonString) async {
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
Future<void> import(dynamic jsonMap) async {
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
// clear to restore defaults
|
||||
await reset(includeInternalKeys: false);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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<AvesEntry>? entries]) {
|
||||
|
@ -292,6 +292,18 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
|
||||
Future<void> refreshEntry(AvesEntry entry, Set<EntryDataType> 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}));
|
||||
}
|
||||
|
|
|
@ -30,6 +30,12 @@ mixin LocationMixin on SourceBase {
|
|||
Future<void> locateEntries(AnalysisController controller, Set<AvesEntry> 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<String, int> _filterEntryCountMap = {};
|
||||
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
||||
|
||||
void invalidateCountryFilterSummary([Set<AvesEntry>? entries]) {
|
||||
void invalidateCountryFilterSummary({Set<AvesEntry>? entries, Set<String>? countryCodes}) {
|
||||
if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
|
||||
|
||||
Set<String>? 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));
|
||||
}
|
||||
|
|
|
@ -77,16 +77,21 @@ mixin TagMixin on SourceBase {
|
|||
final Map<String, int> _filterEntryCountMap = {};
|
||||
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
||||
|
||||
void invalidateTagFilterSummary([Set<AvesEntry>? entries]) {
|
||||
void invalidateTagFilterSummary({Set<AvesEntry>? entries, Set<String>? tags}) {
|
||||
if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
|
||||
|
||||
Set<String>? 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));
|
||||
}
|
||||
|
|
|
@ -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<String, String> _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();
|
||||
}
|
||||
|
||||
|
|