Merge branch 'develop'

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

View file

@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="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

View file

@ -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

View file

@ -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(),
)
}

View file

@ -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(

View file

@ -47,6 +47,9 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_ITXT_DIR_NAME
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_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 -> {

View file

@ -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)
})

View file

@ -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()
}
}

View file

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

View file

@ -363,13 +363,17 @@ object ExifInterfaceHelper {
}
}
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) {
fun ExifInterface.getSafeDateMillis(tag: String, subSecTag: String?, save: (value: Long) -> Unit) {
if (this.hasAttribute(tag)) {
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)

View file

@ -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]

View file

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

View file

@ -65,6 +65,20 @@ object Metadata {
}
}
fun parseSubSecond(subSecond: String?): Int {
if (subSecond != null) {
try {
val millis = (".$subSecond".toDouble() * 1000).toInt()
if (millis in 0..999) {
return millis
}
} catch (e: NumberFormatException) {
// ignore
}
}
return 0
}
// not sure which standards are used for the different video formats,
// 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),

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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"

View file

@ -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) {

View file

@ -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'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View file

@ -137,6 +137,13 @@
"accessibilityAnimationsRemove": "Verhinderung von Bildschirmeffekten",
"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",
@ -570,4 +597,4 @@
"filePickerOpenFrom": "Öffnen von",
"filePickerNoItems": "Keine Elemente",
"filePickerUseThisFolder": "Diesen Ordner verwenden"
}
}

View file

@ -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",

View file

@ -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",

View file

@ -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 dune autre date",
"editEntryDateDialogCopyItem": "Copier dun 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 d1 élément} other{Échec de la suppression de {count} éléments}}",
"collectionCopyFailureFeedback": "{count, plural, =1{Échec de la copie d1 élément} other{Échec de la copie de {count} éléments}}",
"collectionMoveFailureFeedback": "{count, plural, =1{Échec du déplacement d1 élément} other{Échec du déplacement de {count} éléments}}",
"collectionRenameFailureFeedback": "{count, plural, =1{Échec du renommage d1 élément} other{Échec du renommage de {count} éléments}}",
"collectionEditFailureFeedback": "{count, plural, =1{Échec de la modification d1 élément} other{Échec de la modification de {count} éléments}}",
"collectionExportFailureFeedback": "{count, plural, =1{Échec de lexport d1 page} other{Échec de lexport de {count} pages}}",
"collectionCopySuccessFeedback": "{count, plural, =1{1 élément copié} other{{count} éléments copiés}}",
"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 dactualisation de l'écran",
"settingsDisplayRefreshRateModeTitle": "Fréquence dactualisation",
"settingsSectionLanguage": "Langue & Formats",
"settingsLanguage": "Langue",
"settingsCoordinateFormatTile": "Format de coordonnées",

View file

@ -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",

View file

@ -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": "座標形式",

View file

@ -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": "좌표 표현",

View file

@ -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",

View file

@ -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));
}

View file

@ -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;
}

View file

@ -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:

View file

@ -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:

View file

@ -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;

View file

@ -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;
}

View file

@ -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:

View file

@ -1,4 +1,3 @@
import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/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);
}

View file

@ -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;

View file

@ -12,11 +12,12 @@ import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/filters/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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

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

View file

@ -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,

View file

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

View file

@ -2,6 +2,7 @@ import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/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;

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