Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2022-03-28 12:36:28 +09:00
commit 0c3ac4ebaa
235 changed files with 3774 additions and 1607 deletions

View file

@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.6.3"></a>[v1.6.3] - 2022-03-28
### Added
- Theme: light/dark/black and color highlights settings
- Collection: bulk renaming
- Video: speed and muted state indicators
- Info: option to set date from other item
- Info: improved DNG tags display
- warn and optionally set metadata date before moving undated items
- Settings: display refresh rate hint
### Changed
- Viewer: quick action defaults
- cataloguing includes date sub-second data if present (requires rescan)
### Removed
- metadata editing support for DNG
### Fixed
- app launch despite faulty storage volumes on Android 11+
## <a id="v1.6.2"></a>[v1.6.2] - 2022-03-07 ## <a id="v1.6.2"></a>[v1.6.2] - 2022-03-07
### Added ### Added

View file

@ -147,7 +147,7 @@ dependencies {
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.16.0' implementation 'com.drewnoakes:metadata-extractor:2.17.0'
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory // forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android // forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android

View file

@ -164,11 +164,18 @@ class MainActivity : FlutterActivity() {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// save access permissions across reboots val canPersist = (data.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
val takeFlags = (data.flags if (canPersist) {
and (Intent.FLAG_GRANT_READ_URI_PERMISSION // save access permissions across reboots
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) val takeFlags = (data.flags
contentResolver.takePersistableUriPermission(treeUri, takeFlags) and (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
try {
contentResolver.takePersistableUriPermission(treeUri, takeFlags)
} catch (e: SecurityException) {
Log.w(LOG_TAG, "failed to take persistable URI permission for uri=$treeUri", e)
}
}
} }
// resume pending action // resume pending action
@ -201,9 +208,11 @@ class MainActivity : FlutterActivity() {
} }
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> { Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri))?.let { uri -> (intent.data ?: (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
return hashMapOf( return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW, INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to intent.type, // MIME type is optional INTENT_DATA_KEY_MIME_TYPE to type,
INTENT_DATA_KEY_URI to uri.toString(), INTENT_DATA_KEY_URI to uri.toString(),
) )
} }

View file

@ -33,7 +33,10 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.util.PathUtils import io.flutter.util.PathUtils
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException import java.io.IOException
@ -84,7 +87,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
}.mapValues { it.value?.path }.toMutableMap() }.mapValues { it.value?.path }.toMutableMap()
dirs["externalCacheDirs"] = context.externalCacheDirs.joinToString { it.path } dirs["externalCacheDirs"] = context.externalCacheDirs.joinToString { it.path }
dirs["externalFilesDirs"] = context.getExternalFilesDirs(null).joinToString { it.path } dirs["externalFilesDirs"] = context.getExternalFilesDirs(null).joinToString { it?.path ?: "null" }
// used by flutter plugin `path_provider` // used by flutter plugin `path_provider`
dirs.putAll( dirs.putAll(

View file

@ -47,6 +47,9 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_ITXT_DIR_NAME
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME
import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractPngProfile import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractPngProfile
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateDigitizedMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateModifiedMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateOriginalMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
@ -74,7 +77,10 @@ import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.ParseException import java.text.ParseException
@ -163,15 +169,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// tags // tags
val tags = dir.tags val tags = dir.tags
if (dir is ExifDirectoryBase) { if (dir is ExifDirectoryBase) {
if (dir.isGeoTiff()) { when {
// split GeoTIFF tags in their own directory dir.isGeoTiff() -> {
val byGeoTiff = tags.groupBy { ExifTags.isGeoTiffTag(it.tagType) } // split GeoTIFF tags in their own directory
metadataMap["GeoTIFF"] = HashMap<String, String>().apply { val geoTiffDirMap = metadataMap["GeoTIFF"] ?: HashMap()
byGeoTiff[true]?.map { exifTagMapper(it) }?.let { putAll(it) } metadataMap["GeoTIFF"] = geoTiffDirMap
val byGeoTiff = tags.groupBy { ExifTags.isGeoTiffTag(it.tagType) }
byGeoTiff[true]?.map { exifTagMapper(it) }?.let { geoTiffDirMap.putAll(it) }
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
} }
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } mimeType == MimeTypes.DNG -> {
} else { // split DNG tags in their own directory
dirMap.putAll(tags.map { exifTagMapper(it) }) val dngDirMap = metadataMap["DNG"] ?: HashMap()
metadataMap["DNG"] = dngDirMap
val byDng = tags.groupBy { ExifTags.isDngTag(it.tagType) }
byDng[true]?.map { exifTagMapper(it) }?.let { dngDirMap.putAll(it) }
byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
}
else -> dirMap.putAll(tags.map { exifTagMapper(it) })
} }
} else if (dir.isPngTextDir()) { } else if (dir.isPngTextDir()) {
metadataMap.remove(thisDirName) metadataMap.remove(thisDirName)
@ -432,11 +447,17 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// EXIF // EXIF
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getSafeDateMillis(ExifDirectoryBase.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it } dir.getDateOriginalMillis { metadataMap[KEY_DATE_MILLIS] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
// fetch date modified from SubIFD directory first, as the sub-second tag is here
dir.getDateModifiedMillis { metadataMap[KEY_DATE_MILLIS] = it }
}
} }
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
dir.getSafeDateMillis(ExifDirectoryBase.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it } // fallback to fetch date modified from IFD0 directory, without the sub-second tag
// in case there was no SubIFD directory
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, null)?.let { metadataMap[KEY_DATE_MILLIS] = it }
} }
dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) { dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) {
val orientation = it val orientation = it
@ -560,9 +581,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input) val exif = ExifInterface(input)
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it } exif.getSafeDateMillis(ExifInterface.TAG_DATETIME_ORIGINAL, ExifInterface.TAG_SUBSEC_TIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it } exif.getSafeDateMillis(ExifInterface.TAG_DATETIME, ExifInterface.TAG_SUBSEC_TIME) { metadataMap[KEY_DATE_MILLIS] = it }
} }
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) {
if (exif.isFlipped) flags = flags or MASK_IS_FLIPPED if (exif.isFlipped) flags = flags or MASK_IS_FLIPPED
@ -901,9 +922,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = ImageMetadataReader.readMetadata(input)
val tag = when (field) { val tag = when (field) {
ExifInterface.TAG_DATETIME -> ExifDirectoryBase.TAG_DATETIME ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifDirectoryBase.TAG_DATETIME_DIGITIZED ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifDirectoryBase.TAG_DATETIME_ORIGINAL ExifInterface.TAG_DATETIME_ORIGINAL -> ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL
ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP
else -> { else -> {
result.error("getDate-field", "unsupported ExifInterface field=$field", null) result.error("getDate-field", "unsupported ExifInterface field=$field", null)
@ -912,11 +933,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
when (tag) { when (tag) {
ExifDirectoryBase.TAG_DATETIME, ExifIFD0Directory.TAG_DATETIME -> {
ExifDirectoryBase.TAG_DATETIME_DIGITIZED, for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
ExifDirectoryBase.TAG_DATETIME_ORIGINAL -> { dir.getDateModifiedMillis { dateMillis = it }
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) { }
dir.getSafeDateMillis(tag) { dateMillis = it } if (dateMillis == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, null)?.let { dateMillis = it }
}
}
}
ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateDigitizedMillis { dateMillis = it }
}
}
ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateOriginalMillis { dateMillis = it }
} }
} }
GpsDirectory.TAG_DATE_STAMP -> { GpsDirectory.TAG_DATE_STAMP -> {

View file

@ -199,27 +199,34 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
} }
private suspend fun rename() { private suspend fun rename() {
if (arguments !is Map<*, *> || entryMapList.isEmpty()) { if (arguments !is Map<*, *>) {
endOfStream() endOfStream()
return return
} }
val newName = arguments["newName"] as String? val rawEntryMap = arguments["entriesToNewName"] as Map<*, *>?
if (newName == null) { if (rawEntryMap == null || rawEntryMap.isEmpty()) {
error("rename-args", "failed because of missing arguments", null) error("rename-args", "failed because of missing arguments", null)
return return
} }
val entriesToNewName = HashMap<AvesEntry, String>()
rawEntryMap.forEach {
@Suppress("unchecked_cast")
val rawEntry = it.key as FieldMap
val newName = it.value as String
entriesToNewName[AvesEntry(rawEntry)] = newName
}
// assume same provider for all entries // assume same provider for all entries
val firstEntry = entryMapList.first() val firstEntry = entriesToNewName.keys.first()
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } val provider = getProvider(firstEntry.uri)
if (provider == null) { if (provider == null) {
error("rename-provider", "failed to find provider for entry=$firstEntry", null) error("rename-provider", "failed to find provider for entry=$firstEntry", null)
return return
} }
val entries = entryMapList.map(::AvesEntry) provider.renameMultiple(activity, entriesToNewName, ::isCancelledOp, object : ImageOpCallback {
provider.renameMultiple(activity, newName, entries, ::isCancelledOp, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = success(fields) override fun onSuccess(fields: FieldMap) = success(fields)
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message) override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
}) })

View file

@ -13,6 +13,7 @@ import deckers.thibault.aves.PendingStorageAccessResultHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.PermissionManager
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -80,6 +81,11 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
return return
} }
if (uris.any { !StorageUtils.isMediaStoreContentUri(it) }) {
error("requestMediaFileAccess-nonmediastore", "request is only valid for Media Store content URIs, uris=$uris", null)
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
error("requestMediaFileAccess-unsupported", "media file bulk access is not allowed before Android R", null) error("requestMediaFileAccess-unsupported", "media file bulk access is not allowed before Android R", null)
return return
@ -148,12 +154,17 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
fun onGranted(uri: Uri) { fun onGranted(uri: Uri) {
ioScope.launch { ioScope.launch {
activity.contentResolver.openInputStream(uri)?.use { input -> try {
val buffer = ByteArray(BUFFER_SIZE) activity.contentResolver.openInputStream(uri)?.use { input ->
var len: Int val buffer = ByteArray(BUFFER_SIZE)
while (input.read(buffer).also { len = it } != -1) { var len: Int
success(buffer.copyOf(len)) while (input.read(buffer).also { len = it } != -1) {
success(buffer.copyOf(len))
}
} }
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to open input stream for uri=$uri", e)
} finally {
endOfStream() endOfStream()
} }
} }

View file

@ -0,0 +1,229 @@
package deckers.thibault.aves.metadata
// DNG v1.6.0.0
// cf https://helpx.adobe.com/content/dam/help/en/photoshop/pdf/dng_spec_1_6_0_0.pdf
object DngTags {
private const val DNG_VERSION = 0xC612
private const val DNG_BACKWARD_VERSION = 0xC613
private const val UNIQUE_CAMERA_MODEL = 0xC614
private const val LOCALIZED_CAMERA_MODEL = 0xC615
private const val CFA_PLANE_COLOR = 0xC616
private const val CFA_LAYOUT = 0xC617
private const val LINEARIZATION_TABLE = 0xC618
private const val BLACK_LEVEL_REPEAT_DIM = 0xC619
private const val BLACK_LEVEL = 0xC61A
private const val BLACK_LEVEL_DELTA_H = 0xC61B
private const val BLACK_LEVEL_DELTA_V = 0xC61C
private const val WHITE_LEVEL = 0xC61D
private const val DEFAULT_SCALE = 0xC61E
private const val DEFAULT_CROP_ORIGIN = 0xC61F
private const val DEFAULT_CROP_SIZE = 0xC620
private const val COLOR_MATRIX_1 = 0xC621
private const val COLOR_MATRIX_2 = 0xC622
private const val CAMERA_CALIBRATION_1 = 0xC623
private const val CAMERA_CALIBRATION_2 = 0xC624
private const val REDUCTION_MATRIX_1 = 0xC625
private const val REDUCTION_MATRIX_2 = 0xC626
private const val ANALOG_BALANCE = 0xC627
private const val AS_SHOT_NEUTRAL = 0xC628
private const val AS_SHOT_WHITE_XY = 0xC629
private const val BASELINE_EXPOSURE = 0xC62A
private const val BASELINE_NOISE = 0xC62B
private const val BASELINE_SHARPNESS = 0xC62C
private const val BAYER_GREEN_SPLIT = 0xC62D
private const val LINEAR_RESPONSE_LIMIT = 0xC62E
private const val CAMERA_SERIAL_NUMBER = 0xC62F
private const val LENS_INFO = 0xC630
private const val CHROMA_BLUR_RADIUS = 0xC631
private const val ANTI_ALIAS_STRENGTH = 0xC632
private const val SHADOW_SCALE = 0xC633
private const val DNG_PRIVATE_DATA = 0xC634
private const val MAKER_NOTE_SAFETY = 0xC635
private const val CALIBRATION_ILLUMINANT_1 = 0xC65A
private const val CALIBRATION_ILLUMINANT_2 = 0xC65B
private const val BEST_QUALITY_SCALE = 0xC65C
private const val RAW_DATA_UNIQUE_ID = 0xC65D
private const val ORIGINAL_RAW_FILE_NAME = 0xC68B
private const val ORIGINAL_RAW_FILE_DATA = 0xC68C
private const val ACTIVE_AREA = 0xC68D
private const val MASKED_AREAS = 0xC68E
private const val AS_SHOT_ICC_PROFILE = 0xC68F
private const val AS_SHOT_PRE_PROFILE_MATRIX = 0xC690
private const val CURRENT_ICC_PROFILE = 0xC691
private const val CURRENT_PRE_PROFILE_MATRIX = 0xC692
private const val COLORIMETRIC_REFERENCE = 0xC6BF
private const val CAMERA_CALIBRATION_SIGNATURE = 0xC6F3
private const val PROFILE_CALIBRATION_SIGNATURE = 0xC6F4
private const val EXTRA_CAMERA_PROFILES = 0xC6F5
private const val AS_SHOT_PROFILE_NAME = 0xC6F6
private const val NOISE_REDUCTION_APPLIED = 0xC6F7
private const val PROFILE_NAME = 0xC6F8
private const val PROFILE_HUE_SAT_MAP_DIMS = 0xC6F9
private const val PROFILE_HUE_SAT_MAP_DATA_1 = 0xC6FA
private const val PROFILE_HUE_SAT_MAP_DATA_2 = 0xC6FB
private const val PROFILE_TONE_CURVE = 0xC6FC
private const val PROFILE_EMBED_POLICY = 0xC6FD
private const val PROFILE_COPYRIGHT = 0xC6FE
private const val FORWARD_MATRIX_1 = 0xC714
private const val FORWARD_MATRIX_2 = 0xC715
private const val PREVIEW_APPLICATION_NAME = 0xC716
private const val PREVIEW_APPLICATION_VERSION = 0xC717
private const val PREVIEW_SETTINGS_NAME = 0xC718
private const val PREVIEW_SETTINGS_DIGEST = 0xC719
private const val PREVIEW_COLOR_SPACE = 0xC71A
private const val PREVIEW_DATE_TIME = 0xC71B
private const val RAW_IMAGE_DIGEST = 0xC71C
private const val ORIGINAL_RAW_FILE_DIGEST = 0xC71D
private const val SUB_TILE_BLOCK_SIZE = 0xC71E
private const val ROW_INTERLEAVE_FACTOR = 0xC71F
private const val PROFILE_LOOK_TABLE_DIMS = 0xC725
private const val PROFILE_LOOK_TABLE_DATA = 0xC726
private const val OPCODE_LIST_1 = 0xC740
private const val OPCODE_LIST_2 = 0xC741
private const val OPCODE_LIST_3 = 0xC74E
private const val NOISE_PROFILE = 0xC761
private const val ORIGINAL_DEFAULT_FINAL_SIZE = 0xC791
private const val ORIGINAL_BEST_QUALITY_FINAL_SIZE = 0xC792
private const val ORIGINAL_DEFAULT_CROP_SIZE = 0xC793
private const val PROFILE_HUE_SAT_MAP_ENCODING = 0xC7A3
private const val PROFILE_LOOK_TABLE_ENCODING = 0xC7A4
private const val BASELINE_EXPOSURE_OFFSET = 0xC7A5
private const val DEFAULT_BLACK_RENDER = 0xC7A6
private const val NEW_RAW_IMAGE_DIGEST = 0xC7A7
private const val RAW_TO_PREVIEW_GAIN = 0xC7A8
private const val DEFAULT_USER_CROP = 0xC7B5
private const val DEPTH_FORMAT = 0xC7E9
private const val DEPTH_NEAR = 0xC7EA
private const val DEPTH_FAR = 0xC7EB
private const val DEPTH_UNITS = 0xC7EC
private const val DEPTH_MEASURE_TYPE = 0xC7ED
private const val ENHANCE_PARAMS = 0xC7EE
private const val PROFILE_GAIN_TABLE_MAP = 0xCD2D
private const val SEMANTIC_NAME = 0xCD2E
private const val SEMANTIC_INSTANCE_ID = 0xCD30
private const val CALIBRATION_ILLUMINANT_3 = 0xCD31
private const val CAMERA_CALIBRATION_3 = 0xCD32
private const val COLOR_MATRIX_3 = 0xCD33
private const val FORWARD_MATRIX_3 = 0xCD34
private const val ILLUMINANT_DATA_1 = 0xCD35
private const val ILLUMINANT_DATA_2 = 0xCD36
private const val ILLUMINANT_DATA_3 = 0xCD37
private const val MASK_SUB_AREA = 0xCD38
private const val PROFILE_HUE_SAT_MAP_DATA_3 = 0xCD39
private const val REDUCTION_MATRIX_3 = 0xCD3A
private const val RGB_TABLES = 0xCD3F
val tagNameMap = hashMapOf(
DNG_VERSION to "DNG Version",
DNG_BACKWARD_VERSION to "DNG Backward Version",
UNIQUE_CAMERA_MODEL to "Unique Camera Model",
LOCALIZED_CAMERA_MODEL to "Localized Camera Model",
CFA_PLANE_COLOR to "CFA Plane Color",
CFA_LAYOUT to "CFA Layout",
LINEARIZATION_TABLE to "Linearization Table",
BLACK_LEVEL_REPEAT_DIM to "Black Level Repeat Dim",
BLACK_LEVEL to "Black Level",
BLACK_LEVEL_DELTA_H to "Black Level Delta H",
BLACK_LEVEL_DELTA_V to "Black Level Delta V",
WHITE_LEVEL to "White Level",
DEFAULT_SCALE to "Default Scale",
DEFAULT_CROP_ORIGIN to "Default Crop Origin",
DEFAULT_CROP_SIZE to "Default Crop Size",
COLOR_MATRIX_1 to "Color Matrix 1",
COLOR_MATRIX_2 to "Color Matrix 2",
CAMERA_CALIBRATION_1 to "Camera Calibration 1",
CAMERA_CALIBRATION_2 to "Camera Calibration 2",
REDUCTION_MATRIX_1 to "Reduction Matrix 1",
REDUCTION_MATRIX_2 to "Reduction Matrix 2",
ANALOG_BALANCE to "Analog Balance",
AS_SHOT_NEUTRAL to "As Shot Neutral",
AS_SHOT_WHITE_XY to "As Shot White XY",
BASELINE_EXPOSURE to "Baseline Exposure",
BASELINE_NOISE to "Baseline Noise",
BASELINE_SHARPNESS to "Baseline Sharpness",
BAYER_GREEN_SPLIT to "Bayer Green Split",
LINEAR_RESPONSE_LIMIT to "Linear Response Limit",
CAMERA_SERIAL_NUMBER to "Camera Serial Number",
LENS_INFO to "Lens Info",
CHROMA_BLUR_RADIUS to "Chroma Blur Radius",
ANTI_ALIAS_STRENGTH to "Anti Alias Strength",
SHADOW_SCALE to "Shadow Scale",
DNG_PRIVATE_DATA to "DNG Private Data",
MAKER_NOTE_SAFETY to "Maker Note Safety",
CALIBRATION_ILLUMINANT_1 to "Calibration Illuminant 1",
CALIBRATION_ILLUMINANT_2 to "Calibration Illuminant 2",
BEST_QUALITY_SCALE to "Best Quality Scale",
RAW_DATA_UNIQUE_ID to "Raw Data Unique ID",
ORIGINAL_RAW_FILE_NAME to "Original Raw File Name",
ORIGINAL_RAW_FILE_DATA to "Original Raw File Data",
ACTIVE_AREA to "Active Area",
MASKED_AREAS to "Masked Areas",
AS_SHOT_ICC_PROFILE to "As Shot ICC Profile",
AS_SHOT_PRE_PROFILE_MATRIX to "As Shot Pre Profile Matrix",
CURRENT_ICC_PROFILE to "Current ICC Profile",
CURRENT_PRE_PROFILE_MATRIX to "Current Pre Profile Matrix",
COLORIMETRIC_REFERENCE to "Colorimetric Reference",
CAMERA_CALIBRATION_SIGNATURE to "Camera Calibration Signature",
PROFILE_CALIBRATION_SIGNATURE to "Profile Calibration Signature",
EXTRA_CAMERA_PROFILES to "Extra Camera Profiles",
AS_SHOT_PROFILE_NAME to "As Shot Profile Name",
NOISE_REDUCTION_APPLIED to "Noise Reduction Applied",
PROFILE_NAME to "Profile Name",
PROFILE_HUE_SAT_MAP_DIMS to "Profile Hue Sat Map Dims",
PROFILE_HUE_SAT_MAP_DATA_1 to "Profile Hue Sat Map Data 1",
PROFILE_HUE_SAT_MAP_DATA_2 to "Profile Hue Sat Map Data 2",
PROFILE_TONE_CURVE to "Profile Tone Curve",
PROFILE_EMBED_POLICY to "Profile Embed Policy",
PROFILE_COPYRIGHT to "Profile Copyright",
FORWARD_MATRIX_1 to "Forward Matrix 1",
FORWARD_MATRIX_2 to "Forward Matrix 2",
PREVIEW_APPLICATION_NAME to "Preview Application Name",
PREVIEW_APPLICATION_VERSION to "Preview Application Version",
PREVIEW_SETTINGS_NAME to "Preview Settings Name",
PREVIEW_SETTINGS_DIGEST to "Preview Settings Digest",
PREVIEW_COLOR_SPACE to "Preview Color Space",
PREVIEW_DATE_TIME to "Preview Date Time",
RAW_IMAGE_DIGEST to "Raw Image Digest",
ORIGINAL_RAW_FILE_DIGEST to "Original Raw File Digest",
SUB_TILE_BLOCK_SIZE to "Sub Tile Block Size",
ROW_INTERLEAVE_FACTOR to "Row Interleave Factor",
PROFILE_LOOK_TABLE_DIMS to "Profile Look Table Dims",
PROFILE_LOOK_TABLE_DATA to "Profile Look Table Data",
OPCODE_LIST_1 to "Opcode List 1",
OPCODE_LIST_2 to "Opcode List 2",
OPCODE_LIST_3 to "Opcode List 3",
NOISE_PROFILE to "Noise Profile",
ORIGINAL_DEFAULT_FINAL_SIZE to "Original Default Final Size",
ORIGINAL_BEST_QUALITY_FINAL_SIZE to "Original Best Quality Final Size",
ORIGINAL_DEFAULT_CROP_SIZE to "Original Default Crop Size",
PROFILE_HUE_SAT_MAP_ENCODING to "Profile Hue Sat Map Encoding",
PROFILE_LOOK_TABLE_ENCODING to "Profile Look Table Encoding",
BASELINE_EXPOSURE_OFFSET to "Baseline Exposure Offset",
DEFAULT_BLACK_RENDER to "Default Black Render",
NEW_RAW_IMAGE_DIGEST to "New Raw Image Digest",
RAW_TO_PREVIEW_GAIN to "Raw To Preview Gain",
DEFAULT_USER_CROP to "Default User Crop",
DEPTH_FORMAT to "Depth Format",
DEPTH_NEAR to "Depth Near",
DEPTH_FAR to "Depth Far",
DEPTH_UNITS to "Depth Units",
DEPTH_MEASURE_TYPE to "Depth Measure Type",
ENHANCE_PARAMS to "Enhance Params",
PROFILE_GAIN_TABLE_MAP to "Profile Gain Table Map",
SEMANTIC_NAME to "Semantic Name",
SEMANTIC_INSTANCE_ID to "Semantic Instance ID",
CALIBRATION_ILLUMINANT_3 to "Calibration Illuminant 3",
CAMERA_CALIBRATION_3 to "Camera Calibration 3",
COLOR_MATRIX_3 to "Color Matrix 3",
FORWARD_MATRIX_3 to "Forward Matrix 3",
ILLUMINANT_DATA_1 to "Illuminant Data 1",
ILLUMINANT_DATA_2 to "Illuminant Data 2",
ILLUMINANT_DATA_3 to "Illuminant Data 3",
MASK_SUB_AREA to "Mask Sub Area",
PROFILE_HUE_SAT_MAP_DATA_3 to "Profile Hue Sat Map Data 3",
REDUCTION_MATRIX_3 to "Reduction Matrix 3",
RGB_TABLES to "RGB Tables",
)
val tags = tagNameMap.keys
}

View file

@ -363,13 +363,17 @@ object ExifInterfaceHelper {
} }
} }
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) { fun ExifInterface.getSafeDateMillis(tag: String, subSecTag: String?, save: (value: Long) -> Unit) {
if (this.hasAttribute(tag)) { if (this.hasAttribute(tag)) {
val dateString = this.getAttribute(tag) val dateString = this.getAttribute(tag)
if (dateString != null) { if (dateString != null) {
try { try {
DATETIME_FORMAT.parse(dateString)?.let { date -> DATETIME_FORMAT.parse(dateString)?.let { date ->
save(date.time) var dateMillis = date.time
if (subSecTag != null && this.hasAttribute(subSecTag)) {
dateMillis += Metadata.parseSubSecond(this.getAttribute(subSecTag))
}
save(dateMillis)
} }
} catch (e: ParseException) { } catch (e: ParseException) {
Log.w(LOG_TAG, "failed to parse date=$dateString", e) Log.w(LOG_TAG, "failed to parse date=$dateString", e)

View file

@ -1,155 +1,55 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
// Exif tags missing from `metadata-extractor` /*
Exif tags missing from `metadata-extractor`
Photoshop
https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFphotoshop.pdf
*/
object ExifTags { object ExifTags {
// XPosition
// Tag = 286 (011E.H)
private const val TAG_X_POSITION = 0x011e private const val TAG_X_POSITION = 0x011e
// YPosition
// Tag = 287 (011F.H)
private const val TAG_Y_POSITION = 0x011f private const val TAG_Y_POSITION = 0x011f
private const val TAG_T4_OPTIONS = 0x0124
// ColorMap private const val TAG_T6_OPTIONS = 0x0125
// Tag = 320 (0140.H)
private const val TAG_COLOR_MAP = 0x0140 private const val TAG_COLOR_MAP = 0x0140
// ExtraSamples
// Tag = 338 (0152.H)
// values:
// EXTRASAMPLE_UNSPECIFIED 0 // unspecified data
// EXTRASAMPLE_ASSOCALPHA 1 // associated alpha data
// EXTRASAMPLE_UNASSALPHA 2 // unassociated alpha data
private const val TAG_EXTRA_SAMPLES = 0x0152 private const val TAG_EXTRA_SAMPLES = 0x0152
// SampleFormat
// Tag = 339 (0153.H)
// values:
// SAMPLEFORMAT_UINT 1 // unsigned integer data
// SAMPLEFORMAT_INT 2 // signed integer data
// SAMPLEFORMAT_IEEEFP 3 // IEEE floating point data
// SAMPLEFORMAT_VOID 4 // untyped data
// SAMPLEFORMAT_COMPLEXINT 5 // complex signed int
// SAMPLEFORMAT_COMPLEXIEEEFP 6 // complex ieee floating
private const val TAG_SAMPLE_FORMAT = 0x0153 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 private const val TAG_RATING_PERCENT = 0x4749
private const val SONY_RAW_FILE_TYPE = 0x7000
/* private const val SONY_TONE_CURVE = 0x7010
SGI
tags 32995-32999
*/
// Matteing
// Tag = 32995 (80E3.H)
// obsoleted by the 6.0 ExtraSamples (338)
private const val TAG_MATTEING = 0x80e3 private const val TAG_MATTEING = 0x80e3
/* // sensing method (0x9217) redundant with sensing method (0xA217)
GeoTIFF private const val TAG_SENSING_METHOD = 0x9217
*/
// ModelPixelScaleTag (optional)
// Tag = 33550 (830E.H)
// Type = DOUBLE
// Count = 3
const val TAG_MODEL_PIXEL_SCALE = 0x830e
// ModelTiepointTag (conditional)
// Tag = 33922 (8482.H)
// Type = DOUBLE
// Count = 6*K, K = number of tiepoints
const val TAG_MODEL_TIEPOINT = 0x8482
// ModelTransformationTag (conditional)
// Tag = 34264 (85D8.H)
// Type = DOUBLE
// Count = 16
const val TAG_MODEL_TRANSFORMATION = 0x85d8
// GeoKeyDirectoryTag (mandatory)
// Tag = 34735 (87AF.H)
// Type = UNSIGNED SHORT
// Count = variable, >= 4
const val TAG_GEO_KEY_DIRECTORY = 0x87af
// GeoDoubleParamsTag (optional)
// Tag = 34736 (87BO.H)
// Type = DOUBLE
// Count = variable
private const val TAG_GEO_DOUBLE_PARAMS = 0x87b0
// GeoAsciiParamsTag (optional)
// Tag = 34737 (87B1.H)
// Type = ASCII
// Count = variable
private const val TAG_GEO_ASCII_PARAMS = 0x87b1
/*
Photoshop
https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFphotoshop.pdf
*/
// ImageSourceData
// Tag = 37724 (935C.H)
// Type = UNDEFINED
private const val TAG_IMAGE_SOURCE_DATA = 0x935c private const val TAG_IMAGE_SOURCE_DATA = 0x935c
private const val TAG_GDAL_METADATA = 0xa480
/* private const val TAG_GDAL_NO_DATA = 0xa481
DNG
https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf
*/
// CameraSerialNumber
// Tag = 50735 (C62F.H)
// Type = ASCII
// Count = variable
private const val TAG_CAMERA_SERIAL_NUMBER = 0xc62f
// OriginalRawFileName (optional)
// Tag = 50827 (C68B.H)
// Type = ASCII or BYTE
// Count = variable
private const val TAG_ORIGINAL_RAW_FILE_NAME = 0xc68b
private val geotiffTags = listOf(
TAG_GEO_ASCII_PARAMS,
TAG_GEO_DOUBLE_PARAMS,
TAG_GEO_KEY_DIRECTORY,
TAG_MODEL_PIXEL_SCALE,
TAG_MODEL_TIEPOINT,
TAG_MODEL_TRANSFORMATION,
)
private val tagNameMap = hashMapOf( private val tagNameMap = hashMapOf(
TAG_X_POSITION to "X Position", TAG_X_POSITION to "X Position",
TAG_Y_POSITION to "Y Position", TAG_Y_POSITION to "Y Position",
TAG_T4_OPTIONS to "T4 Options",
TAG_T6_OPTIONS to "T6 Options",
TAG_COLOR_MAP to "Color Map", TAG_COLOR_MAP to "Color Map",
TAG_EXTRA_SAMPLES to "Extra Samples", TAG_EXTRA_SAMPLES to "Extra Samples",
TAG_SAMPLE_FORMAT to "Sample Format", TAG_SAMPLE_FORMAT to "Sample Format",
TAG_RATING_PERCENT to "Rating Percent", TAG_RATING_PERCENT to "Rating Percent",
// SGI SONY_RAW_FILE_TYPE to "Sony Raw File Type",
SONY_TONE_CURVE to "Sony Tone Curve",
TAG_MATTEING to "Matteing", TAG_MATTEING to "Matteing",
// GeoTIFF TAG_SENSING_METHOD to "Sensing Method (0x9217)",
TAG_GEO_ASCII_PARAMS to "Geo Ascii Params",
TAG_GEO_DOUBLE_PARAMS to "Geo Double Params",
TAG_GEO_KEY_DIRECTORY to "Geo Key Directory",
TAG_MODEL_PIXEL_SCALE to "Model Pixel Scale",
TAG_MODEL_TIEPOINT to "Model Tiepoint",
TAG_MODEL_TRANSFORMATION to "Model Transformation",
// Photoshop
TAG_IMAGE_SOURCE_DATA to "Image Source Data", TAG_IMAGE_SOURCE_DATA to "Image Source Data",
// DNG TAG_GDAL_METADATA to "GDAL Metadata",
TAG_CAMERA_SERIAL_NUMBER to "Camera Serial Number", TAG_GDAL_NO_DATA to "GDAL No Data",
TAG_ORIGINAL_RAW_FILE_NAME to "Original Raw File Name", ).apply {
) putAll(DngTags.tagNameMap)
putAll(GeoTiffTags.tagNameMap)
}
fun isGeoTiffTag(tag: Int) = geotiffTags.contains(tag) fun isDngTag(tag: Int) = DngTags.tags.contains(tag)
fun isGeoTiffTag(tag: Int) = GeoTiffTags.tags.contains(tag)
fun getTagName(tag: Int): String? { fun getTagName(tag: Int): String? {
return tagNameMap[tag] return tagNameMap[tag]

View file

@ -0,0 +1,50 @@
package deckers.thibault.aves.metadata
object GeoTiffTags {
// ModelPixelScaleTag (optional)
// Tag = 33550 (830E.H)
// Type = DOUBLE
// Count = 3
const val TAG_MODEL_PIXEL_SCALE = 0x830e
// ModelTiepointTag (conditional)
// Tag = 33922 (8482.H)
// Type = DOUBLE
// Count = 6*K, K = number of tiepoints
const val TAG_MODEL_TIEPOINT = 0x8482
// ModelTransformationTag (conditional)
// Tag = 34264 (85D8.H)
// Type = DOUBLE
// Count = 16
const val TAG_MODEL_TRANSFORMATION = 0x85d8
// GeoKeyDirectoryTag (mandatory)
// Tag = 34735 (87AF.H)
// Type = UNSIGNED SHORT
// Count = variable, >= 4
const val TAG_GEO_KEY_DIRECTORY = 0x87af
// GeoDoubleParamsTag (optional)
// Tag = 34736 (87BO.H)
// Type = DOUBLE
// Count = variable
private const val TAG_GEO_DOUBLE_PARAMS = 0x87b0
// GeoAsciiParamsTag (optional)
// Tag = 34737 (87B1.H)
// Type = ASCII
// Count = variable
private const val TAG_GEO_ASCII_PARAMS = 0x87b1
val tagNameMap = hashMapOf(
TAG_GEO_ASCII_PARAMS to "Geo Ascii Params",
TAG_GEO_DOUBLE_PARAMS to "Geo Double Params",
TAG_GEO_KEY_DIRECTORY to "Geo Key Directory",
TAG_MODEL_PIXEL_SCALE to "Model Pixel Scale",
TAG_MODEL_TIEPOINT to "Model Tiepoint",
TAG_MODEL_TRANSFORMATION to "Model Transformation",
)
val tags = tagNameMap.keys
}

View file

@ -65,6 +65,20 @@ object Metadata {
} }
} }
fun parseSubSecond(subSecond: String?): Int {
if (subSecond != null) {
try {
val millis = (".$subSecond".toDouble() * 1000).toInt()
if (millis in 0..999) {
return millis
}
} catch (e: NumberFormatException) {
// ignore
}
}
return 0
}
// not sure which standards are used for the different video formats, // not sure which standards are used for the different video formats,
// but looks like some form of ISO 8601 `basic format`: // but looks like some form of ISO 8601 `basic format`:
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)? // yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
@ -96,18 +110,7 @@ object Metadata {
null null
} ?: return 0 } ?: return 0
var dateMillis = date.time return date.time + parseSubSecond(subSecond)
if (subSecond != null) {
try {
val millis = (".$subSecond".toDouble() * 1000).toInt()
if (millis in 0..999) {
dateMillis += millis.toLong()
}
} catch (e: NumberFormatException) {
// ignore
}
}
return dateMillis
} }
// opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1), // opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),

View file

@ -6,7 +6,9 @@ import com.drew.lang.Rational
import com.drew.lang.SequentialByteArrayReader import com.drew.lang.SequentialByteArrayReader
import com.drew.metadata.Directory import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader import com.drew.metadata.exif.ExifReader
import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.iptc.IptcReader import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory import com.drew.metadata.png.PngDirectory
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
@ -53,11 +55,34 @@ object MetadataExtractorHelper {
if (this.containsTag(tag)) save(this.getRational(tag)) if (this.containsTag(tag)) save(this.getRational(tag))
} }
fun Directory.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) { fun Directory.getSafeDateMillis(tag: Int, subSecond: String?): Long? {
if (this.containsTag(tag)) { if (this.containsTag(tag)) {
val date = this.getDate(tag, null, TimeZone.getDefault()) val date = this.getDate(tag, subSecond, TimeZone.getDefault())
if (date != null) save(date.time) if (date != null) return date.time
} }
return null
}
// time tag and sub-second tag are *not* in the same directory
fun ExifSubIFDDirectory.getDateModifiedMillis(save: (value: Long) -> Unit) {
val parent = parent
if (parent is ExifIFD0Directory) {
val subSecond = getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME)
val dateMillis = parent.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, subSecond)
if (dateMillis != null) save(dateMillis)
}
}
fun ExifSubIFDDirectory.getDateDigitizedMillis(save: (value: Long) -> Unit) {
val subSecond = getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED)
val dateMillis = this.getSafeDateMillis(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED, subSecond)
if (dateMillis != null) save(dateMillis)
}
fun ExifSubIFDDirectory.getDateOriginalMillis(save: (value: Long) -> Unit) {
val subSecond = getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL)
val dateMillis = this.getSafeDateMillis(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL, subSecond)
if (dateMillis != null) save(dateMillis)
} }
// geotiff // geotiff
@ -69,13 +94,13 @@ object MetadataExtractorHelper {
- If the ModelPixelScaleTag is included in an IFD, then a ModelTiepointTag SHALL also be included. - If the ModelPixelScaleTag is included in an IFD, then a ModelTiepointTag SHALL also be included.
*/ */
fun ExifDirectoryBase.isGeoTiff(): Boolean { fun ExifDirectoryBase.isGeoTiff(): Boolean {
if (!this.containsTag(ExifTags.TAG_GEO_KEY_DIRECTORY)) return false if (!this.containsTag(GeoTiffTags.TAG_GEO_KEY_DIRECTORY)) return false
val modelTiepoint = this.containsTag(ExifTags.TAG_MODEL_TIEPOINT) val modelTiepoint = this.containsTag(GeoTiffTags.TAG_MODEL_TIEPOINT)
val modelTransformation = this.containsTag(ExifTags.TAG_MODEL_TRANSFORMATION) val modelTransformation = this.containsTag(GeoTiffTags.TAG_MODEL_TRANSFORMATION)
if (!modelTiepoint && !modelTransformation) return false if (!modelTiepoint && !modelTransformation) return false
val modelPixelScale = this.containsTag(ExifTags.TAG_MODEL_PIXEL_SCALE) val modelPixelScale = this.containsTag(GeoTiffTags.TAG_MODEL_PIXEL_SCALE)
if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiepoint)) return false if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiepoint)) return false
return true return true

View file

@ -185,7 +185,7 @@ class SourceEntry {
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it } dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it }
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = it } dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = it }
dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { sourceRotationDegrees = getRotationDegreesForExifCode(it) } dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { sourceRotationDegrees = getRotationDegreesForExifCode(it) }
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME) { sourceDateTakenMillis = it } dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, null)?.let { sourceDateTakenMillis = it }
} }
// dimensions reported in EXIF do not always match the image // dimensions reported in EXIF do not always match the image
@ -218,7 +218,7 @@ class SourceEntry {
exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it } exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it }
exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it } exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it }
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { sourceRotationDegrees = exif.rotationDegrees } exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { sourceRotationDegrees = exif.rotationDegrees }
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME) { sourceDateTakenMillis = it } exif.getSafeDateMillis(ExifInterface.TAG_DATETIME, ExifInterface.TAG_SUBSEC_TIME) { sourceDateTakenMillis = it }
} }
} catch (e: Exception) { } catch (e: Exception) {
// ExifInterface initialization can fail with a RuntimeException // ExifInterface initialization can fail with a RuntimeException

View file

@ -62,8 +62,7 @@ abstract class ImageProvider {
open suspend fun renameMultiple( open suspend fun renameMultiple(
activity: Activity, activity: Activity,
newFileName: String, entriesToNewName: Map<AvesEntry, String>,
entries: List<AvesEntry>,
isCancelledOp: CancelCheck, isCancelledOp: CancelCheck,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
@ -143,7 +142,7 @@ abstract class ImageProvider {
var desiredNameWithoutExtension = if (sourceEntry.path != null) { var desiredNameWithoutExtension = if (sourceEntry.path != null) {
val sourceFileName = File(sourceEntry.path).name val sourceFileName = File(sourceEntry.path).name
sourceFileName.replaceFirst(FILE_EXTENSION_PATTERN, "") sourceFileName.substringBeforeLast(".")
} else { } else {
sourceUri.lastPathSegment!! sourceUri.lastPathSegment!!
} }
@ -757,7 +756,13 @@ abstract class ImageProvider {
ExifInterface.TAG_DATETIME_DIGITIZED, ExifInterface.TAG_DATETIME_DIGITIZED,
).forEach { field -> ).forEach { field ->
if (fields.contains(field)) { if (fields.contains(field)) {
exif.getSafeDateMillis(field) { date -> val subSecTag = when (field) {
ExifInterface.TAG_DATETIME -> ExifInterface.TAG_SUBSEC_TIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifInterface.TAG_SUBSEC_TIME_DIGITIZED
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifInterface.TAG_SUBSEC_TIME_ORIGINAL
else -> null
}
exif.getSafeDateMillis(field, subSecTag) { date ->
exif.setAttribute(field, ExifInterfaceHelper.DATETIME_FORMAT.format(date + shiftMillis)) exif.setAttribute(field, ExifInterfaceHelper.DATETIME_FORMAT.format(date + shiftMillis))
} }
} }
@ -964,8 +969,6 @@ abstract class ImageProvider {
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ImageProvider>() private val LOG_TAG = LogUtils.createTag<ImageProvider>()
val FILE_EXTENSION_PATTERN = Regex("[.][^.]+$")
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP) val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
// used when skipping a move/creation op because the target file already exists // used when skipping a move/creation op because the target file already exists

View file

@ -191,7 +191,6 @@ class MediaStoreImageProvider : ImageProvider() {
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH) val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE) val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
@ -229,7 +228,6 @@ class MediaStoreImageProvider : ImageProvider() {
"height" to height, "height" to height,
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, "sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
"sizeBytes" to cursor.getLong(sizeColumn), "sizeBytes" to cursor.getLong(sizeColumn),
"title" to cursor.getString(titleColumn),
"dateModifiedSecs" to dateModifiedSecs, "dateModifiedSecs" to dateModifiedSecs,
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null, "sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
"durationMillis" to durationMillis, "durationMillis" to durationMillis,
@ -489,7 +487,7 @@ class MediaStoreImageProvider : ImageProvider() {
return skippedFieldMap return skippedFieldMap
} }
val desiredNameWithoutExtension = desiredName.replaceFirst(FILE_EXTENSION_PATTERN, "") val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, activity = activity,
dir = targetDir, dir = targetDir,
@ -587,12 +585,14 @@ class MediaStoreImageProvider : ImageProvider() {
override suspend fun renameMultiple( override suspend fun renameMultiple(
activity: Activity, activity: Activity,
newFileName: String, entriesToNewName: Map<AvesEntry, String>,
entries: List<AvesEntry>,
isCancelledOp: CancelCheck, isCancelledOp: CancelCheck,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
for (entry in entries) { for (kv in entriesToNewName) {
val entry = kv.key
val desiredName = kv.value
val sourceUri = entry.uri val sourceUri = entry.uri
val sourcePath = entry.path val sourcePath = entry.path
val mimeType = entry.mimeType val mimeType = entry.mimeType
@ -602,19 +602,20 @@ class MediaStoreImageProvider : ImageProvider() {
"success" to false, "success" to false,
) )
if (sourcePath != null) { // prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store
if (sourcePath != null && !desiredName.startsWith('.')) {
try { try {
val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle( val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle(
activity = activity, activity = activity,
mimeType = mimeType, mimeType = mimeType,
oldMediaUri = sourceUri, oldMediaUri = sourceUri,
oldPath = sourcePath, oldPath = sourcePath,
newFileName = newFileName, desiredName = desiredName,
) )
result["newFields"] = newFields result["newFields"] = newFields
result["success"] = true result["success"] = true
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to rename to newFileName=$newFileName entry with sourcePath=$sourcePath", e) Log.w(LOG_TAG, "failed to rename to newFileName=$desiredName entry with sourcePath=$sourcePath", e)
} }
} }
callback.onSuccess(result) callback.onSuccess(result)
@ -626,10 +627,24 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType: String, mimeType: String,
oldMediaUri: Uri, oldMediaUri: Uri,
oldPath: String, oldPath: String,
newFileName: String, desiredName: String,
): FieldMap { ): FieldMap {
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val oldFile = File(oldPath) val oldFile = File(oldPath)
val newFile = File(oldFile.parent, newFileName) if (oldFile.nameWithoutExtension == desiredNameWithoutExtension) return skippedFieldMap
val dir = oldFile.parent ?: return skippedFieldMap
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity,
dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
conflictStrategy = NameConflictStrategy.RENAME,
) ?: return skippedFieldMap
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
val newFile = File(dir, targetFileName)
return when { return when {
oldFile == newFile -> skippedFieldMap oldFile == newFile -> skippedFieldMap
StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile) StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile)
@ -681,8 +696,11 @@ class MediaStoreImageProvider : ImageProvider() {
newFile: File newFile: File
): FieldMap { ): FieldMap {
Log.d(LOG_TAG, "rename document at uri=$oldMediaUri path=$oldPath") Log.d(LOG_TAG, "rename document at uri=$oldMediaUri path=$oldPath")
val df = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri)
df ?: throw Exception("failed to get document at path=$oldPath")
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
val renamed = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri)?.renameTo(newFile.name) ?: false val renamed = df.renameTo(newFile.name)
if (!renamed) { if (!renamed) {
throw Exception("failed to rename document at path=$oldPath") throw Exception("failed to rename document at path=$oldPath")
} }
@ -763,8 +781,6 @@ class MediaStoreImageProvider : ImageProvider() {
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
val projection = arrayOf( val projection = arrayOf(
MediaStore.MediaColumns.DATE_MODIFIED, MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.TITLE,
) )
try { try {
val cursor = context.contentResolver.query(uri, projection, null, null, null) val cursor = context.contentResolver.query(uri, projection, null, null, null)
@ -774,8 +790,6 @@ class MediaStoreImageProvider : ImageProvider() {
newFields["contentId"] = uri.tryParseId() newFields["contentId"] = uri.tryParseId()
newFields["path"] = path newFields["path"] = path
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
cursor.close() cursor.close()
return newFields return newFields
} }
@ -846,8 +860,6 @@ class MediaStoreImageProvider : ImageProvider() {
MediaColumns.PATH, MediaColumns.PATH,
MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.SIZE,
// TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`?
MediaStore.MediaColumns.TITLE,
MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.HEIGHT,
MediaStore.MediaColumns.DATE_MODIFIED, MediaStore.MediaColumns.DATE_MODIFIED,

View file

@ -26,7 +26,7 @@ object MimeTypes {
private const val CR2 = "image/x-canon-cr2" private const val CR2 = "image/x-canon-cr2"
private const val CRW = "image/x-canon-crw" private const val CRW = "image/x-canon-crw"
private const val DCR = "image/x-kodak-dcr" private const val DCR = "image/x-kodak-dcr"
private const val DNG = "image/x-adobe-dng" const val DNG = "image/x-adobe-dng"
private const val ERF = "image/x-epson-erf" private const val ERF = "image/x-epson-erf"
private const val K25 = "image/x-kodak-k25" private const val K25 = "image/x-kodak-k25"
private const val KDC = "image/x-kodak-kdc" private const val KDC = "image/x-kodak-kdc"

View file

@ -36,13 +36,14 @@ object StorageUtils {
const val TRASH_PATH_PLACEHOLDER = "#trash" const val TRASH_PATH_PLACEHOLDER = "#trash"
private fun isAppFile(context: Context, path: String): Boolean { private fun isAppFile(context: Context, path: String): Boolean {
return context.getExternalFilesDirs(null).any { filesDir -> path.startsWith(filesDir.path) } val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
return filesDirs.any { path.startsWith(it.path) }
} }
private fun appExternalFilesDirFor(context: Context, path: String): File? { private fun appExternalFilesDirFor(context: Context, path: String): File? {
val filesDirs = context.getExternalFilesDirs(null) val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
val volumePath = getVolumePath(context, path) val volumePath = getVolumePath(context, path)
return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.first() return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.firstOrNull()
} }
fun trashDirFor(context: Context, path: String): File? { fun trashDirFor(context: Context, path: String): File? {
@ -115,6 +116,15 @@ object StorageUtils {
} }
private fun findPrimaryVolumePath(context: Context): String? { private fun findPrimaryVolumePath(context: Context): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
val path = sm?.primaryStorageVolume?.directory?.path
if (path != null) {
return ensureTrailingSeparator(path)
}
}
// fallback
try { try {
// we want: // we want:
// /storage/emulated/0/ // /storage/emulated/0/
@ -130,9 +140,16 @@ object StorageUtils {
} }
private fun findVolumePaths(context: Context): Array<String> { private fun findVolumePaths(context: Context): Array<String> {
// Final set of paths if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val paths = HashSet<String>() val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
val paths = sm?.storageVolumes?.mapNotNull { it.directory?.path }
if (paths != null) {
return paths.map(::ensureTrailingSeparator).toTypedArray()
}
}
// fallback
val paths = HashSet<String>()
try { try {
// Primary emulated SD-CARD // Primary emulated SD-CARD
val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: "" val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: ""
@ -143,7 +160,8 @@ object StorageUtils {
var validFiles: Boolean var validFiles: Boolean
do { do {
// `getExternalFilesDirs` sometimes include `null` when called right after getting read access // `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 // (e.g. on API 30 emulator) so we retry until the file system is ready.
// TODO TLAD It can also include `null` when there is a faulty SD card.
val externalFilesDirs = context.getExternalFilesDirs(null) val externalFilesDirs = context.getExternalFilesDirs(null)
validFiles = !externalFilesDirs.contains(null) validFiles = !externalFilesDirs.contains(null)
if (validFiles) { if (validFiles) {

View file

@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.1' classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// GMS & Firebase Crashlytics are not actually used by all flavors // GMS & Firebase Crashlytics are not actually used by all flavors
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.10'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View file

@ -0,0 +1,4 @@
In v1.6.3:
- enjoy the light theme
- rename items in bulk
Full changelog available on GitHub

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View file

@ -137,6 +137,13 @@
"accessibilityAnimationsRemove": "Verhinderung von Bildschirmeffekten", "accessibilityAnimationsRemove": "Verhinderung von Bildschirmeffekten",
"accessibilityAnimationsKeep": "Bildschirmeffekte beibehalten", "accessibilityAnimationsKeep": "Bildschirmeffekte beibehalten",
"displayRefreshRatePreferHighest": "Höchste Rate",
"displayRefreshRatePreferLowest": "Niedrigste Rate",
"themeBrightnessLight": "Hell",
"themeBrightnessDark": "Dunkel",
"themeBrightnessBlack": "Schwarz",
"albumTierNew": "Neu", "albumTierNew": "Neu",
"albumTierPinned": "Angeheftet", "albumTierPinned": "Angeheftet",
"albumTierSpecial": "Häufig verwendet", "albumTierSpecial": "Häufig verwendet",
@ -170,6 +177,8 @@
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Dieses Element in den Papierkorb verschieben?} other{Diese {count} Elemente in den Papierkorb verschieben?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{Dieses Element in den Papierkorb verschieben?} other{Diese {count} Elemente in den Papierkorb verschieben?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Element gelöscht werden soll?} other{Sicher, dass diese {count} Elemente gelöscht werden sollen?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Element gelöscht werden soll?} other{Sicher, dass diese {count} Elemente gelöscht werden sollen?}}",
"moveUndatedConfirmationDialogMessage": "Einige Artikel haben kein Metadaten-Datum. Ihr aktuelles Datum wird durch diesen Vorgang zurückgesetzt, es sei denn, es wurde ein Metadaten-Datum festgelegt.",
"moveUndatedConfirmationDialogSetDate": "Datum einstellen",
"videoResumeDialogMessage": "Soll bei {time} weiter abspielt werden?", "videoResumeDialogMessage": "Soll bei {time} weiter abspielt werden?",
"videoStartOverButtonLabel": "NEU BEGINNEN", "videoStartOverButtonLabel": "NEU BEGINNEN",
@ -189,6 +198,14 @@
"renameAlbumDialogLabel": "Neuer Name", "renameAlbumDialogLabel": "Neuer Name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Verzeichnis existiert bereits", "renameAlbumDialogLabelAlreadyExistsHelper": "Verzeichnis existiert bereits",
"renameEntrySetPageTitle": "Umbenennen",
"renameEntrySetPagePatternFieldLabel": "Benennungsmuster",
"renameEntrySetPageInsertTooltip": "Feld einfügen",
"renameEntrySetPagePreview": "Vorschau",
"renameProcessorCounter": "Zähler",
"renameProcessorName": "Name",
"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?}}", "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?}}", "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?}}",
@ -201,6 +218,7 @@
"editEntryDateDialogTitle": "Datum & Uhrzeit", "editEntryDateDialogTitle": "Datum & Uhrzeit",
"editEntryDateDialogSetCustom": "Datum einstellen", "editEntryDateDialogSetCustom": "Datum einstellen",
"editEntryDateDialogCopyField": "Von anderem Datum kopieren", "editEntryDateDialogCopyField": "Von anderem Datum kopieren",
"editEntryDateDialogCopyItem": "Von einem anderen Element kopieren",
"editEntryDateDialogExtractFromTitle": "Auszug aus dem Titel", "editEntryDateDialogExtractFromTitle": "Auszug aus dem Titel",
"editEntryDateDialogShift": "Verschieben", "editEntryDateDialogShift": "Verschieben",
"editEntryDateDialogSourceFileModifiedDate": "Änderungsdatum der Datei", "editEntryDateDialogSourceFileModifiedDate": "Änderungsdatum der Datei",
@ -308,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}", "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}}", "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}}", "collectionMoveFailureFeedback": "{count, plural, =1{1 Element konnte nicht verschoben werden} other{{count} Elemente konnten nicht verschoben werden}}",
"collectionRenameFailureFeedback": "{count, plural, =1{Fehler beim Umbenennen eines Elements} other{Fehler beim Umbenennen {count} Elemente}}",
"collectionEditFailureFeedback": "{count, plural, =1{1 Element konnte nicht bearbeitet werden} other{{count} 1 Elemente konnten nicht bearbeitet 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}}", "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}}", "collectionCopySuccessFeedback": "{count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}", "collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element unmebannt} other{{count} Elemente umbenannt}}",
"collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}", "collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
"collectionEmptyFavourites": "Keine Favoriten", "collectionEmptyFavourites": "Keine Favoriten",
@ -393,6 +413,7 @@
"settingsConfirmationDialogTitle": "Bestätigungsdialoge", "settingsConfirmationDialogTitle": "Bestätigungsdialoge",
"settingsConfirmationDialogDeleteItems": "Vor dem endgültigen Löschen von Elementen fragen", "settingsConfirmationDialogDeleteItems": "Vor dem endgültigen Löschen von Elementen fragen",
"settingsConfirmationDialogMoveToBinItems": "Vor dem Verschieben von Elementen in den Papierkorb fragen", "settingsConfirmationDialogMoveToBinItems": "Vor dem Verschieben von Elementen in den Papierkorb fragen",
"settingsConfirmationDialogMoveUndatedItems": "Vor Verschiebung von Objekten ohne Metadaten-Datum fragen",
"settingsNavigationDrawerTile": "Menü Navigation", "settingsNavigationDrawerTile": "Menü Navigation",
"settingsNavigationDrawerEditorTitle": "Menü Navigation", "settingsNavigationDrawerEditorTitle": "Menü Navigation",
@ -501,6 +522,12 @@
"settingsTimeToTakeActionTile": "Zeit zum Reagieren", "settingsTimeToTakeActionTile": "Zeit zum Reagieren",
"settingsTimeToTakeActionTitle": "Zeit zum Reagieren", "settingsTimeToTakeActionTitle": "Zeit zum Reagieren",
"settingsSectionDisplay": "Anzeige",
"settingsThemeBrightness": "Thema",
"settingsThemeColorHighlights": "Farbige Highlights",
"settingsDisplayRefreshRateModeTile": "Bildwiederholrate der Anzeige",
"settingsDisplayRefreshRateModeTitle": "Bildwiederholrate",
"settingsSectionLanguage": "Sprache & Formate", "settingsSectionLanguage": "Sprache & Formate",
"settingsLanguage": "Sprache", "settingsLanguage": "Sprache",
"settingsCoordinateFormatTile": "Koordinatenformat", "settingsCoordinateFormatTile": "Koordinatenformat",
@ -570,4 +597,4 @@
"filePickerOpenFrom": "Öffnen von", "filePickerOpenFrom": "Öffnen von",
"filePickerNoItems": "Keine Elemente", "filePickerNoItems": "Keine Elemente",
"filePickerUseThisFolder": "Diesen Ordner verwenden" "filePickerUseThisFolder": "Diesen Ordner verwenden"
} }

View file

@ -177,6 +177,13 @@
"accessibilityAnimationsRemove": "Prevent screen effects", "accessibilityAnimationsRemove": "Prevent screen effects",
"accessibilityAnimationsKeep": "Keep screen effects", "accessibilityAnimationsKeep": "Keep screen effects",
"displayRefreshRatePreferHighest": "Highest rate",
"displayRefreshRatePreferLowest": "Lowest rate",
"themeBrightnessLight": "Light",
"themeBrightnessDark": "Dark",
"themeBrightnessBlack": "Black",
"albumTierNew": "New", "albumTierNew": "New",
"albumTierPinned": "Pinned", "albumTierPinned": "Pinned",
"albumTierSpecial": "Common", "albumTierSpecial": "Common",
@ -282,6 +289,8 @@
"count": {} "count": {}
} }
}, },
"moveUndatedConfirmationDialogMessage": "Save item dates before proceeding?",
"moveUndatedConfirmationDialogSetDate": "Save dates",
"videoResumeDialogMessage": "Do you want to resume playing at {time}?", "videoResumeDialogMessage": "Do you want to resume playing at {time}?",
"@videoResumeDialogMessage": { "@videoResumeDialogMessage": {
@ -309,6 +318,14 @@
"renameAlbumDialogLabel": "New name", "renameAlbumDialogLabel": "New name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists", "renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
"renameEntrySetPageTitle": "Rename",
"renameEntrySetPagePatternFieldLabel": "Naming pattern",
"renameEntrySetPageInsertTooltip": "Insert field",
"renameEntrySetPagePreview": "Preview",
"renameProcessorCounter": "Counter",
"renameProcessorName": "Name",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and its item?} other{Delete this album and its {count} items?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and its item?} other{Delete this album and its {count} items?}}",
"@deleteSingleAlbumConfirmationDialogMessage": { "@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
@ -331,6 +348,7 @@
"editEntryDateDialogTitle": "Date & Time", "editEntryDateDialogTitle": "Date & Time",
"editEntryDateDialogSetCustom": "Set custom date", "editEntryDateDialogSetCustom": "Set custom date",
"editEntryDateDialogCopyField": "Copy from other date", "editEntryDateDialogCopyField": "Copy from other date",
"editEntryDateDialogCopyItem": "Copy from other item",
"editEntryDateDialogExtractFromTitle": "Extract from title", "editEntryDateDialogExtractFromTitle": "Extract from title",
"editEntryDateDialogShift": "Shift", "editEntryDateDialogShift": "Shift",
"editEntryDateDialogSourceFileModifiedDate": "File modified date", "editEntryDateDialogSourceFileModifiedDate": "File modified date",
@ -453,6 +471,12 @@
"count": {} "count": {}
} }
}, },
"collectionRenameFailureFeedback": "{count, plural, =1{Failed to rename 1 item} other{Failed to rename {count} items}}",
"@collectionRenameFailureFeedback": {
"placeholders": {
"count": {}
}
},
"collectionEditFailureFeedback": "{count, plural, =1{Failed to edit 1 item} other{Failed to edit {count} items}}", "collectionEditFailureFeedback": "{count, plural, =1{Failed to edit 1 item} other{Failed to edit {count} items}}",
"@collectionEditFailureFeedback": { "@collectionEditFailureFeedback": {
"placeholders": { "placeholders": {
@ -477,6 +501,12 @@
"count": {} "count": {}
} }
}, },
"collectionRenameSuccessFeedback": "{count, plural, =1{Renamed 1 item} other{Renamed {count} items}}",
"@collectionRenameSuccessFeedback": {
"placeholders": {
"count": {}
}
},
"collectionEditSuccessFeedback": "{count, plural, =1{Edited 1 item} other{Edited {count} items}}", "collectionEditSuccessFeedback": "{count, plural, =1{Edited 1 item} other{Edited {count} items}}",
"@collectionEditSuccessFeedback": { "@collectionEditSuccessFeedback": {
"placeholders": { "placeholders": {
@ -563,6 +593,7 @@
"settingsConfirmationDialogTitle": "Confirmation Dialogs", "settingsConfirmationDialogTitle": "Confirmation Dialogs",
"settingsConfirmationDialogDeleteItems": "Ask before deleting items forever", "settingsConfirmationDialogDeleteItems": "Ask before deleting items forever",
"settingsConfirmationDialogMoveToBinItems": "Ask before moving items to the recycle bin", "settingsConfirmationDialogMoveToBinItems": "Ask before moving items to the recycle bin",
"settingsConfirmationDialogMoveUndatedItems": "Ask before moving undated items",
"settingsNavigationDrawerTile": "Navigation menu", "settingsNavigationDrawerTile": "Navigation menu",
"settingsNavigationDrawerEditorTitle": "Navigation Menu", "settingsNavigationDrawerEditorTitle": "Navigation Menu",
@ -671,6 +702,12 @@
"settingsTimeToTakeActionTile": "Time to take action", "settingsTimeToTakeActionTile": "Time to take action",
"settingsTimeToTakeActionTitle": "Time to Take Action", "settingsTimeToTakeActionTitle": "Time to Take Action",
"settingsSectionDisplay": "Display",
"settingsThemeBrightness": "Theme",
"settingsThemeColorHighlights": "Color highlights",
"settingsDisplayRefreshRateModeTile": "Display refresh rate",
"settingsDisplayRefreshRateModeTitle": "Refresh Rate",
"settingsSectionLanguage": "Language & Formats", "settingsSectionLanguage": "Language & Formats",
"settingsLanguage": "Language", "settingsLanguage": "Language",
"settingsCoordinateFormatTile": "Coordinate format", "settingsCoordinateFormatTile": "Coordinate format",

View file

@ -68,6 +68,8 @@
"entryActionRemoveFavourite": "Quitar de favoritos", "entryActionRemoveFavourite": "Quitar de favoritos",
"videoActionCaptureFrame": "Capturar fotograma", "videoActionCaptureFrame": "Capturar fotograma",
"videoActionMute": "Silenciar",
"videoActionUnmute": "Dejar de silenciar",
"videoActionPause": "Pausa", "videoActionPause": "Pausa",
"videoActionPlay": "Reproducir", "videoActionPlay": "Reproducir",
"videoActionReplay10": "Retroceder 10 segundos", "videoActionReplay10": "Retroceder 10 segundos",
@ -135,6 +137,13 @@
"accessibilityAnimationsRemove": "Prevenir efectos en pantalla", "accessibilityAnimationsRemove": "Prevenir efectos en pantalla",
"accessibilityAnimationsKeep": "Mantener efectos en pantalla", "accessibilityAnimationsKeep": "Mantener efectos en pantalla",
"displayRefreshRatePreferHighest": "Alta tasa",
"displayRefreshRatePreferLowest": "Baja tasa",
"themeBrightnessLight": "Claro",
"themeBrightnessDark": "Obscuro",
"themeBrightnessBlack": "Negro",
"albumTierNew": "Nuevo", "albumTierNew": "Nuevo",
"albumTierPinned": "Fijado", "albumTierPinned": "Fijado",
"albumTierSpecial": "Común", "albumTierSpecial": "Común",
@ -151,7 +160,6 @@
"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.", "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", "notEnoughSpaceDialogTitle": "Espacio insuficiente",
"notEnoughSpaceDialogMessage": "Esta operación necesita {neededSize} de espacio libre en «{volume}» para completarse, pero sólo hay {freeSize} disponible.", "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", "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.", "missingSystemFilePickerDialogMessage": "El selector de archivos del sistema no se encuentra disponible o fue deshabilitado. Por favor habilítelo e intente nuevamente.",
@ -168,8 +176,9 @@
"noMatchingAppDialogMessage": "No se encontraron aplicaciones para manejar esto.", "noMatchingAppDialogMessage": "No se encontraron aplicaciones para manejar esto.",
"binEntriesConfirmationDialogMessage": "{count, plural, =1{¿Mover este elemento al cesto de basura?} other{¿Mover estos {count} elementos al cesto de basura?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{¿Mover este elemento al cesto de basura?} other{¿Mover estos {count} elementos al cesto de basura?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de borrar este elemento?} other{¿Está seguro de querer borrar {count} elementos?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de borrar este elemento?} other{¿Está seguro de querer borrar {count} elementos?}}",
"moveUndatedConfirmationDialogMessage": "Algunos elementos no poseen fecha en sus metadatos. Su fecha actual será reemplazada por esta operación a menos que una fecha de metadatos sea fijada.",
"moveUndatedConfirmationDialogSetDate": "Fijar fecha",
"videoResumeDialogMessage": "¿Desea reanudar la reproducción a las {time}?", "videoResumeDialogMessage": "¿Desea reanudar la reproducción a las {time}?",
"videoStartOverButtonLabel": "VOLVER A EMPEZAR", "videoStartOverButtonLabel": "VOLVER A EMPEZAR",
@ -189,6 +198,14 @@
"renameAlbumDialogLabel": "Renombrar", "renameAlbumDialogLabel": "Renombrar",
"renameAlbumDialogLabelAlreadyExistsHelper": "El directorio ya existe", "renameAlbumDialogLabelAlreadyExistsHelper": "El directorio ya existe",
"renameEntrySetPageTitle": "Renombrar",
"renameEntrySetPagePatternFieldLabel": "Patrón de nombramiento",
"renameEntrySetPageInsertTooltip": "Insertar campo",
"renameEntrySetPagePreview": "Vista previa",
"renameProcessorCounter": "Contador",
"renameProcessorName": "Nombre",
"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?}}", "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?}}", "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?}}",
@ -201,6 +218,7 @@
"editEntryDateDialogTitle": "Fecha y hora", "editEntryDateDialogTitle": "Fecha y hora",
"editEntryDateDialogSetCustom": "Establecer fecha personalizada", "editEntryDateDialogSetCustom": "Establecer fecha personalizada",
"editEntryDateDialogCopyField": "Copiar de otra fecha", "editEntryDateDialogCopyField": "Copiar de otra fecha",
"editEntryDateDialogCopyItem": "Copiar de otro elemento",
"editEntryDateDialogExtractFromTitle": "Extraer del título", "editEntryDateDialogExtractFromTitle": "Extraer del título",
"editEntryDateDialogShift": "Cambiar", "editEntryDateDialogShift": "Cambiar",
"editEntryDateDialogSourceFileModifiedDate": "Fecha de modificación del archivo", "editEntryDateDialogSourceFileModifiedDate": "Fecha de modificación del archivo",
@ -308,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, =1{Error al borrar 1 elemento} other{Error al borrar {count} elementos}}", "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}}", "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}}", "collectionMoveFailureFeedback": "{count, plural, =1{Error al mover 1 elemento} other{Error al mover {count} elementos}}",
"collectionRenameFailureFeedback": "{count, plural, =1{Error al renombrar 1 elemento} other{Error al renombrar {count} elementos}}",
"collectionEditFailureFeedback": "{count, plural, =1{Error al editar 1 elemento} other{Error al editar {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}}", "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}}", "collectionCopySuccessFeedback": "{count, plural, =1{1 elemento copiado} other{Copiados{count} elementos}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{1 elemento movido} other{Movidos {count} elementos}}", "collectionMoveSuccessFeedback": "{count, plural, =1{1 elemento movido} other{Movidos {count} elementos}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{1 elemento renombrado} other{Renombrados {count} elementos}}",
"collectionEditSuccessFeedback": "{count, plural, =1{1 elemento editado} other{Editados {count} elementos}}", "collectionEditSuccessFeedback": "{count, plural, =1{1 elemento editado} other{Editados {count} elementos}}",
"collectionEmptyFavourites": "Sin favoritos", "collectionEmptyFavourites": "Sin favoritos",
@ -393,6 +413,7 @@
"settingsConfirmationDialogTitle": "Diálogos de confirmación", "settingsConfirmationDialogTitle": "Diálogos de confirmación",
"settingsConfirmationDialogDeleteItems": "Preguntar antes de eliminar elementos permanentemente", "settingsConfirmationDialogDeleteItems": "Preguntar antes de eliminar elementos permanentemente",
"settingsConfirmationDialogMoveToBinItems": "Preguntar antes de mover elementos al cesto de basura", "settingsConfirmationDialogMoveToBinItems": "Preguntar antes de mover elementos al cesto de basura",
"settingsConfirmationDialogMoveUndatedItems": "Preguntar antes de mover elementos sin una fecha de metadatos",
"settingsNavigationDrawerTile": "Menú de navegación", "settingsNavigationDrawerTile": "Menú de navegación",
"settingsNavigationDrawerEditorTitle": "Menú de navegación", "settingsNavigationDrawerEditorTitle": "Menú de navegación",
@ -501,6 +522,12 @@
"settingsTimeToTakeActionTile": "Retraso para ejecutar una acción", "settingsTimeToTakeActionTile": "Retraso para ejecutar una acción",
"settingsTimeToTakeActionTitle": "Retraso para ejecutar una acción", "settingsTimeToTakeActionTitle": "Retraso para ejecutar una acción",
"settingsSectionDisplay": "Pantalla",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Acentos de color",
"settingsDisplayRefreshRateModeTile": "Tasa de refresco de la pantalla",
"settingsDisplayRefreshRateModeTitle": "Tasa de refresco",
"settingsSectionLanguage": "Idioma y formatos", "settingsSectionLanguage": "Idioma y formatos",
"settingsLanguage": "Idioma", "settingsLanguage": "Idioma",
"settingsCoordinateFormatTile": "Formato de coordenadas", "settingsCoordinateFormatTile": "Formato de coordenadas",

View file

@ -137,6 +137,13 @@
"accessibilityAnimationsRemove": "Empêchez certains effets de lécran", "accessibilityAnimationsRemove": "Empêchez certains effets de lécran",
"accessibilityAnimationsKeep": "Conserver les effets de lécran", "accessibilityAnimationsKeep": "Conserver les effets de lécran",
"displayRefreshRatePreferHighest": "Fréquence maximale",
"displayRefreshRatePreferLowest": "Fréquence minimale",
"themeBrightnessLight": "Clair",
"themeBrightnessDark": "Sombre",
"themeBrightnessBlack": "Noir",
"albumTierNew": "Nouveaux", "albumTierNew": "Nouveaux",
"albumTierPinned": "Épinglés", "albumTierPinned": "Épinglés",
"albumTierSpecial": "Standards", "albumTierSpecial": "Standards",
@ -170,6 +177,8 @@
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Mettre cet élément à la corbeille ?} other{Mettre ces {count} éléments à la corbeille ?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{Mettre cet élément à la corbeille ?} other{Mettre ces {count} éléments à la corbeille ?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Supprimer cet élément ?} other{Supprimer ces {count} éléments ?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Supprimer cet élément ?} other{Supprimer ces {count} éléments ?}}",
"moveUndatedConfirmationDialogMessage": "Sauvegarder les dates des éléments avant de continuer?",
"moveUndatedConfirmationDialogSetDate": "Sauvegarder les dates",
"videoResumeDialogMessage": "Voulez-vous reprendre la lecture à {time} ?", "videoResumeDialogMessage": "Voulez-vous reprendre la lecture à {time} ?",
"videoStartOverButtonLabel": "RECOMMENCER", "videoStartOverButtonLabel": "RECOMMENCER",
@ -189,6 +198,14 @@
"renameAlbumDialogLabel": "Nouveau nom", "renameAlbumDialogLabel": "Nouveau nom",
"renameAlbumDialogLabelAlreadyExistsHelper": "Le dossier existe déjà", "renameAlbumDialogLabelAlreadyExistsHelper": "Le dossier existe déjà",
"renameEntrySetPageTitle": "Renommage",
"renameEntrySetPagePatternFieldLabel": "Modèle de nommage",
"renameEntrySetPageInsertTooltip": "Ajouter un champ",
"renameEntrySetPagePreview": "Aperçu",
"renameProcessorCounter": "Compteur",
"renameProcessorName": "Nom",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et son élément ?} other{Supprimer cet album et ses {count} éléments ?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et son élément ?} other{Supprimer cet album et ses {count} éléments ?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et leur élément ?} other{Supprimer ces albums et leurs {count} éléments ?}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et leur élément ?} other{Supprimer ces albums et leurs {count} éléments ?}}",
@ -201,6 +218,7 @@
"editEntryDateDialogTitle": "Date & Heure", "editEntryDateDialogTitle": "Date & Heure",
"editEntryDateDialogSetCustom": "Régler une date personnalisée", "editEntryDateDialogSetCustom": "Régler une date personnalisée",
"editEntryDateDialogCopyField": "Copier dune autre date", "editEntryDateDialogCopyField": "Copier dune autre date",
"editEntryDateDialogCopyItem": "Copier dun autre élément",
"editEntryDateDialogExtractFromTitle": "Extraire du titre", "editEntryDateDialogExtractFromTitle": "Extraire du titre",
"editEntryDateDialogShift": "Décaler", "editEntryDateDialogShift": "Décaler",
"editEntryDateDialogSourceFileModifiedDate": "Date de modification du fichier", "editEntryDateDialogSourceFileModifiedDate": "Date de modification du fichier",
@ -308,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, =1{Échec de la suppression d1 élément} other{Échec de la suppression de {count} éléments}}", "collectionDeleteFailureFeedback": "{count, plural, =1{Échec de la suppression d1 élément} other{Échec de la suppression de {count} éléments}}",
"collectionCopyFailureFeedback": "{count, plural, =1{Échec de la copie d1 élément} other{Échec de la copie de {count} éléments}}", "collectionCopyFailureFeedback": "{count, plural, =1{Échec de la copie d1 élément} other{Échec de la copie de {count} éléments}}",
"collectionMoveFailureFeedback": "{count, plural, =1{Échec du déplacement d1 élément} other{Échec du déplacement de {count} éléments}}", "collectionMoveFailureFeedback": "{count, plural, =1{Échec du déplacement d1 élément} other{Échec du déplacement de {count} éléments}}",
"collectionRenameFailureFeedback": "{count, plural, =1{Échec du renommage d1 élément} other{Échec du renommage de {count} éléments}}",
"collectionEditFailureFeedback": "{count, plural, =1{Échec de la modification d1 élément} other{Échec de la modification de {count} éléments}}", "collectionEditFailureFeedback": "{count, plural, =1{Échec de la modification d1 élément} other{Échec de la modification de {count} éléments}}",
"collectionExportFailureFeedback": "{count, plural, =1{Échec de lexport d1 page} other{Échec de lexport de {count} pages}}", "collectionExportFailureFeedback": "{count, plural, =1{Échec de lexport d1 page} other{Échec de lexport de {count} pages}}",
"collectionCopySuccessFeedback": "{count, plural, =1{1 élément copié} other{{count} éléments copiés}}", "collectionCopySuccessFeedback": "{count, plural, =1{1 élément copié} other{{count} éléments copiés}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{1 élément déplacé} other{{count} éléments déplacés}}", "collectionMoveSuccessFeedback": "{count, plural, =1{1 élément déplacé} other{{count} éléments déplacés}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{1 élément renommé} other{{count} éléments renommés}}",
"collectionEditSuccessFeedback": "{count, plural, =1{1 élément modifié} other{{count} éléments modifiés}}", "collectionEditSuccessFeedback": "{count, plural, =1{1 élément modifié} other{{count} éléments modifiés}}",
"collectionEmptyFavourites": "Aucun favori", "collectionEmptyFavourites": "Aucun favori",
@ -393,6 +413,7 @@
"settingsConfirmationDialogTitle": "Demandes de confirmation", "settingsConfirmationDialogTitle": "Demandes de confirmation",
"settingsConfirmationDialogDeleteItems": "Suppression définitive déléments", "settingsConfirmationDialogDeleteItems": "Suppression définitive déléments",
"settingsConfirmationDialogMoveToBinItems": "Mise déléments à la corbeille", "settingsConfirmationDialogMoveToBinItems": "Mise déléments à la corbeille",
"settingsConfirmationDialogMoveUndatedItems": "Déplacement déléments non datés",
"settingsNavigationDrawerTile": "Menu de navigation", "settingsNavigationDrawerTile": "Menu de navigation",
"settingsNavigationDrawerEditorTitle": "Menu de navigation", "settingsNavigationDrawerEditorTitle": "Menu de navigation",
@ -501,6 +522,12 @@
"settingsTimeToTakeActionTile": "Délai pour effectuer une action", "settingsTimeToTakeActionTile": "Délai pour effectuer une action",
"settingsTimeToTakeActionTitle": "Délai pour effectuer une action", "settingsTimeToTakeActionTitle": "Délai pour effectuer une action",
"settingsSectionDisplay": "Affichage",
"settingsThemeBrightness": "Thème",
"settingsThemeColorHighlights": "Surlignages colorés",
"settingsDisplayRefreshRateModeTile": "Fréquence dactualisation de l'écran",
"settingsDisplayRefreshRateModeTitle": "Fréquence dactualisation",
"settingsSectionLanguage": "Langue & Formats", "settingsSectionLanguage": "Langue & Formats",
"settingsLanguage": "Langue", "settingsLanguage": "Langue",
"settingsCoordinateFormatTile": "Format de coordonnées", "settingsCoordinateFormatTile": "Format de coordonnées",

View file

@ -68,6 +68,8 @@
"entryActionRemoveFavourite": "Hapus dari favorit", "entryActionRemoveFavourite": "Hapus dari favorit",
"videoActionCaptureFrame": "Tangkap bingkai", "videoActionCaptureFrame": "Tangkap bingkai",
"videoActionMute": "Mute",
"videoActionUnmute": "Unmute",
"videoActionPause": "Henti", "videoActionPause": "Henti",
"videoActionPlay": "Mainkan", "videoActionPlay": "Mainkan",
"videoActionReplay10": "Mundurkan 10 detik", "videoActionReplay10": "Mundurkan 10 detik",
@ -112,6 +114,11 @@
"videoLoopModeShortOnly": "Hanya video pendek", "videoLoopModeShortOnly": "Hanya video pendek",
"videoLoopModeAlways": "Selalu", "videoLoopModeAlways": "Selalu",
"videoControlsPlay": "Putar",
"videoControlsPlaySeek": "Putar dan cari",
"videoControlsPlayOutside": "Buka dengan pemutar lain",
"videoControlsNone": "Tidak ada",
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"mapStyleGoogleTerrain": "Google Maps (Terrain)", "mapStyleGoogleTerrain": "Google Maps (Terrain)",
@ -130,6 +137,13 @@
"accessibilityAnimationsRemove": "Mencegah efek layar", "accessibilityAnimationsRemove": "Mencegah efek layar",
"accessibilityAnimationsKeep": "Simpan efek layar", "accessibilityAnimationsKeep": "Simpan efek layar",
"displayRefreshRatePreferHighest": "Penyegaran tertinggi",
"displayRefreshRatePreferLowest": "Penyegaran terendah",
"themeBrightnessLight": "Terang",
"themeBrightnessDark": "Gelap",
"themeBrightnessBlack": "Hitam",
"albumTierNew": "Baru", "albumTierNew": "Baru",
"albumTierPinned": "Disemat", "albumTierPinned": "Disemat",
"albumTierSpecial": "Umum", "albumTierSpecial": "Umum",
@ -163,6 +177,8 @@
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Pindahkan benda ini ke tong sampah?} other{Pindahkan {count} benda ke tempat sampah?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{Pindahkan benda ini ke tong sampah?} other{Pindahkan {count} benda ke tempat sampah?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}",
"moveUndatedConfirmationDialogMessage": "Beberapa benda tidak mempunyai tanggal metadata. Tanggal mereka sekarang akan diatur ulang dengan operasi ini kecuali ada tanggal metadata yang ditetapkan.",
"moveUndatedConfirmationDialogSetDate": "Atur tanggal",
"videoResumeDialogMessage": "Apakah Anda ingin melanjutkan di {time}?", "videoResumeDialogMessage": "Apakah Anda ingin melanjutkan di {time}?",
"videoStartOverButtonLabel": "ULANG DARI AWAL", "videoStartOverButtonLabel": "ULANG DARI AWAL",
@ -182,6 +198,14 @@
"renameAlbumDialogLabel": "Nama baru", "renameAlbumDialogLabel": "Nama baru",
"renameAlbumDialogLabelAlreadyExistsHelper": "Direktori sudah ada", "renameAlbumDialogLabelAlreadyExistsHelper": "Direktori sudah ada",
"renameEntrySetPageTitle": "Ganti nama",
"renameEntrySetPagePatternFieldLabel": "Pola penamaan",
"renameEntrySetPageInsertTooltip": "Masukkan bidang",
"renameEntrySetPagePreview": "Pratinjau",
"renameProcessorCounter": "Menangkal",
"renameProcessorName": "Nama",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Yakin ingin menghapus album ini dan bendanya?} other{Anda yakin ingin menghapus album ini dan {count} bendanya?}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Yakin ingin menghapus album ini dan bendanya?} other{Anda yakin ingin menghapus album ini dan {count} bendanya?}}",
@ -194,6 +218,7 @@
"editEntryDateDialogTitle": "Tanggal & Waktu", "editEntryDateDialogTitle": "Tanggal & Waktu",
"editEntryDateDialogSetCustom": "Atur tanggal khusus", "editEntryDateDialogSetCustom": "Atur tanggal khusus",
"editEntryDateDialogCopyField": "Salin dari tanggal lain", "editEntryDateDialogCopyField": "Salin dari tanggal lain",
"editEntryDateDialogCopyItem": "Salin dari benda lain",
"editEntryDateDialogExtractFromTitle": "Ekstrak dari judul", "editEntryDateDialogExtractFromTitle": "Ekstrak dari judul",
"editEntryDateDialogShift": "Geser", "editEntryDateDialogShift": "Geser",
"editEntryDateDialogSourceFileModifiedDate": "Tanggal modifikasi file", "editEntryDateDialogSourceFileModifiedDate": "Tanggal modifikasi file",
@ -301,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, other{Gagal untuk menghapus {count} benda}}", "collectionDeleteFailureFeedback": "{count, plural, other{Gagal untuk menghapus {count} benda}}",
"collectionCopyFailureFeedback": "{count, plural, other{Gagal untuk menyalin {count} benda}}", "collectionCopyFailureFeedback": "{count, plural, other{Gagal untuk menyalin {count} benda}}",
"collectionMoveFailureFeedback": "{count, plural, other{Gagal untuk menggerakkan {count} benda}}", "collectionMoveFailureFeedback": "{count, plural, other{Gagal untuk menggerakkan {count} benda}}",
"collectionRenameFailureFeedback": "{count, plural, other{Gagal untuk menggantikan nama {count} benda}}",
"collectionEditFailureFeedback": "{count, plural, other{Gagal untuk mengubah {count} benda}}", "collectionEditFailureFeedback": "{count, plural, other{Gagal untuk mengubah {count} benda}}",
"collectionExportFailureFeedback": "{count, plural, other{Gagal untuk mengekspor {count} halaman}}", "collectionExportFailureFeedback": "{count, plural, other{Gagal untuk mengekspor {count} halaman}}",
"collectionCopySuccessFeedback": "{count, plural, other{Menyalin {count} benda}}", "collectionCopySuccessFeedback": "{count, plural, other{Menyalin {count} benda}}",
"collectionMoveSuccessFeedback": "{count, plural, other{{count} benda terpindah}}", "collectionMoveSuccessFeedback": "{count, plural, other{{count} benda terpindah}}",
"collectionRenameSuccessFeedback": "{count, plural, other{Tergantikan nama untuk {count} benda}}",
"collectionEditSuccessFeedback": "{count, plural, other{Mengubah {count} benda}}", "collectionEditSuccessFeedback": "{count, plural, other{Mengubah {count} benda}}",
"collectionEmptyFavourites": "Tidak ada favorit", "collectionEmptyFavourites": "Tidak ada favorit",
@ -386,6 +413,8 @@
"settingsConfirmationDialogTitle": "Dialog Konfirmasi", "settingsConfirmationDialogTitle": "Dialog Konfirmasi",
"settingsConfirmationDialogDeleteItems": "Tanya sebelum menghapus benda selamanya", "settingsConfirmationDialogDeleteItems": "Tanya sebelum menghapus benda selamanya",
"settingsConfirmationDialogMoveToBinItems": "Tanya sebelum memindahkan benda ke tong sampah", "settingsConfirmationDialogMoveToBinItems": "Tanya sebelum memindahkan benda ke tong sampah",
"settingsConfirmationDialogMoveUndatedItems": "Tanyakan sebelum memindahkan barang tanpa metadata tanggal",
"settingsNavigationDrawerTile": "Menu navigasi", "settingsNavigationDrawerTile": "Menu navigasi",
"settingsNavigationDrawerEditorTitle": "Menu Navigasi", "settingsNavigationDrawerEditorTitle": "Menu Navigasi",
"settingsNavigationDrawerBanner": "Sentuh dan tahan untuk memindahkan dan menyusun ulang benda menu.", "settingsNavigationDrawerBanner": "Sentuh dan tahan untuk memindahkan dan menyusun ulang benda menu.",
@ -429,6 +458,7 @@
"settingsViewerShowInformation": "Tampilkan informasi", "settingsViewerShowInformation": "Tampilkan informasi",
"settingsViewerShowInformationSubtitle": "Tampilkan judul, tanggal, lokasi, dll.", "settingsViewerShowInformationSubtitle": "Tampilkan judul, tanggal, lokasi, dll.",
"settingsViewerShowShootingDetails": "Tampilkan detail pemotretan", "settingsViewerShowShootingDetails": "Tampilkan detail pemotretan",
"settingsViewerShowOverlayThumbnails": "Tampilkan thumbnail",
"settingsViewerEnableOverlayBlurEffect": "Efek Kabur", "settingsViewerEnableOverlayBlurEffect": "Efek Kabur",
"settingsVideoPageTitle": "Pengaturan Video", "settingsVideoPageTitle": "Pengaturan Video",
@ -454,6 +484,13 @@
"settingsSubtitleThemeTextAlignmentCenter": "Tengah", "settingsSubtitleThemeTextAlignmentCenter": "Tengah",
"settingsSubtitleThemeTextAlignmentRight": "Kanan", "settingsSubtitleThemeTextAlignmentRight": "Kanan",
"settingsVideoControlsTile": "Kontrol",
"settingsVideoControlsTitle": "Kontrol",
"settingsVideoButtonsTile": "Tombol",
"settingsVideoButtonsTitle": "Tombol",
"settingsVideoGestureDoubleTapTogglePlay": "Ketuk dua kali untuk mainkan/hentikan",
"settingsVideoGestureSideDoubleTapSeek": "Ketuk dua kali di tepi layar untuk mencari kebelakang/kedepan",
"settingsSectionPrivacy": "Privasi", "settingsSectionPrivacy": "Privasi",
"settingsAllowInstalledAppAccess": "Izinkan akses ke inventori aplikasi", "settingsAllowInstalledAppAccess": "Izinkan akses ke inventori aplikasi",
"settingsAllowInstalledAppAccessSubtitle": "Digunakan untuk meningkatkan tampilan album", "settingsAllowInstalledAppAccessSubtitle": "Digunakan untuk meningkatkan tampilan album",
@ -485,6 +522,12 @@
"settingsTimeToTakeActionTile": "Waktu untuk mengambil tindakan", "settingsTimeToTakeActionTile": "Waktu untuk mengambil tindakan",
"settingsTimeToTakeActionTitle": "Saatnya Bertindak", "settingsTimeToTakeActionTitle": "Saatnya Bertindak",
"settingsSectionDisplay": "Tampilan",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Highlight warna",
"settingsDisplayRefreshRateModeTile": "Tingkat penyegaran tampilan",
"settingsDisplayRefreshRateModeTitle": "Tingkat Penyegaran",
"settingsSectionLanguage": "Bahasa & Format", "settingsSectionLanguage": "Bahasa & Format",
"settingsLanguage": "Bahasa", "settingsLanguage": "Bahasa",
"settingsCoordinateFormatTile": "Format koordinat", "settingsCoordinateFormatTile": "Format koordinat",

View file

@ -137,6 +137,13 @@
"accessibilityAnimationsRemove": "画面エフェクトを利用しない", "accessibilityAnimationsRemove": "画面エフェクトを利用しない",
"accessibilityAnimationsKeep": "画面エフェクトを利用", "accessibilityAnimationsKeep": "画面エフェクトを利用",
"displayRefreshRatePreferHighest": "高レート",
"displayRefreshRatePreferLowest": "低レート",
"themeBrightnessLight": "ライト",
"themeBrightnessDark": "ダーク",
"themeBrightnessBlack": "黒",
"albumTierNew": "新規", "albumTierNew": "新規",
"albumTierPinned": "固定", "albumTierPinned": "固定",
"albumTierSpecial": "全体", "albumTierSpecial": "全体",
@ -170,6 +177,8 @@
"binEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムをごみ箱に移動しますか?} other{{count} 件のアイテムをごみ箱に移動しますか?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムをごみ箱に移動しますか?} other{{count} 件のアイテムをごみ箱に移動しますか?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}",
"moveUndatedConfirmationDialogMessage": "いくつかのアイテムはメタデータ上に日付がありません。メタデータ上の日付が設定されない場合、この操作によりこれらの現在の日付はリセットされます",
"moveUndatedConfirmationDialogSetDate": "日付を設定",
"videoResumeDialogMessage": " {time} の時点から再生を再開しますか?", "videoResumeDialogMessage": " {time} の時点から再生を再開しますか?",
"videoStartOverButtonLabel": "最初から再生", "videoStartOverButtonLabel": "最初から再生",
@ -189,6 +198,14 @@
"renameAlbumDialogLabel": "新しい名前", "renameAlbumDialogLabel": "新しい名前",
"renameAlbumDialogLabelAlreadyExistsHelper": "ディレクトリが既に存在します", "renameAlbumDialogLabelAlreadyExistsHelper": "ディレクトリが既に存在します",
"renameEntrySetPageTitle": "名前を変更",
"renameEntrySetPagePatternFieldLabel": "名前付けのパターン",
"renameEntrySetPageInsertTooltip": "フィールドを挿入",
"renameEntrySetPagePreview": "プレビュー",
"renameProcessorCounter": "連番",
"renameProcessorName": "名前",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{このアルバムとアルバム内のアイテムを削除しますか?} other{このアルバムとアルバム内の {count} 件のアイテムを削除しますか?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{このアルバムとアルバム内のアイテムを削除しますか?} other{このアルバムとアルバム内の {count} 件のアイテムを削除しますか?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{これらのアルバムとアルバム内のアイテムを削除しますか?} other{これらのアルバムとアルバム内の {count} 件のアイテムを削除しますか?}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{これらのアルバムとアルバム内のアイテムを削除しますか?} other{これらのアルバムとアルバム内の {count} 件のアイテムを削除しますか?}}",
@ -201,6 +218,7 @@
"editEntryDateDialogTitle": "日時", "editEntryDateDialogTitle": "日時",
"editEntryDateDialogSetCustom": "日を設定する", "editEntryDateDialogSetCustom": "日を設定する",
"editEntryDateDialogCopyField": "他の日からコピーする", "editEntryDateDialogCopyField": "他の日からコピーする",
"editEntryDateDialogCopyItem": "他のアイテムからコピーする",
"editEntryDateDialogExtractFromTitle": "タイトルから抽出する", "editEntryDateDialogExtractFromTitle": "タイトルから抽出する",
"editEntryDateDialogShift": "シフト", "editEntryDateDialogShift": "シフト",
"editEntryDateDialogSourceFileModifiedDate": "ファイル更新日", "editEntryDateDialogSourceFileModifiedDate": "ファイル更新日",
@ -308,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, other{{count} 件のアイテムを削除できませんでした}}", "collectionDeleteFailureFeedback": "{count, plural, other{{count} 件のアイテムを削除できませんでした}}",
"collectionCopyFailureFeedback": "{count, plural, other{{count} 件のアイテムをコピーできませんでした}}", "collectionCopyFailureFeedback": "{count, plural, other{{count} 件のアイテムをコピーできませんでした}}",
"collectionMoveFailureFeedback": "{count, plural, other{{count} 件のアイテムを移動できませんでした}}", "collectionMoveFailureFeedback": "{count, plural, other{{count} 件のアイテムを移動できませんでした}}",
"collectionRenameFailureFeedback": "{count, plural, other{{count} 件のアイテム名を変更できませんでした}}",
"collectionEditFailureFeedback": "{count, plural, other{{count} 件のアイテムを編集できませんでした}}", "collectionEditFailureFeedback": "{count, plural, other{{count} 件のアイテムを編集できませんでした}}",
"collectionExportFailureFeedback": "{count, plural, other{{count} ページをエクスポートできませんでした}}", "collectionExportFailureFeedback": "{count, plural, other{{count} ページをエクスポートできませんでした}}",
"collectionCopySuccessFeedback": "{count, plural, other{{count} 件のアイテムをコピーしました}}", "collectionCopySuccessFeedback": "{count, plural, other{{count} 件のアイテムをコピーしました}}",
"collectionMoveSuccessFeedback": "{count, plural, other{{count} 件のアイテムを移動しました}}", "collectionMoveSuccessFeedback": "{count, plural, other{{count} 件のアイテムを移動しました}}",
"collectionRenameSuccessFeedback": "{count, plural, other{{count} 件のアイテム名を変更しました}}",
"collectionEditSuccessFeedback": "{count, plural, other{{count} 件のアイテムを編集しました}}", "collectionEditSuccessFeedback": "{count, plural, other{{count} 件のアイテムを編集しました}}",
"collectionEmptyFavourites": "お気に入りはありません", "collectionEmptyFavourites": "お気に入りはありません",
@ -393,6 +413,7 @@
"settingsConfirmationDialogTitle": "確認メッセージ", "settingsConfirmationDialogTitle": "確認メッセージ",
"settingsConfirmationDialogDeleteItems": "アイテムを完全に削除する前に確認", "settingsConfirmationDialogDeleteItems": "アイテムを完全に削除する前に確認",
"settingsConfirmationDialogMoveToBinItems": "アイテムをごみ箱に移動する前に確認", "settingsConfirmationDialogMoveToBinItems": "アイテムをごみ箱に移動する前に確認",
"settingsConfirmationDialogMoveUndatedItems": "メタデータ上に日付のないアイテムを移動する前に確認",
"settingsNavigationDrawerTile": "ナビゲーション メニュー", "settingsNavigationDrawerTile": "ナビゲーション メニュー",
"settingsNavigationDrawerEditorTitle": "ナビゲーション メニュー", "settingsNavigationDrawerEditorTitle": "ナビゲーション メニュー",
@ -448,8 +469,6 @@
"settingsVideoLoopModeTile": "ループ モード", "settingsVideoLoopModeTile": "ループ モード",
"settingsVideoLoopModeTitle": "ループ モード", "settingsVideoLoopModeTitle": "ループ モード",
"settingsVideoQuickActionsTile": "動画のクイック アクション",
"settingsVideoQuickActionEditorTitle": "クイック アクション",
"settingsSubtitleThemeTile": "字幕", "settingsSubtitleThemeTile": "字幕",
"settingsSubtitleThemeTitle": "字幕", "settingsSubtitleThemeTitle": "字幕",
"settingsSubtitleThemeSample": "これはサンプルです。", "settingsSubtitleThemeSample": "これはサンプルです。",
@ -503,6 +522,12 @@
"settingsTimeToTakeActionTile": "操作までの時間", "settingsTimeToTakeActionTile": "操作までの時間",
"settingsTimeToTakeActionTitle": "操作までの時間", "settingsTimeToTakeActionTitle": "操作までの時間",
"settingsSectionDisplay": "ディスプレイ",
"settingsThemeBrightness": "テーマ",
"settingsThemeColorHighlights": "カラー強調表示",
"settingsDisplayRefreshRateModeTile": "ディスプレイ リフレッシュ レート",
"settingsDisplayRefreshRateModeTitle": "リフレッシュレート",
"settingsSectionLanguage": "言語と形式", "settingsSectionLanguage": "言語と形式",
"settingsLanguage": "言語", "settingsLanguage": "言語",
"settingsCoordinateFormatTile": "座標形式", "settingsCoordinateFormatTile": "座標形式",

View file

@ -137,6 +137,13 @@
"accessibilityAnimationsRemove": "화면 효과 제한", "accessibilityAnimationsRemove": "화면 효과 제한",
"accessibilityAnimationsKeep": "화면 효과 유지", "accessibilityAnimationsKeep": "화면 효과 유지",
"displayRefreshRatePreferHighest": "가장 높은 재생률",
"displayRefreshRatePreferLowest": "가장 낮은 재생률",
"themeBrightnessLight": "라이트",
"themeBrightnessDark": "다크",
"themeBrightnessBlack": "검은색",
"albumTierNew": "신규", "albumTierNew": "신규",
"albumTierPinned": "고정", "albumTierPinned": "고정",
"albumTierSpecial": "기본", "albumTierSpecial": "기본",
@ -170,6 +177,8 @@
"binEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 휴지통으로 이동하시겠습니까?} other{항목 {count}개를 휴지통으로 이동하시겠습니까?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 휴지통으로 이동하시겠습니까?} other{항목 {count}개를 휴지통으로 이동하시겠습니까?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 삭제하시겠습니까?} other{항목 {count}개를 삭제하시겠습니까?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 삭제하시겠습니까?} other{항목 {count}개를 삭제하시겠습니까?}}",
"moveUndatedConfirmationDialogMessage": "이 작업을 계속하기 전에 항목의 날짜를 지정하시겠습니까?",
"moveUndatedConfirmationDialogSetDate": "날짜 지정하기",
"videoResumeDialogMessage": "{time}부터 재개하시겠습니까?", "videoResumeDialogMessage": "{time}부터 재개하시겠습니까?",
"videoStartOverButtonLabel": "처음부터", "videoStartOverButtonLabel": "처음부터",
@ -189,6 +198,14 @@
"renameAlbumDialogLabel": "앨범 이름", "renameAlbumDialogLabel": "앨범 이름",
"renameAlbumDialogLabelAlreadyExistsHelper": "사용 중인 이름입니다", "renameAlbumDialogLabelAlreadyExistsHelper": "사용 중인 이름입니다",
"renameEntrySetPageTitle": "이름 변경",
"renameEntrySetPagePatternFieldLabel": "이름 양식",
"renameEntrySetPageInsertTooltip": "필드 추가",
"renameEntrySetPagePreview": "미리보기",
"renameProcessorCounter": "숫자 증가",
"renameProcessorName": "이름",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
@ -201,6 +218,7 @@
"editEntryDateDialogTitle": "날짜 및 시간", "editEntryDateDialogTitle": "날짜 및 시간",
"editEntryDateDialogSetCustom": "지정 날짜로 편집", "editEntryDateDialogSetCustom": "지정 날짜로 편집",
"editEntryDateDialogCopyField": "다른 날짜에서 지정", "editEntryDateDialogCopyField": "다른 날짜에서 지정",
"editEntryDateDialogCopyItem": "다른 항목에서 지정",
"editEntryDateDialogExtractFromTitle": "제목에서 추출", "editEntryDateDialogExtractFromTitle": "제목에서 추출",
"editEntryDateDialogShift": "시간 이동", "editEntryDateDialogShift": "시간 이동",
"editEntryDateDialogSourceFileModifiedDate": "파일 수정한 날짜", "editEntryDateDialogSourceFileModifiedDate": "파일 수정한 날짜",
@ -308,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, other{항목 {count}개를 삭제하지 못했습니다}}", "collectionDeleteFailureFeedback": "{count, plural, other{항목 {count}개를 삭제하지 못했습니다}}",
"collectionCopyFailureFeedback": "{count, plural, other{항목 {count}개를 복사하지 못했습니다}}", "collectionCopyFailureFeedback": "{count, plural, other{항목 {count}개를 복사하지 못했습니다}}",
"collectionMoveFailureFeedback": "{count, plural, other{항목 {count}개를 이동하지 못했습니다}}", "collectionMoveFailureFeedback": "{count, plural, other{항목 {count}개를 이동하지 못했습니다}}",
"collectionRenameFailureFeedback": "{count, plural, other{항목 {count}개의 이름을 변경하지 못했습니다}}",
"collectionEditFailureFeedback": "{count, plural, other{항목 {count}개를 편집하지 못했습니다}}", "collectionEditFailureFeedback": "{count, plural, other{항목 {count}개를 편집하지 못했습니다}}",
"collectionExportFailureFeedback": "{count, plural, other{항목 {count}개를 내보내지 못했습니다}}", "collectionExportFailureFeedback": "{count, plural, other{항목 {count}개를 내보내지 못했습니다}}",
"collectionCopySuccessFeedback": "{count, plural, other{항목 {count}개를 복사했습니다}}", "collectionCopySuccessFeedback": "{count, plural, other{항목 {count}개를 복사했습니다}}",
"collectionMoveSuccessFeedback": "{count, plural, other{항목 {count}개를 이동했습니다}}", "collectionMoveSuccessFeedback": "{count, plural, other{항목 {count}개를 이동했습니다}}",
"collectionRenameSuccessFeedback": "{count, plural, other{항목 {count}개의 이름을 변경했습니다}}",
"collectionEditSuccessFeedback": "{count, plural, other{항목 {count}개를 편집했습니다}}", "collectionEditSuccessFeedback": "{count, plural, other{항목 {count}개를 편집했습니다}}",
"collectionEmptyFavourites": "즐겨찾기가 없습니다", "collectionEmptyFavourites": "즐겨찾기가 없습니다",
@ -393,6 +413,7 @@
"settingsConfirmationDialogTitle": "확정 대화상자", "settingsConfirmationDialogTitle": "확정 대화상자",
"settingsConfirmationDialogDeleteItems": "항목을 완전히 삭제 시", "settingsConfirmationDialogDeleteItems": "항목을 완전히 삭제 시",
"settingsConfirmationDialogMoveToBinItems": "항목을 휴지통으로 이동 시", "settingsConfirmationDialogMoveToBinItems": "항목을 휴지통으로 이동 시",
"settingsConfirmationDialogMoveUndatedItems": "날짜가 지정되지 않은 항목을 이동 시",
"settingsNavigationDrawerTile": "탐색 메뉴", "settingsNavigationDrawerTile": "탐색 메뉴",
"settingsNavigationDrawerEditorTitle": "탐색 메뉴", "settingsNavigationDrawerEditorTitle": "탐색 메뉴",
@ -501,6 +522,12 @@
"settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간", "settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간",
"settingsTimeToTakeActionTitle": "액션 취하기 전 대기 시간", "settingsTimeToTakeActionTitle": "액션 취하기 전 대기 시간",
"settingsSectionDisplay": "디스플레이",
"settingsThemeBrightness": "테마",
"settingsThemeColorHighlights": "색 강조",
"settingsDisplayRefreshRateModeTile": "화면 재생률",
"settingsDisplayRefreshRateModeTitle": "화면 재생률",
"settingsSectionLanguage": "언어 및 표시 형식", "settingsSectionLanguage": "언어 및 표시 형식",
"settingsLanguage": "언어", "settingsLanguage": "언어",
"settingsCoordinateFormatTile": "좌표 표현", "settingsCoordinateFormatTile": "좌표 표현",

View file

@ -137,6 +137,13 @@
"accessibilityAnimationsRemove": "Prevenir efeitos de tela", "accessibilityAnimationsRemove": "Prevenir efeitos de tela",
"accessibilityAnimationsKeep": "Manter efeitos de tela", "accessibilityAnimationsKeep": "Manter efeitos de tela",
"displayRefreshRatePreferHighest": "Taxa mais alta",
"displayRefreshRatePreferLowest": "Taxa mais baixa",
"themeBrightnessLight": "Claro",
"themeBrightnessDark": "Escuro",
"themeBrightnessBlack": "Preto",
"albumTierNew": "Novo", "albumTierNew": "Novo",
"albumTierPinned": "Fixada", "albumTierPinned": "Fixada",
"albumTierSpecial": "Comum", "albumTierSpecial": "Comum",
@ -170,6 +177,8 @@
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Mover esse item para a lixeira?} other{Mova estes {count} itens para a lixeira?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{Mover esse item para a lixeira?} other{Mova estes {count} itens para a lixeira?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Tem certeza de que deseja excluir este item?} other{Tem certeza de que deseja excluir estes {count} itens?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Tem certeza de que deseja excluir este item?} other{Tem certeza de que deseja excluir estes {count} itens?}}",
"moveUndatedConfirmationDialogMessage": "Alguns itens não têm data de metadados. Sua data atual será redefinida por esta operação, a menos que um data de metadados é definida.",
"moveUndatedConfirmationDialogSetDate": "Definir data",
"videoResumeDialogMessage": "Deseja continuar jogando em {time}?", "videoResumeDialogMessage": "Deseja continuar jogando em {time}?",
"videoStartOverButtonLabel": "RECOMEÇAR", "videoStartOverButtonLabel": "RECOMEÇAR",
@ -189,6 +198,14 @@
"renameAlbumDialogLabel": "Novo nome", "renameAlbumDialogLabel": "Novo nome",
"renameAlbumDialogLabelAlreadyExistsHelper": "O diretório já existe", "renameAlbumDialogLabelAlreadyExistsHelper": "O diretório já existe",
"renameEntrySetPageTitle": "Renomear",
"renameEntrySetPagePatternFieldLabel": "Padrão de nomeação",
"renameEntrySetPageInsertTooltip": "Inserir campo",
"renameEntrySetPagePreview": "Visualizar",
"renameProcessorCounter": "Contador",
"renameProcessorName": "Nome",
"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?}}", "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?}}", "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?}}",
@ -201,6 +218,7 @@
"editEntryDateDialogTitle": "Data e hora", "editEntryDateDialogTitle": "Data e hora",
"editEntryDateDialogSetCustom": "Definir data personalizada", "editEntryDateDialogSetCustom": "Definir data personalizada",
"editEntryDateDialogCopyField": "Copiar de outra data", "editEntryDateDialogCopyField": "Copiar de outra data",
"editEntryDateDialogCopyItem": "Copiar de outro item",
"editEntryDateDialogExtractFromTitle": "Extrair do título", "editEntryDateDialogExtractFromTitle": "Extrair do título",
"editEntryDateDialogShift": "Mudança", "editEntryDateDialogShift": "Mudança",
"editEntryDateDialogSourceFileModifiedDate": "Data de modificação do arquivo", "editEntryDateDialogSourceFileModifiedDate": "Data de modificação do arquivo",
@ -308,10 +326,12 @@
"collectionDeleteFailureFeedback": "{count, plural, =1{Falha ao excluir 1 item} other{Falha ao excluir {count} itens}}", "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}}", "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}}", "collectionMoveFailureFeedback": "{count, plural, =1{Falha ao mover 1 item} other{Falha ao mover {count} itens}}",
"collectionRenameFailureFeedback": "{count, plural, =1{Falhei em renomear 1 item} other{Falha ao renomear {count} itens}}",
"collectionEditFailureFeedback": "{count, plural, =1{Falha ao editar 1 item} other{Falha ao editar {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}}", "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}}", "collectionCopySuccessFeedback": "{count, plural, =1{1 item copiado} other{Copiado {count} itens}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{1 item movido} other{Mudou-se {count} itens}}", "collectionMoveSuccessFeedback": "{count, plural, =1{1 item movido} other{Mudou-se {count} itens}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{1 item renomeado} other{Renomeado {count} itens}}",
"collectionEditSuccessFeedback": "{count, plural, =1{Editado 1 item} other{Editado {count} itens}}", "collectionEditSuccessFeedback": "{count, plural, =1{Editado 1 item} other{Editado {count} itens}}",
"collectionEmptyFavourites": "Nenhum favorito", "collectionEmptyFavourites": "Nenhum favorito",
@ -393,6 +413,7 @@
"settingsConfirmationDialogTitle": "Caixas de diálogo de confirmação", "settingsConfirmationDialogTitle": "Caixas de diálogo de confirmação",
"settingsConfirmationDialogDeleteItems": "Pergunte antes de excluir itens para sempre", "settingsConfirmationDialogDeleteItems": "Pergunte antes de excluir itens para sempre",
"settingsConfirmationDialogMoveToBinItems": "Pergunte antes de mover itens para a lixeira", "settingsConfirmationDialogMoveToBinItems": "Pergunte antes de mover itens para a lixeira",
"settingsConfirmationDialogMoveUndatedItems": "Pergunte antes de mover itens sem data de metadados",
"settingsNavigationDrawerTile": "Menu de navegação", "settingsNavigationDrawerTile": "Menu de navegação",
"settingsNavigationDrawerEditorTitle": "Menu de navegação", "settingsNavigationDrawerEditorTitle": "Menu de navegação",
@ -501,6 +522,12 @@
"settingsTimeToTakeActionTile": "Tempo para executar uma ação", "settingsTimeToTakeActionTile": "Tempo para executar uma ação",
"settingsTimeToTakeActionTitle": "Tempo para executar uma ação", "settingsTimeToTakeActionTitle": "Tempo para executar uma ação",
"settingsSectionDisplay": "Tela",
"settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Destaques de cores",
"settingsDisplayRefreshRateModeTile": "Taxa de atualização de exibição",
"settingsDisplayRefreshRateModeTitle": "Taxa de atualização",
"settingsSectionLanguage": "Idioma e Formatos", "settingsSectionLanguage": "Idioma e Formatos",
"settingsLanguage": "Língua", "settingsLanguage": "Língua",
"settingsCoordinateFormatTile": "Formato de coordenadas", "settingsCoordinateFormatTile": "Formato de coordenadas",

View file

@ -30,8 +30,9 @@ void mainCommon(AppFlavor flavor) {
// Errors during the widget build phase will show by default: // Errors during the widget build phase will show by default:
// - in debug mode: error on red background // - in debug mode: error on red background
// - in release mode: plain grey background // - in profile/release mode: plain grey background
// This can be modified via `ErrorWidget.builder` // This can be modified via `ErrorWidget.builder`
// ErrorWidget.builder = (details) => ErrorWidget(details.exception);
runApp(AvesApp(flavor: flavor)); runApp(AvesApp(flavor: flavor));
} }

View file

@ -124,7 +124,7 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.unpin; return AIcons.unpin;
// selecting (single filter) // selecting (single filter)
case ChipSetAction.rename: case ChipSetAction.rename:
return AIcons.rename; return AIcons.name;
case ChipSetAction.setCover: case ChipSetAction.setCover:
return AIcons.setCover; return AIcons.setCover;
} }

View file

@ -177,7 +177,8 @@ extension ExtraEntryAction on EntryAction {
switch (this) { switch (this) {
case EntryAction.debug: case EntryAction.debug:
return ShaderMask( return ShaderMask(
shaderCallback: AColors.debugGradient.createShader, shaderCallback: AvesColorsData.debugGradient.createShader,
blendMode: BlendMode.srcIn,
child: child, child: child,
); );
default: default:
@ -200,7 +201,7 @@ extension ExtraEntryAction on EntryAction {
case EntryAction.print: case EntryAction.print:
return AIcons.print; return AIcons.print;
case EntryAction.rename: case EntryAction.rename:
return AIcons.rename; return AIcons.name;
case EntryAction.copy: case EntryAction.copy:
return AIcons.copy; return AIcons.copy;
case EntryAction.move: case EntryAction.move:

View file

@ -55,7 +55,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
switch (this) { switch (this) {
case EntryInfoAction.debug: case EntryInfoAction.debug:
return ShaderMask( return ShaderMask(
shaderCallback: AColors.debugGradient.createShader, shaderCallback: AvesColorsData.debugGradient.createShader,
blendMode: BlendMode.srcIn,
child: child, child: child,
); );
default: default:

View file

@ -23,6 +23,7 @@ enum EntrySetAction {
restore, restore,
copy, copy,
move, move,
rename,
toggleFavourite, toggleFavourite,
rotateCCW, rotateCCW,
rotateCW, rotateCW,
@ -68,6 +69,7 @@ class EntrySetActions {
EntrySetAction.restore, EntrySetAction.restore,
EntrySetAction.copy, EntrySetAction.copy,
EntrySetAction.move, EntrySetAction.move,
EntrySetAction.rename,
EntrySetAction.toggleFavourite, EntrySetAction.toggleFavourite,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.stats, EntrySetAction.stats,
@ -81,6 +83,7 @@ class EntrySetActions {
EntrySetAction.delete, EntrySetAction.delete,
EntrySetAction.copy, EntrySetAction.copy,
EntrySetAction.move, EntrySetAction.move,
EntrySetAction.rename,
EntrySetAction.toggleFavourite, EntrySetAction.toggleFavourite,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.stats, EntrySetAction.stats,
@ -137,6 +140,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return context.l10n.collectionActionCopy; return context.l10n.collectionActionCopy;
case EntrySetAction.move: case EntrySetAction.move:
return context.l10n.collectionActionMove; return context.l10n.collectionActionMove;
case EntrySetAction.rename:
return context.l10n.entryActionRename;
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return context.l10n.entryActionAddFavourite; return context.l10n.entryActionAddFavourite;
@ -200,6 +205,8 @@ extension ExtraEntrySetAction on EntrySetAction {
return AIcons.copy; return AIcons.copy;
case EntrySetAction.move: case EntrySetAction.move:
return AIcons.move; return AIcons.move;
case EntrySetAction.rename:
return AIcons.name;
case EntrySetAction.toggleFavourite: case EntrySetAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return AIcons.favourite; return AIcons.favourite;

View file

@ -167,6 +167,7 @@ class AvesEntry {
_directory = null; _directory = null;
_filename = null; _filename = null;
_extension = null; _extension = null;
_bestTitle = null;
} }
String? get path => _path; String? get path => _path;
@ -258,9 +259,10 @@ class AvesEntry {
bool get canRotateAndFlip => canEdit && canEditExif; bool get canRotateAndFlip => canEdit && canEditExif;
// as of androidx.exifinterface:exifinterface:1.3.3 // as of androidx.exifinterface:exifinterface:1.3.3
// `exifinterface` declares support for DNG, but `exifinterface` strips non-standard Exif tags when saving attributes,
// and DNG requires DNG-specific tags saved along standard Exif. So `exifinterface` actually breaks DNG files.
bool get canEditExif { bool get canEditExif {
switch (mimeType.toLowerCase()) { switch (mimeType.toLowerCase()) {
case MimeTypes.dng:
case MimeTypes.jpeg: case MimeTypes.jpeg:
case MimeTypes.png: case MimeTypes.png:
case MimeTypes.webp: case MimeTypes.webp:
@ -454,7 +456,7 @@ class AvesEntry {
String? _bestTitle; String? _bestTitle;
String? get bestTitle { String? get bestTitle {
_bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : sourceTitle; _bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : (filenameWithoutExtension ?? sourceTitle);
return _bestTitle; return _bestTitle;
} }

View file

@ -42,6 +42,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
switch (appliedModifier.action) { switch (appliedModifier.action) {
case DateEditAction.setCustom: case DateEditAction.setCustom:
case DateEditAction.copyField: case DateEditAction.copyField:
case DateEditAction.copyItem:
case DateEditAction.extractFromTitle: case DateEditAction.extractFromTitle:
editCreateDateXmp(descriptions, appliedModifier.setDateTime); editCreateDateXmp(descriptions, appliedModifier.setDateTime);
break; break;
@ -319,6 +320,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final date = parseUnknownDateFormat(bestTitle); final date = parseUnknownDateFormat(bestTitle);
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null; return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
case DateEditAction.setCustom: case DateEditAction.setCustom:
case DateEditAction.copyItem:
return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!); return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!);
case DateEditAction.shift: case DateEditAction.shift:
case DateEditAction.remove: case DateEditAction.remove:

View file

@ -1,4 +1,3 @@
import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
@ -7,13 +6,11 @@ import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:provider/provider.dart';
class AlbumFilter extends CollectionFilter { class AlbumFilter extends CollectionFilter {
static const type = 'album'; static const type = 'album';
static final Map<String, Color> _appColors = {};
final String album; final String album;
final String? displayName; final String? displayName;
@ -56,6 +53,7 @@ class AlbumFilter extends CollectionFilter {
@override @override
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {
final colors = context.watch<AvesColorsData>();
// do not use async/await and rely on `SynchronousFuture` // do not use async/await and rely on `SynchronousFuture`
// to prevent rebuilding of the `FutureBuilder` listening on this future // to prevent rebuilding of the `FutureBuilder` listening on this future
final albumType = androidFileUtils.getAlbumType(album); final albumType = androidFileUtils.getAlbumType(album);
@ -63,31 +61,19 @@ class AlbumFilter extends CollectionFilter {
case AlbumType.regular: case AlbumType.regular:
break; break;
case AlbumType.app: case AlbumType.app:
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); final appColor = colors.appColor(album);
if (appColor != null) return appColor;
final packageName = androidFileUtils.getAlbumAppPackageName(album);
if (packageName != null) {
return PaletteGenerator.fromImageProvider(
AppIconImage(packageName: packageName, size: 24),
).then((palette) async {
// `dominantColor` is most representative but can have low contrast with a dark background
// `vibrantColor` is usually representative and has good contrast with a dark background
final color = palette.vibrantColor?.color ?? (await super.color(context));
_appColors[album] = color;
return color;
});
}
break; break;
case AlbumType.camera: case AlbumType.camera:
return SynchronousFuture(AColors.albumCamera); return SynchronousFuture(colors.albumCamera);
case AlbumType.download: case AlbumType.download:
return SynchronousFuture(AColors.albumDownload); return SynchronousFuture(colors.albumDownload);
case AlbumType.screenRecordings: case AlbumType.screenRecordings:
return SynchronousFuture(AColors.albumScreenRecordings); return SynchronousFuture(colors.albumScreenRecordings);
case AlbumType.screenshots: case AlbumType.screenshots:
return SynchronousFuture(AColors.albumScreenshots); return SynchronousFuture(colors.albumScreenshots);
case AlbumType.videoCaptures: case AlbumType.videoCaptures:
return SynchronousFuture(AColors.albumVideoCaptures); return SynchronousFuture(colors.albumVideoCaptures);
} }
return super.color(context); return super.color(context);
} }

View file

@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FavouriteFilter extends CollectionFilter { class FavouriteFilter extends CollectionFilter {
static const type = 'favourite'; static const type = 'favourite';
@ -33,7 +34,10 @@ class FavouriteFilter extends CollectionFilter {
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size);
@override @override
Future<Color> color(BuildContext context) => SynchronousFuture(AColors.favourite); Future<Color> color(BuildContext context) {
final colors = context.watch<AvesColorsData>();
return SynchronousFuture(colors.favourite);
}
@override @override
String get category => type; String get category => type;

View file

@ -12,11 +12,12 @@ import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.dart';
import 'package:aves/utils/color_utils.dart'; import 'package:aves/theme/colors.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
@immutable @immutable
abstract class CollectionFilter extends Equatable implements Comparable<CollectionFilter> { abstract class CollectionFilter extends Equatable implements Comparable<CollectionFilter> {
@ -93,7 +94,10 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null; Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null;
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); Future<Color> color(BuildContext context) {
final colors = context.watch<AvesColorsData>();
return SynchronousFuture(colors.fromString(getLabel(context)));
}
String get category; String get category;

View file

@ -1,12 +1,14 @@
import 'dart:async';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/utils/mime_utils.dart'; import 'package:aves/utils/mime_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class MimeFilter extends CollectionFilter { class MimeFilter extends CollectionFilter {
static const type = 'mime'; static const type = 'mime';
@ -15,7 +17,6 @@ class MimeFilter extends CollectionFilter {
late final EntryFilter _test; late final EntryFilter _test;
late final String _label; late final String _label;
late final IconData _icon; late final IconData _icon;
late final Color _color;
static final image = MimeFilter(MimeTypes.anyImage); static final image = MimeFilter(MimeTypes.anyImage);
static final video = MimeFilter(MimeTypes.anyVideo); static final video = MimeFilter(MimeTypes.anyVideo);
@ -25,7 +26,6 @@ class MimeFilter extends CollectionFilter {
MimeFilter(this.mime) { MimeFilter(this.mime) {
IconData? icon; IconData? icon;
Color? color;
var lowMime = mime.toLowerCase(); var lowMime = mime.toLowerCase();
if (lowMime.endsWith('/*')) { if (lowMime.endsWith('/*')) {
lowMime = lowMime.substring(0, lowMime.length - 2); lowMime = lowMime.substring(0, lowMime.length - 2);
@ -33,17 +33,14 @@ class MimeFilter extends CollectionFilter {
_label = lowMime.toUpperCase(); _label = lowMime.toUpperCase();
if (mime == MimeTypes.anyImage) { if (mime == MimeTypes.anyImage) {
icon = AIcons.image; icon = AIcons.image;
color = AColors.image;
} else if (mime == MimeTypes.anyVideo) { } else if (mime == MimeTypes.anyVideo) {
icon = AIcons.video; icon = AIcons.video;
color = AColors.video;
} }
} else { } else {
_test = (entry) => entry.mimeType == lowMime; _test = (entry) => entry.mimeType == lowMime;
_label = MimeUtils.displayType(lowMime); _label = MimeUtils.displayType(lowMime);
} }
_icon = icon ?? AIcons.vector; _icon = icon ?? AIcons.vector;
_color = color ?? stringToColor(_label);
} }
MimeFilter.fromMap(Map<String, dynamic> json) MimeFilter.fromMap(Map<String, dynamic> json)
@ -79,7 +76,17 @@ class MimeFilter extends CollectionFilter {
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override @override
Future<Color> color(BuildContext context) => SynchronousFuture(_color); Future<Color> color(BuildContext context) {
final colors = context.watch<AvesColorsData>();
switch (mime) {
case MimeTypes.anyImage:
return SynchronousFuture(colors.image);
case MimeTypes.anyVideo:
return SynchronousFuture(colors.video);
default:
return SynchronousFuture(colors.fromString(_label));
}
}
@override @override
String get category => type; String get category => type;

View file

@ -1,10 +1,11 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class QueryFilter extends CollectionFilter { class QueryFilter extends CollectionFilter {
static const type = 'query'; static const type = 'query';
@ -67,7 +68,14 @@ class QueryFilter extends CollectionFilter {
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size);
@override @override
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); Future<Color> color(BuildContext context) {
if (colorful) {
return super.color(context);
}
final colors = context.watch<AvesColorsData>();
return SynchronousFuture(colors.neutral);
}
@override @override
String get category => type; String get category => type;

View file

@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class TypeFilter extends CollectionFilter { class TypeFilter extends CollectionFilter {
static const type = 'type'; static const type = 'type';
@ -18,7 +19,6 @@ class TypeFilter extends CollectionFilter {
final String itemType; final String itemType;
late final EntryFilter _test; late final EntryFilter _test;
late final IconData _icon; late final IconData _icon;
late final Color _color;
static final animated = TypeFilter._private(_animated); static final animated = TypeFilter._private(_animated);
static final geotiff = TypeFilter._private(_geotiff); static final geotiff = TypeFilter._private(_geotiff);
@ -35,32 +35,26 @@ class TypeFilter extends CollectionFilter {
case _animated: case _animated:
_test = (entry) => entry.isAnimated; _test = (entry) => entry.isAnimated;
_icon = AIcons.animated; _icon = AIcons.animated;
_color = AColors.animated;
break; break;
case _geotiff: case _geotiff:
_test = (entry) => entry.isGeotiff; _test = (entry) => entry.isGeotiff;
_icon = AIcons.geo; _icon = AIcons.geo;
_color = AColors.geotiff;
break; break;
case _motionPhoto: case _motionPhoto:
_test = (entry) => entry.isMotionPhoto; _test = (entry) => entry.isMotionPhoto;
_icon = AIcons.motionPhoto; _icon = AIcons.motionPhoto;
_color = AColors.motionPhoto;
break; break;
case _panorama: case _panorama:
_test = (entry) => entry.isImage && entry.is360; _test = (entry) => entry.isImage && entry.is360;
_icon = AIcons.threeSixty; _icon = AIcons.threeSixty;
_color = AColors.panorama;
break; break;
case _raw: case _raw:
_test = (entry) => entry.isRaw; _test = (entry) => entry.isRaw;
_icon = AIcons.raw; _icon = AIcons.raw;
_color = AColors.raw;
break; break;
case _sphericalVideo: case _sphericalVideo:
_test = (entry) => entry.isVideo && entry.is360; _test = (entry) => entry.isVideo && entry.is360;
_icon = AIcons.threeSixty; _icon = AIcons.threeSixty;
_color = AColors.sphericalVideo;
break; break;
} }
} }
@ -106,7 +100,24 @@ class TypeFilter extends CollectionFilter {
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override @override
Future<Color> color(BuildContext context) => SynchronousFuture(_color); Future<Color> color(BuildContext context) {
final colors = context.watch<AvesColorsData>();
switch (itemType) {
case _animated:
return SynchronousFuture(colors.animated);
case _geotiff:
return SynchronousFuture(colors.geotiff);
case _motionPhoto:
return SynchronousFuture(colors.motionPhoto);
case _panorama:
return SynchronousFuture(colors.panorama);
case _raw:
return SynchronousFuture(colors.raw);
case _sphericalVideo:
return SynchronousFuture(colors.sphericalVideo);
}
return super.color(context);
}
@override @override
String get category => type; String get category => type;

View file

@ -24,30 +24,30 @@ class DateModifier extends Equatable {
List<Object?> get props => [action, fields, setDateTime, copyFieldSource, shiftMinutes]; List<Object?> get props => [action, fields, setDateTime, copyFieldSource, shiftMinutes];
const DateModifier._private( const DateModifier._private(
this.action, this.action, {
this.fields, { this.fields = const {},
this.setDateTime, this.setDateTime,
this.copyFieldSource, this.copyFieldSource,
this.shiftMinutes, this.shiftMinutes,
}); });
factory DateModifier.setCustom(Set<MetadataField> fields, DateTime dateTime) { factory DateModifier.setCustom(Set<MetadataField> fields, DateTime dateTime) {
return DateModifier._private(DateEditAction.setCustom, fields, setDateTime: dateTime); return DateModifier._private(DateEditAction.setCustom, fields: fields, setDateTime: dateTime);
} }
factory DateModifier.copyField(Set<MetadataField> fields, DateFieldSource copyFieldSource) { factory DateModifier.copyField(DateFieldSource copyFieldSource) {
return DateModifier._private(DateEditAction.copyField, fields, copyFieldSource: copyFieldSource); return DateModifier._private(DateEditAction.copyField, copyFieldSource: copyFieldSource);
} }
factory DateModifier.extractFromTitle(Set<MetadataField> fields) { factory DateModifier.extractFromTitle() {
return DateModifier._private(DateEditAction.extractFromTitle, fields); return const DateModifier._private(DateEditAction.extractFromTitle);
} }
factory DateModifier.shift(Set<MetadataField> fields, int shiftMinutes) { factory DateModifier.shift(Set<MetadataField> fields, int shiftMinutes) {
return DateModifier._private(DateEditAction.shift, fields, shiftMinutes: shiftMinutes); return DateModifier._private(DateEditAction.shift, fields: fields, shiftMinutes: shiftMinutes);
} }
factory DateModifier.remove(Set<MetadataField> fields) { factory DateModifier.remove(Set<MetadataField> fields) {
return DateModifier._private(DateEditAction.remove, fields); return DateModifier._private(DateEditAction.remove, fields: fields);
} }
} }

View file

@ -3,6 +3,7 @@ import 'package:aves/model/metadata/fields.dart';
enum DateEditAction { enum DateEditAction {
setCustom, setCustom,
copyField, copyField,
copyItem,
extractFromTitle, extractFromTitle,
shift, shift,
remove, remove,

View file

@ -12,6 +12,8 @@ class OverlayMetadata extends Equatable {
bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null; bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null;
bool get isNotEmpty => !isEmpty;
const OverlayMetadata({ const OverlayMetadata({
this.aperture, this.aperture,
this.exposureTime, this.exposureTime,

View file

@ -0,0 +1,184 @@
import 'package:aves/model/entry.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@immutable
class NamingPattern {
final List<NamingProcessor> processors;
static final processorPattern = RegExp(r'<(.+?)(,(.+?))?>');
static const processorOptionSeparator = ',';
static const optionKeyValueSeparator = '=';
const NamingPattern(this.processors);
factory NamingPattern.from({
required String userPattern,
required int entryCount,
}) {
final processors = <NamingProcessor>[];
const defaultCounterStart = 1;
final defaultCounterPadding = '$entryCount'.length;
var index = 0;
final matches = processorPattern.allMatches(userPattern);
matches.forEach((match) {
final start = match.start;
final end = match.end;
if (index < start) {
processors.add(LiteralNamingProcessor(userPattern.substring(index, start)));
index = start;
}
final processorKey = match.group(1);
final processorOptions = match.group(3);
switch (processorKey) {
case DateNamingProcessor.key:
if (processorOptions != null) {
processors.add(DateNamingProcessor(processorOptions.trim()));
}
break;
case NameNamingProcessor.key:
processors.add(const NameNamingProcessor());
break;
case CounterNamingProcessor.key:
int? start, padding;
_applyProcessorOptions(processorOptions, (key, value) {
final valueInt = int.tryParse(value);
if (valueInt != null) {
switch (key) {
case CounterNamingProcessor.optionStart:
start = valueInt;
break;
case CounterNamingProcessor.optionPadding:
padding = valueInt;
break;
}
}
});
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
break;
default:
debugPrint('unsupported naming processor: ${match.group(0)}');
break;
}
index = end;
});
if (index < userPattern.length) {
processors.add(LiteralNamingProcessor(userPattern.substring(index, userPattern.length)));
}
return NamingPattern(processors);
}
static void _applyProcessorOptions(String? processorOptions, void Function(String key, String value) applyOption) {
if (processorOptions != null) {
processorOptions.split(processorOptionSeparator).map((v) => v.trim()).forEach((kv) {
final parts = kv.split(optionKeyValueSeparator);
if (parts.length >= 2) {
final key = parts[0];
final value = parts.skip(1).join(optionKeyValueSeparator);
applyOption(key, value);
}
});
}
}
static int getInsertionOffset(String userPattern, int offset) {
offset = offset.clamp(0, userPattern.length);
final matches = processorPattern.allMatches(userPattern);
for (final match in matches) {
final start = match.start;
final end = match.end;
if (offset <= start) return offset;
if (offset <= end) return end;
}
return offset;
}
static String defaultPatternFor(String processorKey) {
switch (processorKey) {
case DateNamingProcessor.key:
return '<$processorKey, yyyyMMdd-HHmmss>';
case CounterNamingProcessor.key:
case NameNamingProcessor.key:
default:
return '<$processorKey>';
}
}
String apply(AvesEntry entry, int index) => processors.map((v) => v.process(entry, index) ?? '').join().trimLeft();
}
@immutable
abstract class NamingProcessor extends Equatable {
const NamingProcessor();
String? process(AvesEntry entry, int index);
}
@immutable
class LiteralNamingProcessor extends NamingProcessor {
final String text;
@override
List<Object?> get props => [text];
const LiteralNamingProcessor(this.text);
@override
String? process(AvesEntry entry, int index) => text;
}
@immutable
class DateNamingProcessor extends NamingProcessor {
static const key = 'date';
final DateFormat format;
@override
List<Object?> get props => [format.pattern];
DateNamingProcessor(String pattern) : format = DateFormat(pattern);
@override
String? process(AvesEntry entry, int index) {
final date = entry.bestDate;
return date != null ? format.format(date) : null;
}
}
@immutable
class NameNamingProcessor extends NamingProcessor {
static const key = 'name';
@override
List<Object?> get props => [];
const NameNamingProcessor();
@override
String? process(AvesEntry entry, int index) => entry.filenameWithoutExtension;
}
@immutable
class CounterNamingProcessor extends NamingProcessor {
final int start;
final int padding;
static const key = 'counter';
static const optionStart = 'start';
static const optionPadding = 'padding';
@override
List<Object?> get props => [start, padding];
const CounterNamingProcessor({
required this.start,
required this.padding,
});
@override
String? process(AvesEntry entry, int index) => '${index + start}'.padLeft(padding, '0');
}

View file

@ -2,6 +2,7 @@ import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/naming_pattern.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
@ -15,13 +16,20 @@ class SettingsDefaults {
static const canUseAnalysisService = true; static const canUseAnalysisService = true;
static const isInstalledAppAccessAllowed = false; static const isInstalledAppAccessAllowed = false;
static const isErrorReportingAllowed = false; static const isErrorReportingAllowed = false;
static const displayRefreshRateMode = DisplayRefreshRateMode.auto;
static const themeBrightness = AvesThemeBrightness.system;
static const themeColorMode = AvesThemeColorMode.polychrome;
static const tileLayout = TileLayout.grid; static const tileLayout = TileLayout.grid;
static const entryRenamingPattern = '<${DateNamingProcessor.key}, yyyyMMdd-HHmmss> <${NameNamingProcessor.key}>';
// navigation // navigation
static const mustBackTwiceToExit = true; static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly; static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection; static const homePage = HomePageSetting.collection;
static const confirmationDialogs = ConfirmationDialog.values; static const confirmDeleteForever = true;
static const confirmMoveToBin = true;
static const confirmMoveUndatedItems = true;
static const setMetadataDateBeforeFileOp = false;
static final drawerTypeBookmarks = [ static final drawerTypeBookmarks = [
null, null,
MimeFilter.video, MimeFilter.video,
@ -58,9 +66,10 @@ class SettingsDefaults {
// viewer // viewer
static const viewerQuickActions = [ static const viewerQuickActions = [
EntryAction.rotateScreen,
EntryAction.toggleFavourite, EntryAction.toggleFavourite,
EntryAction.share, EntryAction.share,
EntryAction.rotateScreen, EntryAction.delete,
]; ];
static const showOverlayOnOpening = true; static const showOverlayOnOpening = true;
static const showOverlayMinimap = false; static const showOverlayMinimap = false;

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