Merge branch 'develop'
25
CHANGELOG.md
|
@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <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
|
||||
|
||||
### Added
|
||||
|
|
|
@ -147,7 +147,7 @@ dependencies {
|
|||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
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
|
||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
|
||||
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android
|
||||
|
|
|
@ -164,11 +164,18 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
// save access permissions across reboots
|
||||
val takeFlags = (data.flags
|
||||
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
|
||||
contentResolver.takePersistableUriPermission(treeUri, takeFlags)
|
||||
val canPersist = (data.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
|
||||
if (canPersist) {
|
||||
// save access permissions across reboots
|
||||
val takeFlags = (data.flags
|
||||
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
|
||||
|
@ -201,9 +208,11 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
|
||||
(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(
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,10 @@ import io.flutter.plugin.common.MethodCall
|
|||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
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 java.io.IOException
|
||||
|
||||
|
@ -84,7 +87,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
}.mapValues { it.value?.path }.toMutableMap()
|
||||
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`
|
||||
dirs.putAll(
|
||||
|
|
|
@ -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_TIME_DIR_NAME
|
||||
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.getSafeDateMillis
|
||||
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.MethodChannel
|
||||
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.StandardCharsets
|
||||
import java.text.ParseException
|
||||
|
@ -163,15 +169,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
// tags
|
||||
val tags = dir.tags
|
||||
if (dir is ExifDirectoryBase) {
|
||||
if (dir.isGeoTiff()) {
|
||||
// split GeoTIFF tags in their own directory
|
||||
val byGeoTiff = tags.groupBy { ExifTags.isGeoTiffTag(it.tagType) }
|
||||
metadataMap["GeoTIFF"] = HashMap<String, String>().apply {
|
||||
byGeoTiff[true]?.map { exifTagMapper(it) }?.let { putAll(it) }
|
||||
when {
|
||||
dir.isGeoTiff() -> {
|
||||
// split GeoTIFF tags in their own directory
|
||||
val geoTiffDirMap = metadataMap["GeoTIFF"] ?: HashMap()
|
||||
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) }
|
||||
} else {
|
||||
dirMap.putAll(tags.map { exifTagMapper(it) })
|
||||
mimeType == MimeTypes.DNG -> {
|
||||
// split DNG tags in their own directory
|
||||
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()) {
|
||||
metadataMap.remove(thisDirName)
|
||||
|
@ -432,11 +447,17 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
// EXIF
|
||||
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)) {
|
||||
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) {
|
||||
val orientation = it
|
||||
|
@ -560,9 +581,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { 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)) {
|
||||
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) {
|
||||
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 ->
|
||||
val metadata = ImageMetadataReader.readMetadata(input)
|
||||
val tag = when (field) {
|
||||
ExifInterface.TAG_DATETIME -> ExifDirectoryBase.TAG_DATETIME
|
||||
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifDirectoryBase.TAG_DATETIME_DIGITIZED
|
||||
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifDirectoryBase.TAG_DATETIME_ORIGINAL
|
||||
ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
|
||||
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED
|
||||
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL
|
||||
ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP
|
||||
else -> {
|
||||
result.error("getDate-field", "unsupported ExifInterface field=$field", null)
|
||||
|
@ -912,11 +933,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
|
||||
when (tag) {
|
||||
ExifDirectoryBase.TAG_DATETIME,
|
||||
ExifDirectoryBase.TAG_DATETIME_DIGITIZED,
|
||||
ExifDirectoryBase.TAG_DATETIME_ORIGINAL -> {
|
||||
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) {
|
||||
dir.getSafeDateMillis(tag) { dateMillis = it }
|
||||
ExifIFD0Directory.TAG_DATETIME -> {
|
||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||
dir.getDateModifiedMillis { 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 -> {
|
||||
|
|
|
@ -199,27 +199,34 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
}
|
||||
|
||||
private suspend fun rename() {
|
||||
if (arguments !is Map<*, *> || entryMapList.isEmpty()) {
|
||||
if (arguments !is Map<*, *>) {
|
||||
endOfStream()
|
||||
return
|
||||
}
|
||||
|
||||
val newName = arguments["newName"] as String?
|
||||
if (newName == null) {
|
||||
val rawEntryMap = arguments["entriesToNewName"] as Map<*, *>?
|
||||
if (rawEntryMap == null || rawEntryMap.isEmpty()) {
|
||||
error("rename-args", "failed because of missing arguments", null)
|
||||
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
|
||||
val firstEntry = entryMapList.first()
|
||||
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
|
||||
val firstEntry = entriesToNewName.keys.first()
|
||||
val provider = getProvider(firstEntry.uri)
|
||||
if (provider == null) {
|
||||
error("rename-provider", "failed to find provider for entry=$firstEntry", null)
|
||||
return
|
||||
}
|
||||
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
provider.renameMultiple(activity, newName, entries, ::isCancelledOp, object : ImageOpCallback {
|
||||
provider.renameMultiple(activity, entriesToNewName, ::isCancelledOp, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||
})
|
||||
|
|
|
@ -13,6 +13,7 @@ import deckers.thibault.aves.PendingStorageAccessResultHandler
|
|||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.PermissionManager
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.EventChannel.EventSink
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -80,6 +81,11 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
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) {
|
||||
error("requestMediaFileAccess-unsupported", "media file bulk access is not allowed before Android R", null)
|
||||
return
|
||||
|
@ -148,12 +154,17 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
|
||||
fun onGranted(uri: Uri) {
|
||||
ioScope.launch {
|
||||
activity.contentResolver.openInputStream(uri)?.use { input ->
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var len: Int
|
||||
while (input.read(buffer).also { len = it } != -1) {
|
||||
success(buffer.copyOf(len))
|
||||
try {
|
||||
activity.contentResolver.openInputStream(uri)?.use { input ->
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var len: Int
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)) {
|
||||
val dateString = this.getAttribute(tag)
|
||||
if (dateString != null) {
|
||||
try {
|
||||
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) {
|
||||
Log.w(LOG_TAG, "failed to parse date=$dateString", e)
|
||||
|
|
|
@ -1,155 +1,55 @@
|
|||
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 {
|
||||
// XPosition
|
||||
// Tag = 286 (011E.H)
|
||||
private const val TAG_X_POSITION = 0x011e
|
||||
|
||||
// YPosition
|
||||
// Tag = 287 (011F.H)
|
||||
private const val TAG_Y_POSITION = 0x011f
|
||||
|
||||
// ColorMap
|
||||
// Tag = 320 (0140.H)
|
||||
private const val TAG_T4_OPTIONS = 0x0124
|
||||
private const val TAG_T6_OPTIONS = 0x0125
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
// Rating tag used by Windows, value in percent
|
||||
// Tag = 18249 (4749.H)
|
||||
// Type = SHORT
|
||||
private const val TAG_RATING_PERCENT = 0x4749
|
||||
|
||||
/*
|
||||
SGI
|
||||
tags 32995-32999
|
||||
*/
|
||||
|
||||
// Matteing
|
||||
// Tag = 32995 (80E3.H)
|
||||
// obsoleted by the 6.0 ExtraSamples (338)
|
||||
private const val SONY_RAW_FILE_TYPE = 0x7000
|
||||
private const val SONY_TONE_CURVE = 0x7010
|
||||
private const val TAG_MATTEING = 0x80e3
|
||||
|
||||
/*
|
||||
GeoTIFF
|
||||
*/
|
||||
|
||||
// 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
|
||||
// sensing method (0x9217) redundant with sensing method (0xA217)
|
||||
private const val TAG_SENSING_METHOD = 0x9217
|
||||
private const val TAG_IMAGE_SOURCE_DATA = 0x935c
|
||||
|
||||
/*
|
||||
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 const val TAG_GDAL_METADATA = 0xa480
|
||||
private const val TAG_GDAL_NO_DATA = 0xa481
|
||||
|
||||
private val tagNameMap = hashMapOf(
|
||||
TAG_X_POSITION to "X 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_EXTRA_SAMPLES to "Extra Samples",
|
||||
TAG_SAMPLE_FORMAT to "Sample Format",
|
||||
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",
|
||||
// GeoTIFF
|
||||
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_SENSING_METHOD to "Sensing Method (0x9217)",
|
||||
TAG_IMAGE_SOURCE_DATA to "Image Source Data",
|
||||
// DNG
|
||||
TAG_CAMERA_SERIAL_NUMBER to "Camera Serial Number",
|
||||
TAG_ORIGINAL_RAW_FILE_NAME to "Original Raw File Name",
|
||||
)
|
||||
TAG_GDAL_METADATA to "GDAL Metadata",
|
||||
TAG_GDAL_NO_DATA to "GDAL No Data",
|
||||
).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? {
|
||||
return tagNameMap[tag]
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
// but looks like some form of ISO 8601 `basic format`:
|
||||
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
|
||||
|
@ -96,18 +110,7 @@ object Metadata {
|
|||
null
|
||||
} ?: return 0
|
||||
|
||||
var dateMillis = date.time
|
||||
if (subSecond != null) {
|
||||
try {
|
||||
val millis = (".$subSecond".toDouble() * 1000).toInt()
|
||||
if (millis in 0..999) {
|
||||
dateMillis += millis.toLong()
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return dateMillis
|
||||
return date.time + parseSubSecond(subSecond)
|
||||
}
|
||||
|
||||
// opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
|
||||
|
|
|
@ -6,7 +6,9 @@ import com.drew.lang.Rational
|
|||
import com.drew.lang.SequentialByteArrayReader
|
||||
import com.drew.metadata.Directory
|
||||
import com.drew.metadata.exif.ExifDirectoryBase
|
||||
import com.drew.metadata.exif.ExifIFD0Directory
|
||||
import com.drew.metadata.exif.ExifReader
|
||||
import com.drew.metadata.exif.ExifSubIFDDirectory
|
||||
import com.drew.metadata.iptc.IptcReader
|
||||
import com.drew.metadata.png.PngDirectory
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -53,11 +55,34 @@ object MetadataExtractorHelper {
|
|||
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)) {
|
||||
val date = this.getDate(tag, null, TimeZone.getDefault())
|
||||
if (date != null) save(date.time)
|
||||
val date = this.getDate(tag, subSecond, TimeZone.getDefault())
|
||||
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
|
||||
|
@ -69,13 +94,13 @@ object MetadataExtractorHelper {
|
|||
- If the ModelPixelScaleTag is included in an IFD, then a ModelTiepointTag SHALL also be included.
|
||||
*/
|
||||
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 modelTransformation = this.containsTag(ExifTags.TAG_MODEL_TRANSFORMATION)
|
||||
val modelTiepoint = this.containsTag(GeoTiffTags.TAG_MODEL_TIEPOINT)
|
||||
val modelTransformation = this.containsTag(GeoTiffTags.TAG_MODEL_TRANSFORMATION)
|
||||
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
|
||||
|
||||
return true
|
||||
|
|
|
@ -185,7 +185,7 @@ class SourceEntry {
|
|||
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it }
|
||||
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = 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
|
||||
|
@ -218,7 +218,7 @@ class SourceEntry {
|
|||
exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it }
|
||||
exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it }
|
||||
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) {
|
||||
// ExifInterface initialization can fail with a RuntimeException
|
||||
|
|
|
@ -62,8 +62,7 @@ abstract class ImageProvider {
|
|||
|
||||
open suspend fun renameMultiple(
|
||||
activity: Activity,
|
||||
newFileName: String,
|
||||
entries: List<AvesEntry>,
|
||||
entriesToNewName: Map<AvesEntry, String>,
|
||||
isCancelledOp: CancelCheck,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
|
@ -143,7 +142,7 @@ abstract class ImageProvider {
|
|||
|
||||
var desiredNameWithoutExtension = if (sourceEntry.path != null) {
|
||||
val sourceFileName = File(sourceEntry.path).name
|
||||
sourceFileName.replaceFirst(FILE_EXTENSION_PATTERN, "")
|
||||
sourceFileName.substringBeforeLast(".")
|
||||
} else {
|
||||
sourceUri.lastPathSegment!!
|
||||
}
|
||||
|
@ -757,7 +756,13 @@ abstract class ImageProvider {
|
|||
ExifInterface.TAG_DATETIME_DIGITIZED,
|
||||
).forEach { 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))
|
||||
}
|
||||
}
|
||||
|
@ -964,8 +969,6 @@ abstract class ImageProvider {
|
|||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ImageProvider>()
|
||||
|
||||
val FILE_EXTENSION_PATTERN = Regex("[.][^.]+$")
|
||||
|
||||
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
|
||||
|
||||
// used when skipping a move/creation op because the target file already exists
|
||||
|
|
|
@ -191,7 +191,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
|
||||
val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
|
||||
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.TITLE)
|
||||
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
|
||||
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||
|
@ -229,7 +228,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
"height" to height,
|
||||
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
|
||||
"sizeBytes" to cursor.getLong(sizeColumn),
|
||||
"title" to cursor.getString(titleColumn),
|
||||
"dateModifiedSecs" to dateModifiedSecs,
|
||||
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
|
||||
"durationMillis" to durationMillis,
|
||||
|
@ -489,7 +487,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
return skippedFieldMap
|
||||
}
|
||||
|
||||
val desiredNameWithoutExtension = desiredName.replaceFirst(FILE_EXTENSION_PATTERN, "")
|
||||
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
|
||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||
activity = activity,
|
||||
dir = targetDir,
|
||||
|
@ -587,12 +585,14 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
override suspend fun renameMultiple(
|
||||
activity: Activity,
|
||||
newFileName: String,
|
||||
entries: List<AvesEntry>,
|
||||
entriesToNewName: Map<AvesEntry, String>,
|
||||
isCancelledOp: CancelCheck,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
for (entry in entries) {
|
||||
for (kv in entriesToNewName) {
|
||||
val entry = kv.key
|
||||
val desiredName = kv.value
|
||||
|
||||
val sourceUri = entry.uri
|
||||
val sourcePath = entry.path
|
||||
val mimeType = entry.mimeType
|
||||
|
@ -602,19 +602,20 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
"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 {
|
||||
val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle(
|
||||
activity = activity,
|
||||
mimeType = mimeType,
|
||||
oldMediaUri = sourceUri,
|
||||
oldPath = sourcePath,
|
||||
newFileName = newFileName,
|
||||
desiredName = desiredName,
|
||||
)
|
||||
result["newFields"] = newFields
|
||||
result["success"] = true
|
||||
} 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)
|
||||
|
@ -626,10 +627,24 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType: String,
|
||||
oldMediaUri: Uri,
|
||||
oldPath: String,
|
||||
newFileName: String,
|
||||
desiredName: String,
|
||||
): FieldMap {
|
||||
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
|
||||
|
||||
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 {
|
||||
oldFile == newFile -> skippedFieldMap
|
||||
StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile)
|
||||
|
@ -681,8 +696,11 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
newFile: File
|
||||
): FieldMap {
|
||||
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")
|
||||
val renamed = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri)?.renameTo(newFile.name) ?: false
|
||||
val renamed = df.renameTo(newFile.name)
|
||||
if (!renamed) {
|
||||
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
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.TITLE,
|
||||
)
|
||||
try {
|
||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||
|
@ -774,8 +790,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
newFields["contentId"] = uri.tryParseId()
|
||||
newFields["path"] = path
|
||||
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()
|
||||
return newFields
|
||||
}
|
||||
|
@ -846,8 +860,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
MediaColumns.PATH,
|
||||
MediaStore.MediaColumns.MIME_TYPE,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
// TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`?
|
||||
MediaStore.MediaColumns.TITLE,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
MediaStore.MediaColumns.HEIGHT,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
|
|
|
@ -26,7 +26,7 @@ object MimeTypes {
|
|||
private const val CR2 = "image/x-canon-cr2"
|
||||
private const val CRW = "image/x-canon-crw"
|
||||
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 K25 = "image/x-kodak-k25"
|
||||
private const val KDC = "image/x-kodak-kdc"
|
||||
|
|
|
@ -36,13 +36,14 @@ object StorageUtils {
|
|||
const val TRASH_PATH_PLACEHOLDER = "#trash"
|
||||
|
||||
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? {
|
||||
val filesDirs = context.getExternalFilesDirs(null)
|
||||
val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
|
||||
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? {
|
||||
|
@ -115,6 +116,15 @@ object StorageUtils {
|
|||
}
|
||||
|
||||
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 {
|
||||
// we want:
|
||||
// /storage/emulated/0/
|
||||
|
@ -130,9 +140,16 @@ object StorageUtils {
|
|||
}
|
||||
|
||||
private fun findVolumePaths(context: Context): Array<String> {
|
||||
// Final set of paths
|
||||
val paths = HashSet<String>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
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 {
|
||||
// Primary emulated SD-CARD
|
||||
val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: ""
|
||||
|
@ -143,7 +160,8 @@ object StorageUtils {
|
|||
var validFiles: Boolean
|
||||
do {
|
||||
// `getExternalFilesDirs` sometimes include `null` when called right after getting read access
|
||||
// (e.g. on API 30 emulator) so we retry until the file system is ready
|
||||
// (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)
|
||||
validFiles = !externalFilesDirs.contains(null)
|
||||
if (validFiles) {
|
||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
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"
|
||||
// GMS & Firebase Crashlytics are not actually used by all flavors
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
|
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 364 KiB |
4
fastlane/metadata/android/en-US/changelogs/1069.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.6.3:
|
||||
- enjoy the light theme
|
||||
- rename items in bulk
|
||||
Full changelog available on GitHub
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 367 KiB After Width: | Height: | Size: 365 KiB |
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 364 KiB |
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 364 KiB |
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 271 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 364 KiB |
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 273 KiB |
Before Width: | Height: | Size: 494 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 362 KiB |
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 496 KiB |
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 363 KiB |
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 274 KiB |
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 495 KiB |
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 364 KiB |
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 273 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 494 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 215 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 364 KiB |
|
@ -137,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "Verhinderung von Bildschirmeffekten",
|
||||
"accessibilityAnimationsKeep": "Bildschirmeffekte beibehalten",
|
||||
|
||||
"displayRefreshRatePreferHighest": "Höchste Rate",
|
||||
"displayRefreshRatePreferLowest": "Niedrigste Rate",
|
||||
|
||||
"themeBrightnessLight": "Hell",
|
||||
"themeBrightnessDark": "Dunkel",
|
||||
"themeBrightnessBlack": "Schwarz",
|
||||
|
||||
"albumTierNew": "Neu",
|
||||
"albumTierPinned": "Angeheftet",
|
||||
"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?}}",
|
||||
"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?",
|
||||
"videoStartOverButtonLabel": "NEU BEGINNEN",
|
||||
|
@ -189,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "Neuer Name",
|
||||
"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?}}",
|
||||
"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",
|
||||
"editEntryDateDialogSetCustom": "Datum einstellen",
|
||||
"editEntryDateDialogCopyField": "Von anderem Datum kopieren",
|
||||
"editEntryDateDialogCopyItem": "Von einem anderen Element kopieren",
|
||||
"editEntryDateDialogExtractFromTitle": "Auszug aus dem Titel",
|
||||
"editEntryDateDialogShift": "Verschieben",
|
||||
"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}}",
|
||||
"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}}",
|
||||
"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}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, =1{1 Seite konnte nicht exportiert werden} other{{count} Seiten konnten nicht exportiert werden}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element unmebannt} other{{count} Elemente umbenannt}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
|
||||
|
||||
"collectionEmptyFavourites": "Keine Favoriten",
|
||||
|
@ -393,6 +413,7 @@
|
|||
"settingsConfirmationDialogTitle": "Bestätigungsdialoge",
|
||||
"settingsConfirmationDialogDeleteItems": "Vor dem endgültigen Löschen von Elementen fragen",
|
||||
"settingsConfirmationDialogMoveToBinItems": "Vor dem Verschieben von Elementen in den Papierkorb fragen",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "Vor Verschiebung von Objekten ohne Metadaten-Datum fragen",
|
||||
|
||||
"settingsNavigationDrawerTile": "Menü Navigation",
|
||||
"settingsNavigationDrawerEditorTitle": "Menü Navigation",
|
||||
|
@ -501,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "Zeit zum Reagieren",
|
||||
"settingsTimeToTakeActionTitle": "Zeit zum Reagieren",
|
||||
|
||||
"settingsSectionDisplay": "Anzeige",
|
||||
"settingsThemeBrightness": "Thema",
|
||||
"settingsThemeColorHighlights": "Farbige Highlights",
|
||||
"settingsDisplayRefreshRateModeTile": "Bildwiederholrate der Anzeige",
|
||||
"settingsDisplayRefreshRateModeTitle": "Bildwiederholrate",
|
||||
|
||||
"settingsSectionLanguage": "Sprache & Formate",
|
||||
"settingsLanguage": "Sprache",
|
||||
"settingsCoordinateFormatTile": "Koordinatenformat",
|
||||
|
|
|
@ -177,6 +177,13 @@
|
|||
"accessibilityAnimationsRemove": "Prevent screen effects",
|
||||
"accessibilityAnimationsKeep": "Keep screen effects",
|
||||
|
||||
"displayRefreshRatePreferHighest": "Highest rate",
|
||||
"displayRefreshRatePreferLowest": "Lowest rate",
|
||||
|
||||
"themeBrightnessLight": "Light",
|
||||
"themeBrightnessDark": "Dark",
|
||||
"themeBrightnessBlack": "Black",
|
||||
|
||||
"albumTierNew": "New",
|
||||
"albumTierPinned": "Pinned",
|
||||
"albumTierSpecial": "Common",
|
||||
|
@ -282,6 +289,8 @@
|
|||
"count": {}
|
||||
}
|
||||
},
|
||||
"moveUndatedConfirmationDialogMessage": "Save item dates before proceeding?",
|
||||
"moveUndatedConfirmationDialogSetDate": "Save dates",
|
||||
|
||||
"videoResumeDialogMessage": "Do you want to resume playing at {time}?",
|
||||
"@videoResumeDialogMessage": {
|
||||
|
@ -309,6 +318,14 @@
|
|||
"renameAlbumDialogLabel": "New name",
|
||||
"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": {
|
||||
"placeholders": {
|
||||
|
@ -331,6 +348,7 @@
|
|||
"editEntryDateDialogTitle": "Date & Time",
|
||||
"editEntryDateDialogSetCustom": "Set custom date",
|
||||
"editEntryDateDialogCopyField": "Copy from other date",
|
||||
"editEntryDateDialogCopyItem": "Copy from other item",
|
||||
"editEntryDateDialogExtractFromTitle": "Extract from title",
|
||||
"editEntryDateDialogShift": "Shift",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "File modified date",
|
||||
|
@ -453,6 +471,12 @@
|
|||
"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": {
|
||||
"placeholders": {
|
||||
|
@ -477,6 +501,12 @@
|
|||
"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": {
|
||||
"placeholders": {
|
||||
|
@ -563,6 +593,7 @@
|
|||
"settingsConfirmationDialogTitle": "Confirmation Dialogs",
|
||||
"settingsConfirmationDialogDeleteItems": "Ask before deleting items forever",
|
||||
"settingsConfirmationDialogMoveToBinItems": "Ask before moving items to the recycle bin",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "Ask before moving undated items",
|
||||
|
||||
"settingsNavigationDrawerTile": "Navigation menu",
|
||||
"settingsNavigationDrawerEditorTitle": "Navigation Menu",
|
||||
|
@ -671,6 +702,12 @@
|
|||
"settingsTimeToTakeActionTile": "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",
|
||||
"settingsLanguage": "Language",
|
||||
"settingsCoordinateFormatTile": "Coordinate format",
|
||||
|
|
|
@ -68,6 +68,8 @@
|
|||
"entryActionRemoveFavourite": "Quitar de favoritos",
|
||||
|
||||
"videoActionCaptureFrame": "Capturar fotograma",
|
||||
"videoActionMute": "Silenciar",
|
||||
"videoActionUnmute": "Dejar de silenciar",
|
||||
"videoActionPause": "Pausa",
|
||||
"videoActionPlay": "Reproducir",
|
||||
"videoActionReplay10": "Retroceder 10 segundos",
|
||||
|
@ -135,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "Prevenir efectos en pantalla",
|
||||
"accessibilityAnimationsKeep": "Mantener efectos en pantalla",
|
||||
|
||||
"displayRefreshRatePreferHighest": "Alta tasa",
|
||||
"displayRefreshRatePreferLowest": "Baja tasa",
|
||||
|
||||
"themeBrightnessLight": "Claro",
|
||||
"themeBrightnessDark": "Obscuro",
|
||||
"themeBrightnessBlack": "Negro",
|
||||
|
||||
"albumTierNew": "Nuevo",
|
||||
"albumTierPinned": "Fijado",
|
||||
"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.",
|
||||
"notEnoughSpaceDialogTitle": "Espacio insuficiente",
|
||||
"notEnoughSpaceDialogMessage": "Esta operación necesita {neededSize} de espacio libre en «{volume}» para completarse, pero sólo hay {freeSize} disponible.",
|
||||
|
||||
"missingSystemFilePickerDialogTitle": "Selector de archivos del sistema no disponible",
|
||||
"missingSystemFilePickerDialogMessage": "El selector de archivos del sistema no se encuentra disponible o fue deshabilitado. Por favor habilítelo e intente nuevamente.",
|
||||
|
||||
|
@ -168,8 +176,9 @@
|
|||
"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?}}",
|
||||
|
||||
"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}?",
|
||||
"videoStartOverButtonLabel": "VOLVER A EMPEZAR",
|
||||
|
@ -189,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "Renombrar",
|
||||
"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?}}",
|
||||
"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",
|
||||
"editEntryDateDialogSetCustom": "Establecer fecha personalizada",
|
||||
"editEntryDateDialogCopyField": "Copiar de otra fecha",
|
||||
"editEntryDateDialogCopyItem": "Copiar de otro elemento",
|
||||
"editEntryDateDialogExtractFromTitle": "Extraer del título",
|
||||
"editEntryDateDialogShift": "Cambiar",
|
||||
"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}}",
|
||||
"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}}",
|
||||
"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}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, =1{Error al exportar 1 página} other{Error al exportar {count} páginas}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{1 elemento copiado} other{Copiados{count} elementos}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{1 elemento movido} other{Movidos {count} elementos}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{1 elemento renombrado} other{Renombrados {count} elementos}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{1 elemento editado} other{Editados {count} elementos}}",
|
||||
|
||||
"collectionEmptyFavourites": "Sin favoritos",
|
||||
|
@ -393,6 +413,7 @@
|
|||
"settingsConfirmationDialogTitle": "Diálogos de confirmación",
|
||||
"settingsConfirmationDialogDeleteItems": "Preguntar antes de eliminar elementos permanentemente",
|
||||
"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",
|
||||
"settingsNavigationDrawerEditorTitle": "Menú de navegación",
|
||||
|
@ -501,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "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",
|
||||
"settingsLanguage": "Idioma",
|
||||
"settingsCoordinateFormatTile": "Formato de coordenadas",
|
||||
|
|
|
@ -137,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "Empêchez certains 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",
|
||||
"albumTierPinned": "Épinglés",
|
||||
"albumTierSpecial": "Standards",
|
||||
|
@ -170,6 +177,8 @@
|
|||
|
||||
"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 ?}}",
|
||||
"moveUndatedConfirmationDialogMessage": "Sauvegarder les dates des éléments avant de continuer?",
|
||||
"moveUndatedConfirmationDialogSetDate": "Sauvegarder les dates",
|
||||
|
||||
"videoResumeDialogMessage": "Voulez-vous reprendre la lecture à {time} ?",
|
||||
"videoStartOverButtonLabel": "RECOMMENCER",
|
||||
|
@ -189,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "Nouveau nom",
|
||||
"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 ?}}",
|
||||
"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",
|
||||
"editEntryDateDialogSetCustom": "Régler une date personnalisée",
|
||||
"editEntryDateDialogCopyField": "Copier d’une autre date",
|
||||
"editEntryDateDialogCopyItem": "Copier d’un autre élément",
|
||||
"editEntryDateDialogExtractFromTitle": "Extraire du titre",
|
||||
"editEntryDateDialogShift": "Décaler",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Date de modification du fichier",
|
||||
|
@ -308,10 +326,12 @@
|
|||
"collectionDeleteFailureFeedback": "{count, plural, =1{Échec de la suppression d’1 élément} other{Échec de la suppression de {count} éléments}}",
|
||||
"collectionCopyFailureFeedback": "{count, plural, =1{Échec de la copie d’1 élément} other{Échec de la copie de {count} éléments}}",
|
||||
"collectionMoveFailureFeedback": "{count, plural, =1{Échec du déplacement d’1 élément} other{Échec du déplacement de {count} éléments}}",
|
||||
"collectionRenameFailureFeedback": "{count, plural, =1{Échec du renommage d’1 élément} other{Échec du renommage de {count} éléments}}",
|
||||
"collectionEditFailureFeedback": "{count, plural, =1{Échec de la modification d’1 élément} other{Échec de la modification de {count} éléments}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, =1{Échec de l’export d’1 page} other{Échec de l’export de {count} pages}}",
|
||||
"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}}",
|
||||
"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}}",
|
||||
|
||||
"collectionEmptyFavourites": "Aucun favori",
|
||||
|
@ -393,6 +413,7 @@
|
|||
"settingsConfirmationDialogTitle": "Demandes de confirmation",
|
||||
"settingsConfirmationDialogDeleteItems": "Suppression définitive d’éléments",
|
||||
"settingsConfirmationDialogMoveToBinItems": "Mise d’éléments à la corbeille",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "Déplacement d’éléments non datés",
|
||||
|
||||
"settingsNavigationDrawerTile": "Menu de navigation",
|
||||
"settingsNavigationDrawerEditorTitle": "Menu de navigation",
|
||||
|
@ -501,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "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 d’actualisation de l'écran",
|
||||
"settingsDisplayRefreshRateModeTitle": "Fréquence d’actualisation",
|
||||
|
||||
"settingsSectionLanguage": "Langue & Formats",
|
||||
"settingsLanguage": "Langue",
|
||||
"settingsCoordinateFormatTile": "Format de coordonnées",
|
||||
|
|
|
@ -68,6 +68,8 @@
|
|||
"entryActionRemoveFavourite": "Hapus dari favorit",
|
||||
|
||||
"videoActionCaptureFrame": "Tangkap bingkai",
|
||||
"videoActionMute": "Mute",
|
||||
"videoActionUnmute": "Unmute",
|
||||
"videoActionPause": "Henti",
|
||||
"videoActionPlay": "Mainkan",
|
||||
"videoActionReplay10": "Mundurkan 10 detik",
|
||||
|
@ -112,6 +114,11 @@
|
|||
"videoLoopModeShortOnly": "Hanya video pendek",
|
||||
"videoLoopModeAlways": "Selalu",
|
||||
|
||||
"videoControlsPlay": "Putar",
|
||||
"videoControlsPlaySeek": "Putar dan cari",
|
||||
"videoControlsPlayOutside": "Buka dengan pemutar lain",
|
||||
"videoControlsNone": "Tidak ada",
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
||||
|
@ -130,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "Mencegah efek layar",
|
||||
"accessibilityAnimationsKeep": "Simpan efek layar",
|
||||
|
||||
"displayRefreshRatePreferHighest": "Penyegaran tertinggi",
|
||||
"displayRefreshRatePreferLowest": "Penyegaran terendah",
|
||||
|
||||
"themeBrightnessLight": "Terang",
|
||||
"themeBrightnessDark": "Gelap",
|
||||
"themeBrightnessBlack": "Hitam",
|
||||
|
||||
"albumTierNew": "Baru",
|
||||
"albumTierPinned": "Disemat",
|
||||
"albumTierSpecial": "Umum",
|
||||
|
@ -163,6 +177,8 @@
|
|||
|
||||
"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?}}",
|
||||
"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}?",
|
||||
"videoStartOverButtonLabel": "ULANG DARI AWAL",
|
||||
|
@ -182,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "Nama baru",
|
||||
"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?}}",
|
||||
"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",
|
||||
"editEntryDateDialogSetCustom": "Atur tanggal khusus",
|
||||
"editEntryDateDialogCopyField": "Salin dari tanggal lain",
|
||||
"editEntryDateDialogCopyItem": "Salin dari benda lain",
|
||||
"editEntryDateDialogExtractFromTitle": "Ekstrak dari judul",
|
||||
"editEntryDateDialogShift": "Geser",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Tanggal modifikasi file",
|
||||
|
@ -301,10 +326,12 @@
|
|||
"collectionDeleteFailureFeedback": "{count, plural, other{Gagal untuk menghapus {count} benda}}",
|
||||
"collectionCopyFailureFeedback": "{count, plural, other{Gagal untuk menyalin {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}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, other{Gagal untuk mengekspor {count} halaman}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, other{Menyalin {count} benda}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, other{{count} benda terpindah}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, other{Tergantikan nama untuk {count} benda}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, other{Mengubah {count} benda}}",
|
||||
|
||||
"collectionEmptyFavourites": "Tidak ada favorit",
|
||||
|
@ -386,6 +413,8 @@
|
|||
"settingsConfirmationDialogTitle": "Dialog Konfirmasi",
|
||||
"settingsConfirmationDialogDeleteItems": "Tanya sebelum menghapus benda selamanya",
|
||||
"settingsConfirmationDialogMoveToBinItems": "Tanya sebelum memindahkan benda ke tong sampah",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "Tanyakan sebelum memindahkan barang tanpa metadata tanggal",
|
||||
|
||||
"settingsNavigationDrawerTile": "Menu navigasi",
|
||||
"settingsNavigationDrawerEditorTitle": "Menu Navigasi",
|
||||
"settingsNavigationDrawerBanner": "Sentuh dan tahan untuk memindahkan dan menyusun ulang benda menu.",
|
||||
|
@ -429,6 +458,7 @@
|
|||
"settingsViewerShowInformation": "Tampilkan informasi",
|
||||
"settingsViewerShowInformationSubtitle": "Tampilkan judul, tanggal, lokasi, dll.",
|
||||
"settingsViewerShowShootingDetails": "Tampilkan detail pemotretan",
|
||||
"settingsViewerShowOverlayThumbnails": "Tampilkan thumbnail",
|
||||
"settingsViewerEnableOverlayBlurEffect": "Efek Kabur",
|
||||
|
||||
"settingsVideoPageTitle": "Pengaturan Video",
|
||||
|
@ -454,6 +484,13 @@
|
|||
"settingsSubtitleThemeTextAlignmentCenter": "Tengah",
|
||||
"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",
|
||||
"settingsAllowInstalledAppAccess": "Izinkan akses ke inventori aplikasi",
|
||||
"settingsAllowInstalledAppAccessSubtitle": "Digunakan untuk meningkatkan tampilan album",
|
||||
|
@ -485,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "Waktu untuk mengambil tindakan",
|
||||
"settingsTimeToTakeActionTitle": "Saatnya Bertindak",
|
||||
|
||||
"settingsSectionDisplay": "Tampilan",
|
||||
"settingsThemeBrightness": "Tema",
|
||||
"settingsThemeColorHighlights": "Highlight warna",
|
||||
"settingsDisplayRefreshRateModeTile": "Tingkat penyegaran tampilan",
|
||||
"settingsDisplayRefreshRateModeTitle": "Tingkat Penyegaran",
|
||||
|
||||
"settingsSectionLanguage": "Bahasa & Format",
|
||||
"settingsLanguage": "Bahasa",
|
||||
"settingsCoordinateFormatTile": "Format koordinat",
|
||||
|
|
|
@ -137,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "画面エフェクトを利用しない",
|
||||
"accessibilityAnimationsKeep": "画面エフェクトを利用",
|
||||
|
||||
"displayRefreshRatePreferHighest": "高レート",
|
||||
"displayRefreshRatePreferLowest": "低レート",
|
||||
|
||||
"themeBrightnessLight": "ライト",
|
||||
"themeBrightnessDark": "ダーク",
|
||||
"themeBrightnessBlack": "黒",
|
||||
|
||||
"albumTierNew": "新規",
|
||||
"albumTierPinned": "固定",
|
||||
"albumTierSpecial": "全体",
|
||||
|
@ -170,6 +177,8 @@
|
|||
|
||||
"binEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムをごみ箱に移動しますか?} other{{count} 件のアイテムをごみ箱に移動しますか?}}",
|
||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}",
|
||||
"moveUndatedConfirmationDialogMessage": "いくつかのアイテムはメタデータ上に日付がありません。メタデータ上の日付が設定されない場合、この操作によりこれらの現在の日付はリセットされます",
|
||||
"moveUndatedConfirmationDialogSetDate": "日付を設定",
|
||||
|
||||
"videoResumeDialogMessage": " {time} の時点から再生を再開しますか?",
|
||||
"videoStartOverButtonLabel": "最初から再生",
|
||||
|
@ -189,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "新しい名前",
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "ディレクトリが既に存在します",
|
||||
|
||||
"renameEntrySetPageTitle": "名前を変更",
|
||||
"renameEntrySetPagePatternFieldLabel": "名前付けのパターン",
|
||||
"renameEntrySetPageInsertTooltip": "フィールドを挿入",
|
||||
"renameEntrySetPagePreview": "プレビュー",
|
||||
|
||||
"renameProcessorCounter": "連番",
|
||||
"renameProcessorName": "名前",
|
||||
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{このアルバムとアルバム内のアイテムを削除しますか?} other{このアルバムとアルバム内の {count} 件のアイテムを削除しますか?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{これらのアルバムとアルバム内のアイテムを削除しますか?} other{これらのアルバムとアルバム内の {count} 件のアイテムを削除しますか?}}",
|
||||
|
||||
|
@ -201,6 +218,7 @@
|
|||
"editEntryDateDialogTitle": "日時",
|
||||
"editEntryDateDialogSetCustom": "日を設定する",
|
||||
"editEntryDateDialogCopyField": "他の日からコピーする",
|
||||
"editEntryDateDialogCopyItem": "他のアイテムからコピーする",
|
||||
"editEntryDateDialogExtractFromTitle": "タイトルから抽出する",
|
||||
"editEntryDateDialogShift": "シフト",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "ファイル更新日",
|
||||
|
@ -308,10 +326,12 @@
|
|||
"collectionDeleteFailureFeedback": "{count, plural, other{{count} 件のアイテムを削除できませんでした}}",
|
||||
"collectionCopyFailureFeedback": "{count, plural, other{{count} 件のアイテムをコピーできませんでした}}",
|
||||
"collectionMoveFailureFeedback": "{count, plural, other{{count} 件のアイテムを移動できませんでした}}",
|
||||
"collectionRenameFailureFeedback": "{count, plural, other{{count} 件のアイテム名を変更できませんでした}}",
|
||||
"collectionEditFailureFeedback": "{count, plural, other{{count} 件のアイテムを編集できませんでした}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, other{{count} ページをエクスポートできませんでした}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, other{{count} 件のアイテムをコピーしました}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, other{{count} 件のアイテムを移動しました}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, other{{count} 件のアイテム名を変更しました}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, other{{count} 件のアイテムを編集しました}}",
|
||||
|
||||
"collectionEmptyFavourites": "お気に入りはありません",
|
||||
|
@ -393,6 +413,7 @@
|
|||
"settingsConfirmationDialogTitle": "確認メッセージ",
|
||||
"settingsConfirmationDialogDeleteItems": "アイテムを完全に削除する前に確認",
|
||||
"settingsConfirmationDialogMoveToBinItems": "アイテムをごみ箱に移動する前に確認",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "メタデータ上に日付のないアイテムを移動する前に確認",
|
||||
|
||||
"settingsNavigationDrawerTile": "ナビゲーション メニュー",
|
||||
"settingsNavigationDrawerEditorTitle": "ナビゲーション メニュー",
|
||||
|
@ -448,8 +469,6 @@
|
|||
"settingsVideoLoopModeTile": "ループ モード",
|
||||
"settingsVideoLoopModeTitle": "ループ モード",
|
||||
|
||||
"settingsVideoQuickActionsTile": "動画のクイック アクション",
|
||||
"settingsVideoQuickActionEditorTitle": "クイック アクション",
|
||||
"settingsSubtitleThemeTile": "字幕",
|
||||
"settingsSubtitleThemeTitle": "字幕",
|
||||
"settingsSubtitleThemeSample": "これはサンプルです。",
|
||||
|
@ -503,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "操作までの時間",
|
||||
"settingsTimeToTakeActionTitle": "操作までの時間",
|
||||
|
||||
"settingsSectionDisplay": "ディスプレイ",
|
||||
"settingsThemeBrightness": "テーマ",
|
||||
"settingsThemeColorHighlights": "カラー強調表示",
|
||||
"settingsDisplayRefreshRateModeTile": "ディスプレイ リフレッシュ レート",
|
||||
"settingsDisplayRefreshRateModeTitle": "リフレッシュレート",
|
||||
|
||||
"settingsSectionLanguage": "言語と形式",
|
||||
"settingsLanguage": "言語",
|
||||
"settingsCoordinateFormatTile": "座標形式",
|
||||
|
|
|
@ -137,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "화면 효과 제한",
|
||||
"accessibilityAnimationsKeep": "화면 효과 유지",
|
||||
|
||||
"displayRefreshRatePreferHighest": "가장 높은 재생률",
|
||||
"displayRefreshRatePreferLowest": "가장 낮은 재생률",
|
||||
|
||||
"themeBrightnessLight": "라이트",
|
||||
"themeBrightnessDark": "다크",
|
||||
"themeBrightnessBlack": "검은색",
|
||||
|
||||
"albumTierNew": "신규",
|
||||
"albumTierPinned": "고정",
|
||||
"albumTierSpecial": "기본",
|
||||
|
@ -170,6 +177,8 @@
|
|||
|
||||
"binEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 휴지통으로 이동하시겠습니까?} other{항목 {count}개를 휴지통으로 이동하시겠습니까?}}",
|
||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{이 항목을 삭제하시겠습니까?} other{항목 {count}개를 삭제하시겠습니까?}}",
|
||||
"moveUndatedConfirmationDialogMessage": "이 작업을 계속하기 전에 항목의 날짜를 지정하시겠습니까?",
|
||||
"moveUndatedConfirmationDialogSetDate": "날짜 지정하기",
|
||||
|
||||
"videoResumeDialogMessage": "{time}부터 재개하시겠습니까?",
|
||||
"videoStartOverButtonLabel": "처음부터",
|
||||
|
@ -189,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "앨범 이름",
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "사용 중인 이름입니다",
|
||||
|
||||
"renameEntrySetPageTitle": "이름 변경",
|
||||
"renameEntrySetPagePatternFieldLabel": "이름 양식",
|
||||
"renameEntrySetPageInsertTooltip": "필드 추가",
|
||||
"renameEntrySetPagePreview": "미리보기",
|
||||
|
||||
"renameProcessorCounter": "숫자 증가",
|
||||
"renameProcessorName": "이름",
|
||||
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, other{이 앨범들의 항목 {count}개를 삭제하시겠습니까?}}",
|
||||
|
||||
|
@ -201,6 +218,7 @@
|
|||
"editEntryDateDialogTitle": "날짜 및 시간",
|
||||
"editEntryDateDialogSetCustom": "지정 날짜로 편집",
|
||||
"editEntryDateDialogCopyField": "다른 날짜에서 지정",
|
||||
"editEntryDateDialogCopyItem": "다른 항목에서 지정",
|
||||
"editEntryDateDialogExtractFromTitle": "제목에서 추출",
|
||||
"editEntryDateDialogShift": "시간 이동",
|
||||
"editEntryDateDialogSourceFileModifiedDate": "파일 수정한 날짜",
|
||||
|
@ -308,10 +326,12 @@
|
|||
"collectionDeleteFailureFeedback": "{count, plural, other{항목 {count}개를 삭제하지 못했습니다}}",
|
||||
"collectionCopyFailureFeedback": "{count, plural, other{항목 {count}개를 복사하지 못했습니다}}",
|
||||
"collectionMoveFailureFeedback": "{count, plural, other{항목 {count}개를 이동하지 못했습니다}}",
|
||||
"collectionRenameFailureFeedback": "{count, plural, other{항목 {count}개의 이름을 변경하지 못했습니다}}",
|
||||
"collectionEditFailureFeedback": "{count, plural, other{항목 {count}개를 편집하지 못했습니다}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, other{항목 {count}개를 내보내지 못했습니다}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, other{항목 {count}개를 복사했습니다}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, other{항목 {count}개를 이동했습니다}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, other{항목 {count}개의 이름을 변경했습니다}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, other{항목 {count}개를 편집했습니다}}",
|
||||
|
||||
"collectionEmptyFavourites": "즐겨찾기가 없습니다",
|
||||
|
@ -393,6 +413,7 @@
|
|||
"settingsConfirmationDialogTitle": "확정 대화상자",
|
||||
"settingsConfirmationDialogDeleteItems": "항목을 완전히 삭제 시",
|
||||
"settingsConfirmationDialogMoveToBinItems": "항목을 휴지통으로 이동 시",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "날짜가 지정되지 않은 항목을 이동 시",
|
||||
|
||||
"settingsNavigationDrawerTile": "탐색 메뉴",
|
||||
"settingsNavigationDrawerEditorTitle": "탐색 메뉴",
|
||||
|
@ -501,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간",
|
||||
"settingsTimeToTakeActionTitle": "액션 취하기 전 대기 시간",
|
||||
|
||||
"settingsSectionDisplay": "디스플레이",
|
||||
"settingsThemeBrightness": "테마",
|
||||
"settingsThemeColorHighlights": "색 강조",
|
||||
"settingsDisplayRefreshRateModeTile": "화면 재생률",
|
||||
"settingsDisplayRefreshRateModeTitle": "화면 재생률",
|
||||
|
||||
"settingsSectionLanguage": "언어 및 표시 형식",
|
||||
"settingsLanguage": "언어",
|
||||
"settingsCoordinateFormatTile": "좌표 표현",
|
||||
|
|
|
@ -137,6 +137,13 @@
|
|||
"accessibilityAnimationsRemove": "Prevenir efeitos de tela",
|
||||
"accessibilityAnimationsKeep": "Manter efeitos de tela",
|
||||
|
||||
"displayRefreshRatePreferHighest": "Taxa mais alta",
|
||||
"displayRefreshRatePreferLowest": "Taxa mais baixa",
|
||||
|
||||
"themeBrightnessLight": "Claro",
|
||||
"themeBrightnessDark": "Escuro",
|
||||
"themeBrightnessBlack": "Preto",
|
||||
|
||||
"albumTierNew": "Novo",
|
||||
"albumTierPinned": "Fixada",
|
||||
"albumTierSpecial": "Comum",
|
||||
|
@ -170,6 +177,8 @@
|
|||
|
||||
"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?}}",
|
||||
"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}?",
|
||||
"videoStartOverButtonLabel": "RECOMEÇAR",
|
||||
|
@ -189,6 +198,14 @@
|
|||
"renameAlbumDialogLabel": "Novo nome",
|
||||
"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?}}",
|
||||
"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",
|
||||
"editEntryDateDialogSetCustom": "Definir data personalizada",
|
||||
"editEntryDateDialogCopyField": "Copiar de outra data",
|
||||
"editEntryDateDialogCopyItem": "Copiar de outro item",
|
||||
"editEntryDateDialogExtractFromTitle": "Extrair do título",
|
||||
"editEntryDateDialogShift": "Mudança",
|
||||
"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}}",
|
||||
"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}}",
|
||||
"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}}",
|
||||
"collectionExportFailureFeedback": "{count, plural, =1{Falha ao exportar 1 página} other{Falha ao exportar {count} páginas}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{1 item copiado} other{Copiado {count} itens}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{1 item movido} other{Mudou-se {count} itens}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{1 item renomeado} other{Renomeado {count} itens}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{Editado 1 item} other{Editado {count} itens}}",
|
||||
|
||||
"collectionEmptyFavourites": "Nenhum favorito",
|
||||
|
@ -393,6 +413,7 @@
|
|||
"settingsConfirmationDialogTitle": "Caixas de diálogo de confirmação",
|
||||
"settingsConfirmationDialogDeleteItems": "Pergunte antes de excluir itens para sempre",
|
||||
"settingsConfirmationDialogMoveToBinItems": "Pergunte antes de mover itens para a lixeira",
|
||||
"settingsConfirmationDialogMoveUndatedItems": "Pergunte antes de mover itens sem data de metadados",
|
||||
|
||||
"settingsNavigationDrawerTile": "Menu de navegação",
|
||||
"settingsNavigationDrawerEditorTitle": "Menu de navegação",
|
||||
|
@ -501,6 +522,12 @@
|
|||
"settingsTimeToTakeActionTile": "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",
|
||||
"settingsLanguage": "Língua",
|
||||
"settingsCoordinateFormatTile": "Formato de coordenadas",
|
||||
|
|
|
@ -30,8 +30,9 @@ void mainCommon(AppFlavor flavor) {
|
|||
|
||||
// Errors during the widget build phase will show by default:
|
||||
// - 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`
|
||||
// ErrorWidget.builder = (details) => ErrorWidget(details.exception);
|
||||
|
||||
runApp(AvesApp(flavor: flavor));
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
return AIcons.unpin;
|
||||
// selecting (single filter)
|
||||
case ChipSetAction.rename:
|
||||
return AIcons.rename;
|
||||
return AIcons.name;
|
||||
case ChipSetAction.setCover:
|
||||
return AIcons.setCover;
|
||||
}
|
||||
|
|
|
@ -177,7 +177,8 @@ extension ExtraEntryAction on EntryAction {
|
|||
switch (this) {
|
||||
case EntryAction.debug:
|
||||
return ShaderMask(
|
||||
shaderCallback: AColors.debugGradient.createShader,
|
||||
shaderCallback: AvesColorsData.debugGradient.createShader,
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: child,
|
||||
);
|
||||
default:
|
||||
|
@ -200,7 +201,7 @@ extension ExtraEntryAction on EntryAction {
|
|||
case EntryAction.print:
|
||||
return AIcons.print;
|
||||
case EntryAction.rename:
|
||||
return AIcons.rename;
|
||||
return AIcons.name;
|
||||
case EntryAction.copy:
|
||||
return AIcons.copy;
|
||||
case EntryAction.move:
|
||||
|
|
|
@ -55,7 +55,8 @@ extension ExtraEntryInfoAction on EntryInfoAction {
|
|||
switch (this) {
|
||||
case EntryInfoAction.debug:
|
||||
return ShaderMask(
|
||||
shaderCallback: AColors.debugGradient.createShader,
|
||||
shaderCallback: AvesColorsData.debugGradient.createShader,
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: child,
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -23,6 +23,7 @@ enum EntrySetAction {
|
|||
restore,
|
||||
copy,
|
||||
move,
|
||||
rename,
|
||||
toggleFavourite,
|
||||
rotateCCW,
|
||||
rotateCW,
|
||||
|
@ -68,6 +69,7 @@ class EntrySetActions {
|
|||
EntrySetAction.restore,
|
||||
EntrySetAction.copy,
|
||||
EntrySetAction.move,
|
||||
EntrySetAction.rename,
|
||||
EntrySetAction.toggleFavourite,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.stats,
|
||||
|
@ -81,6 +83,7 @@ class EntrySetActions {
|
|||
EntrySetAction.delete,
|
||||
EntrySetAction.copy,
|
||||
EntrySetAction.move,
|
||||
EntrySetAction.rename,
|
||||
EntrySetAction.toggleFavourite,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.stats,
|
||||
|
@ -137,6 +140,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return context.l10n.collectionActionCopy;
|
||||
case EntrySetAction.move:
|
||||
return context.l10n.collectionActionMove;
|
||||
case EntrySetAction.rename:
|
||||
return context.l10n.entryActionRename;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.entryActionAddFavourite;
|
||||
|
@ -200,6 +205,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return AIcons.copy;
|
||||
case EntrySetAction.move:
|
||||
return AIcons.move;
|
||||
case EntrySetAction.rename:
|
||||
return AIcons.name;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return AIcons.favourite;
|
||||
|
|
|
@ -167,6 +167,7 @@ class AvesEntry {
|
|||
_directory = null;
|
||||
_filename = null;
|
||||
_extension = null;
|
||||
_bestTitle = null;
|
||||
}
|
||||
|
||||
String? get path => _path;
|
||||
|
@ -258,9 +259,10 @@ class AvesEntry {
|
|||
bool get canRotateAndFlip => canEdit && canEditExif;
|
||||
|
||||
// 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 {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
case MimeTypes.dng:
|
||||
case MimeTypes.jpeg:
|
||||
case MimeTypes.png:
|
||||
case MimeTypes.webp:
|
||||
|
@ -454,7 +456,7 @@ class AvesEntry {
|
|||
String? _bestTitle;
|
||||
|
||||
String? get bestTitle {
|
||||
_bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : sourceTitle;
|
||||
_bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : (filenameWithoutExtension ?? sourceTitle);
|
||||
return _bestTitle;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
switch (appliedModifier.action) {
|
||||
case DateEditAction.setCustom:
|
||||
case DateEditAction.copyField:
|
||||
case DateEditAction.copyItem:
|
||||
case DateEditAction.extractFromTitle:
|
||||
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
||||
break;
|
||||
|
@ -319,6 +320,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
final date = parseUnknownDateFormat(bestTitle);
|
||||
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
||||
case DateEditAction.setCustom:
|
||||
case DateEditAction.copyItem:
|
||||
return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!);
|
||||
case DateEditAction.shift:
|
||||
case DateEditAction.remove:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/services/common/services.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:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumFilter extends CollectionFilter {
|
||||
static const type = 'album';
|
||||
|
||||
static final Map<String, Color> _appColors = {};
|
||||
|
||||
final String album;
|
||||
final String? displayName;
|
||||
|
||||
|
@ -56,6 +53,7 @@ class AlbumFilter extends CollectionFilter {
|
|||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
// do not use async/await and rely on `SynchronousFuture`
|
||||
// to prevent rebuilding of the `FutureBuilder` listening on this future
|
||||
final albumType = androidFileUtils.getAlbumType(album);
|
||||
|
@ -63,31 +61,19 @@ class AlbumFilter extends CollectionFilter {
|
|||
case AlbumType.regular:
|
||||
break;
|
||||
case AlbumType.app:
|
||||
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!);
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
final appColor = colors.appColor(album);
|
||||
if (appColor != null) return appColor;
|
||||
break;
|
||||
case AlbumType.camera:
|
||||
return SynchronousFuture(AColors.albumCamera);
|
||||
return SynchronousFuture(colors.albumCamera);
|
||||
case AlbumType.download:
|
||||
return SynchronousFuture(AColors.albumDownload);
|
||||
return SynchronousFuture(colors.albumDownload);
|
||||
case AlbumType.screenRecordings:
|
||||
return SynchronousFuture(AColors.albumScreenRecordings);
|
||||
return SynchronousFuture(colors.albumScreenRecordings);
|
||||
case AlbumType.screenshots:
|
||||
return SynchronousFuture(AColors.albumScreenshots);
|
||||
return SynchronousFuture(colors.albumScreenshots);
|
||||
case AlbumType.videoCaptures:
|
||||
return SynchronousFuture(AColors.albumVideoCaptures);
|
||||
return SynchronousFuture(colors.albumVideoCaptures);
|
||||
}
|
||||
return super.color(context);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FavouriteFilter extends CollectionFilter {
|
||||
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);
|
||||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) => SynchronousFuture(AColors.favourite);
|
||||
Future<Color> color(BuildContext context) {
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
return SynchronousFuture(colors.favourite);
|
||||
}
|
||||
|
||||
@override
|
||||
String get category => type;
|
||||
|
|
|
@ -12,11 +12,12 @@ import 'package:aves/model/filters/rating.dart';
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/filters/trash.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:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@immutable
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MimeFilter extends CollectionFilter {
|
||||
static const type = 'mime';
|
||||
|
@ -15,7 +17,6 @@ class MimeFilter extends CollectionFilter {
|
|||
late final EntryFilter _test;
|
||||
late final String _label;
|
||||
late final IconData _icon;
|
||||
late final Color _color;
|
||||
|
||||
static final image = MimeFilter(MimeTypes.anyImage);
|
||||
static final video = MimeFilter(MimeTypes.anyVideo);
|
||||
|
@ -25,7 +26,6 @@ class MimeFilter extends CollectionFilter {
|
|||
|
||||
MimeFilter(this.mime) {
|
||||
IconData? icon;
|
||||
Color? color;
|
||||
var lowMime = mime.toLowerCase();
|
||||
if (lowMime.endsWith('/*')) {
|
||||
lowMime = lowMime.substring(0, lowMime.length - 2);
|
||||
|
@ -33,17 +33,14 @@ class MimeFilter extends CollectionFilter {
|
|||
_label = lowMime.toUpperCase();
|
||||
if (mime == MimeTypes.anyImage) {
|
||||
icon = AIcons.image;
|
||||
color = AColors.image;
|
||||
} else if (mime == MimeTypes.anyVideo) {
|
||||
icon = AIcons.video;
|
||||
color = AColors.video;
|
||||
}
|
||||
} else {
|
||||
_test = (entry) => entry.mimeType == lowMime;
|
||||
_label = MimeUtils.displayType(lowMime);
|
||||
}
|
||||
_icon = icon ?? AIcons.vector;
|
||||
_color = color ?? stringToColor(_label);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/colors.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/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class QueryFilter extends CollectionFilter {
|
||||
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);
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TypeFilter extends CollectionFilter {
|
||||
static const type = 'type';
|
||||
|
@ -18,7 +19,6 @@ class TypeFilter extends CollectionFilter {
|
|||
final String itemType;
|
||||
late final EntryFilter _test;
|
||||
late final IconData _icon;
|
||||
late final Color _color;
|
||||
|
||||
static final animated = TypeFilter._private(_animated);
|
||||
static final geotiff = TypeFilter._private(_geotiff);
|
||||
|
@ -35,32 +35,26 @@ class TypeFilter extends CollectionFilter {
|
|||
case _animated:
|
||||
_test = (entry) => entry.isAnimated;
|
||||
_icon = AIcons.animated;
|
||||
_color = AColors.animated;
|
||||
break;
|
||||
case _geotiff:
|
||||
_test = (entry) => entry.isGeotiff;
|
||||
_icon = AIcons.geo;
|
||||
_color = AColors.geotiff;
|
||||
break;
|
||||
case _motionPhoto:
|
||||
_test = (entry) => entry.isMotionPhoto;
|
||||
_icon = AIcons.motionPhoto;
|
||||
_color = AColors.motionPhoto;
|
||||
break;
|
||||
case _panorama:
|
||||
_test = (entry) => entry.isImage && entry.is360;
|
||||
_icon = AIcons.threeSixty;
|
||||
_color = AColors.panorama;
|
||||
break;
|
||||
case _raw:
|
||||
_test = (entry) => entry.isRaw;
|
||||
_icon = AIcons.raw;
|
||||
_color = AColors.raw;
|
||||
break;
|
||||
case _sphericalVideo:
|
||||
_test = (entry) => entry.isVideo && entry.is360;
|
||||
_icon = AIcons.threeSixty;
|
||||
_color = AColors.sphericalVideo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +100,24 @@ class TypeFilter extends CollectionFilter {
|
|||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -24,30 +24,30 @@ class DateModifier extends Equatable {
|
|||
List<Object?> get props => [action, fields, setDateTime, copyFieldSource, shiftMinutes];
|
||||
|
||||
const DateModifier._private(
|
||||
this.action,
|
||||
this.fields, {
|
||||
this.action, {
|
||||
this.fields = const {},
|
||||
this.setDateTime,
|
||||
this.copyFieldSource,
|
||||
this.shiftMinutes,
|
||||
});
|
||||
|
||||
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) {
|
||||
return DateModifier._private(DateEditAction.copyField, fields, copyFieldSource: copyFieldSource);
|
||||
factory DateModifier.copyField(DateFieldSource copyFieldSource) {
|
||||
return DateModifier._private(DateEditAction.copyField, copyFieldSource: copyFieldSource);
|
||||
}
|
||||
|
||||
factory DateModifier.extractFromTitle(Set<MetadataField> fields) {
|
||||
return DateModifier._private(DateEditAction.extractFromTitle, fields);
|
||||
factory DateModifier.extractFromTitle() {
|
||||
return const DateModifier._private(DateEditAction.extractFromTitle);
|
||||
}
|
||||
|
||||
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) {
|
||||
return DateModifier._private(DateEditAction.remove, fields);
|
||||
return DateModifier._private(DateEditAction.remove, fields: fields);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/model/metadata/fields.dart';
|
|||
enum DateEditAction {
|
||||
setCustom,
|
||||
copyField,
|
||||
copyItem,
|
||||
extractFromTitle,
|
||||
shift,
|
||||
remove,
|
||||
|
|
|
@ -12,6 +12,8 @@ class OverlayMetadata extends Equatable {
|
|||
|
||||
bool get isEmpty => aperture == null && exposureTime == null && focalLength == null && iso == null;
|
||||
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
const OverlayMetadata({
|
||||
this.aperture,
|
||||
this.exposureTime,
|
||||
|
|
184
lib/model/naming_pattern.dart
Normal 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');
|
||||
}
|
|
@ -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/filters/favourite.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/source/enums.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
|
@ -15,13 +16,20 @@ class SettingsDefaults {
|
|||
static const canUseAnalysisService = true;
|
||||
static const isInstalledAppAccessAllowed = 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 entryRenamingPattern = '<${DateNamingProcessor.key}, yyyyMMdd-HHmmss> <${NameNamingProcessor.key}>';
|
||||
|
||||
// navigation
|
||||
static const mustBackTwiceToExit = true;
|
||||
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
||||
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 = [
|
||||
null,
|
||||
MimeFilter.video,
|
||||
|
@ -58,9 +66,10 @@ class SettingsDefaults {
|
|||
|
||||
// viewer
|
||||
static const viewerQuickActions = [
|
||||
EntryAction.rotateScreen,
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.share,
|
||||
EntryAction.rotateScreen,
|
||||
EntryAction.delete,
|
||||
];
|
||||
static const showOverlayOnOpening = true;
|
||||
static const showOverlayMinimap = false;
|
||||
|
|