diff --git a/CHANGELOG.md b/CHANGELOG.md
index f76ab7a9a..006314a32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.5.10] - 2022-01-07
+
+### Added
+
+- Collection: toggle favourites in bulk
+- Info: edit ratings of JPG/GIF/PNG/TIFF images via XMP
+- Info: edit date of GIF images via XMP
+- Info: option to set date from other fields
+- Spanish translation (thanks n-berenice)
+
+### Changed
+
+- editing an item orientation, rating or tags automatically sets a metadata date (from the file
+ modified date), if it is missing
+- Viewer: when opening an item from another app, it is now possible to scroll to other items in the
+ album
+
+### Fixed
+
+- Exif and IPTC raw profile extraction from PNG in some cases
+
## [v1.5.9] - 2021-12-22
### Added
@@ -37,7 +58,8 @@ All notable changes to this project will be documented in this file.
### Changed
- Settings: select hidden path directory with a custom file picker instead of the native SAF one
-- Viewer: video cover (before playing the video) is now loaded at original resolution and can be zoomed
+- Viewer: video cover (before playing the video) is now loaded at original resolution and can be
+ zoomed
### Fixed
@@ -75,7 +97,8 @@ All notable changes to this project will be documented in this file.
### Changed
-- use build flavors to match distribution channels: `play` (same as original) and `izzy` (no Crashlytics)
+- use build flavors to match distribution channels: `play` (same as original) and `izzy` (no
+ Crashlytics)
- use 12/24 hour format settings from device to display times
- Privacy: consent request on first launch for installed app inventory access
- use File API to rename and delete items, when possible (primary storage, Android <11)
diff --git a/README.md b/README.md
index 78f1bc157..c4975eba2 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,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.
+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.
### Donations
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
index e9c9fbc26..be8ee35ee 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
@@ -1,6 +1,7 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
+import android.content.Intent
import android.content.res.Resources
import android.os.Build
import androidx.core.content.pm.ShortcutManagerCompat
@@ -18,6 +19,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
"getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
+ "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
else -> result.notImplemented()
}
}
@@ -34,7 +36,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
// but using hybrid composition would make it usable on API 19 too,
// cf https://github.com/flutter/flutter/issues/23728
"canRenderGoogleMaps" to (sdkInt >= Build.VERSION_CODES.KITKAT_WATCH),
- "hasFilePicker" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
"supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q),
)
@@ -82,6 +83,15 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(Build.VERSION.SDK_INT)
}
+ private fun isSystemFilePickerEnabled(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).resolveActivity(context.packageManager) != null
+ } else {
+ false
+ }
+ result.success(enabled)
+ }
+
companion object {
const val CHANNEL = "deckers.thibault/aves/device"
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt
index 9040083d5..adc3449f5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt
@@ -20,8 +20,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
"rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) }
"flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) }
"editDate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::editDate) }
- "setIptc" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::setIptc) }
- "setXmp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::setXmp) }
+ "editMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::editMetadata) }
"removeTypes" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::removeTypes) }
else -> result.notImplemented()
}
@@ -99,12 +98,11 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
})
}
- private fun setIptc(call: MethodCall, result: MethodChannel.Result) {
- val iptc = call.argument>("iptc")
+ private fun editMetadata(call: MethodCall, result: MethodChannel.Result) {
+ val metadata = call.argument("metadata")
val entryMap = call.argument("entry")
- val postEditScan = call.argument("postEditScan")
- if (entryMap == null || postEditScan == null) {
- result.error("setIptc-args", "failed because of missing arguments", null)
+ if (entryMap == null || metadata == null) {
+ result.error("editMetadata-args", "failed because of missing arguments", null)
return
}
@@ -112,48 +110,19 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {
- result.error("setIptc-args", "failed because entry fields are missing", null)
+ result.error("editMetadata-args", "failed because entry fields are missing", null)
return
}
val provider = getProvider(uri)
if (provider == null) {
- result.error("setIptc-provider", "failed to find provider for uri=$uri", null)
+ result.error("editMetadata-provider", "failed to find provider for uri=$uri", null)
return
}
- provider.setIptc(activity, path, uri, mimeType, postEditScan, iptc = iptc, callback = object : ImageOpCallback {
+ provider.editMetadata(activity, path, uri, mimeType, metadata, callback = object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
- override fun onFailure(throwable: Throwable) = result.error("setIptc-failure", "failed to set IPTC for mimeType=$mimeType uri=$uri", throwable.message)
- })
- }
-
- private fun setXmp(call: MethodCall, result: MethodChannel.Result) {
- val xmp = call.argument("xmp")
- val extendedXmp = call.argument("extendedXmp")
- val entryMap = call.argument("entry")
- if (entryMap == null) {
- result.error("setXmp-args", "failed because of missing arguments", null)
- return
- }
-
- val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) }
- val path = entryMap["path"] as String?
- val mimeType = entryMap["mimeType"] as String?
- if (uri == null || path == null || mimeType == null) {
- result.error("setXmp-args", "failed because entry fields are missing", null)
- return
- }
-
- val provider = getProvider(uri)
- if (provider == null) {
- result.error("setXmp-provider", "failed to find provider for uri=$uri", null)
- return
- }
-
- provider.setXmp(activity, path, uri, mimeType, coreXmp = xmp, extendedXmp = extendedXmp, callback = object : ImageOpCallback {
- override fun onSuccess(fields: FieldMap) = result.success(fields)
- override fun onFailure(throwable: Throwable) = result.error("setXmp-failure", "failed to set XMP for mimeType=$mimeType uri=$uri", throwable.message)
+ override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable.message)
})
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
index 29c52af31..b461e2b9f 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
@@ -19,7 +19,10 @@ import com.drew.lang.KeyValuePair
import com.drew.lang.Rational
import com.drew.metadata.Tag
import com.drew.metadata.avi.AviDirectory
-import com.drew.metadata.exif.*
+import com.drew.metadata.exif.ExifDirectoryBase
+import com.drew.metadata.exif.ExifIFD0Directory
+import com.drew.metadata.exif.ExifSubIFDDirectory
+import com.drew.metadata.exif.GpsDirectory
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory
import com.drew.metadata.iptc.IptcDirectory
@@ -78,6 +81,7 @@ import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.text.ParseException
import java.util.*
+import kotlin.math.roundToInt
import kotlin.math.roundToLong
class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
@@ -92,6 +96,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
"getXmp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getXmp) }
"hasContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::hasContentResolverProp) }
"getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) }
+ "getDate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getDate) }
else -> result.notImplemented()
}
}
@@ -113,8 +118,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
- foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
- foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
+ foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
+ foundXmp = metadata.directories.any { it is XmpDirectory && it.tagCount > 0 }
val uuidDirCount = HashMap()
val dirByName = metadata.directories.filter {
it.tagCount > 0
@@ -158,25 +163,16 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// tags
val tags = dir.tags
- if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
- fun tagMapper(it: Tag): Pair {
- val name = if (it.hasTagName()) {
- it.tagName
- } else {
- TiffTags.getTagName(it.tagType) ?: it.tagName
- }
- return Pair(name, it.description)
- }
-
- if (dir is ExifIFD0Directory && dir.isGeoTiff()) {
+ if (dir is ExifDirectoryBase) {
+ if (dir.isGeoTiff()) {
// split GeoTIFF tags in their own directory
- val byGeoTiff = tags.groupBy { TiffTags.isGeoTiffTag(it.tagType) }
+ val byGeoTiff = tags.groupBy { ExifTags.isGeoTiffTag(it.tagType) }
metadataMap["GeoTIFF"] = HashMap().apply {
- byGeoTiff[true]?.map { tagMapper(it) }?.let { putAll(it) }
+ byGeoTiff[true]?.map { exifTagMapper(it) }?.let { putAll(it) }
}
- byGeoTiff[false]?.map { tagMapper(it) }?.let { dirMap.putAll(it) }
+ byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
} else {
- dirMap.putAll(tags.map { tagMapper(it) })
+ dirMap.putAll(tags.map { exifTagMapper(it) })
}
} else if (dir.isPngTextDir()) {
metadataMap.remove(thisDirName)
@@ -205,10 +201,15 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val dirs = extractPngProfile(key, valueString)
if (dirs?.any() == true) {
dirs.forEach { profileDir ->
- val profileDirName = profileDir.name
+ val profileDirName = "${dir.name}/${profileDir.name}"
val profileDirMap = metadataMap[profileDirName] ?: HashMap()
metadataMap[profileDirName] = profileDirMap
- profileDirMap.putAll(profileDir.tags.map { Pair(it.tagName, it.description) })
+ val profileTags = profileDir.tags
+ if (profileDir is ExifDirectoryBase) {
+ profileDirMap.putAll(profileTags.map { exifTagMapper(it) })
+ } else {
+ profileDirMap.putAll(profileTags.map { Pair(it.tagName, it.description) })
+ }
}
null
} else {
@@ -357,22 +358,22 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
return dirMap
}
- // legend: ME=MetadataExtractor, EI=ExifInterface, MMR=MediaMetadataRetriever
// set `KEY_DATE_MILLIS` from these fields (by precedence):
- // - ME / Exif / DATETIME_ORIGINAL
- // - ME / Exif / DATETIME
- // - EI / Exif / DATETIME_ORIGINAL
- // - EI / Exif / DATETIME
- // - ME / XMP / xmp:CreateDate
- // - ME / XMP / photoshop:DateCreated
- // - ME / PNG / TIME / LAST_MODIFICATION_TIME
- // - MMR / METADATA_KEY_DATE
+ // - Exif / DATETIME_ORIGINAL
+ // - Exif / DATETIME
+ // - XMP / xmp:CreateDate
+ // - XMP / photoshop:DateCreated
+ // - PNG / TIME / LAST_MODIFICATION_TIME
+ // - Video / METADATA_KEY_DATE
// set `KEY_XMP_TITLE_DESCRIPTION` from these fields (by precedence):
- // - ME / XMP / dc:title
- // - ME / XMP / dc:description
+ // - XMP / dc:title
+ // - XMP / dc:description
// set `KEY_XMP_SUBJECTS` from these fields (by precedence):
- // - ME / XMP / dc:subject
- // - ME / IPTC / keywords
+ // - XMP / dc:subject
+ // - IPTC / keywords
+ // set `KEY_RATING` from these fields (by precedence):
+ // - XMP / xmp:Rating
+ // - XMP / MicrosoftPhoto:Rating
private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument("mimeType")
val uri = call.argument("uri")?.let { Uri.parse(it) }
@@ -407,7 +408,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
- foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
+ foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
// File type
for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) {
@@ -432,13 +433,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// EXIF
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
- dir.getSafeDateMillis(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it }
+ dir.getSafeDateMillis(ExifDirectoryBase.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it }
}
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
- dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it }
+ dir.getSafeDateMillis(ExifDirectoryBase.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it }
}
- dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) {
+ dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) {
val orientation = it
if (isFlippedForExifCode(orientation)) flags = flags or MASK_IS_FLIPPED
metadataMap[KEY_ROTATION_DEGREES] = getRotationDegreesForExifCode(orientation)
@@ -458,22 +459,31 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
val xmpMeta = dir.xmpMeta
try {
- if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME)) {
- val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME)
- val values = (1 until count + 1).map { xmpMeta.getArrayItem(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME, it).value }
+ if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)) {
+ val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME)
+ val values = (1 until count + 1).map { xmpMeta.getArrayItem(XMP.DC_SCHEMA_NS, XMP.DC_SUBJECT_PROP_NAME, it).value }
metadataMap[KEY_XMP_SUBJECTS] = values.joinToString(XMP_SUBJECTS_SEPARATOR)
}
- xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it }
+ xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it }
if (!metadataMap.containsKey(KEY_XMP_TITLE_DESCRIPTION)) {
- xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DESCRIPTION_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it }
+ xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it }
}
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
- xmpMeta.getSafeDateMillis(XMP.XMP_SCHEMA_NS, XMP.CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
+ xmpMeta.getSafeDateMillis(XMP.XMP_SCHEMA_NS, XMP.XMP_CREATE_DATE_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
xmpMeta.getSafeDateMillis(XMP.PHOTOSHOP_SCHEMA_NS, XMP.PS_DATE_CREATED_PROP_NAME) { metadataMap[KEY_DATE_MILLIS] = it }
}
}
+ xmpMeta.getSafeInt(XMP.XMP_SCHEMA_NS, XMP.XMP_RATING_PROP_NAME) { metadataMap[KEY_RATING] = it }
+ if (!metadataMap.containsKey(KEY_RATING)) {
+ xmpMeta.getSafeInt(XMP.MICROSOFTPHOTO_SCHEMA_NS, XMP.MS_RATING_PROP_NAME) { percentRating ->
+ // values of 1,25,50,75,99% correspond to 1,2,3,4,5 stars
+ val standardRating = (percentRating / 25f).roundToInt() + 1
+ metadataMap[KEY_RATING] = standardRating
+ }
+ }
+
// identification of panorama (aka photo sphere)
if (xmpMeta.isPanorama()) {
flags = flags or MASK_IS_360
@@ -659,10 +669,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val metadata = ImageMetadataReader.readMetadata(input)
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
foundExif = true
- dir.getSafeRational(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
- dir.getSafeRational(ExifSubIFDDirectory.TAG_EXPOSURE_TIME, saveExposureTime)
- dir.getSafeRational(ExifSubIFDDirectory.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
- dir.getSafeInt(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
+ dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
+ dir.getSafeRational(ExifDirectoryBase.TAG_EXPOSURE_TIME, saveExposureTime)
+ dir.getSafeRational(ExifDirectoryBase.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
+ dir.getSafeInt(ExifDirectoryBase.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
}
}
} catch (e: Exception) {
@@ -876,6 +886,57 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(value?.toString())
}
+ private fun getDate(call: MethodCall, result: MethodChannel.Result) {
+ val mimeType = call.argument("mimeType")
+ val uri = call.argument("uri")?.let { Uri.parse(it) }
+ val sizeBytes = call.argument("sizeBytes")?.toLong()
+ val field = call.argument("field")
+ if (mimeType == null || uri == null || field == null) {
+ result.error("getDate-args", "failed because of missing arguments", null)
+ return
+ }
+
+ var dateMillis: Long? = null
+ if (canReadWithMetadataExtractor(mimeType)) {
+ try {
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
+ val metadata = ImageMetadataReader.readMetadata(input)
+ val tag = when (field) {
+ ExifInterface.TAG_DATETIME -> ExifDirectoryBase.TAG_DATETIME
+ ExifInterface.TAG_DATETIME_DIGITIZED -> ExifDirectoryBase.TAG_DATETIME_DIGITIZED
+ ExifInterface.TAG_DATETIME_ORIGINAL -> ExifDirectoryBase.TAG_DATETIME_ORIGINAL
+ ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP
+ else -> {
+ result.error("getDate-field", "unsupported ExifInterface field=$field", null)
+ return
+ }
+ }
+
+ when (tag) {
+ ExifDirectoryBase.TAG_DATETIME,
+ ExifDirectoryBase.TAG_DATETIME_DIGITIZED,
+ ExifDirectoryBase.TAG_DATETIME_ORIGINAL -> {
+ for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) {
+ dir.getSafeDateMillis(tag) { dateMillis = it }
+ }
+ }
+ GpsDirectory.TAG_DATE_STAMP -> {
+ for (dir in metadata.getDirectoriesOfType(GpsDirectory::class.java)) {
+ dir.gpsDate?.let { dateMillis = it.time }
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
+ } catch (e: NoClassDefFoundError) {
+ Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
+ }
+ }
+
+ result.success(dateMillis)
+ }
+
companion object {
private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
@@ -905,6 +966,15 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
omitXmpMetaElement = false // e.g. ...
}
+ private fun exifTagMapper(it: Tag): Pair {
+ val name = if (it.hasTagName()) {
+ it.tagName
+ } else {
+ ExifTags.getTagName(it.tagType) ?: it.tagName
+ }
+ return Pair(name, it.description)
+ }
+
// catalog metadata
private const val KEY_MIME_TYPE = "mimeType"
private const val KEY_DATE_MILLIS = "dateMillis"
@@ -914,6 +984,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private const val KEY_LONGITUDE = "longitude"
private const val KEY_XMP_SUBJECTS = "xmpSubjects"
private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription"
+ private const val KEY_RATING = "rating"
private const val MASK_IS_ANIMATED = 1 shl 0
private const val MASK_IS_FLIPPED = 1 shl 1
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt
index 8970a02d9..6d134fae4 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt
@@ -170,7 +170,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied)
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
} else {
- MainActivity.notifyError("failed to resolve activity for intent=$intent")
+ MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
onDenied()
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/TiffTags.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt
similarity index 94%
rename from android/app/src/main/kotlin/deckers/thibault/aves/metadata/TiffTags.kt
rename to android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt
index 2f8a9c23c..4dbe77450 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/TiffTags.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt
@@ -1,6 +1,7 @@
package deckers.thibault.aves.metadata
-object TiffTags {
+// Exif tags missing from `metadata-extractor`
+object ExifTags {
// XPosition
// Tag = 286 (011E.H)
private const val TAG_X_POSITION = 0x011e
@@ -32,6 +33,12 @@ object TiffTags {
// SAMPLEFORMAT_COMPLEXIEEEFP 6 // complex ieee floating
private const val TAG_SAMPLE_FORMAT = 0x0153
+
+ // Rating tag used by Windows, value in percent
+ // Tag = 18249 (4749.H)
+ // Type = SHORT
+ private const val TAG_RATING_PERCENT = 0x4749
+
/*
SGI
tags 32995-32999
@@ -125,6 +132,7 @@ object TiffTags {
TAG_COLOR_MAP to "Color Map",
TAG_EXTRA_SAMPLES to "Extra Samples",
TAG_SAMPLE_FORMAT to "Sample Format",
+ TAG_RATING_PERCENT to "Rating Percent",
// SGI
TAG_MATTEING to "Matteing",
// GeoTIFF
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
index 5c84e153b..84d5959f2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt
@@ -1,25 +1,35 @@
package deckers.thibault.aves.metadata
+import android.util.Log
+import com.drew.lang.ByteArrayReader
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
+import deckers.thibault.aves.utils.LogUtils
import java.text.SimpleDateFormat
import java.util.*
object MetadataExtractorHelper {
+ private val LOG_TAG = LogUtils.createTag()
+
const val PNG_ITXT_DIR_NAME = "PNG-iTXt"
private const val PNG_TEXT_DIR_NAME = "PNG-tEXt"
const val PNG_TIME_DIR_NAME = "PNG-tIME"
private const val PNG_ZTXT_DIR_NAME = "PNG-zTXt"
+ private const val PNG_RAW_PROFILE_EXIF = "Raw profile type exif"
+ private const val PNG_RAW_PROFILE_IPTC = "Raw profile type iptc"
val PNG_LAST_MODIFICATION_TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.ROOT)
// Pattern to extract profile name, length, and text data
// of raw profiles (EXIF, IPTC, etc.) in PNG `zTXt` chunks
// e.g. "iptc [...] 114 [...] 3842494d040400[...]"
+ // e.g. "exif [...] 134 [...] 4578696600004949[...]"
private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL)
// extensions
@@ -59,14 +69,14 @@ object MetadataExtractorHelper {
- If the ModelTransformationTag is included in an IFD, then a ModelPixelScaleTag SHALL NOT be included
- If the ModelPixelScaleTag is included in an IFD, then a ModelTiepointTag SHALL also be included.
*/
- fun ExifIFD0Directory.isGeoTiff(): Boolean {
- if (!this.containsTag(TiffTags.TAG_GEO_KEY_DIRECTORY)) return false
+ fun ExifDirectoryBase.isGeoTiff(): Boolean {
+ if (!this.containsTag(ExifTags.TAG_GEO_KEY_DIRECTORY)) return false
- val modelTiepoint = this.containsTag(TiffTags.TAG_MODEL_TIEPOINT)
- val modelTransformation = this.containsTag(TiffTags.TAG_MODEL_TRANSFORMATION)
+ val modelTiepoint = this.containsTag(ExifTags.TAG_MODEL_TIEPOINT)
+ val modelTransformation = this.containsTag(ExifTags.TAG_MODEL_TRANSFORMATION)
if (!modelTiepoint && !modelTransformation) return false
- val modelPixelScale = this.containsTag(TiffTags.TAG_MODEL_PIXEL_SCALE)
+ val modelPixelScale = this.containsTag(ExifTags.TAG_MODEL_PIXEL_SCALE)
if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiepoint)) return false
return true
@@ -77,22 +87,29 @@ object MetadataExtractorHelper {
fun Directory.isPngTextDir(): Boolean = this is PngDirectory && setOf(PNG_ITXT_DIR_NAME, PNG_TEXT_DIR_NAME, PNG_ZTXT_DIR_NAME).contains(this.name)
fun extractPngProfile(key: String, valueString: String): Iterable? {
- when (key) {
- "Raw profile type iptc" -> {
- val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString)
- if (match != null) {
- val dataString = match.groupValues[3]
- val hexString = dataString.replace(Regex("[\\r\\n]"), "")
- val dataBytes = hexStringToByteArray(hexString)
- if (dataBytes != null) {
- val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE)
- if (start != -1) {
- val segmentBytes = dataBytes.copyOfRange(fromIndex = start, toIndex = dataBytes.size - start)
- val metadata = com.drew.metadata.Metadata()
- IptcReader().extract(SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.size.toLong())
- return metadata.directories
+ if (key == PNG_RAW_PROFILE_EXIF || key == PNG_RAW_PROFILE_IPTC) {
+ val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString)
+ if (match != null) {
+ val dataString = match.groupValues[3]
+ val hexString = dataString.replace(Regex("[\\r\\n]"), "")
+ val dataBytes = hexString.decodeHex()
+ if (dataBytes != null) {
+ val metadata = com.drew.metadata.Metadata()
+ when (key) {
+ PNG_RAW_PROFILE_EXIF -> {
+ if (ExifReader.startsWithJpegExifPreamble(dataBytes)) {
+ ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length)
+ }
+ }
+ PNG_RAW_PROFILE_IPTC -> {
+ val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE)
+ if (start != -1) {
+ val segmentBytes = dataBytes.copyOfRange(fromIndex = start, toIndex = dataBytes.size)
+ IptcReader().extract(SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.size.toLong())
+ }
}
}
+ return metadata.directories
}
}
}
@@ -101,15 +118,18 @@ object MetadataExtractorHelper {
// convenience methods
- private fun hexStringToByteArray(hexString: String): ByteArray? {
- if (hexString.length % 2 != 0) return null
+ private fun String.decodeHex(): ByteArray? {
+ if (length % 2 != 0) return null
- val dataBytes = ByteArray(hexString.length / 2)
- var i = 0
- while (i < hexString.length) {
- dataBytes[i / 2] = hexString.substring(i, i + 2).toByte(16)
- i += 2
+ try {
+ val byteIterator = chunkedSequence(2)
+ .map { it.toInt(16).toByte() }
+ .iterator()
+
+ return ByteArray(length / 2) { byteIterator.next() }
+ } catch (e: NumberFormatException) {
+ Log.w(LOG_TAG, "failed to decode hex string=$this", e)
}
- return dataBytes
+ return null
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
index 72c38f447..2819915e8 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
@@ -15,6 +15,7 @@ object XMP {
// standard namespaces
// cf com.adobe.internal.xmp.XMPConst
const val DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/"
+ const val MICROSOFTPHOTO_SCHEMA_NS = "http://ns.microsoft.com/photo/1.0/"
const val PHOTOSHOP_SCHEMA_NS = "http://ns.adobe.com/photoshop/1.0/"
const val XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/"
private const val XMP_GIMG_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/g/img/"
@@ -27,11 +28,13 @@ object XMP {
const val CONTAINER_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/"
private const val CONTAINER_ITEM_SCHEMA_NS = "http://ns.google.com/photos/1.0/container/item/"
- const val SUBJECT_PROP_NAME = "dc:subject"
- const val TITLE_PROP_NAME = "dc:title"
- const val DESCRIPTION_PROP_NAME = "dc:description"
+ const val DC_DESCRIPTION_PROP_NAME = "dc:description"
+ const val DC_SUBJECT_PROP_NAME = "dc:subject"
+ const val DC_TITLE_PROP_NAME = "dc:title"
+ const val MS_RATING_PROP_NAME = "MicrosoftPhoto:Rating"
const val PS_DATE_CREATED_PROP_NAME = "photoshop:DateCreated"
- const val CREATE_DATE_PROP_NAME = "xmp:CreateDate"
+ const val XMP_CREATE_DATE_PROP_NAME = "xmp:CreateDate"
+ const val XMP_RATING_PROP_NAME = "xmp:Rating"
private const val GENERIC_LANG = ""
private const val SPECIFIC_LANG = "en-US"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 1e04ca4ac..9ef5a9112 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -800,63 +800,47 @@ abstract class ImageProvider {
}
}
- fun setIptc(
+ fun editMetadata(
context: Context,
path: String,
uri: Uri,
mimeType: String,
- postEditScan: Boolean,
+ modifier: FieldMap,
callback: ImageOpCallback,
- iptc: List? = null,
) {
- val newFields = HashMap()
+ if (modifier.containsKey("iptc")) {
+ val iptc = (modifier["iptc"] as List<*>?)?.filterIsInstance()
+ if (!editIptc(
+ context = context,
+ path = path,
+ uri = uri,
+ mimeType = mimeType,
+ callback = callback,
+ iptc = iptc,
+ )
+ ) return
+ }
- val success = editIptc(
- context = context,
- path = path,
- uri = uri,
- mimeType = mimeType,
- callback = callback,
- iptc = iptc,
- )
-
- if (success) {
- if (postEditScan) {
- scanPostMetadataEdit(context, path, uri, mimeType, newFields, callback)
- } else {
- callback.onSuccess(HashMap())
+ if (modifier.containsKey("xmp")) {
+ val xmp = modifier["xmp"] as Map<*, *>?
+ if (xmp != null) {
+ val coreXmp = xmp["xmp"] as String?
+ val extendedXmp = xmp["extendedXmp"] as String?
+ if (!editXmp(
+ context = context,
+ path = path,
+ uri = uri,
+ mimeType = mimeType,
+ callback = callback,
+ coreXmp = coreXmp,
+ extendedXmp = extendedXmp,
+ )
+ ) return
}
- } else {
- callback.onFailure(Exception("failed to set IPTC"))
}
- }
- fun setXmp(
- context: Context,
- path: String,
- uri: Uri,
- mimeType: String,
- callback: ImageOpCallback,
- coreXmp: String? = null,
- extendedXmp: String? = null,
- ) {
val newFields = HashMap()
-
- val success = editXmp(
- context = context,
- path = path,
- uri = uri,
- mimeType = mimeType,
- callback = callback,
- coreXmp = coreXmp,
- extendedXmp = extendedXmp,
- )
-
- if (success) {
- scanPostMetadataEdit(context, path, uri, mimeType, newFields, callback)
- } else {
- callback.onFailure(Exception("failed to set XMP"))
- }
+ scanPostMetadataEdit(context, path, uri, mimeType, newFields, callback)
}
fun removeMetadataTypes(
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
index e77a1648e..02eeaf485 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
@@ -55,7 +55,7 @@ object PermissionManager {
MainActivity.pendingStorageAccessResultHandlers[MainActivity.DOCUMENT_TREE_ACCESS_REQUEST] = PendingStorageAccessResultHandler(path, onGranted, onDenied)
activity.startActivityForResult(intent, MainActivity.DOCUMENT_TREE_ACCESS_REQUEST)
} else {
- MainActivity.notifyError("failed to resolve activity for intent=$intent")
+ MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
onDenied()
}
}
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..aadf5be08
--- /dev/null
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Aves
+ Búsqueda
+ Videos
+ Explorar medios
+ Explorar imágenes & videos
+ Explorando medios
+ Anular
+
\ No newline at end of file
diff --git a/fastlane/metadata/android/de/images/featureGraphic.png b/fastlane/metadata/android/de/images/featureGraphic.png
new file mode 100644
index 000000000..a0b3a3e77
Binary files /dev/null and b/fastlane/metadata/android/de/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/es-MX/full_description.txt b/fastlane/metadata/android/es-MX/full_description.txt
new file mode 100644
index 000000000..5015acfec
--- /dev/null
+++ b/fastlane/metadata/android/es-MX/full_description.txt
@@ -0,0 +1,5 @@
+Aves puede manejar todo tipo de imágenes y videos, incluyendo los típicos JPEG y MP4, pero además cosas mas exóticas como TIFF multipágina, SVG, viejos AVI y más! Inspecciona su colección multimedia para identificar fotos en movimiento, panoramas (conocidas como fotos esféricas), videos en 360° y también archivos GeoTIFF.
+
+La navegación y búsqueda son partes importantes de Aves. Su propósito es que los usuarios puedan fácimente ir de álbumes a fotos, etiquetas, mapas, etc.
+
+Aves se integra con Android (desde API 19 a 31, por ej. desde KitKat hasta S) con características como vínculos de aplicación y manejo de búsqueda global. También funciona como un visor y seleccionador multimedia.
\ No newline at end of file
diff --git a/fastlane/metadata/android/es-MX/images/featureGraphic.png b/fastlane/metadata/android/es-MX/images/featureGraphic.png
new file mode 100644
index 000000000..6bcf1d781
Binary files /dev/null and b/fastlane/metadata/android/es-MX/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/es-MX/short_description.txt b/fastlane/metadata/android/es-MX/short_description.txt
new file mode 100644
index 000000000..03b729cdc
--- /dev/null
+++ b/fastlane/metadata/android/es-MX/short_description.txt
@@ -0,0 +1 @@
+Galería y visor de metadatos
\ No newline at end of file
diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart
index e583467f7..97faccc96 100644
--- a/lib/image_providers/region_provider.dart
+++ b/lib/image_providers/region_provider.dart
@@ -49,6 +49,7 @@ class RegionProvider extends ImageProvider {
}
return await decode(bytes);
} catch (error) {
+ // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType region decoding failed (page $pageId)');
}
diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart
index 57fe937bd..3c5b5699c 100644
--- a/lib/image_providers/thumbnail_provider.dart
+++ b/lib/image_providers/thumbnail_provider.dart
@@ -50,7 +50,8 @@ class ThumbnailProvider extends ImageProvider {
}
return await decode(bytes);
} catch (error) {
- debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error');
+ // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
+ debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType decoding failed (page $pageId)');
}
}
diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart
index 416d407f1..1aeca959e 100644
--- a/lib/image_providers/uri_image_provider.dart
+++ b/lib/image_providers/uri_image_provider.dart
@@ -68,6 +68,7 @@ class UriImage extends ImageProvider with EquatableMixin {
}
return await decode(bytes);
} catch (error) {
+ // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType decoding failed (page $pageId)');
} finally {
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 8a61c59b7..867ecab0d 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -22,7 +22,7 @@
"nextTooltip": "Nächste",
"showTooltip": "Anzeigen",
"hideTooltip": "Ausblenden",
- "removeTooltip": "Entfernen",
+ "actionRemove": "Entfernen",
"resetButtonTooltip": "Zurücksetzen",
"doubleBackExitMessage": "Tippen Sie zum Verlassen erneut auf „Zurück“.",
@@ -73,12 +73,15 @@
"videoActionSettings": "Einstellungen",
"entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten",
+ "entryInfoActionEditRating": "Bewertung bearbeiten",
"entryInfoActionEditTags": "Tags bearbeiten",
"entryInfoActionRemoveMetadata": "Metadaten entfernen",
"filterFavouriteLabel": "Favorit",
"filterLocationEmptyLabel": "Ungeortet",
"filterTagEmptyLabel": "Unmarkiert",
+ "filterRatingUnratedLabel": "Nicht bewertet",
+ "filterRatingRejectedLabel": "Verworfen",
"filterTypeAnimatedLabel": "Animationen",
"filterTypeMotionPhotoLabel": "Bewegtes Foto",
"filterTypePanoramaLabel": "Panorama",
@@ -137,6 +140,8 @@
"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.",
"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.",
"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}.}}",
@@ -178,14 +183,17 @@
"renameEntryDialogLabel": "Neuer Name",
"editEntryDateDialogTitle": "Datum & Uhrzeit",
- "editEntryDateDialogSet": "Festlegen",
- "editEntryDateDialogShift": "Verschieben",
+ "editEntryDateDialogSetCustom": "Datum einstellen",
+ "editEntryDateDialogCopyField": "Von anderem Datum kopieren",
"editEntryDateDialogExtractFromTitle": "Auszug aus dem Titel",
- "editEntryDateDialogClear": "Aufräumen",
- "editEntryDateDialogFieldSelection": "Feldauswahl",
+ "editEntryDateDialogShift": "Verschieben",
+ "editEntryDateDialogSourceFileModifiedDate": "Änderungsdatum der Datei",
+ "editEntryDateDialogTargetFieldsHeader": "Zu ändernde Felder",
"editEntryDateDialogHours": "Stunden",
"editEntryDateDialogMinutes": "Minuten",
+ "editEntryRatingDialogTitle": "Bewertung",
+
"removeEntryMetadataDialogTitle": "Entfernung von Metadaten",
"removeEntryMetadataDialogMore": "Mehr",
@@ -270,6 +278,7 @@
"collectionSortDate": "Nach Datum",
"collectionSortSize": "Nach Größe",
"collectionSortName": "Nach Album & Dateiname",
+ "collectionSortRating": "Nach Bewertung",
"collectionGroupAlbum": "Nach Album",
"collectionGroupMonth": "Nach Monat",
@@ -343,6 +352,7 @@
"searchSectionCountries": "Länder",
"searchSectionPlaces": "Orte",
"searchSectionTags": "Tags",
+ "searchSectionRating": "Bewertungen",
"settingsPageTitle": "Einstellungen",
"settingsSystemDefault": "System",
@@ -366,8 +376,10 @@
"settingsNavigationDrawerAddAlbum": "Album hinzufügen",
"settingsSectionThumbnails": "Vorschaubilder",
+ "settingsThumbnailShowFavouriteIcon": "Favoriten-Symbol anzeigen",
"settingsThumbnailShowLocationIcon": "Standort-Symbol anzeigen",
"settingsThumbnailShowMotionPhotoIcon": "Bewegungsfoto-Symbol anzeigen",
+ "settingsThumbnailShowRating": "Bewertung anzeigen",
"settingsThumbnailShowRawIcon": "Rohdaten-Symbol anzeigen",
"settingsThumbnailShowVideoDuration": "Videodauer anzeigen",
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 53c770b61..a4b3ac6a8 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -37,7 +37,7 @@
"nextTooltip": "Next",
"showTooltip": "Show",
"hideTooltip": "Hide",
- "removeTooltip": "Remove",
+ "actionRemove": "Remove",
"resetButtonTooltip": "Reset",
"doubleBackExitMessage": "Tap “back” again to exit.",
@@ -88,12 +88,15 @@
"videoActionSettings": "Settings",
"entryInfoActionEditDate": "Edit date & time",
+ "entryInfoActionEditRating": "Edit rating",
"entryInfoActionEditTags": "Edit tags",
"entryInfoActionRemoveMetadata": "Remove metadata",
"filterFavouriteLabel": "Favourite",
"filterLocationEmptyLabel": "Unlocated",
"filterTagEmptyLabel": "Untagged",
+ "filterRatingUnratedLabel": "Unrated",
+ "filterRatingRejectedLabel": "Rejected",
"filterTypeAnimatedLabel": "Animated",
"filterTypeMotionPhotoLabel": "Motion Photo",
"filterTypePanoramaLabel": "Panorama",
@@ -109,10 +112,12 @@
"@coordinateDms": {
"placeholders": {
"coordinate": {
- "type": "String"
+ "type": "String",
+ "example": "38° 41′ 47.72″"
},
"direction": {
- "type": "String"
+ "type": "String",
+ "example": "S"
}
}
},
@@ -159,7 +164,9 @@
"@otherDirectoryDescription": {
"placeholders": {
"name": {
- "type": "String"
+ "type": "String",
+ "example": "Pictures",
+ "description": "the name of a specific directory"
}
}
},
@@ -168,10 +175,13 @@
"@storageAccessDialogMessage": {
"placeholders": {
"directory": {
- "type": "String"
+ "type": "String",
+ "description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
- "type": "String"
+ "type": "String",
+ "example": "SD card",
+ "description": "the name of a storage volume"
}
}
},
@@ -180,10 +190,13 @@
"@restrictedAccessDialogMessage": {
"placeholders": {
"directory": {
- "type": "String"
+ "type": "String",
+ "description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
- "type": "String"
+ "type": "String",
+ "example": "SD card",
+ "description": "the name of a storage volume"
}
}
},
@@ -192,16 +205,22 @@
"@notEnoughSpaceDialogMessage": {
"placeholders": {
"neededSize": {
- "type": "String"
+ "type": "String",
+ "example": "314 MB"
},
"freeSize": {
- "type": "String"
+ "type": "String",
+ "example": "123 MB"
},
"volume": {
- "type": "String"
+ "type": "String",
+ "example": "SD card",
+ "description": "the name of a storage volume"
}
}
},
+ "missingSystemFilePickerDialogTitle": "Missing System File Picker",
+ "missingSystemFilePickerDialogMessage": "The system file picker is missing or disabled. Please enable it and try again.",
"unsupportedTypeDialogTitle": "Unsupported Types",
"unsupportedTypeDialogMessage": "{count, plural, =1{This operation is not supported for items of the following type: {types}.} other{This operation is not supported for items of the following types: {types}.}}",
@@ -209,7 +228,9 @@
"placeholders": {
"count": {},
"types": {
- "type": "String"
+ "type": "String",
+ "example": "GIF, TIFF, MP4",
+ "description": "a list of unsupported types"
}
}
},
@@ -233,7 +254,10 @@
"videoResumeDialogMessage": "Do you want to resume playing at {time}?",
"@videoResumeDialogMessage": {
"placeholders": {
- "time": {}
+ "time": {
+ "type": "String",
+ "example": "13:37"
+ }
}
},
"videoStartOverButtonLabel": "START OVER",
@@ -271,14 +295,17 @@
"renameEntryDialogLabel": "New name",
"editEntryDateDialogTitle": "Date & Time",
- "editEntryDateDialogSet": "Set",
- "editEntryDateDialogShift": "Shift",
+ "editEntryDateDialogSetCustom": "Set custom date",
+ "editEntryDateDialogCopyField": "Copy from other date",
"editEntryDateDialogExtractFromTitle": "Extract from title",
- "editEntryDateDialogClear": "Clear",
- "editEntryDateDialogFieldSelection": "Field selection",
+ "editEntryDateDialogShift": "Shift",
+ "editEntryDateDialogSourceFileModifiedDate": "File modified date",
+ "editEntryDateDialogTargetFieldsHeader": "Fields to modify",
"editEntryDateDialogHours": "Hours",
"editEntryDateDialogMinutes": "Minutes",
+ "editEntryRatingDialogTitle": "Rating",
+
"removeEntryMetadataDialogTitle": "Metadata Removal",
"removeEntryMetadataDialogMore": "More",
@@ -338,10 +365,12 @@
"@aboutCreditsTranslatorLine": {
"placeholders": {
"language": {
- "type": "String"
+ "type": "String",
+ "example": "Sumerian"
},
"names": {
- "type": "String"
+ "type": "String",
+ "example": "Reggie Lampert"
}
}
},
@@ -378,6 +407,7 @@
"collectionSortDate": "By date",
"collectionSortSize": "By size",
"collectionSortName": "By album & file name",
+ "collectionSortRating": "By rating",
"collectionGroupAlbum": "By album",
"collectionGroupMonth": "By month",
@@ -491,6 +521,7 @@
"searchSectionCountries": "Countries",
"searchSectionPlaces": "Places",
"searchSectionTags": "Tags",
+ "searchSectionRating": "Ratings",
"settingsPageTitle": "Settings",
"settingsSystemDefault": "System",
@@ -514,8 +545,10 @@
"settingsNavigationDrawerAddAlbum": "Add album",
"settingsSectionThumbnails": "Thumbnails",
+ "settingsThumbnailShowFavouriteIcon": "Show favourite icon",
"settingsThumbnailShowLocationIcon": "Show location icon",
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
+ "settingsThumbnailShowRating": "Show rating",
"settingsThumbnailShowRawIcon": "Show raw icon",
"settingsThumbnailShowVideoDuration": "Show video duration",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
new file mode 100644
index 000000000..f23a0af12
--- /dev/null
+++ b/lib/l10n/app_es.arb
@@ -0,0 +1,540 @@
+{
+ "appName": "Aves",
+ "welcomeMessage": "Bienvenido a Aves",
+ "welcomeOptional": "Opcional",
+ "welcomeTermsToggle": "Acepto los términos y condiciones",
+ "itemCount": "{count, plural, =1{1 elemento} other{{count} elementos}}",
+
+ "timeSeconds": "{seconds, plural, =1{1 segundo} other{{seconds} segundos}}",
+ "timeMinutes": "{minutes, plural, =1{1 minuto} other{{minutes} minutos}}",
+
+ "applyButtonLabel": "APLICAR",
+ "deleteButtonLabel": "BORRAR",
+ "nextButtonLabel": "SIGUIENTE",
+ "showButtonLabel": "MOSTRAR",
+ "hideButtonLabel": "OCULTAR",
+ "continueButtonLabel": "CONTINUAR",
+
+ "cancelTooltip": "Cancelar",
+ "changeTooltip": "Cambiar",
+ "clearTooltip": "Limpiar",
+ "previousTooltip": "Anterior",
+ "nextTooltip": "Siguiente",
+ "showTooltip": "Mostrar",
+ "hideTooltip": "Ocultar",
+ "actionRemove": "Remover",
+ "resetButtonTooltip": "Restablecer",
+
+ "doubleBackExitMessage": "Presione «atrás» nuevamente para salir.",
+
+ "sourceStateLoading": "Cargando",
+ "sourceStateCataloguing": "Catalogando",
+ "sourceStateLocatingCountries": "Ubicando países",
+ "sourceStateLocatingPlaces": "Ubicando lugares",
+
+ "chipActionDelete": "Borrar",
+ "chipActionGoToAlbumPage": "Mostrar en Álbumes",
+ "chipActionGoToCountryPage": "Mostrar en Países",
+ "chipActionGoToTagPage": "Mostrar en Etiquetas",
+ "chipActionHide": "Esconder",
+ "chipActionPin": "Fijar",
+ "chipActionUnpin": "Dejar de fijar",
+ "chipActionRename": "Renombrar",
+ "chipActionSetCover": "Elegir portada",
+ "chipActionCreateAlbum": "Crear álbum",
+
+ "entryActionCopyToClipboard": "Copiar al portapapeles",
+ "entryActionDelete": "Borrar",
+ "entryActionExport": "Exportar",
+ "entryActionInfo": "Información",
+ "entryActionRename": "Renombrar",
+ "entryActionRotateCCW": "Rotar en sentido antihorario",
+ "entryActionRotateCW": "Rotar en sentido horario",
+ "entryActionFlip": "Voltear horizontalmente",
+ "entryActionPrint": "Imprimir",
+ "entryActionShare": "Compartir",
+ "entryActionViewSource": "Ver fuente",
+ "entryActionViewMotionPhotoVideo": "Abrir foto en movimiento",
+ "entryActionEdit": "Editar con…",
+ "entryActionOpen": "Abrir con…",
+ "entryActionSetAs": "Establecer como…",
+ "entryActionOpenMap": "Mostrar en aplicación de mapa…",
+ "entryActionRotateScreen": "Rotar pantalla",
+ "entryActionAddFavourite": "Agregar a favoritos",
+ "entryActionRemoveFavourite": "Quitar de favoritos",
+
+ "videoActionCaptureFrame": "Capturar fotograma",
+ "videoActionPause": "Pausa",
+ "videoActionPlay": "Reproducir",
+ "videoActionReplay10": "Retroceder 10 segundos",
+ "videoActionSkip10": "Adelantar 10 segundos",
+ "videoActionSelectStreams": "Seleccionar pistas",
+ "videoActionSetSpeed": "Velocidad de reproducción",
+ "videoActionSettings": "Ajustes",
+
+ "entryInfoActionEditDate": "Editar fecha y hora",
+ "entryInfoActionEditRating": "Editar clasificación",
+ "entryInfoActionEditTags": "Editar etiquetas",
+ "entryInfoActionRemoveMetadata": "Eliminar metadatos",
+
+ "filterFavouriteLabel": "Favorito",
+ "filterLocationEmptyLabel": "No localizado",
+ "filterTagEmptyLabel": "Sin etiquetar",
+ "filterRatingUnratedLabel": "Sin clasificar",
+ "filterRatingRejectedLabel": "Rechazado",
+ "filterTypeAnimatedLabel": "Animado",
+ "filterTypeMotionPhotoLabel": "Foto en movimiento",
+ "filterTypePanoramaLabel": "Panorámica",
+ "filterTypeRawLabel": "Raw",
+ "filterTypeSphericalVideoLabel": "Video en 360°",
+ "filterTypeGeotiffLabel": "GeoTIFF",
+ "filterMimeImageLabel": "Imagen",
+ "filterMimeVideoLabel": "Video",
+
+ "coordinateFormatDms": "GMS",
+ "coordinateFormatDecimal": "Grados decimales",
+ "coordinateDms": "{coordinate} {direction}",
+ "coordinateDmsNorth": "N",
+ "coordinateDmsSouth": "S",
+ "coordinateDmsEast": "E",
+ "coordinateDmsWest": "O",
+
+ "unitSystemMetric": "Métrico",
+ "unitSystemImperial": "Imperial",
+
+ "videoLoopModeNever": "Nunca",
+ "videoLoopModeShortOnly": "Sólo videos cortos",
+ "videoLoopModeAlways": "Siempre",
+
+ "mapStyleGoogleNormal": "Mapas de Google",
+ "mapStyleGoogleHybrid": "Mapas de Google (Híbrido)",
+ "mapStyleGoogleTerrain": "Mapas de Google (Superficie)",
+ "mapStyleOsmHot": "OSM Humanitario",
+ "mapStyleStamenToner": "Stamen Monocromático (Toner)",
+ "mapStyleStamenWatercolor": "Stamen Acuarela (Watercolor)",
+
+ "nameConflictStrategyRename": "Renombrar",
+ "nameConflictStrategyReplace": "Reemplazar",
+ "nameConflictStrategySkip": "Saltear",
+
+ "keepScreenOnNever": "Nunca",
+ "keepScreenOnViewerOnly": "Sólo en el visor",
+ "keepScreenOnAlways": "Siempre",
+
+ "accessibilityAnimationsRemove": "Prevenir efectos en pantalla",
+ "accessibilityAnimationsKeep": "Mantener efectos en pantalla",
+
+ "albumTierNew": "Nuevo",
+ "albumTierPinned": "Fijado",
+ "albumTierSpecial": "Común",
+ "albumTierApps": "Aplicaciones",
+ "albumTierRegular": "Otros",
+
+ "storageVolumeDescriptionFallbackPrimary": "Almacenamiento interno",
+ "storageVolumeDescriptionFallbackNonPrimary": "Tarjeta de memoria",
+ "rootDirectoryDescription": "el directorio raíz",
+ "otherDirectoryDescription": "el directorio «{name}»",
+ "storageAccessDialogTitle": "Acceso al almacenamiento",
+ "storageAccessDialogMessage": "Por favor seleccione {directory} en «{volume}» en la siguiente pantalla para permitir a esta aplicación tener acceso.",
+ "restrictedAccessDialogTitle": "Acceso restringido",
+ "restrictedAccessDialogMessage": "Esta aplicación no tiene permiso para modificar archivos de {directory} en «{volume}».\n\nPor favor use un gestor de archivos o la aplicación de galería preinstalada para mover los elementos a otro directorio.",
+ "notEnoughSpaceDialogTitle": "Espacio insuficiente",
+ "notEnoughSpaceDialogMessage": "Esta operación necesita {neededSize} de espacio libre en «{volume}» para completarse, pero sólo hay {freeSize} disponible.",
+
+ "missingSystemFilePickerDialogTitle": "Selector de archivos del sistema no disponible",
+ "missingSystemFilePickerDialogMessage": "El selector de archivos del sistema no se encuentra disponible o fue deshabilitado. Por favor habilítelo e intente nuevamente.",
+
+ "unsupportedTypeDialogTitle": "Tipos de archivo incompatibles",
+ "unsupportedTypeDialogMessage": "{count, plural, =1{Esta operación no está disponible para un elemento del siguiente tipo: {types}.} other{Esta operación no está disponible para elementos de los siguientes tipos: {types}.}}",
+
+ "nameConflictDialogSingleSourceMessage": "Algunos archivos en el directorio de destino tienen el mismo nombre.",
+ "nameConflictDialogMultipleSourceMessage": "Algunos archivos tienen el mismo nombre.",
+
+ "addShortcutDialogLabel": "Etiqueta del atajo",
+ "addShortcutButtonLabel": "AGREGAR",
+
+ "noMatchingAppDialogTitle": "Sin aplicación compatible",
+ "noMatchingAppDialogMessage": "No se encontraron aplicaciones para manejar esto.",
+
+ "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de borrar este elemento?} other{¿Está seguro de querer borrar {count} elementos?}}",
+
+ "videoResumeDialogMessage": "¿Desea reanudar la reproducción a las {time}?",
+ "videoStartOverButtonLabel": "VOLVER A EMPEZAR",
+ "videoResumeButtonLabel": "REANUDAR",
+
+ "setCoverDialogTitle": "Elegir carátula",
+ "setCoverDialogLatest": "Elemento más reciente",
+ "setCoverDialogCustom": "Personalizado",
+
+ "hideFilterConfirmationDialogMessage": "Fotos y videos que concuerden serán ocultados de su colección. Puede volver a mostrarlos desde los ajustes de «Privacidad».\n\n¿Está seguro de que desea ocultarlos?",
+
+ "newAlbumDialogTitle": "Álbum nuevo",
+ "newAlbumDialogNameLabel": "Nombre del álbum",
+ "newAlbumDialogNameLabelAlreadyExistsHelper": "El directorio ya existe",
+ "newAlbumDialogStorageLabel": "Almacenamiento:",
+
+ "renameAlbumDialogLabel": "Renombrar",
+ "renameAlbumDialogLabelAlreadyExistsHelper": "El directorio ya existe",
+
+ "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar este álbum y un elemento?} other{¿Está seguro de que desea borrar este álbum y sus {count} elementos?}}",
+ "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar estos álbumes y un elemento?} other{¿Está seguro de que desea borrar estos álbumes y sus {count} elementos?}}",
+
+ "exportEntryDialogFormat": "Formato:",
+
+ "renameEntryDialogLabel": "Renombrar",
+
+ "editEntryDateDialogTitle": "Fecha y hora",
+ "editEntryDateDialogSetCustom": "Establecer fecha personalizada",
+ "editEntryDateDialogCopyField": "Copiar de otra fecha",
+ "editEntryDateDialogExtractFromTitle": "Extraer del título",
+ "editEntryDateDialogShift": "Cambiar",
+ "editEntryDateDialogSourceFileModifiedDate": "Fecha de modificación del archivo",
+ "editEntryDateDialogTargetFieldsHeader": "Campos a modificar",
+ "editEntryDateDialogHours": "Horas",
+ "editEntryDateDialogMinutes": "Minutos",
+
+ "editEntryRatingDialogTitle": "Clasificación",
+
+ "removeEntryMetadataDialogTitle": "Eliminación de metadatos",
+ "removeEntryMetadataDialogMore": "Más",
+
+ "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP es necesario para reproducir la animación de una foto en movimiento.\n\n¿Está seguro de que desea removerlo?",
+
+ "videoSpeedDialogLabel": "Velocidad de reproducción",
+
+ "videoStreamSelectionDialogVideo": "Video",
+ "videoStreamSelectionDialogAudio": "Audio",
+ "videoStreamSelectionDialogText": "Subtítulos",
+ "videoStreamSelectionDialogOff": "Desactivado",
+ "videoStreamSelectionDialogTrack": "Pista",
+ "videoStreamSelectionDialogNoSelection": "No hay otras pistas.",
+
+ "genericSuccessFeedback": "¡Completado!",
+ "genericFailureFeedback": "Falló",
+
+ "menuActionConfigureView": "Ver",
+ "menuActionSelect": "Seleccionar",
+ "menuActionSelectAll": "Seleccionar todo",
+ "menuActionSelectNone": "Deseleccionar",
+ "menuActionMap": "Mapa",
+ "menuActionStats": "Estadísticas",
+
+ "viewDialogTabSort": "Ordenar",
+ "viewDialogTabGroup": "Grupo",
+ "viewDialogTabLayout": "Disposición",
+
+ "tileLayoutGrid": "Cuadrícula",
+ "tileLayoutList": "Lista",
+
+ "aboutPageTitle": "Acerca de",
+ "aboutLinkSources": "Fuentes",
+ "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",
+ "aboutBugCopyInfoInstruction": "Copiar información del sistema",
+ "aboutBugCopyInfoButton": "Copiar",
+ "aboutBugReportInstruction": "Reportar en GitHub con los registros y la información del sistema",
+ "aboutBugReportButton": "Reportar",
+
+ "aboutCredits": "Créditos",
+ "aboutCreditsWorldAtlas1": "Esta aplicación usa un archivo TopoJSON de",
+ "aboutCreditsWorldAtlas2": "bajo licencia ISC.",
+ "aboutCreditsTranslators": "Traductores:",
+ "aboutCreditsTranslatorLine": "{language}: {names}",
+
+ "aboutLicenses": "Licencias de código abierto",
+ "aboutLicensesBanner": "Esta aplicación usa los siguientes paquetes y librerías de código abierto.",
+ "aboutLicensesAndroidLibraries": "Librerías de Android",
+ "aboutLicensesFlutterPlugins": "Añadidos de Flutter",
+ "aboutLicensesFlutterPackages": "Paquetes de Flutter",
+ "aboutLicensesDartPackages": "Paquetes de Dart",
+ "aboutLicensesShowAllButtonLabel": "Mostrar todas las licencias",
+
+ "policyPageTitle": "Política de privacidad",
+
+ "collectionPageTitle": "Colección",
+ "collectionPickPageTitle": "Elegir",
+ "collectionSelectionPageTitle": "{count, plural, =0{Seleccionar} =1{1 elemento} other{{count} elementos}}",
+
+ "collectionActionShowTitleSearch": "Mostrar filtros de títulos",
+ "collectionActionHideTitleSearch": "Ocultar filtros de títulos",
+ "collectionActionAddShortcut": "Agregar atajo",
+ "collectionActionCopy": "Copiar a álbum",
+ "collectionActionMove": "Mover a álbum",
+ "collectionActionRescan": "Volver a buscar",
+ "collectionActionEdit": "Editar",
+
+ "collectionSearchTitlesHintText": "Buscar títulos",
+
+ "collectionSortDate": "Por fecha",
+ "collectionSortSize": "Por tamaño",
+ "collectionSortName": "Por nombre de álbum y archivo",
+ "collectionSortRating": "Por clasificación",
+
+ "collectionGroupAlbum": "Por álbum",
+ "collectionGroupMonth": "Por mes",
+ "collectionGroupDay": "Por día",
+ "collectionGroupNone": "No agrupar",
+
+ "sectionUnknown": "Desconocido",
+ "dateToday": "Hoy",
+ "dateYesterday": "Ayer",
+ "dateThisMonth": "Este mes",
+ "collectionDeleteFailureFeedback": "{count, plural, =1{Error al borrar 1 elemento} other{Error al borrar {count} elementos}}",
+ "collectionCopyFailureFeedback": "{count, plural, =1{Error al copiar 1 item} other{Error al copiar {count} elementos}}",
+ "collectionMoveFailureFeedback": "{count, plural, =1{Error al mover 1 elemento} other{Error al mover {count} elementos}}",
+ "collectionEditFailureFeedback": "{count, plural, =1{Error al editar 1 elemento} other{Error al editar {count} elementos}}",
+ "collectionExportFailureFeedback": "{count, plural, =1{Error al exportar 1 página} other{Error al exportar {count} páginas}}",
+ "collectionCopySuccessFeedback": "{count, plural, =1{1 elemento copiado} other{Copiados{count} elementos}}",
+ "collectionMoveSuccessFeedback": "{count, plural, =1{1 elemento movido} other{Movidos {count} elementos}}",
+ "collectionEditSuccessFeedback": "{count, plural, =1{1 elemento editado} other{Editados {count} elementos}}",
+
+ "collectionEmptyFavourites": "Sin favoritos",
+ "collectionEmptyVideos": "Sin videos",
+ "collectionEmptyImages": "Sin imágenes",
+
+ "collectionSelectSectionTooltip": "Seleccionar sección",
+ "collectionDeselectSectionTooltip": "Deseleccionar sección",
+
+ "drawerCollectionAll": "Toda la colección",
+ "drawerCollectionFavourites": "Favoritos",
+ "drawerCollectionImages": "Imágenes",
+ "drawerCollectionVideos": "Videos",
+ "drawerCollectionAnimated": "Animaciones",
+ "drawerCollectionMotionPhotos": "Fotos en movimiento",
+ "drawerCollectionPanoramas": "Panorámicas",
+ "drawerCollectionRaws": "Fotos Raw",
+ "drawerCollectionSphericalVideos": "Videos en 360°",
+
+ "chipSortDate": "Por fecha",
+ "chipSortName": "Por nombre",
+ "chipSortCount": "Por número de elementos",
+
+ "albumGroupTier": "Por nivel",
+ "albumGroupVolume": "Por volumen de almacenamiento",
+ "albumGroupNone": "No agrupar",
+
+ "albumPickPageTitleCopy": "Copiar a álbum",
+ "albumPickPageTitleExport": "Exportar a álbum",
+ "albumPickPageTitleMove": "Mover a álbum",
+ "albumPickPageTitlePick": "Elegir álbum",
+
+ "albumCamera": "Cámara",
+ "albumDownload": "Descargar",
+ "albumScreenshots": "Capturas de pantalla",
+ "albumScreenRecordings": "Grabaciones de pantalla",
+ "albumVideoCaptures": "Capturas en video",
+
+ "albumPageTitle": "Álbumes",
+ "albumEmpty": "Sin álbumes",
+ "createAlbumTooltip": "Crear álbum",
+ "createAlbumButtonLabel": "CREAR",
+ "newFilterBanner": "nuevo",
+
+ "countryPageTitle": "Países",
+ "countryEmpty": "Sin países",
+
+ "tagPageTitle": "Etiquetas",
+ "tagEmpty": "Sin etiquetas",
+
+ "searchCollectionFieldHint": "Buscar en colección",
+ "searchSectionRecent": "Reciente",
+ "searchSectionAlbums": "Álbumes",
+ "searchSectionCountries": "Países",
+ "searchSectionPlaces": "Lugares",
+ "searchSectionTags": "Etiquetas",
+ "searchSectionRating": "Clasificaciones",
+
+ "settingsPageTitle": "Ajustes",
+ "settingsSystemDefault": "Sistema",
+ "settingsDefault": "Restablecer",
+
+ "settingsActionExport": "Exportar",
+ "settingsActionImport": "Importar",
+
+ "settingsSectionNavigation": "Navegación",
+ "settingsHome": "Inicio",
+ "settingsKeepScreenOnTile": "Mantener pantalla encendida",
+ "settingsKeepScreenOnTitle": "Mantener pantalla encendida",
+ "settingsDoubleBackExit": "Presione «atrás» dos veces para salir",
+
+ "settingsNavigationDrawerTile": "Menú de navegación",
+ "settingsNavigationDrawerEditorTitle": "Menú de navegación",
+ "settingsNavigationDrawerBanner": "Toque y mantenga para mover y reordenar elementos del menú.",
+ "settingsNavigationDrawerTabTypes": "Tipos",
+ "settingsNavigationDrawerTabAlbums": "Álbumes",
+ "settingsNavigationDrawerTabPages": "Páginas",
+ "settingsNavigationDrawerAddAlbum": "Agregar álbum",
+
+ "settingsSectionThumbnails": "Miniaturas",
+ "settingsThumbnailShowFavouriteIcon": "Mostrar icono de favoritos",
+ "settingsThumbnailShowLocationIcon": "Mostrar icono de ubicación",
+ "settingsThumbnailShowMotionPhotoIcon": "Mostrar icono de foto en movimiento",
+ "settingsThumbnailShowRating": "Mostrar clasificación",
+ "settingsThumbnailShowRawIcon": "Mostrar icono Raw",
+ "settingsThumbnailShowVideoDuration": "Mostrar duración de video",
+
+ "settingsCollectionQuickActionsTile": "Acciones rápidas",
+ "settingsCollectionQuickActionEditorTitle": "Acciones rápidas",
+ "settingsCollectionQuickActionTabBrowsing": "Búsqueda",
+ "settingsCollectionQuickActionTabSelecting": "Selección",
+ "settingsCollectionBrowsingQuickActionEditorBanner": "Toque y mantenga para mover botones y seleccionar cuáles acciones se muestran mientras busca elementos.",
+ "settingsCollectionSelectionQuickActionEditorBanner": "Toque y mantenga para mover botones y seleccionar cuáles acciones se muestran mientras selecciona elementos.",
+
+ "settingsSectionViewer": "Visor",
+ "settingsViewerUseCutout": "Usar área recortada",
+ "settingsViewerMaximumBrightness": "Brillo máximo",
+ "settingsMotionPhotoAutoPlay": "Reproducir automáticamente fotos en movimiento",
+ "settingsImageBackground": "Imagen de fondo",
+
+ "settingsViewerQuickActionsTile": "Acciones rápidas",
+ "settingsViewerQuickActionEditorTitle": "Acciones rápidas",
+ "settingsViewerQuickActionEditorBanner": "Toque y mantenga para mover botones y seleccionar cuáles acciones se muestran en el visor.",
+ "settingsViewerQuickActionEditorDisplayedButtons": "Botones mostrados",
+ "settingsViewerQuickActionEditorAvailableButtons": "Botones disponibles",
+ "settingsViewerQuickActionEmpty": "Sin botones",
+
+ "settingsViewerOverlayTile": "Incrustaciones",
+ "settingsViewerOverlayTitle": "Incrustaciones",
+ "settingsViewerShowOverlayOnOpening": "Mostrar durante apertura",
+ "settingsViewerShowMinimap": "Mostrar mapa en miniatura",
+ "settingsViewerShowInformation": "Mostrar información",
+ "settingsViewerShowInformationSubtitle": "Mostrar título, fecha, ubicación, etc.",
+ "settingsViewerShowShootingDetails": "Mostrar detalles de toma",
+ "settingsViewerEnableOverlayBlurEffect": "Efecto de difuminado",
+
+ "settingsVideoPageTitle": "Ajustes de video",
+ "settingsSectionVideo": "Video",
+ "settingsVideoShowVideos": "Mostrar videos",
+ "settingsVideoEnableHardwareAcceleration": "Aceleración por hardware",
+ "settingsVideoEnableAutoPlay": "Reproducción automática",
+ "settingsVideoLoopModeTile": "Modo bucle",
+ "settingsVideoLoopModeTitle": "Modo bucle",
+ "settingsVideoQuickActionsTile": "Acciones rápidas para videos",
+ "settingsVideoQuickActionEditorTitle": "Acciones rápidas",
+
+ "settingsSubtitleThemeTile": "Subtítulos",
+ "settingsSubtitleThemeTitle": "Subtítulos",
+ "settingsSubtitleThemeSample": "Esto es un ejemplo.",
+ "settingsSubtitleThemeTextAlignmentTile": "Alineación de texto",
+ "settingsSubtitleThemeTextAlignmentTitle": "Alineación de texto",
+ "settingsSubtitleThemeTextSize": "Tamaño de texto",
+ "settingsSubtitleThemeShowOutline": "Mostrar contorno y sombra",
+ "settingsSubtitleThemeTextColor": "Color de texto",
+ "settingsSubtitleThemeTextOpacity": "Opacidad de texto",
+ "settingsSubtitleThemeBackgroundColor": "Color de fondo",
+ "settingsSubtitleThemeBackgroundOpacity": "Opacidad de fondo",
+ "settingsSubtitleThemeTextAlignmentLeft": "Izquierda",
+ "settingsSubtitleThemeTextAlignmentCenter": "Centro",
+ "settingsSubtitleThemeTextAlignmentRight": "Derecha",
+
+ "settingsSectionPrivacy": "Privacidad",
+ "settingsAllowInstalledAppAccess": "Permita el acceso a lista de aplicaciones",
+ "settingsAllowInstalledAppAccessSubtitle": "Usado para mejorar los álbumes mostrados",
+ "settingsAllowErrorReporting": "Permitir reporte de errores anónimo",
+ "settingsSaveSearchHistory": "Guardar historial de búsqueda",
+
+ "settingsHiddenItemsTile": "Elementos ocultos",
+ "settingsHiddenItemsTitle": "Elementos ocultos",
+
+ "settingsHiddenFiltersTitle": "Filtros",
+ "settingsHiddenFiltersBanner": "Fotos y videos que concuerden con los filtros no aparecerán en su colección.",
+ "settingsHiddenFiltersEmpty": "Sin filtros",
+
+ "settingsHiddenPathsTitle": "Ubicaciones ocultas",
+ "settingsHiddenPathsBanner": "Fotos y videos que se encuentren en estos directorios y cualquiera de sus subdirectorios no aparecerán en su colección.",
+ "addPathTooltip": "Añadir ubicación",
+
+ "settingsStorageAccessTile": "Acceso al almacenamiento",
+ "settingsStorageAccessTitle": "Acceso al almacenamiento",
+ "settingsStorageAccessBanner": "Algunos directorios requieren un permiso de acceso explícito para que sea posible modificar los archivos que contienen. Puede revisar los directorios con permiso aquí.",
+ "settingsStorageAccessEmpty": "Sin permisos de acceso",
+ "settingsStorageAccessRevokeTooltip": "Revocar",
+
+ "settingsSectionAccessibility": "Accesibilidad",
+ "settingsRemoveAnimationsTile": "Remover animaciones",
+ "settingsRemoveAnimationsTitle": "Remove animaciones",
+ "settingsTimeToTakeActionTile": "Hora de entrar en acción",
+ "settingsTimeToTakeActionTitle": "Hora de entrar en acción",
+
+ "settingsSectionLanguage": "Idioma y formatos",
+ "settingsLanguage": "Idioma",
+ "settingsCoordinateFormatTile": "Formato de coordenadas",
+ "settingsCoordinateFormatTitle": "Formato de coordenadas",
+ "settingsUnitSystemTile": "Unidades",
+ "settingsUnitSystemTitle": "Unidades",
+
+ "statsPageTitle": "Stats",
+ "statsWithGps": "{count, plural, =1{1 elemento con ubicación} other{{count} elementos con ubicación}}",
+ "statsTopCountries": "Países principales",
+ "statsTopPlaces": "Lugares principales",
+ "statsTopTags": "Etiquetas principales",
+
+ "viewerOpenPanoramaButtonLabel": "ABRIR PANORÁMICA",
+ "viewerErrorUnknown": "¡Ups!",
+ "viewerErrorDoesNotExist": "El archivo no existe.",
+
+ "viewerInfoPageTitle": "Información",
+ "viewerInfoBackToViewerTooltip": "Regresar al visor",
+
+ "viewerInfoUnknown": "Desconocido",
+ "viewerInfoLabelTitle": "Título",
+ "viewerInfoLabelDate": "Fecha",
+ "viewerInfoLabelResolution": "Resolución",
+ "viewerInfoLabelSize": "Tamaño",
+ "viewerInfoLabelUri": "URI",
+ "viewerInfoLabelPath": "Ubicación",
+ "viewerInfoLabelDuration": "Duración",
+ "viewerInfoLabelOwner": "Propiedad de",
+ "viewerInfoLabelCoordinates": "Coordinadas",
+ "viewerInfoLabelAddress": "Dirección",
+
+ "mapStyleTitle": "Estilo de mapa",
+ "mapStyleTooltip": "Selección de estilo de mapa",
+ "mapZoomInTooltip": "Acercar",
+ "mapZoomOutTooltip": "Alejar",
+ "mapPointNorthUpTooltip": "Apuntar el Norte hacia arriba",
+ "mapAttributionOsmHot": "Datos de mapas © [OpenStreetMap](https://www.openstreetmap.org/copyright) contribuidores • Teselas por [HOT](https://www.hotosm.org/) • Alojado por [OSM France](https://openstreetmap.fr/)",
+ "mapAttributionStamen": "Datos de mapas © [OpenStreetMap](https://www.openstreetmap.org/copyright) contribuidores • Teselas por [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
+ "openMapPageTooltip": "Ver en página del mapa",
+ "mapEmptyRegion": "Sin imágenes en esta región",
+
+ "viewerInfoOpenEmbeddedFailureFeedback": "Fallo al extraer datos embutidos",
+ "viewerInfoOpenLinkText": "Abrir",
+ "viewerInfoViewXmlLinkText": "Ver XML",
+
+ "viewerInfoSearchFieldLabel": "Buscar metadatos",
+ "viewerInfoSearchEmpty": "Sin claves concordantes",
+ "viewerInfoSearchSuggestionDate": "Fecha y hora",
+ "viewerInfoSearchSuggestionDescription": "Descripción",
+ "viewerInfoSearchSuggestionDimensions": "Dimensiones",
+ "viewerInfoSearchSuggestionResolution": "Resolución",
+ "viewerInfoSearchSuggestionRights": "Derechos",
+
+ "tagEditorPageTitle": "Editar Etiquetas",
+ "tagEditorPageNewTagFieldLabel": "Nueva etiqueta",
+ "tagEditorPageAddTagTooltip": "Añadir etiqueta",
+ "tagEditorSectionRecent": "Reciente",
+
+ "panoramaEnableSensorControl": "Activar control de sensores",
+ "panoramaDisableSensorControl": "Desactivar control de sensores",
+
+ "sourceViewerPageTitle": "Fuente",
+
+ "filePickerShowHiddenFiles": "Mostrar archivos ocultos",
+ "filePickerDoNotShowHiddenFiles": "No mostrar archivos ocultos",
+ "filePickerOpenFrom": "Abrir desde",
+ "filePickerNoItems": "Sin elementos",
+ "filePickerUseThisFolder": "Usar esta carpeta",
+ "@filePickerUseThisFolder": {}
+}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index d1c2e2b7c..701a808e8 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -22,7 +22,7 @@
"nextTooltip": "Suivant",
"showTooltip": "Afficher",
"hideTooltip": "Masquer",
- "removeTooltip": "Supprimer",
+ "actionRemove": "Supprimer",
"resetButtonTooltip": "Réinitialiser",
"doubleBackExitMessage": "Pressez «\u00A0retour\u00A0» à nouveau pour quitter.",
@@ -73,12 +73,15 @@
"videoActionSettings": "Préférences",
"entryInfoActionEditDate": "Modifier la date",
+ "entryInfoActionEditRating": "Modifier la notation",
"entryInfoActionEditTags": "Modifier les libellés",
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
"filterFavouriteLabel": "Favori",
"filterLocationEmptyLabel": "Sans lieu",
"filterTagEmptyLabel": "Sans libellé",
+ "filterRatingUnratedLabel": "Sans notation",
+ "filterRatingRejectedLabel": "Rejeté",
"filterTypeAnimatedLabel": "Animation",
"filterTypeMotionPhotoLabel": "Photo animée",
"filterTypePanoramaLabel": "Panorama",
@@ -137,6 +140,8 @@
"restrictedAccessDialogMessage": "Cette app ne peut pas modifier les fichiers du {directory} de «\u00A0{volume}\u00A0».\n\nVeuillez utiliser une app pré-installée pour déplacer les fichiers vers un autre dossier.",
"notEnoughSpaceDialogTitle": "Espace insuffisant",
"notEnoughSpaceDialogMessage": "Cette opération nécessite {neededSize} d’espace disponible sur «\u00A0{volume}\u00A0», mais il ne reste que {freeSize}.",
+ "missingSystemFilePickerDialogTitle": "Sélecteur de fichiers désactivé",
+ "missingSystemFilePickerDialogMessage": "Le sélecteur de fichiers du système est absent ou désactivé. Veuillez le réactiver et réessayer.",
"unsupportedTypeDialogTitle": "Formats non supportés",
"unsupportedTypeDialogMessage": "{count, plural, =1{Cette opération n’est pas disponible pour les fichiers au format suivant : {types}.} other{Cette opération n’est pas disponible pour les fichiers aux formats suivants : {types}.}}",
@@ -178,14 +183,17 @@
"renameEntryDialogLabel": "Nouveau nom",
"editEntryDateDialogTitle": "Date & Heure",
- "editEntryDateDialogSet": "Régler",
- "editEntryDateDialogShift": "Décaler",
+ "editEntryDateDialogSetCustom": "Régler une date personnalisée",
+ "editEntryDateDialogCopyField": "Copier d'une autre date",
"editEntryDateDialogExtractFromTitle": "Extraire du titre",
- "editEntryDateDialogClear": "Effacer",
- "editEntryDateDialogFieldSelection": "Champs affectés",
+ "editEntryDateDialogShift": "Décaler",
+ "editEntryDateDialogSourceFileModifiedDate": "Date de modification du fichier",
+ "editEntryDateDialogTargetFieldsHeader": "Champs à modifier",
"editEntryDateDialogHours": "Heures",
"editEntryDateDialogMinutes": "Minutes",
+ "editEntryRatingDialogTitle": "Notation",
+
"removeEntryMetadataDialogTitle": "Retrait de métadonnées",
"removeEntryMetadataDialogMore": "Voir plus",
@@ -270,6 +278,7 @@
"collectionSortDate": "par date",
"collectionSortSize": "par taille",
"collectionSortName": "alphabétique",
+ "collectionSortRating": "par notation",
"collectionGroupAlbum": "par album",
"collectionGroupMonth": "par mois",
@@ -343,6 +352,7 @@
"searchSectionCountries": "Pays",
"searchSectionPlaces": "Lieux",
"searchSectionTags": "Libellés",
+ "searchSectionRating": "Notations",
"settingsPageTitle": "Réglages",
"settingsSystemDefault": "Système",
@@ -366,8 +376,10 @@
"settingsNavigationDrawerAddAlbum": "Ajouter un album",
"settingsSectionThumbnails": "Vignettes",
+ "settingsThumbnailShowFavouriteIcon": "Afficher l’icône de favori",
"settingsThumbnailShowLocationIcon": "Afficher l’icône de lieu",
"settingsThumbnailShowMotionPhotoIcon": "Afficher l’icône de photo animée",
+ "settingsThumbnailShowRating": "Afficher la notation",
"settingsThumbnailShowRawIcon": "Afficher l’icône de photo raw",
"settingsThumbnailShowVideoDuration": "Afficher la durée de la vidéo",
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index 5ec0b4db0..ac2d83ce4 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -22,7 +22,7 @@
"nextTooltip": "다음",
"showTooltip": "보기",
"hideTooltip": "숨기기",
- "removeTooltip": "제거",
+ "actionRemove": "제거",
"resetButtonTooltip": "복원",
"doubleBackExitMessage": "종료하려면 한번 더 누르세요.",
@@ -72,13 +72,16 @@
"videoActionSetSpeed": "재생 배속",
"videoActionSettings": "설정",
- "entryInfoActionEditDate": "날짜와 시간 수정",
+ "entryInfoActionEditDate": "날짜 및 시간 수정",
+ "entryInfoActionEditRating": "별점 수정",
"entryInfoActionEditTags": "태그 수정",
"entryInfoActionRemoveMetadata": "메타데이터 삭제",
"filterFavouriteLabel": "즐겨찾기",
"filterLocationEmptyLabel": "장소 없음",
"filterTagEmptyLabel": "태그 없음",
+ "filterRatingUnratedLabel": "별점 없음",
+ "filterRatingRejectedLabel": "거부됨",
"filterTypeAnimatedLabel": "애니메이션",
"filterTypeMotionPhotoLabel": "모션 포토",
"filterTypePanoramaLabel": "파노라마",
@@ -137,6 +140,8 @@
"restrictedAccessDialogMessage": "“{volume}”의 {directory}에 있는 파일의 접근이 제한됩니다.\n\n기본으로 설치된 갤러리나 파일 관리 앱을 사용해서 다른 폴더로 파일을 이동하세요.",
"notEnoughSpaceDialogTitle": "저장공간 부족",
"notEnoughSpaceDialogMessage": "“{volume}”에 필요 공간은 {neededSize}인데 사용 가능한 용량은 {freeSize}만 남아있습니다.",
+ "missingSystemFilePickerDialogTitle": "기본 파일 선택기 없음",
+ "missingSystemFilePickerDialogMessage": "기본 파일 선택기가 없거나 비활성화딥니다. 파일 선택기를 켜고 다시 시도하세요.",
"unsupportedTypeDialogTitle": "미지원 형식",
"unsupportedTypeDialogMessage": "{count, plural, other{이 작업은 다음 항목의 형식을 지원하지 않습니다: {types}.}}",
@@ -178,14 +183,17 @@
"renameEntryDialogLabel": "이름",
"editEntryDateDialogTitle": "날짜 및 시간",
- "editEntryDateDialogSet": "편집",
- "editEntryDateDialogShift": "시간 이동",
+ "editEntryDateDialogSetCustom": "지정 날짜로 편집",
+ "editEntryDateDialogCopyField": "다른 날짜에서 지정",
"editEntryDateDialogExtractFromTitle": "제목에서 추출",
- "editEntryDateDialogClear": "삭제",
- "editEntryDateDialogFieldSelection": "필드 선택",
+ "editEntryDateDialogShift": "시간 이동",
+ "editEntryDateDialogSourceFileModifiedDate": "파일 수정한 날짜",
+ "editEntryDateDialogTargetFieldsHeader": "수정할 필드",
"editEntryDateDialogHours": "시간",
"editEntryDateDialogMinutes": "분",
+ "editEntryRatingDialogTitle": "별점",
+
"removeEntryMetadataDialogTitle": "메타데이터 삭제",
"removeEntryMetadataDialogMore": "더 보기",
@@ -270,6 +278,7 @@
"collectionSortDate": "날짜",
"collectionSortSize": "크기",
"collectionSortName": "이름",
+ "collectionSortRating": "별점",
"collectionGroupAlbum": "앨범별로",
"collectionGroupMonth": "월별로",
@@ -343,6 +352,7 @@
"searchSectionCountries": "국가",
"searchSectionPlaces": "장소",
"searchSectionTags": "태그",
+ "searchSectionRating": "별점",
"settingsPageTitle": "설정",
"settingsSystemDefault": "시스템",
@@ -366,8 +376,10 @@
"settingsNavigationDrawerAddAlbum": "앨범 추가",
"settingsSectionThumbnails": "섬네일",
+ "settingsThumbnailShowFavouriteIcon": "즐겨찾기 아이콘 표시",
"settingsThumbnailShowLocationIcon": "위치 아이콘 표시",
"settingsThumbnailShowMotionPhotoIcon": "모션 포토 아이콘 표시",
+ "settingsThumbnailShowRating": "별점 표시",
"settingsThumbnailShowRawIcon": "Raw 아이콘 표시",
"settingsThumbnailShowVideoDuration": "동영상 길이 표시",
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index af5601f72..1d8649cc9 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -22,7 +22,7 @@
"nextTooltip": "Следующий",
"showTooltip": "Показать",
"hideTooltip": "Скрыть",
- "removeTooltip": "Удалить",
+ "actionRemove": "Удалить",
"resetButtonTooltip": "Сбросить",
"doubleBackExitMessage": "Нажмите «назад» еще раз, чтобы выйти.",
@@ -73,12 +73,15 @@
"videoActionSettings": "Настройки",
"entryInfoActionEditDate": "Изменить дату и время",
+ "entryInfoActionEditRating": "Изменить рейтинг",
"entryInfoActionEditTags": "Изменить теги",
"entryInfoActionRemoveMetadata": "Удалить метаданные",
"filterFavouriteLabel": "Избранное",
"filterLocationEmptyLabel": "Без местоположения",
"filterTagEmptyLabel": "Без тегов",
+ "filterRatingUnratedLabel": "Без рейтинга",
+ "filterRatingRejectedLabel": "Отклонённые",
"filterTypeAnimatedLabel": "GIF",
"filterTypeMotionPhotoLabel": "Живое фото",
"filterTypePanoramaLabel": "Панорама",
@@ -137,6 +140,8 @@
"restrictedAccessDialogMessage": "Этому приложению не разрешается изменять файлы в каталоге {directory} накопителя «{volume}».\n\nПожалуйста, используйте предустановленный файловый менеджер или галерею, чтобы переместить элементы в другой каталог.",
"notEnoughSpaceDialogTitle": "Недостаточно свободного места.",
"notEnoughSpaceDialogMessage": "Для завершения этой операции требуется {neededSize} свободного места на «{volume}», но осталось только {freeSize}.",
+ "missingSystemFilePickerDialogTitle": "Отсутствует системное приложение выбора файлов",
+ "missingSystemFilePickerDialogMessage": "Системное приложение выбора файлов отсутствует или отключено. Пожалуйста, включите его и повторите попытку.",
"unsupportedTypeDialogTitle": "Неподдерживаемые форматы",
"unsupportedTypeDialogMessage": "{count, plural, =1{Эта операция не поддерживается для объектов следующего формата: {types}.} other{Эта операция не поддерживается для объектов следующих форматов: {types}.}}",
@@ -178,14 +183,17 @@
"renameEntryDialogLabel": "Новое название",
"editEntryDateDialogTitle": "Дата и время",
- "editEntryDateDialogSet": "Задать",
- "editEntryDateDialogShift": "Сдвиг",
+ "editEntryDateDialogSetCustom": "Задайте дату",
+ "editEntryDateDialogCopyField": "Копировать с другой даты",
"editEntryDateDialogExtractFromTitle": "Извлечь из названия",
- "editEntryDateDialogClear": "Очистить",
- "editEntryDateDialogFieldSelection": "Выбор поля",
+ "editEntryDateDialogShift": "Сдвиг",
+ "editEntryDateDialogSourceFileModifiedDate": "Дата изменения файла",
+ "editEntryDateDialogTargetFieldsHeader": "Поля для изменения",
"editEntryDateDialogHours": "Часов",
"editEntryDateDialogMinutes": "Минут",
+ "editEntryRatingDialogTitle": "Рейтинг",
+
"removeEntryMetadataDialogTitle": "Удаление метаданных",
"removeEntryMetadataDialogMore": "Дополнительно",
@@ -270,6 +278,7 @@
"collectionSortDate": "По дате",
"collectionSortSize": "По размеру",
"collectionSortName": "По имени альбома и файла",
+ "collectionSortRating": "По рейтингу",
"collectionGroupAlbum": "По альбому",
"collectionGroupMonth": "По месяцу",
@@ -343,6 +352,7 @@
"searchSectionCountries": "Страны",
"searchSectionPlaces": "Локации",
"searchSectionTags": "Теги",
+ "searchSectionRating": "Рейтинги",
"settingsPageTitle": "Настройки",
"settingsSystemDefault": "Система",
@@ -368,6 +378,7 @@
"settingsSectionThumbnails": "Эскизы",
"settingsThumbnailShowLocationIcon": "Показать значок местоположения",
"settingsThumbnailShowMotionPhotoIcon": "Показать значок живого фото",
+ "settingsThumbnailShowRating": "Показывать рейтинг",
"settingsThumbnailShowRawIcon": "Показать значок RAW-файла",
"settingsThumbnailShowVideoDuration": "Показывать продолжительность видео",
diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart
index 901133187..ffc6ca198 100644
--- a/lib/model/actions/entry_info_actions.dart
+++ b/lib/model/actions/entry_info_actions.dart
@@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart';
enum EntryInfoAction {
// general
editDate,
+ editRating,
editTags,
removeMetadata,
// motion photo
@@ -14,6 +15,7 @@ enum EntryInfoAction {
class EntryInfoActions {
static const all = [
EntryInfoAction.editDate,
+ EntryInfoAction.editRating,
EntryInfoAction.editTags,
EntryInfoAction.removeMetadata,
EntryInfoAction.viewMotionPhotoVideo,
@@ -26,6 +28,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
// general
case EntryInfoAction.editDate:
return context.l10n.entryInfoActionEditDate;
+ case EntryInfoAction.editRating:
+ return context.l10n.entryInfoActionEditRating;
case EntryInfoAction.editTags:
return context.l10n.entryInfoActionEditTags;
case EntryInfoAction.removeMetadata:
@@ -45,8 +49,10 @@ extension ExtraEntryInfoAction on EntryInfoAction {
// general
case EntryInfoAction.editDate:
return AIcons.date;
+ case EntryInfoAction.editRating:
+ return AIcons.editRating;
case EntryInfoAction.editTags:
- return AIcons.addTag;
+ return AIcons.editTags;
case EntryInfoAction.removeMetadata:
return AIcons.clear;
// motion photo
diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart
index 2d97dabca..f3601560b 100644
--- a/lib/model/actions/entry_set_actions.dart
+++ b/lib/model/actions/entry_set_actions.dart
@@ -21,10 +21,12 @@ enum EntrySetAction {
copy,
move,
rescan,
+ toggleFavourite,
rotateCCW,
rotateCW,
flip,
editDate,
+ editRating,
editTags,
removeMetadata,
}
@@ -50,6 +52,7 @@ class EntrySetActions {
EntrySetAction.delete,
EntrySetAction.copy,
EntrySetAction.move,
+ EntrySetAction.toggleFavourite,
EntrySetAction.rescan,
EntrySetAction.map,
EntrySetAction.stats,
@@ -93,6 +96,9 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.collectionActionMove;
case EntrySetAction.rescan:
return context.l10n.collectionActionRescan;
+ case EntrySetAction.toggleFavourite:
+ // different data depending on toggle state
+ return context.l10n.entryActionAddFavourite;
case EntrySetAction.rotateCCW:
return context.l10n.entryActionRotateCCW;
case EntrySetAction.rotateCW:
@@ -101,6 +107,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.entryActionFlip;
case EntrySetAction.editDate:
return context.l10n.entryInfoActionEditDate;
+ case EntrySetAction.editRating:
+ return context.l10n.entryInfoActionEditRating;
case EntrySetAction.editTags:
return context.l10n.entryInfoActionEditTags;
case EntrySetAction.removeMetadata:
@@ -147,6 +155,9 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.move;
case EntrySetAction.rescan:
return AIcons.refresh;
+ case EntrySetAction.toggleFavourite:
+ // different data depending on toggle state
+ return AIcons.favourite;
case EntrySetAction.rotateCCW:
return AIcons.rotateLeft;
case EntrySetAction.rotateCW:
@@ -155,8 +166,10 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.flip;
case EntrySetAction.editDate:
return AIcons.date;
+ case EntrySetAction.editRating:
+ return AIcons.editRating;
case EntrySetAction.editTags:
- return AIcons.addTag;
+ return AIcons.editTags;
case EntrySetAction.removeMetadata:
return AIcons.clear;
}
diff --git a/lib/model/actions/events.dart b/lib/model/actions/events.dart
index 248e9dd88..e6fea841f 100644
--- a/lib/model/actions/events.dart
+++ b/lib/model/actions/events.dart
@@ -1,9 +1,13 @@
+import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
@immutable
-class ActionEvent {
+class ActionEvent extends Equatable {
final T action;
+ @override
+ List