diff --git a/.flutter b/.flutter
index 2ad6cd72c..f72efea43 160000
--- a/.flutter
+++ b/.flutter
@@ -1 +1 @@
-Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da
+Subproject commit f72efea43c3013323d1b95cff571f3c1caa37583
diff --git a/.gitignore b/.gitignore
index 60b39d58b..695e072ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,9 +32,6 @@ migrate_working_dir/
.pub/
/build/
-# Web related
-lib/generated_plugin_registrant.dart
-
# Symbolication related
app.*.symbols
diff --git a/.metadata b/.metadata
index fdef59c1a..0bc5b71a0 100644
--- a/.metadata
+++ b/.metadata
@@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
-# This file should be version controlled and should not be manually edited.
+# This file should be version controlled.
version:
- revision: bc7bc940836f1f834699625426795fd6f07c18ec
- channel: beta
+ revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
+ channel: stable
project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
+ base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
+ - platform: android
+ create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
+ base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66a4b977c..e474e7a16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.8.5] - 2023-04-18
+
+### Added
+
+- Collection: optional support for Samsung and Sony burst patterns
+- Video: action to lock viewer
+- Info: improved state/place display (requires rescan, limited to AU/GB/IN/US)
+- Info: edit tags with state placeholder
+- Info: show metadata from MP4 user data box
+- Countries: show states for selected countries
+- Tags: delete selected tags from all media in collection
+- improved support for system font scale
+
+### Changed
+
+- upgraded Flutter to stable v3.7.11
+- when an album becomes empty, the folder will be deleted only if it is a non-app/common album
+- TV: section header focus/highlight
+
+### Fixed
+
+- permission confusion when removable volume changes
+- Viewer: flickering on first scale animation in some cases
+
## [v1.8.4] - 2023-03-17
### Added
@@ -115,7 +139,8 @@ All notable changes to this project will be documented in this file.
### Changed
-- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment`
+- editing description writes XMP `dc:description`, and clears Exif `ImageDescription`
+ / `UserComment`
- in the tag editor, tapping on applied tag applies it to all items instead of removing it
- pin app bar when selecting items
diff --git a/android/.gitignore b/android/.gitignore
index 0a741cb43..6f568019d 100644
--- a/android/.gitignore
+++ b/android/.gitignore
@@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle b/android/app/build.gradle
index eb27e5c2e..140e804f3 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -46,6 +46,16 @@ if (keystorePropertiesFile.exists()) {
android {
compileSdkVersion 33
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -148,6 +158,7 @@ android {
// which lead to: UnsatisfiedLinkError...couldn't find "libflutter.so"
// cf https://github.com/flutter/flutter/issues/37566#issuecomment-640879500
ndk {
+ //noinspection ChromeOsAbiSupport
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
@@ -183,9 +194,10 @@ repositories {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
- implementation 'androidx.core:core-ktx:1.9.0'
+ implementation "androidx.appcompat:appcompat:1.6.1"
+ implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
- implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
+ implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
@@ -193,9 +205,9 @@ dependencies {
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
- implementation 'com.github.bumptech.glide:glide:4.15.0'
+ implementation 'com.github.bumptech.glide:glide:4.15.1'
// SLF4J implementation for `mp4parser`
- implementation 'org.slf4j:slf4j-simple:2.0.6'
+ implementation 'org.slf4j:slf4j-simple:2.0.7'
// forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
@@ -210,7 +222,7 @@ dependencies {
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300'
kapt 'androidx.annotation:annotation:1.6.0'
- kapt 'com.github.bumptech.glide:compiler:4.15.0'
+ kapt 'com.github.bumptech.glide:compiler:4.15.1'
compileOnly rootProject.findProject(':streams_channel')
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ee04303e8..5b85aa256 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
Gradle v7.4 / Android Gradle Plugin v7.3.0 recommend:
- removing "package" from AndroidManifest.xml
- adding it as "namespace" in app/build.gradle
-This change eventually prevents building the app with Flutter v3.3.3.
+This change eventually prevents building the app with Flutter v3.7.11.
-->
{
when (val action = intent?.action) {
Intent.ACTION_MAIN -> {
+ if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
+ return hashMapOf(
+ INTENT_DATA_KEY_SAFE_MODE to true,
+ )
+ }
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
val filters = extractFiltersFromIntent(intent)
return hashMapOf(
@@ -393,7 +398,16 @@ open class MainActivity : FlutterFragmentActivity() {
)
.build()
- ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search))
+ val safeMode = ShortcutInfoCompat.Builder(this, "safeMode")
+ .setShortLabel(getString(R.string.safe_mode_shortcut_short_label))
+ .setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_safe_mode else R.drawable.ic_shortcut_safe_mode))
+ .setIntent(
+ Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
+ .putExtra(EXTRA_KEY_SAFE_MODE, true)
+ )
+ .build()
+
+ ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search, safeMode))
}
private fun onAnalysisCompleted() {
@@ -428,12 +442,14 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query"
+ const val INTENT_DATA_KEY_SAFE_MODE = "safeMode"
const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
+ const val EXTRA_KEY_SAFE_MODE = "safeMode"
const val EXTRA_KEY_WIDGET_ID = "widgetId"
// request code to pending runnable
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
index 0ec5ace5c..812b86e91 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
@@ -38,10 +38,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import org.mp4parser.IsoFile
-import org.mp4parser.PropertyBoxParserImpl
-import org.mp4parser.boxes.iso14496.part12.FreeBox
-import org.mp4parser.boxes.iso14496.part12.MediaDataBox
-import org.mp4parser.boxes.iso14496.part12.SampleTableBox
import java.io.FileInputStream
import java.io.IOException
@@ -341,23 +337,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
pfd.use {
FileInputStream(it.fileDescriptor).use { stream ->
stream.channel.use { channel ->
- val boxParser = PropertyBoxParserImpl().apply {
- val skippedTypes = listOf(
- // parsing `MediaDataBox` can take a long time
- MediaDataBox.TYPE,
- // parsing `SampleTableBox` or `FreeBox` may yield OOM
- SampleTableBox.TYPE, FreeBox.TYPE,
- // some files are padded with `0` but the parser does not stop, reads type "0000",
- // then a large size from following "0000", which may yield OOM
- "0000",
- )
- setBoxSkipper { type, size ->
- if (skippedTypes.contains(type)) return@setBoxSkipper true
- if (size > Mp4ParserHelper.BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
- false
- }
- }
- IsoFile(channel, boxParser).use { isoFile ->
+ IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile ->
isoFile.dumpBoxes(sb)
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
index f35eaaa39..8a6a0b8d3 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
@@ -42,6 +42,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
+ "canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
index 1358fdb69..feda79f93 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
@@ -160,9 +160,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
thisDirName = "Spherical Video"
metadataMap[thisDirName] = HashMap(GSpherical(bytes).describe())
}
+
QuickTimeMetadata.PROF_UUID -> {
// redundant with info derived on the Dart side
}
+
QuickTimeMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
@@ -187,6 +189,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
else -> {
val uuidPart = uuid.substringBefore('-')
thisDirName = "${dir.name} $uuidPart"
@@ -268,11 +271,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// skip `Geo double/ascii params`, as their content is split and presented through various GeoTIFF keys
ExifGeoTiffTags.TAG_GEO_DOUBLE_PARAMS,
ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> ArrayList()
+
else -> listOf(exifTagMapper(tag))
}
}?.let { geoTiffDirMap.putAll(it) }
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
}
+
mimeType == MimeTypes.DNG -> {
// split DNG tags in their own directory
val dngDirMap = metadataMap[DIR_DNG] ?: HashMap()
@@ -281,9 +286,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
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) })
}
}
+
dir.isPngTextDir() -> {
metadataMap.remove(thisDirName)
dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap()
@@ -332,6 +339,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
}
}
@@ -406,6 +414,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
if (isVideo(mimeType)) {
+ // `metadata-extractor` do not extract custom tags in user data box
+ val userDataDir = Mp4ParserHelper.getUserData(context, mimeType, uri)
+ if (userDataDir.isNotEmpty()) {
+ metadataMap[Metadata.DIR_MP4_USER_DATA] = userDataDir
+ }
+
// this is used as fallback when the video metadata cannot be found on the Dart side
// and to identify whether there is an accessible cover image
// do not include HEIC here
@@ -641,12 +655,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
MimeTypes.GIF -> {
// identification of animated GIF
if (metadata.containsDirectoryOfType(GifAnimationDirectory::class.java)) {
flags = flags or MASK_IS_ANIMATED
}
}
+
MimeTypes.WEBP -> {
// identification of animated WEBP
for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) {
@@ -655,6 +671,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
MimeTypes.TIFF -> {
// identification of GeoTIFF
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
@@ -1119,16 +1136,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
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 -> {
for (dir in metadata.getDirectoriesOfType(GpsDirectory::class.java)) {
dir.gpsDate?.let { dateMillis = it.time }
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
index c2e9d364d..c5f319dd2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
@@ -101,7 +101,17 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
endOfStream()
}
- private fun createFile() {
+ private suspend fun safeStartActivityForResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) {
+ if (intent.resolveActivity(activity.packageManager) != null) {
+ MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied)
+ activity.startActivityForResult(intent, requestCode)
+ } else {
+ MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
+ onDenied()
+ }
+ }
+
+ private suspend fun createFile() {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
@@ -116,12 +126,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
return
}
- val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = mimeType
- putExtra(Intent.EXTRA_TITLE, name)
- }
- MainActivity.pendingStorageAccessResultHandlers[MainActivity.CREATE_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
+ fun onGranted(uri: Uri) {
ioScope.launch {
try {
// truncate is necessary when overwriting a longer file
@@ -134,13 +139,20 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
}
endOfStream()
}
- }, {
+ }
+
+ fun onDenied() {
success(null)
endOfStream()
- })
- activity.startActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST)
- }
+ }
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = mimeType
+ putExtra(Intent.EXTRA_TITLE, name)
+ }
+ safeStartActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
+ }
private suspend fun openFile() {
@SuppressLint("ObsoleteSdkInt")
@@ -178,13 +190,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
addCategory(Intent.CATEGORY_OPENABLE)
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
}
- if (intent.resolveActivity(activity.packageManager) != null) {
- MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied)
- activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
- } else {
- MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
- onDenied()
- }
+ safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
}
private fun pickCollectionFilters() {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
index 54ac83280..699dc4009 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
@@ -33,6 +33,7 @@ object Metadata {
const val DIR_DNG = "DNG" // custom
const val DIR_EXIF_GEOTIFF = "GeoTIFF" // custom
const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom
+ const val DIR_MP4_USER_DATA = "User Data" // custom
// types of metadata
const val TYPE_COMMENT = "comment"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt
index 8edb548df..8bb9e096c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt
@@ -2,11 +2,22 @@ package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
+import android.util.Log
+import deckers.thibault.aves.utils.LogUtils
+import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
+import deckers.thibault.aves.utils.toByteArray
+import deckers.thibault.aves.utils.toHex
import org.mp4parser.*
+import org.mp4parser.boxes.UnknownBox
import org.mp4parser.boxes.UserBox
+import org.mp4parser.boxes.apple.AppleCoverBox
import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox
+import org.mp4parser.boxes.apple.AppleItemListBox
+import org.mp4parser.boxes.apple.AppleVariableSignedIntegerBox
+import org.mp4parser.boxes.apple.Utf8AppleDataBox
import org.mp4parser.boxes.iso14496.part12.*
+import org.mp4parser.boxes.threegpp.ts26244.AuthorBox
import org.mp4parser.support.AbstractBox
import org.mp4parser.support.Matrix
import org.mp4parser.tools.Path
@@ -15,8 +26,10 @@ import java.io.FileInputStream
import java.nio.channels.Channels
object Mp4ParserHelper {
+ private val LOG_TAG = LogUtils.createTag()
+
// arbitrary size to detect boxes that may yield an OOM
- const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
+ private const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List> {
// we can skip uninteresting boxes with a seekable data source
@@ -214,10 +227,8 @@ object Mp4ParserHelper {
sb.appendLine("${"\t".repeat(indent)}[$boxType] ${box.javaClass.simpleName}")
box.dumpBoxes(sb, indent + 1)
}
- is UserBox -> {
- val userTypeHex = box.userType.joinToString("") { "%02x".format(it) }
- sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=$userTypeHex $box")
- }
+
+ is UserBox -> sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=${box.userType.toHex()} $box")
else -> sb.appendLine("${"\t".repeat(indent)}[$boxType] $box")
}
} catch (e: Exception) {
@@ -227,10 +238,127 @@ object Mp4ParserHelper {
}
fun Box.toBytes(): ByteArray {
+ if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
val stream = ByteArrayOutputStream(size.toInt())
Channels.newChannel(stream).use { getBox(it) }
return stream.toByteArray()
}
+
+ fun metadataBoxParser() = PropertyBoxParserImpl().apply {
+ val skippedTypes = listOf(
+ // parsing `MediaDataBox` can take a long time
+ MediaDataBox.TYPE,
+ // parsing `SampleTableBox` or `FreeBox` may yield OOM
+ SampleTableBox.TYPE, FreeBox.TYPE,
+ // some files are padded with `0` but the parser does not stop, reads type "0000",
+ // then a large size from following "0000", which may yield OOM
+ "0000",
+ )
+ setBoxSkipper { type, size ->
+ if (skippedTypes.contains(type)) return@setBoxSkipper true
+ if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
+ false
+ }
+ }
+
+ fun getUserData(
+ context: Context,
+ mimeType: String,
+ uri: Uri,
+ ): MutableMap {
+ val fields = HashMap()
+ if (mimeType != MimeTypes.MP4) return fields
+ try {
+ // we can skip uninteresting boxes with a seekable data source
+ val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
+ pfd.use {
+ FileInputStream(it.fileDescriptor).use { stream ->
+ stream.channel.use { channel ->
+ // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
+ IsoFile(channel, metadataBoxParser()).use { isoFile ->
+ val userDataBox = Path.getPath(isoFile.movieBox, UserDataBox.TYPE)
+ fields.putAll(extractBoxFields(userDataBox))
+ }
+ }
+ }
+ }
+ } catch (e: NoClassDefFoundError) {
+ Log.w(LOG_TAG, "failed to parse MP4 for mimeType=$mimeType uri=$uri", e)
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to get User Data box by MP4 parser for mimeType=$mimeType uri=$uri", e)
+ }
+ return fields
+ }
+
+ private fun extractBoxFields(container: Container): HashMap {
+ val fields = HashMap()
+ for (box in container.boxes) {
+ if (box is AbstractBox && !box.isParsed) {
+ box.parseDetails()
+ }
+ val type = box.type
+ val key = boxTypeMetadataKey(type)
+ when (box) {
+ is AuthorBox -> fields[key] = box.author
+ is AppleCoverBox -> fields[key] = "[${box.coverData.size} bytes]"
+ is AppleGPSCoordinatesBox -> fields[key] = box.value
+ is AppleItemListBox -> fields.putAll(extractBoxFields(box))
+ is AppleVariableSignedIntegerBox -> fields[key] = box.value.toString()
+ is Utf8AppleDataBox -> fields[key] = box.value
+
+ is HandlerBox -> {}
+ is MetaBox -> {
+ val handlerBox = Path.getPath(box, HandlerBox.TYPE).apply { parseDetails() }
+ when (val handlerType = handlerBox?.handlerType ?: MetaBox.TYPE) {
+ "mdir" -> fields.putAll(extractBoxFields(box))
+ else -> fields.putAll(extractBoxFields(box).map { Pair("$handlerType/${it.key}", it.value) }.toMap())
+ }
+ }
+
+ is UnknownBox -> {
+ val byteBuffer = box.data
+ val remaining = byteBuffer.remaining()
+ if (remaining > 512) {
+ fields[key] = "[$remaining bytes]"
+ } else {
+ val bytes = byteBuffer.toByteArray()
+ when (type) {
+ "SDLN",
+ "smrd" -> fields[key] = String(bytes)
+
+ else -> fields[key] = "0x${bytes.toHex()}"
+ }
+ }
+ }
+
+ else -> fields[key] = box.toString()
+ }
+ }
+ return fields
+ }
+
+ // cf https://exiftool.org/TagNames/QuickTime.html
+ private fun boxTypeMetadataKey(type: String) = when (type) {
+ "auth" -> "Author"
+ "catg" -> "Category"
+ "covr" -> "Cover Art"
+ "keyw" -> "Keyword"
+ "mcvr" -> "Preview Image"
+ "pcst" -> "Podcast"
+ "SDLN" -> "Play Mode"
+ "stik" -> "Media Type"
+ "©alb" -> "Album"
+ "©ART" -> "Artist"
+ "©aut" -> "Author"
+ "©cmt" -> "Comment"
+ "©day" -> "Year"
+ "©des" -> "Description"
+ "©gen" -> "Genre"
+ "©nam" -> "Title"
+ "©too" -> "Encoder"
+ "©xyz" -> "GPS Coordinates"
+ else -> type
+ }
}
class Mp4TooLargeException(val type: String, message: String) : RuntimeException(message)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt
index 6341ffe53..0c198ad29 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt
@@ -1,5 +1,6 @@
package deckers.thibault.aves.metadata
+import deckers.thibault.aves.utils.toHex
import java.math.BigInteger
import java.nio.charset.Charset
import java.util.*
@@ -51,7 +52,7 @@ object QuickTimeMetadata {
// 0x01: string
0x01 -> String(payload, Charset.forName("UTF-16BE")).trim()
// 0x101: artwork/icon
- else -> "0x${payload.joinToString("") { "%02x".format(it) }}"
+ else -> "0x${payload.toHex()}"
}
val blockTypeString = when (blockType) {
@@ -61,7 +62,7 @@ object QuickTimeMetadata {
0x0A -> "Track property"
0x0B -> "Time zone"
0x0C -> "Modification Time"
- else -> "0x${"%02x".format(blockType)}"
+ else -> "0x${blockType.toByte().toHex()}"
}
blocks.add(
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
index f618c0df1..cb721c9c1 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
@@ -21,13 +21,9 @@ import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import org.mp4parser.IsoFile
-import org.mp4parser.PropertyBoxParserImpl
import org.mp4parser.boxes.UserBox
-import org.mp4parser.boxes.iso14496.part12.FreeBox
-import org.mp4parser.boxes.iso14496.part12.MediaDataBox
-import org.mp4parser.boxes.iso14496.part12.SampleTableBox
import java.io.FileInputStream
-import java.util.*
+import java.util.TimeZone
object XMP {
private val LOG_TAG = LogUtils.createTag()
@@ -156,26 +152,12 @@ object XMP {
pfd.use {
FileInputStream(it.fileDescriptor).use { stream ->
stream.channel.use { channel ->
- val boxParser = PropertyBoxParserImpl().apply {
- val skippedTypes = listOf(
- // parsing `MediaDataBox` can take a long time
- MediaDataBox.TYPE,
- // parsing `SampleTableBox` or `FreeBox` may yield OOM
- SampleTableBox.TYPE, FreeBox.TYPE,
- )
- setBoxSkipper { type, size ->
- if (skippedTypes.contains(type)) return@setBoxSkipper true
- if (size > Mp4ParserHelper.BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
- false
- }
- }
- // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
-
// TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent,
// because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`,
// so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes.
- IsoFile(channel, boxParser).use { isoFile ->
+ // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
+ IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile ->
isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
val boxSize = box.size
if (MemoryUtils.canAllocate(boxSize)) {
@@ -193,6 +175,8 @@ object XMP {
}
}
}
+ } catch (e: NoClassDefFoundError) {
+ Log.w(LOG_TAG, "failed to parse MP4 for mimeType=$mimeType uri=$uri", e)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get XMP by MP4 parser for mimeType=$mimeType uri=$uri", e)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 05fb4df29..c84596bea 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -815,6 +815,8 @@ abstract class ImageProvider {
}
}
}
+ } catch (e: NoClassDefFoundError) {
+ callback.onFailure(e)
} catch (e: Exception) {
callback.onFailure(e)
return false
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/ByteUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ByteUtils.kt
new file mode 100644
index 000000000..f45236ba4
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ByteUtils.kt
@@ -0,0 +1,13 @@
+package deckers.thibault.aves.utils
+
+import java.nio.ByteBuffer
+
+fun ByteBuffer.toByteArray(): ByteArray {
+ val bytes = ByteArray(remaining())
+ get(bytes, 0, bytes.size)
+ return bytes
+}
+
+fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() }
+
+fun Byte.toHex(): String = "%02x".format(this)
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt
index dc1377cf0..5275e0ed3 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt
@@ -17,7 +17,12 @@ import kotlin.coroutines.suspendCoroutine
object FlutterUtils {
private val LOG_TAG = LogUtils.createTag()
- suspend fun initFlutterEngine(context: Context, sharedPreferencesKey: String, callbackHandleKey: String, engineSetter: (engine: FlutterEngine) -> Unit) {
+ suspend fun initFlutterEngine(
+ context: Context,
+ sharedPreferencesKey: String,
+ callbackHandleKey: String,
+ engineSetter: (engine: FlutterEngine) -> Unit,
+ ) {
val callbackHandle = context.getSharedPreferences(sharedPreferencesKey, Context.MODE_PRIVATE).getLong(callbackHandleKey, 0)
if (callbackHandle == 0L) {
Log.e(LOG_TAG, "failed to retrieve registered callback handle for sharedPreferencesKey=$sharedPreferencesKey callbackHandleKey=$callbackHandleKey")
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
index 0996f0a5d..6863489fd 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
@@ -195,11 +195,8 @@ object PermissionManager {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// cf https://developer.android.com/about/versions/11/privacy/storage#directory-access
dirs.add(Environment.DIRECTORY_DOWNLOADS)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // by observation, no documentation
- dirs.add("Android")
- }
+ // depends on device, no documentation
+ dirs.add("Android")
}
return dirs
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
index c73694c99..3de3ca4c0 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
@@ -33,11 +33,23 @@ import java.util.regex.Pattern
object StorageUtils {
private val LOG_TAG = LogUtils.createTag()
- // from `DocumentsContract`
+ private const val SCHEME_CONTENT = ContentResolver.SCHEME_CONTENT
+
+ // cf DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY
private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
+
+ // cf DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
private const val EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary"
- private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
+ private const val TREE_URI_ROOT = "$SCHEME_CONTENT://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
+
+ private val MEDIA_STORE_VOLUME_EXTERNAL = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.VOLUME_EXTERNAL else "external"
+
+ // TODO TLAD get it from `MediaStore.Images.Media.EXTERNAL_CONTENT_URI`?
+ private val IMAGE_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/images/"
+
+ // TODO TLAD get it from `MediaStore.Video.Media.EXTERNAL_CONTENT_URI`?
+ private val VIDEO_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/video/"
private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+")
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
@@ -348,7 +360,17 @@ object StorageUtils {
// fallback when UUID does not appear in the SD card volume path
val primaryVolumePath = getPrimaryVolumePath(context)
- getVolumePaths(context).firstOrNull { it != primaryVolumePath }?.let { return it }
+ getVolumePaths(context).firstOrNull { volumePath ->
+ if (volumePath == primaryVolumePath) {
+ false
+ } else {
+ // exclude volumes that use regular naming scheme with UUID in them
+ // to prevent returning path with the UUID of a new volume
+ // when the argument is the UUID of an obsolete volume
+ val volumeUuid = volumePath.split(File.separator).lastOrNull { it.isNotEmpty() }
+ !(volumeUuid == null || volumeUuid.matches(UUID_PATTERN))
+ }
+ }?.let { return it }
Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid")
return null
@@ -535,7 +557,7 @@ object StorageUtils {
uri ?: return false
// a URI's authority is [userinfo@]host[:port]
// but we only want the host when comparing to Media Store's "authority"
- return ContentResolver.SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)
+ return SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)
}
fun getOriginalUri(context: Context, uri: Uri): Uri {
@@ -544,7 +566,7 @@ object StorageUtils {
val path = uri.path
path ?: return uri
// from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException`
- if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) {
+ if (path.startsWith(IMAGE_PATH_ROOT) || path.startsWith(VIDEO_PATH_ROOT)) {
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return MediaStore.setRequireOriginal(uri)
@@ -601,7 +623,7 @@ object StorageUtils {
return uri
}
- // Build a typical `images` or `videos` content URI from the original content ID.
+ // Build a typical `images` or `video` content URI from the original content ID.
// We cannot safely apply this to a `file` content URI, as it may point to a file not indexed
// by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI.
private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? {
diff --git a/android/app/src/main/res/drawable-v21/ic_shortcut_safe_mode.xml b/android/app/src/main/res/drawable-v21/ic_shortcut_safe_mode.xml
new file mode 100644
index 000000000..dc918373c
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/ic_shortcut_safe_mode.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable-v26/ic_shortcut_safe_mode_foreground.xml b/android/app/src/main/res/drawable-v26/ic_shortcut_safe_mode_foreground.xml
new file mode 100644
index 000000000..fe957d665
--- /dev/null
+++ b/android/app/src/main/res/drawable-v26/ic_shortcut_safe_mode_foreground.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_safe_mode.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_safe_mode.xml
new file mode 100644
index 000000000..40f9ff006
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_shortcut_safe_mode.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-cs/strings.xml b/android/app/src/main/res/values-cs/strings.xml
index 171ff70fa..02638c56a 100644
--- a/android/app/src/main/res/values-cs/strings.xml
+++ b/android/app/src/main/res/values-cs/strings.xml
@@ -9,4 +9,5 @@
Prohledávání médií
Zastavit
Fotorámeček
+ Bezpečný režim
\ No newline at end of file
diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml
index 1fa49e8a6..1ecb251c5 100644
--- a/android/app/src/main/res/values-de/strings.xml
+++ b/android/app/src/main/res/values-de/strings.xml
@@ -9,4 +9,5 @@
Bilder & Videos scannen
Medien scannen
Abbrechen
+ Sicherer Modus
\ No newline at end of file
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
index 3c656e5b6..b2364e4dd 100644
--- a/android/app/src/main/res/values-es/strings.xml
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -9,4 +9,5 @@
Explorar imágenes & videos
Explorando medios
Anular
+ Modo seguro
\ No newline at end of file
diff --git a/android/app/src/main/res/values-eu/strings.xml b/android/app/src/main/res/values-eu/strings.xml
index ca50b6ea6..92a69bac7 100644
--- a/android/app/src/main/res/values-eu/strings.xml
+++ b/android/app/src/main/res/values-eu/strings.xml
@@ -9,4 +9,5 @@
Gelditu
Media eskaneatzen
Aves
+ Modu segurua
\ No newline at end of file
diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml
index 6104e3059..0243698d8 100644
--- a/android/app/src/main/res/values-fr/strings.xml
+++ b/android/app/src/main/res/values-fr/strings.xml
@@ -9,4 +9,5 @@
Analyse des images & vidéos
Analyse des images
Annuler
+ Mode sans échec
\ No newline at end of file
diff --git a/android/app/src/main/res/values-hi/strings.xml b/android/app/src/main/res/values-hi/strings.xml
new file mode 100644
index 000000000..615529ee1
--- /dev/null
+++ b/android/app/src/main/res/values-hi/strings.xml
@@ -0,0 +1,12 @@
+
+
+ मीडिया जाँचा जा राहा है
+ रोके
+ फोटो फ्रेम
+ वॉलपेपर
+ खोजें
+ मीडिया जाँचे
+ ऐवीज
+ वीडियो
+ छवि & वीडियो जाँचे
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-hu/strings.xml b/android/app/src/main/res/values-hu/strings.xml
new file mode 100644
index 000000000..20b83b718
--- /dev/null
+++ b/android/app/src/main/res/values-hu/strings.xml
@@ -0,0 +1,8 @@
+
+
+ Aves
+ Háttérkép
+ Keresés
+ Videók
+ Állj
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-id/strings.xml b/android/app/src/main/res/values-id/strings.xml
index 4a074015a..c8067fc0e 100644
--- a/android/app/src/main/res/values-id/strings.xml
+++ b/android/app/src/main/res/values-id/strings.xml
@@ -9,4 +9,5 @@
Pindai gambar & video
Memindai media
Berhenti
+ Mode aman
\ No newline at end of file
diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml
index cffcd1296..1f254b126 100644
--- a/android/app/src/main/res/values-it/strings.xml
+++ b/android/app/src/main/res/values-it/strings.xml
@@ -9,4 +9,5 @@
Scansione immagini & videos
Scansione in corso
Annulla
+ Modalità provvisoria
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml
index d7f678e4b..7204d99c5 100644
--- a/android/app/src/main/res/values-ja/strings.xml
+++ b/android/app/src/main/res/values-ja/strings.xml
@@ -9,4 +9,5 @@
画像と動画をスキャン
メディアをスキャン中
停止
+ セーフモード
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml
index a226d003b..0c064c633 100644
--- a/android/app/src/main/res/values-ko/strings.xml
+++ b/android/app/src/main/res/values-ko/strings.xml
@@ -9,4 +9,5 @@
사진과 동영상 분석
미디어 분석
취소
+ 안전 모드
\ No newline at end of file
diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml
index fb6d8bcfb..d1759646b 100644
--- a/android/app/src/main/res/values-nb-rNO/strings.xml
+++ b/android/app/src/main/res/values-nb-rNO/strings.xml
@@ -9,4 +9,5 @@
Bakgrunnsbilde
Søk
Stopp
+ Trygt modus
\ No newline at end of file
diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml
index 28f72abad..ed2f0b129 100644
--- a/android/app/src/main/res/values-pl/strings.xml
+++ b/android/app/src/main/res/values-pl/strings.xml
@@ -9,4 +9,5 @@
Zatrzymaj
Aves
Tapeta
+ Tryb bezpieczny
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ro/strings.xml b/android/app/src/main/res/values-ro/strings.xml
index 75837c05f..bb3475bf8 100644
--- a/android/app/src/main/res/values-ro/strings.xml
+++ b/android/app/src/main/res/values-ro/strings.xml
@@ -9,4 +9,5 @@
Scanarea suporturilor
Stop
Căutare
+ Modul de siguranță
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index 65f801bc9..84259422b 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -9,4 +9,5 @@
Сканировать изображения и видео
Сканирование медиа
Стоп
+ Безопасный режим
\ No newline at end of file
diff --git a/android/app/src/main/res/values-uk/strings.xml b/android/app/src/main/res/values-uk/strings.xml
index ddcd946ed..b6c0c0a68 100644
--- a/android/app/src/main/res/values-uk/strings.xml
+++ b/android/app/src/main/res/values-uk/strings.xml
@@ -9,4 +9,5 @@
Стоп
Фоторамка
Сканування медіа
+ Безпечний режим
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index cb2b215c0..f0f317c96 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -3,6 +3,7 @@
Aves
Photo Frame
Wallpaper
+ Safe mode
Search
Videos
Media scan
diff --git a/android/build.gradle b/android/build.gradle
index f15f63e03..c9ccd0367 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,7 +1,6 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
- kotlin_version = '1.7.20'
+ kotlin_version = '1.8.0'
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") }
useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") }
@@ -18,8 +17,7 @@ buildscript {
}
dependencies {
- // TODO TLAD upgrade Android Gradle plugin >=7.3 when this is fixed: https://github.com/flutter/flutter/issues/115100
- classpath 'com.android.tools.build:gradle:7.2.2'
+ classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
if (useCrashlytics) {
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 3a528264c..3c472b99c 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,4 +1,3 @@
-#Thu Oct 22 10:54:33 KST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de/short_description.txt
index 9f8c85a29..66ef72309 100644
--- a/fastlane/metadata/android/de/short_description.txt
+++ b/fastlane/metadata/android/de/short_description.txt
@@ -1 +1 @@
-Galerie und Metadata Explorer
\ No newline at end of file
+Galerie und Metadaten Explorer
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/96.txt b/fastlane/metadata/android/en-US/changelogs/96.txt
new file mode 100644
index 000000000..596f60dac
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/96.txt
@@ -0,0 +1,5 @@
+In v1.8.5:
+- navigate states for some countries (requires rescan)
+- group Samsung and Sony bursts
+- lock viewer when watching videos
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/9601.txt b/fastlane/metadata/android/en-US/changelogs/9601.txt
new file mode 100644
index 000000000..596f60dac
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/9601.txt
@@ -0,0 +1,5 @@
+In v1.8.5:
+- navigate states for some countries (requires rescan)
+- group Samsung and Sony bursts
+- lock viewer when watching videos
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt
new file mode 100644
index 000000000..6c92748f8
--- /dev/null
+++ b/fastlane/metadata/android/hi/full_description.txt
@@ -0,0 +1,5 @@
+Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files.
+
+Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
+
+Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker.
\ No newline at end of file
diff --git a/fastlane/metadata/android/hi/short_description.txt b/fastlane/metadata/android/hi/short_description.txt
new file mode 100644
index 000000000..ba793e562
--- /dev/null
+++ b/fastlane/metadata/android/hi/short_description.txt
@@ -0,0 +1 @@
+गैलरी और मोटाडेटा एक्स्प्लोरर
\ No newline at end of file
diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt
new file mode 100644
index 000000000..6c92748f8
--- /dev/null
+++ b/fastlane/metadata/android/hu/full_description.txt
@@ -0,0 +1,5 @@
+Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files.
+
+Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
+
+Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker.
\ No newline at end of file
diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt
new file mode 100644
index 000000000..8c9445bd5
--- /dev/null
+++ b/fastlane/metadata/android/hu/short_description.txt
@@ -0,0 +1 @@
+Gallery and metadata explorer
\ No newline at end of file
diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt
index a095036ca..9cae013fe 100644
--- a/fastlane/metadata/android/pt-BR/full_description.txt
+++ b/fastlane/metadata/android/pt-BR/full_description.txt
@@ -2,4 +2,4 @@
Navegação e pesquisa é uma parte importante do Aves. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
-Aves integra com Android (de API 19 para 33, i.e. de KitKat para Android 13) com recursos como atalhos de apps e pesquisa global manipulação. Também funciona como um visualizador e selecionador de mídia.
\ No newline at end of file
+Aves integra com Android (de KitKat até Android 13, incluindo TVs Android) com recursos como widgets, atalhos de apps, protetor de tela e pesquisa global. Também funciona como um visualizador e selecionador de mídia.
\ No newline at end of file
diff --git a/lib/convert/convert.dart b/lib/convert/convert.dart
new file mode 100644
index 000000000..43780fce2
--- /dev/null
+++ b/lib/convert/convert.dart
@@ -0,0 +1,3 @@
+export 'metadata/date_field_source.dart';
+export 'metadata/fields.dart';
+export 'metadata/metadata_type.dart';
diff --git a/lib/convert/metadata/date_field_source.dart b/lib/convert/metadata/date_field_source.dart
new file mode 100644
index 000000000..1bc1ee1bd
--- /dev/null
+++ b/lib/convert/metadata/date_field_source.dart
@@ -0,0 +1,18 @@
+import 'package:aves_model/aves_model.dart';
+
+extension ExtraDateFieldSourceConvert on DateFieldSource {
+ MetadataField? toMetadataField() {
+ switch (this) {
+ case DateFieldSource.fileModifiedDate:
+ return null;
+ case DateFieldSource.exifDate:
+ return MetadataField.exifDate;
+ case DateFieldSource.exifDateOriginal:
+ return MetadataField.exifDateOriginal;
+ case DateFieldSource.exifDateDigitized:
+ return MetadataField.exifDateDigitized;
+ case DateFieldSource.exifGpsDate:
+ return MetadataField.exifGpsDatestamp;
+ }
+ }
+}
diff --git a/lib/model/metadata/fields.dart b/lib/convert/metadata/fields.dart
similarity index 65%
rename from lib/model/metadata/fields.dart
rename to lib/convert/metadata/fields.dart
index 17a344aa6..fd677c105 100644
--- a/lib/model/metadata/fields.dart
+++ b/lib/convert/metadata/fields.dart
@@ -1,87 +1,6 @@
-import 'package:aves/model/metadata/enums/enums.dart';
+import 'package:aves_model/aves_model.dart';
-enum MetadataField {
- exifDate,
- exifDateOriginal,
- exifDateDigitized,
- exifGpsAltitude,
- exifGpsAltitudeRef,
- exifGpsAreaInformation,
- exifGpsDatestamp,
- exifGpsDestBearing,
- exifGpsDestBearingRef,
- exifGpsDestDistance,
- exifGpsDestDistanceRef,
- exifGpsDestLatitude,
- exifGpsDestLatitudeRef,
- exifGpsDestLongitude,
- exifGpsDestLongitudeRef,
- exifGpsDifferential,
- exifGpsDOP,
- exifGpsHPositioningError,
- exifGpsImgDirection,
- exifGpsImgDirectionRef,
- exifGpsLatitude,
- exifGpsLatitudeRef,
- exifGpsLongitude,
- exifGpsLongitudeRef,
- exifGpsMapDatum,
- exifGpsMeasureMode,
- exifGpsProcessingMethod,
- exifGpsSatellites,
- exifGpsSpeed,
- exifGpsSpeedRef,
- exifGpsStatus,
- exifGpsTimestamp,
- exifGpsTrack,
- exifGpsTrackRef,
- exifGpsVersionId,
- exifImageDescription,
- exifUserComment,
- mp4GpsCoordinates,
- mp4RotationDegrees,
- mp4Xmp,
- xmpXmpCreateDate,
-}
-
-class MetadataFields {
- static const Set exifGpsFields = {
- MetadataField.exifGpsAltitude,
- MetadataField.exifGpsAltitudeRef,
- MetadataField.exifGpsAreaInformation,
- MetadataField.exifGpsDatestamp,
- MetadataField.exifGpsDestBearing,
- MetadataField.exifGpsDestBearingRef,
- MetadataField.exifGpsDestDistance,
- MetadataField.exifGpsDestDistanceRef,
- MetadataField.exifGpsDestLatitude,
- MetadataField.exifGpsDestLatitudeRef,
- MetadataField.exifGpsDestLongitude,
- MetadataField.exifGpsDestLongitudeRef,
- MetadataField.exifGpsDifferential,
- MetadataField.exifGpsDOP,
- MetadataField.exifGpsHPositioningError,
- MetadataField.exifGpsImgDirection,
- MetadataField.exifGpsImgDirectionRef,
- MetadataField.exifGpsLatitude,
- MetadataField.exifGpsLatitudeRef,
- MetadataField.exifGpsLongitude,
- MetadataField.exifGpsLongitudeRef,
- MetadataField.exifGpsMapDatum,
- MetadataField.exifGpsMeasureMode,
- MetadataField.exifGpsProcessingMethod,
- MetadataField.exifGpsSatellites,
- MetadataField.exifGpsSpeed,
- MetadataField.exifGpsSpeedRef,
- MetadataField.exifGpsStatus,
- MetadataField.exifGpsTimestamp,
- MetadataField.exifGpsTrack,
- MetadataField.exifGpsTrackRef,
- MetadataField.exifGpsVersionId,
- };
-}
-
-extension ExtraMetadataField on MetadataField {
+extension ExtraMetadataFieldConvert on MetadataField {
MetadataType get type {
switch (this) {
case MetadataField.exifDate:
@@ -228,21 +147,4 @@ extension ExtraMetadataField on MetadataField {
return null;
}
}
-
- String get title {
- switch (this) {
- case MetadataField.exifDate:
- return 'Exif date';
- case MetadataField.exifDateOriginal:
- return 'Exif original date';
- case MetadataField.exifDateDigitized:
- return 'Exif digitized date';
- case MetadataField.exifGpsDatestamp:
- return 'Exif GPS date';
- case MetadataField.xmpXmpCreateDate:
- return 'XMP xmp:CreateDate';
- default:
- return name;
- }
- }
}
diff --git a/lib/convert/metadata/metadata_type.dart b/lib/convert/metadata/metadata_type.dart
new file mode 100644
index 000000000..63259e7ba
--- /dev/null
+++ b/lib/convert/metadata/metadata_type.dart
@@ -0,0 +1,28 @@
+import 'package:aves_model/aves_model.dart';
+
+extension ExtraMetadataTypeConvert on MetadataType {
+ String get toPlatform {
+ switch (this) {
+ case MetadataType.comment:
+ return 'comment';
+ case MetadataType.exif:
+ return 'exif';
+ case MetadataType.iccProfile:
+ return 'icc_profile';
+ case MetadataType.iptc:
+ return 'iptc';
+ case MetadataType.jfif:
+ return 'jfif';
+ case MetadataType.jpegAdobe:
+ return 'jpeg_adobe';
+ case MetadataType.jpegDucky:
+ return 'jpeg_ducky';
+ case MetadataType.mp4:
+ return 'mp4';
+ case MetadataType.photoshopIrb:
+ return 'photoshop_irb';
+ case MetadataType.xmp:
+ return 'xmp';
+ }
+ }
+}
diff --git a/lib/geo/states.dart b/lib/geo/states.dart
new file mode 100644
index 000000000..c7b01b3e8
--- /dev/null
+++ b/lib/geo/states.dart
@@ -0,0 +1,140 @@
+import 'package:aves/ref/unicode.dart';
+import 'package:country_code/country_code.dart';
+
+class GeoStates {
+ static final aus = CountryCode.AU.alpha2;
+ static final gbr = CountryCode.GB.alpha2;
+ static final ind = CountryCode.IN.alpha2;
+ static final usa = CountryCode.US.alpha2;
+
+ static final Set stateCountryCodes = {
+ aus,
+ gbr,
+ ind,
+ usa,
+ };
+
+ static final stateCodesByCountryCode = {
+ aus: EmojiStateCodes.aus,
+ gbr: EmojiStateCodes.gbr,
+ ind: EmojiStateCodes.ind,
+ usa: EmojiStateCodes.usa,
+ };
+
+ static const stateCodeByName = {
+ ..._australiaEnglish,
+ ..._indiaEnglish,
+ ..._unitedKingdomEnglish,
+ ..._unitedStatesEnglish,
+ };
+
+ static const _australiaEnglish = {
+ 'Australian Capital Territory': EmojiStateCodes.auAustralianCapitalTerritory,
+ 'New South Wales': EmojiStateCodes.auNewSouthWales,
+ 'Northern Territory': EmojiStateCodes.auNorthernTerritory,
+ 'Queensland': EmojiStateCodes.auQueensland,
+ 'South Australia': EmojiStateCodes.auSouthAustralia,
+ 'Tasmania': EmojiStateCodes.auTasmania,
+ 'Victoria': EmojiStateCodes.auVictoria,
+ 'Western Australia': EmojiStateCodes.auWesternAustralia,
+ };
+
+ static const _indiaEnglish = {
+ 'Andaman and Nicobar Islands': EmojiStateCodes.inAndamanAndNicobarIslands,
+ 'Andhra Pradesh': EmojiStateCodes.inAndhraPradesh,
+ 'Arunachal Pradesh': EmojiStateCodes.inArunachalPradesh,
+ 'Assam': EmojiStateCodes.inAssam,
+ 'Bihar': EmojiStateCodes.inBihar,
+ 'Chandigarh': EmojiStateCodes.inChandigarh,
+ 'Chhattisgarh': EmojiStateCodes.inChhattisgarh,
+ 'Daman and Diu': EmojiStateCodes.inDamanAndDiu,
+ 'Delhi': EmojiStateCodes.inDelhi,
+ 'Dadra and Nagar Haveli': EmojiStateCodes.inDadraAndNagarHaveli,
+ 'Goa': EmojiStateCodes.inGoa,
+ 'Gujarat': EmojiStateCodes.inGujarat,
+ 'Himachal Pradesh': EmojiStateCodes.inHimachalPradesh,
+ 'Haryana': EmojiStateCodes.inHaryana,
+ 'Jharkhand': EmojiStateCodes.inJharkhand,
+ 'Jammu and Kashmir': EmojiStateCodes.inJammuAndKashmir,
+ 'Karnataka': EmojiStateCodes.inKarnataka,
+ 'Kerala': EmojiStateCodes.inKerala,
+ 'Lakshadweep': EmojiStateCodes.inLakshadweep,
+ 'Maharashtra': EmojiStateCodes.inMaharashtra,
+ 'Meghalaya': EmojiStateCodes.inMeghalaya,
+ 'Manipur': EmojiStateCodes.inManipur,
+ 'Madhya Pradesh': EmojiStateCodes.inMadhyaPradesh,
+ 'Mizoram': EmojiStateCodes.inMizoram,
+ 'Nagaland': EmojiStateCodes.inNagaland,
+ 'Odisha': EmojiStateCodes.inOdisha,
+ 'Punjab': EmojiStateCodes.inPunjab,
+ 'Puducherry': EmojiStateCodes.inPuducherry,
+ 'Rajasthan': EmojiStateCodes.inRajasthan,
+ 'Sikkim': EmojiStateCodes.inSikkim,
+ 'Telangana': EmojiStateCodes.inTelangana,
+ 'Tamil Nadu': EmojiStateCodes.inTamilNadu,
+ 'Tripura': EmojiStateCodes.inTripura,
+ 'Uttar Pradesh': EmojiStateCodes.inUttarPradesh,
+ 'Uttarakhand': EmojiStateCodes.inUttarakhand,
+ 'West Bengal': EmojiStateCodes.inWestBengal,
+ };
+
+ static const _unitedKingdomEnglish = {
+ 'England': EmojiStateCodes.gbEngland,
+ 'Northern Ireland': EmojiStateCodes.gbNorthernIreland,
+ 'Scotland': EmojiStateCodes.gbScotland,
+ 'Wales': EmojiStateCodes.gbWales,
+ };
+
+ static const _unitedStatesEnglish = {
+ 'Alabama': EmojiStateCodes.usAlabama,
+ 'Alaska': EmojiStateCodes.usAlaska,
+ 'Arizona': EmojiStateCodes.usArizona,
+ 'Arkansas': EmojiStateCodes.usArkansas,
+ 'California': EmojiStateCodes.usCalifornia,
+ 'Colorado': EmojiStateCodes.usColorado,
+ 'Connecticut': EmojiStateCodes.usConnecticut,
+ 'Delaware': EmojiStateCodes.usDelaware,
+ 'Florida': EmojiStateCodes.usFlorida,
+ 'Georgia': EmojiStateCodes.usGeorgia,
+ 'Hawaii': EmojiStateCodes.usHawaii,
+ 'Idaho': EmojiStateCodes.usIdaho,
+ 'Illinois': EmojiStateCodes.usIllinois,
+ 'Indiana': EmojiStateCodes.usIndiana,
+ 'Iowa': EmojiStateCodes.usIowa,
+ 'Kansas': EmojiStateCodes.usKansas,
+ 'Kentucky': EmojiStateCodes.usKentucky,
+ 'Louisiana': EmojiStateCodes.usLouisiana,
+ 'Maine': EmojiStateCodes.usMaine,
+ 'Maryland': EmojiStateCodes.usMaryland,
+ 'Massachusetts': EmojiStateCodes.usMassachusetts,
+ 'Michigan': EmojiStateCodes.usMichigan,
+ 'Minnesota': EmojiStateCodes.usMinnesota,
+ 'Mississippi': EmojiStateCodes.usMississippi,
+ 'Missouri': EmojiStateCodes.usMissouri,
+ 'Montana': EmojiStateCodes.usMontana,
+ 'Nebraska': EmojiStateCodes.usNebraska,
+ 'Nevada': EmojiStateCodes.usNevada,
+ 'New Hampshire': EmojiStateCodes.usNewHampshire,
+ 'New Jersey': EmojiStateCodes.usNewJersey,
+ 'New Mexico': EmojiStateCodes.usNewMexico,
+ 'New York': EmojiStateCodes.usNewYork,
+ 'North Carolina': EmojiStateCodes.usNorthCarolina,
+ 'North Dakota': EmojiStateCodes.usNorthDakota,
+ 'Ohio': EmojiStateCodes.usOhio,
+ 'Oklahoma': EmojiStateCodes.usOklahoma,
+ 'Oregon': EmojiStateCodes.usOregon,
+ 'Pennsylvania': EmojiStateCodes.usPennsylvania,
+ 'Rhode Island': EmojiStateCodes.usRhodeIsland,
+ 'South Carolina': EmojiStateCodes.usSouthCarolina,
+ 'South Dakota': EmojiStateCodes.usSouthDakota,
+ 'Tennessee': EmojiStateCodes.usTennessee,
+ 'Utah': EmojiStateCodes.usUtah,
+ 'Vermont': EmojiStateCodes.usVermont,
+ 'Virginia': EmojiStateCodes.usVirginia,
+ 'Washington': EmojiStateCodes.usWashington,
+ 'Washington DC': EmojiStateCodes.usWashingtonDC,
+ 'West Virginia': EmojiStateCodes.usWestVirginia,
+ 'Wisconsin': EmojiStateCodes.usWisconsin,
+ 'Wyoming': EmojiStateCodes.usWyoming,
+ };
+}
diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart
index 0729c7ce1..a0aaa32b2 100644
--- a/lib/image_providers/app_icon_image_provider.dart
+++ b/lib/image_providers/app_icon_image_provider.dart
@@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider {
Future _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
try {
- final bytes = await androidAppService.getAppIcon(key.packageName, key.size);
+ final bytes = await appService.getAppIcon(key.packageName, key.size);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
return await decode(buffer);
} catch (error) {
diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb
index 826c8f5c4..c388db77d 100644
--- a/lib/l10n/app_cs.arb
+++ b/lib/l10n/app_cs.arb
@@ -1426,5 +1426,31 @@
"vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultBinUsageDialogMessage": "Některé trezory používají koš.",
- "@vaultBinUsageDialogMessage": {}
+ "@vaultBinUsageDialogMessage": {},
+ "settingsVideoBackgroundMode": "Režim na pozadí",
+ "@settingsVideoBackgroundMode": {},
+ "settingsCollectionBurstPatternsNone": "Žádný",
+ "@settingsCollectionBurstPatternsNone": {},
+ "chipActionShowCountryStates": "Zobrazit země",
+ "@chipActionShowCountryStates": {},
+ "viewerActionLock": "Uzamknout prohlížení",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "Odemknout prohlížení",
+ "@viewerActionUnlock": {},
+ "settingsVideoEnablePip": "Obraz v obraze",
+ "@settingsVideoEnablePip": {},
+ "statePageTitle": "Státy",
+ "@statePageTitle": {},
+ "stateEmpty": "Žádné státy",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "Státy",
+ "@searchStatesSectionTitle": {},
+ "settingsCollectionBurstPatternsTile": "Vzory dávek",
+ "@settingsCollectionBurstPatternsTile": {},
+ "statsTopStatesSectionTitle": "Nejčastější státy",
+ "@statsTopStatesSectionTitle": {},
+ "tagPlaceholderState": "Stát",
+ "@tagPlaceholderState": {},
+ "settingsVideoBackgroundModeDialogTitle": "Režim na pozadí",
+ "@settingsVideoBackgroundModeDialogTitle": {}
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 16361c294..9e83cf533 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -1208,5 +1208,91 @@
"settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.",
"@settingsModificationWarningDialogMessage": {},
"settingsViewerShowDescription": "Beschreibung anzeigen",
- "@settingsViewerShowDescription": {}
+ "@settingsViewerShowDescription": {},
+ "chipActionGoToPlacePage": "In Orten anzeigen",
+ "@chipActionGoToPlacePage": {},
+ "chipActionLock": "Sperren",
+ "@chipActionLock": {},
+ "chipActionCreateVault": "Tresor anlegen",
+ "@chipActionCreateVault": {},
+ "chipActionConfigureVault": "Tresor konfigurieren",
+ "@chipActionConfigureVault": {},
+ "settingsCollectionBurstPatternsTile": "Berstmuster",
+ "@settingsCollectionBurstPatternsTile": {},
+ "settingsVideoEnablePip": "Bild-in-Bild",
+ "@settingsVideoEnablePip": {},
+ "patternDialogEnter": "Muster eingeben",
+ "@patternDialogEnter": {},
+ "tagPlaceholderState": "Staat",
+ "@tagPlaceholderState": {},
+ "settingsDisablingBinWarningDialogMessage": "Die Elemente im Papierkorb werden für immer gelöscht.",
+ "@settingsDisablingBinWarningDialogMessage": {},
+ "chipActionShowCountryStates": "Staaten anzeigen",
+ "@chipActionShowCountryStates": {},
+ "viewerActionLock": "Anzeige sperren",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "Anzeige entsperren",
+ "@viewerActionUnlock": {},
+ "albumTierVaults": "Tresore",
+ "@albumTierVaults": {},
+ "patternDialogConfirm": "Muster bestätigen",
+ "@patternDialogConfirm": {},
+ "exportEntryDialogWriteMetadata": "Metadaten schreiben",
+ "@exportEntryDialogWriteMetadata": {},
+ "drawerPlacePage": "Orte",
+ "@drawerPlacePage": {},
+ "statePageTitle": "Staaten",
+ "@statePageTitle": {},
+ "stateEmpty": "Keine Staaten",
+ "@stateEmpty": {},
+ "placePageTitle": "Orte",
+ "@placePageTitle": {},
+ "placeEmpty": "Keine Orte",
+ "@placeEmpty": {},
+ "settingsCollectionBurstPatternsNone": "Nichts",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsVideoBackgroundMode": "Hintergrund-Modus",
+ "@settingsVideoBackgroundMode": {},
+ "searchStatesSectionTitle": "Staaten",
+ "@searchStatesSectionTitle": {},
+ "settingsConfirmationVaultDataLoss": "Warnung vor Tresordatenverlust anzeigen",
+ "@settingsConfirmationVaultDataLoss": {},
+ "settingsVideoBackgroundModeDialogTitle": "Hintergrund-Modus",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "statsTopStatesSectionTitle": "Top Staaten",
+ "@statsTopStatesSectionTitle": {},
+ "lengthUnitPercent": "%",
+ "@lengthUnitPercent": {},
+ "lengthUnitPixel": "px",
+ "@lengthUnitPixel": {},
+ "vaultLockTypePattern": "Muster",
+ "@vaultLockTypePattern": {},
+ "vaultLockTypePassword": "Passwort",
+ "@vaultLockTypePassword": {},
+ "vaultLockTypePin": "PIN",
+ "@vaultLockTypePin": {},
+ "passwordDialogEnter": "Passwort eingeben",
+ "@passwordDialogEnter": {},
+ "passwordDialogConfirm": "Passwort bestätigen",
+ "@passwordDialogConfirm": {},
+ "authenticateToConfigureVault": "Authentifizierung zum Konfigurieren des Tresors",
+ "@authenticateToConfigureVault": {},
+ "newVaultWarningDialogMessage": "Elemente in Tresoren sind nur für diese App verfügbar und nicht in anderen.\n\nWenn Sie diese App deinstallieren oder die Daten dieser App löschen, gehen alle diese Elemente verloren.",
+ "@newVaultWarningDialogMessage": {},
+ "newVaultDialogTitle": "Neuer Tresor",
+ "@newVaultDialogTitle": {},
+ "configureVaultDialogTitle": "Tresor konfigurieren",
+ "@configureVaultDialogTitle": {},
+ "vaultDialogLockModeWhenScreenOff": "Sperren beim Ausschalten des Bildschirms",
+ "@vaultDialogLockModeWhenScreenOff": {},
+ "vaultDialogLockTypeLabel": "Schloss-Typ",
+ "@vaultDialogLockTypeLabel": {},
+ "pinDialogConfirm": "PIN bestätigen",
+ "@pinDialogConfirm": {},
+ "authenticateToUnlockVault": "Authentifizierung zum Entsperren des Tresors",
+ "@authenticateToUnlockVault": {},
+ "vaultBinUsageDialogMessage": "Einige Tresore verwenden den Papierkorb.",
+ "@vaultBinUsageDialogMessage": {},
+ "pinDialogEnter": "PIN eingeben",
+ "@pinDialogEnter": {}
}
diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb
index ec6cc71a5..4622bb15a 100644
--- a/lib/l10n/app_el.arb
+++ b/lib/l10n/app_el.arb
@@ -723,7 +723,7 @@
"@searchAlbumsSectionTitle": {},
"searchCountriesSectionTitle": "Χωρες",
"@searchCountriesSectionTitle": {},
- "searchPlacesSectionTitle": "Τοποθεσιες",
+ "searchPlacesSectionTitle": "Μερη",
"@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Ετικετες",
"@searchTagsSectionTitle": {},
@@ -1252,5 +1252,47 @@
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"lengthUnitPixel": "px",
- "@lengthUnitPixel": {}
+ "@lengthUnitPixel": {},
+ "chipActionGoToPlacePage": "Εμφάνιση στα μέρη",
+ "@chipActionGoToPlacePage": {},
+ "patternDialogConfirm": "Επιβεβαιώστε το μοτίβο",
+ "@patternDialogConfirm": {},
+ "drawerPlacePage": "Μέρη",
+ "@drawerPlacePage": {},
+ "settingsVideoBackgroundMode": "Αναπαραγωγή στο παρασκήνιο",
+ "@settingsVideoBackgroundMode": {},
+ "chipActionShowCountryStates": "Εμφάνιση πολιτειών",
+ "@chipActionShowCountryStates": {},
+ "viewerActionLock": "Κλείδωμα προβολής",
+ "@viewerActionLock": {},
+ "patternDialogEnter": "Εισάγετε το μοτίβο",
+ "@patternDialogEnter": {},
+ "statePageTitle": "Πολιτειες",
+ "@statePageTitle": {},
+ "stateEmpty": "Χωρίς πολιτεία",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "Πολιτειες",
+ "@searchStatesSectionTitle": {},
+ "settingsCollectionBurstPatternsTile": "Εμφάνιση μοτίβων",
+ "@settingsCollectionBurstPatternsTile": {},
+ "settingsCollectionBurstPatternsNone": "Χωρίς",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsVideoBackgroundModeDialogTitle": "Αναπαραγωγη στο παρασκηνιο",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "statsTopStatesSectionTitle": "Κορυφαιες Πολιτειες",
+ "@statsTopStatesSectionTitle": {},
+ "tagPlaceholderState": "Πολιτεία",
+ "@tagPlaceholderState": {},
+ "exportEntryDialogWriteMetadata": "Εγγραφή μεταδεδομένων",
+ "@exportEntryDialogWriteMetadata": {},
+ "placePageTitle": "Μερη",
+ "@placePageTitle": {},
+ "placeEmpty": "Χωρίς μέρος",
+ "@placeEmpty": {},
+ "settingsVideoEnablePip": "Picture-in-picture",
+ "@settingsVideoEnablePip": {},
+ "viewerActionUnlock": "Ξεκλείδωμα προβολής",
+ "@viewerActionUnlock": {},
+ "vaultLockTypePattern": "Μοτίβο",
+ "@vaultLockTypePattern": {}
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index ad6d058b8..4bc25f2df 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -84,6 +84,7 @@
"chipActionUnpin": "Unpin from top",
"chipActionRename": "Rename",
"chipActionSetCover": "Set cover",
+ "chipActionShowCountryStates": "Show states",
"chipActionCreateAlbum": "Create album",
"chipActionCreateVault": "Create vault",
"chipActionConfigureVault": "Configure vault",
@@ -125,6 +126,8 @@
"videoActionSetSpeed": "Playback speed",
"viewerActionSettings": "Settings",
+ "viewerActionLock": "Lock viewer",
+ "viewerActionUnlock": "Unlock viewer",
"slideshowActionResume": "Resume",
"slideshowActionShowInCollection": "Show in Collection",
@@ -677,6 +680,9 @@
"countryPageTitle": "Countries",
"countryEmpty": "No countries",
+ "statePageTitle": "States",
+ "stateEmpty": "No states",
+
"placePageTitle": "Places",
"placeEmpty": "No places",
@@ -690,6 +696,7 @@
"searchDateSectionTitle": "Date",
"searchAlbumsSectionTitle": "Albums",
"searchCountriesSectionTitle": "Countries",
+ "searchStatesSectionTitle": "States",
"searchPlacesSectionTitle": "Places",
"searchTagsSectionTitle": "Tags",
"searchRatingSectionTitle": "Ratings",
@@ -754,6 +761,9 @@
"settingsCollectionBrowsingQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when browsing items.",
"settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.",
+ "settingsCollectionBurstPatternsTile": "Burst patterns",
+ "settingsCollectionBurstPatternsNone": "None",
+
"settingsViewerSectionTitle": "Viewer",
"settingsViewerGestureSideTapNext": "Tap on screen edges to show previous/next item",
"settingsViewerUseCutout": "Use cutout area",
@@ -892,6 +902,7 @@
}
},
"statsTopCountriesSectionTitle": "Top Countries",
+ "statsTopStatesSectionTitle": "Top States",
"statsTopPlacesSectionTitle": "Top Places",
"statsTopTagsSectionTitle": "Top Tags",
"statsTopAlbumsSectionTitle": "Top Albums",
@@ -948,6 +959,7 @@
"tagEditorSectionPlaceholders": "Placeholders",
"tagPlaceholderCountry": "Country",
+ "tagPlaceholderState": "State",
"tagPlaceholderPlace": "Place",
"panoramaEnableSensorControl": "Enable sensor control",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 769e44b93..de81bf635 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1273,6 +1273,26 @@
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Reproducción de fondo",
"@settingsVideoBackgroundMode": {},
- "settingsVideoBackgroundModeDialogTitle": "Background mode",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "settingsVideoBackgroundModeDialogTitle": "Reproducción de fondo",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsTile": "Modelos de ráfaga",
+ "@settingsCollectionBurstPatternsTile": {},
+ "settingsCollectionBurstPatternsNone": "Ninguno",
+ "@settingsCollectionBurstPatternsNone": {},
+ "tagPlaceholderState": "Estado",
+ "@tagPlaceholderState": {},
+ "viewerActionUnlock": "Desbloquear visor",
+ "@viewerActionUnlock": {},
+ "stateEmpty": "Sin estados",
+ "@stateEmpty": {},
+ "chipActionShowCountryStates": "Mostrar los estados",
+ "@chipActionShowCountryStates": {},
+ "statePageTitle": "Estados",
+ "@statePageTitle": {},
+ "viewerActionLock": "Bloquear visor",
+ "@viewerActionLock": {},
+ "searchStatesSectionTitle": "Estados",
+ "@searchStatesSectionTitle": {},
+ "statsTopStatesSectionTitle": "Estados principales",
+ "@statsTopStatesSectionTitle": {}
}
diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb
index 5c587223b..e1929c0bf 100644
--- a/lib/l10n/app_eu.arb
+++ b/lib/l10n/app_eu.arb
@@ -1432,5 +1432,25 @@
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsNone": "Bat ere ez",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsCollectionBurstPatternsTile": "Segida moduak",
+ "@settingsCollectionBurstPatternsTile": {},
+ "tagPlaceholderState": "Egoera",
+ "@tagPlaceholderState": {},
+ "viewerActionUnlock": "Deskblokeatu bisorea",
+ "@viewerActionUnlock": {},
+ "stateEmpty": "Egoerarik ez",
+ "@stateEmpty": {},
+ "chipActionShowCountryStates": "Erakutsi egoerak",
+ "@chipActionShowCountryStates": {},
+ "statePageTitle": "Egoerak",
+ "@statePageTitle": {},
+ "viewerActionLock": "Blokeatu bisorea",
+ "@viewerActionLock": {},
+ "searchStatesSectionTitle": "Egoerak",
+ "@searchStatesSectionTitle": {},
+ "statsTopStatesSectionTitle": "Egoera Nagusiak",
+ "@statsTopStatesSectionTitle": {}
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 64035ab81..cc28f51e9 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1274,5 +1274,25 @@
"settingsVideoBackgroundMode": "Lecture en arrière-plan",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Arrière-plan",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsNone": "Aucun",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsCollectionBurstPatternsTile": "Modèles de rafale",
+ "@settingsCollectionBurstPatternsTile": {},
+ "tagPlaceholderState": "État",
+ "@tagPlaceholderState": {},
+ "chipActionShowCountryStates": "Afficher les États",
+ "@chipActionShowCountryStates": {},
+ "stateEmpty": "Aucun État",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "États",
+ "@searchStatesSectionTitle": {},
+ "statePageTitle": "États",
+ "@statePageTitle": {},
+ "statsTopStatesSectionTitle": "Top États",
+ "@statsTopStatesSectionTitle": {},
+ "viewerActionLock": "Verrouiller la visionneuse",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "Déverrouiller la visionneuse",
+ "@viewerActionUnlock": {}
}
diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb
new file mode 100644
index 000000000..071513ba8
--- /dev/null
+++ b/lib/l10n/app_hi.arb
@@ -0,0 +1,77 @@
+{
+ "welcomeOptional": "वैकल्पिक",
+ "@welcomeOptional": {},
+ "welcomeTermsToggle": "मैं नियमों और शर्तों पर सहमत हुं",
+ "@welcomeTermsToggle": {},
+ "columnCount": "{count, plural, =1{१ कॉलम} other{{count} कॉलम}}",
+ "@columnCount": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "timeSeconds": "{seconds, plural, =1{ १ सेकंड} other{{seconds} सेकंडस}}",
+ "@timeSeconds": {
+ "placeholders": {
+ "seconds": {}
+ }
+ },
+ "timeDays": "{days, plural, =1{ १ दिन} other{{days} दिन}}",
+ "@timeDays": {
+ "placeholders": {
+ "days": {}
+ }
+ },
+ "applyButtonLabel": "लगाऐ",
+ "@applyButtonLabel": {},
+ "nextButtonLabel": "आगे",
+ "@nextButtonLabel": {},
+ "showButtonLabel": "देखे",
+ "@showButtonLabel": {},
+ "hideButtonLabel": "छिपाए",
+ "@hideButtonLabel": {},
+ "continueButtonLabel": "जारी रखे",
+ "@continueButtonLabel": {},
+ "clearTooltip": "मिटाएं",
+ "@clearTooltip": {},
+ "actionRemove": "हटाएं",
+ "@actionRemove": {},
+ "itemCount": "{count, plural, =1{१ चीज} other{{count} चीजे}}",
+ "@itemCount": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "deleteButtonLabel": "डिलीट",
+ "@deleteButtonLabel": {},
+ "timeMinutes": "{minutes, plural, =1{ १ मिनट} other{{minutes} मिनट}}",
+ "@timeMinutes": {
+ "placeholders": {
+ "minutes": {}
+ }
+ },
+ "focalLength": "{length} एम एम",
+ "@focalLength": {
+ "placeholders": {
+ "length": {
+ "type": "String",
+ "example": "5.4"
+ }
+ }
+ },
+ "nextTooltip": "आगे",
+ "@nextTooltip": {},
+ "appName": "ऐवीज",
+ "@appName": {},
+ "welcomeMessage": "ऐवीज मे आपका स्वागत है",
+ "@welcomeMessage": {},
+ "previousTooltip": "पिछे",
+ "@previousTooltip": {},
+ "hideTooltip": "छिपाए",
+ "@hideTooltip": {},
+ "cancelTooltip": "कैंसिल",
+ "@cancelTooltip": {},
+ "changeTooltip": "बदलें",
+ "@changeTooltip": {},
+ "showTooltip": "देखें",
+ "@showTooltip": {}
+}
diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb
new file mode 100644
index 000000000..dcf3fc14a
--- /dev/null
+++ b/lib/l10n/app_hu.arb
@@ -0,0 +1,186 @@
+{
+ "applyButtonLabel": "ALKALMAZ",
+ "@applyButtonLabel": {},
+ "deleteButtonLabel": "TÖRLÉS",
+ "@deleteButtonLabel": {},
+ "nextButtonLabel": "KÖVETKEZŐ",
+ "@nextButtonLabel": {},
+ "continueButtonLabel": "FOLYTAT",
+ "@continueButtonLabel": {},
+ "previousTooltip": "Előző",
+ "@previousTooltip": {},
+ "nextTooltip": "Következő",
+ "@nextTooltip": {},
+ "saveTooltip": "Mentés",
+ "@saveTooltip": {},
+ "sourceStateLoading": "Betöltés",
+ "@sourceStateLoading": {},
+ "doNotAskAgain": "Ne kérdezd újra",
+ "@doNotAskAgain": {},
+ "chipActionDelete": "Törlés",
+ "@chipActionDelete": {},
+ "appName": "Aves",
+ "@appName": {},
+ "welcomeMessage": "Üdvözöl az Aves",
+ "@welcomeMessage": {},
+ "cancelTooltip": "Mégse",
+ "@cancelTooltip": {},
+ "chipActionCreateAlbum": "Új album",
+ "@chipActionCreateAlbum": {},
+ "entryActionCopyToClipboard": "Vágolapra másolás",
+ "@entryActionCopyToClipboard": {},
+ "entryActionDelete": "Törlés",
+ "@entryActionDelete": {},
+ "entryActionExport": "Exportálás",
+ "@entryActionExport": {},
+ "entryActionInfo": "Infó",
+ "@entryActionInfo": {},
+ "entryActionShare": "Megosztás",
+ "@entryActionShare": {},
+ "entryActionPrint": "Nyomtatás",
+ "@entryActionPrint": {},
+ "entryActionEdit": "Szerkesztés",
+ "@entryActionEdit": {},
+ "entryActionRotateScreen": "Képernyő forgatása",
+ "@entryActionRotateScreen": {},
+ "entryActionAddFavourite": "Kedvencekhez adás",
+ "@entryActionAddFavourite": {},
+ "videoActionMute": "Némítás",
+ "@videoActionMute": {},
+ "viewerActionSettings": "Beállítások",
+ "@viewerActionSettings": {},
+ "entryInfoActionEditDate": "Dátum és idő szerkesztése",
+ "@entryInfoActionEditDate": {},
+ "filterNoTitleLabel": "Névtelen",
+ "@filterNoTitleLabel": {},
+ "filterOnThisDayLabel": "Ezen a napon",
+ "@filterOnThisDayLabel": {},
+ "filterRecentlyAddedLabel": "Nemrég hozzáadva",
+ "@filterRecentlyAddedLabel": {},
+ "filterTypePanoramaLabel": "Panoráma",
+ "@filterTypePanoramaLabel": {},
+ "filterMimeVideoLabel": "Videó",
+ "@filterMimeVideoLabel": {},
+ "albumTierNew": "Új",
+ "@albumTierNew": {},
+ "themeBrightnessDark": "Sötét",
+ "@themeBrightnessDark": {},
+ "vaultLockTypePassword": "Jelszó",
+ "@vaultLockTypePassword": {},
+ "videoControlsPlay": "Lejátszás",
+ "@videoControlsPlay": {},
+ "videoControlsNone": "Nincs",
+ "@videoControlsNone": {},
+ "videoLoopModeAlways": "Mindig",
+ "@videoLoopModeAlways": {},
+ "viewerTransitionNone": "Nincs",
+ "@viewerTransitionNone": {},
+ "storageVolumeDescriptionFallbackPrimary": "Belső tárhely",
+ "@storageVolumeDescriptionFallbackPrimary": {},
+ "storageVolumeDescriptionFallbackNonPrimary": "SD kártya",
+ "@storageVolumeDescriptionFallbackNonPrimary": {},
+ "newAlbumDialogTitle": "Új album",
+ "@newAlbumDialogTitle": {},
+ "newAlbumDialogNameLabel": "Album neve",
+ "@newAlbumDialogNameLabel": {},
+ "newAlbumDialogNameLabelAlreadyExistsHelper": "A mappa már létezik",
+ "@newAlbumDialogNameLabelAlreadyExistsHelper": {},
+ "newAlbumDialogStorageLabel": "Tárhely:",
+ "@newAlbumDialogStorageLabel": {},
+ "renameAlbumDialogLabel": "Új név",
+ "@renameAlbumDialogLabel": {},
+ "renameAlbumDialogLabelAlreadyExistsHelper": "A mappa már létezik",
+ "@renameAlbumDialogLabelAlreadyExistsHelper": {},
+ "renameEntrySetPageTitle": "Átnevezés",
+ "@renameEntrySetPageTitle": {},
+ "renameProcessorName": "Név",
+ "@renameProcessorName": {},
+ "renameEntryDialogLabel": "Új név",
+ "@renameEntryDialogLabel": {},
+ "editEntryDateDialogTitle": "Dátum és idő",
+ "@editEntryDateDialogTitle": {},
+ "videoStreamSelectionDialogText": "Feliratok",
+ "@videoStreamSelectionDialogText": {},
+ "videoStreamSelectionDialogOff": "Ki",
+ "@videoStreamSelectionDialogOff": {},
+ "genericSuccessFeedback": "Kész!",
+ "@genericSuccessFeedback": {},
+ "genericFailureFeedback": "Sikertelen",
+ "@genericFailureFeedback": {},
+ "genericDangerWarningDialogMessage": "Biztos benne?",
+ "@genericDangerWarningDialogMessage": {},
+ "menuActionSlideshow": "Diavetités",
+ "@menuActionSlideshow": {},
+ "coverDialogTabCover": "Borító",
+ "@coverDialogTabCover": {},
+ "appPickDialogNone": "Nincs",
+ "@appPickDialogNone": {},
+ "aboutPageTitle": "Névjegy",
+ "@aboutPageTitle": {},
+ "aboutLinkLicense": "Licensz",
+ "@aboutLinkLicense": {},
+ "aboutBugSectionTitle": "Hiba jelentés",
+ "@aboutBugSectionTitle": {},
+ "aboutBugCopyInfoButton": "Másolás",
+ "@aboutBugCopyInfoButton": {},
+ "aboutTranslatorsSectionTitle": "Fordítók",
+ "@aboutTranslatorsSectionTitle": {},
+ "collectionActionEdit": "Szerkesztés",
+ "@collectionActionEdit": {},
+ "dateToday": "Ma",
+ "@dateToday": {},
+ "dateThisMonth": "Ebben a hónapban",
+ "@dateThisMonth": {},
+ "drawerAboutButton": "Névjegy",
+ "@drawerAboutButton": {},
+ "drawerSettingsButton": "Beállítások",
+ "@drawerSettingsButton": {},
+ "drawerCollectionFavourites": "Kedvencek",
+ "@drawerCollectionFavourites": {},
+ "drawerCollectionImages": "Képek",
+ "@drawerCollectionImages": {},
+ "drawerCollectionVideos": "Videók",
+ "@drawerCollectionVideos": {},
+ "drawerCollectionPanoramas": "Panorámák",
+ "@drawerCollectionPanoramas": {},
+ "albumDownload": "Letöltés",
+ "@albumDownload": {},
+ "albumScreenshots": "Képernyő képek",
+ "@albumScreenshots": {},
+ "albumPageTitle": "Albumok",
+ "@albumPageTitle": {},
+ "newFilterBanner": "új",
+ "@newFilterBanner": {},
+ "chipActionRename": "Átnevez",
+ "@chipActionRename": {},
+ "entryActionRename": "Átnevezés",
+ "@entryActionRename": {},
+ "keepScreenOnNever": "Soha",
+ "@keepScreenOnNever": {},
+ "videoLoopModeNever": "Soha",
+ "@videoLoopModeNever": {},
+ "videoActionPlay": "Lejátszás",
+ "@videoActionPlay": {},
+ "entryInfoActionRemoveMetadata": "Metaadat eltávolítása",
+ "@entryInfoActionRemoveMetadata": {},
+ "albumTierRegular": "Egyebek",
+ "@albumTierRegular": {},
+ "keepScreenOnAlways": "Mindig",
+ "@keepScreenOnAlways": {},
+ "nameConflictStrategyRename": "Átnevezés",
+ "@nameConflictStrategyRename": {},
+ "themeBrightnessBlack": "Fekete",
+ "@themeBrightnessBlack": {},
+ "menuActionMap": "Térkép",
+ "@menuActionMap": {},
+ "collectionPageTitle": "Gyűjtemény",
+ "@collectionPageTitle": {},
+ "sectionUnknown": "Ismeretlen",
+ "@sectionUnknown": {},
+ "dateYesterday": "Tegnap",
+ "@dateYesterday": {},
+ "drawerAlbumPage": "Albumok",
+ "@drawerAlbumPage": {},
+ "albumCamera": "Kamera",
+ "@albumCamera": {}
+}
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index b5037a10a..e6694b165 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -1274,5 +1274,25 @@
"settingsVideoBackgroundMode": "Mode latar belakang",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Mode Latar Belakang",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsTile": "Pola semburan",
+ "@settingsCollectionBurstPatternsTile": {},
+ "settingsCollectionBurstPatternsNone": "Tidak ada",
+ "@settingsCollectionBurstPatternsNone": {},
+ "chipActionShowCountryStates": "Tampilkan wilayah",
+ "@chipActionShowCountryStates": {},
+ "viewerActionUnlock": "Buka kunci penampil",
+ "@viewerActionUnlock": {},
+ "statePageTitle": "Wilayah",
+ "@statePageTitle": {},
+ "stateEmpty": "Tidak ada wilayah",
+ "@stateEmpty": {},
+ "tagPlaceholderState": "Wilayah",
+ "@tagPlaceholderState": {},
+ "viewerActionLock": "Kunci penampil",
+ "@viewerActionLock": {},
+ "searchStatesSectionTitle": "Wilayah",
+ "@searchStatesSectionTitle": {},
+ "statsTopStatesSectionTitle": "Wilayah Teratas",
+ "@statsTopStatesSectionTitle": {}
}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index d2dc5001a..735dbd0e7 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -65,7 +65,7 @@
"@sourceStateLocatingPlaces": {},
"chipActionDelete": "Elimina",
"@chipActionDelete": {},
- "chipActionGoToAlbumPage": "Mostra negli album",
+ "chipActionGoToAlbumPage": "Mostra negli Album",
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Mostra nei Paesi",
"@chipActionGoToCountryPage": {},
@@ -1101,7 +1101,7 @@
"@viewerInfoOpenLinkText": {},
"viewerInfoViewXmlLinkText": "Visualizza XML",
"@viewerInfoViewXmlLinkText": {},
- "viewerInfoSearchFieldLabel": "Metadati di ricerca",
+ "viewerInfoSearchFieldLabel": "Ricerca metadati",
"@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "Nessuna chiave corrispondente",
"@viewerInfoSearchEmpty": {},
@@ -1248,5 +1248,49 @@
"settingsDisablingBinWarningDialogMessage": "Gli elementi nel cestino verranno eliminati permanentemente.",
"@settingsDisablingBinWarningDialogMessage": {},
"configureVaultDialogTitle": "Configura Cassaforte",
- "@configureVaultDialogTitle": {}
+ "@configureVaultDialogTitle": {},
+ "exportEntryDialogWriteMetadata": "Scrivi metadati",
+ "@exportEntryDialogWriteMetadata": {},
+ "chipActionGoToPlacePage": "Mostra nei Luoghi",
+ "@chipActionGoToPlacePage": {},
+ "lengthUnitPercent": "%",
+ "@lengthUnitPercent": {},
+ "lengthUnitPixel": "px",
+ "@lengthUnitPixel": {},
+ "patternDialogEnter": "Inserisci sequenza",
+ "@patternDialogEnter": {},
+ "patternDialogConfirm": "Conferma sequenza",
+ "@patternDialogConfirm": {},
+ "drawerPlacePage": "Luoghi",
+ "@drawerPlacePage": {},
+ "placeEmpty": "Nessun luogo",
+ "@placeEmpty": {},
+ "placePageTitle": "Luoghi",
+ "@placePageTitle": {},
+ "settingsVideoBackgroundMode": "Modalità sottofondo",
+ "@settingsVideoBackgroundMode": {},
+ "settingsVideoBackgroundModeDialogTitle": "Modalità Sottofondo",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsVideoEnablePip": "Picture-in-picture",
+ "@settingsVideoEnablePip": {},
+ "vaultLockTypePattern": "Sequenza",
+ "@vaultLockTypePattern": {},
+ "viewerActionLock": "Blocca visualizzazione",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "Sblocca visualizzazione",
+ "@viewerActionUnlock": {},
+ "statsTopStatesSectionTitle": "Stati più frequenti",
+ "@statsTopStatesSectionTitle": {},
+ "tagPlaceholderState": "Stato",
+ "@tagPlaceholderState": {},
+ "settingsCollectionBurstPatternsNone": "Nessuno",
+ "@settingsCollectionBurstPatternsNone": {},
+ "chipActionShowCountryStates": "Mostra stati",
+ "@chipActionShowCountryStates": {},
+ "statePageTitle": "Stati",
+ "@statePageTitle": {},
+ "stateEmpty": "Nessuno stato",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "Stati",
+ "@searchStatesSectionTitle": {}
}
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index d909c5baf..ed47700b9 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -1170,5 +1170,69 @@
"settingsSubtitleThemeTextPositionTile": "テキストの位置",
"@settingsSubtitleThemeTextPositionTile": {},
"entryInfoActionExportMetadata": "メタデータをエクスポート",
- "@entryInfoActionExportMetadata": {}
+ "@entryInfoActionExportMetadata": {},
+ "subtitlePositionTop": "トップ",
+ "@subtitlePositionTop": {},
+ "configureVaultDialogTitle": "保管庫の設定",
+ "@configureVaultDialogTitle": {},
+ "vaultDialogLockModeWhenScreenOff": "画面オフ時にロック",
+ "@vaultDialogLockModeWhenScreenOff": {},
+ "newVaultDialogTitle": "新しい保管庫",
+ "@newVaultDialogTitle": {},
+ "authenticateToConfigureVault": "保管庫を設定するための認証",
+ "@authenticateToConfigureVault": {},
+ "vaultDialogLockTypeLabel": "ロックの種類",
+ "@vaultDialogLockTypeLabel": {},
+ "pinDialogEnter": "PINを入力",
+ "@pinDialogEnter": {},
+ "patternDialogEnter": "パターンを入力",
+ "@patternDialogEnter": {},
+ "pinDialogConfirm": "PINの確認",
+ "@pinDialogConfirm": {},
+ "passwordDialogEnter": "パスワードを入力",
+ "@passwordDialogEnter": {},
+ "authenticateToUnlockVault": "認証して保管庫のロックを解除する",
+ "@authenticateToUnlockVault": {},
+ "passwordDialogConfirm": "パスワードの確認",
+ "@passwordDialogConfirm": {},
+ "chipActionFilterIn": "フィルター",
+ "@chipActionFilterIn": {},
+ "filterAspectRatioPortraitLabel": "縦向き",
+ "@filterAspectRatioPortraitLabel": {},
+ "filterNoAddressLabel": "位置情報なし",
+ "@filterNoAddressLabel": {},
+ "keepScreenOnVideoPlayback": "動画再生時",
+ "@keepScreenOnVideoPlayback": {},
+ "chipActionGoToPlacePage": "場所別に表示",
+ "@chipActionGoToPlacePage": {},
+ "tagPlaceholderState": "州",
+ "@tagPlaceholderState": {},
+ "vaultLockTypePassword": "パスワード",
+ "@vaultLockTypePassword": {},
+ "tooManyItemsErrorDialogMessage": "少ないアイテムで再度試してください。",
+ "@tooManyItemsErrorDialogMessage": {},
+ "statePageTitle": "州",
+ "@statePageTitle": {},
+ "drawerPlacePage": "場所",
+ "@drawerPlacePage": {},
+ "chipActionLock": "ロック",
+ "@chipActionLock": {},
+ "filterAspectRatioLandscapeLabel": "横向き",
+ "@filterAspectRatioLandscapeLabel": {},
+ "vaultLockTypePin": "PIN",
+ "@vaultLockTypePin": {},
+ "newVaultWarningDialogMessage": "保管庫のアイテムはアプリ内のみで保存しているため、他のアプリでは利用できません。\n\nこのアプリをアンインストールしたり、データを消去したりすると、これらのアイテムはすべて失われます。",
+ "@newVaultWarningDialogMessage": {},
+ "patternDialogConfirm": "パターンの確認",
+ "@patternDialogConfirm": {},
+ "placePageTitle": "場所",
+ "@placePageTitle": {},
+ "settingsVideoEnablePip": "ピクチャインピクチャ",
+ "@settingsVideoEnablePip": {},
+ "vaultLockTypePattern": "パターン",
+ "@vaultLockTypePattern": {},
+ "lengthUnitPixel": "px",
+ "@lengthUnitPixel": {},
+ "lengthUnitPercent": "%",
+ "@lengthUnitPercent": {}
}
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index a6bd96510..7eb29a4d4 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -1274,5 +1274,25 @@
"settingsVideoBackgroundMode": "백그라운드 재생",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsNone": "없음",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsCollectionBurstPatternsTile": "연속 촬영 양식",
+ "@settingsCollectionBurstPatternsTile": {},
+ "tagPlaceholderState": "주",
+ "@tagPlaceholderState": {},
+ "chipActionShowCountryStates": "주 보기",
+ "@chipActionShowCountryStates": {},
+ "stateEmpty": "주가 없습니다",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "주",
+ "@searchStatesSectionTitle": {},
+ "statsTopStatesSectionTitle": "주 랭킹",
+ "@statsTopStatesSectionTitle": {},
+ "statePageTitle": "주",
+ "@statePageTitle": {},
+ "viewerActionLock": "뷰어 잠금",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "뷰어 잠금 해제",
+ "@viewerActionUnlock": {}
}
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 017cba23d..08173de71 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -517,7 +517,7 @@
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
"@aboutCreditsWorldAtlas2": {},
- "aboutTranslatorsSectionTitle": "Vdertalers",
+ "aboutTranslatorsSectionTitle": "Vertalers",
"@aboutTranslatorsSectionTitle": {},
"aboutLicensesSectionTitle": "Open-Source Licenties",
"@aboutLicensesSectionTitle": {},
@@ -1154,5 +1154,11 @@
"settingsAllowMediaManagement": "Mediabeheer toestaan",
"@settingsAllowMediaManagement": {},
"editEntryLocationDialogSetCustom": "Aangepaste locatie instellen",
- "@editEntryLocationDialogSetCustom": {}
+ "@editEntryLocationDialogSetCustom": {},
+ "entryInfoActionExportMetadata": "Metagegevens exporteren",
+ "@entryInfoActionExportMetadata": {},
+ "lengthUnitPercent": "%",
+ "@lengthUnitPercent": {},
+ "vaultLockTypePin": "PIN",
+ "@vaultLockTypePin": {}
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index b8fa68f67..ce2b9ab26 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -1432,5 +1432,25 @@
"settingsVideoBackgroundMode": "Tryb tła",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Tryb tła",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsCollectionBurstPatternsNone": "Brak",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsCollectionBurstPatternsTile": "Wzory wybuchowe",
+ "@settingsCollectionBurstPatternsTile": {},
+ "viewerActionUnlock": "Odblokuj przeglądarkę",
+ "@viewerActionUnlock": {},
+ "viewerActionLock": "Zablokuj przeglądarkę",
+ "@viewerActionLock": {},
+ "statePageTitle": "Stany",
+ "@statePageTitle": {},
+ "stateEmpty": "Brak stanów",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "Stany",
+ "@searchStatesSectionTitle": {},
+ "statsTopStatesSectionTitle": "Najpopularniejsze stany",
+ "@statsTopStatesSectionTitle": {},
+ "tagPlaceholderState": "Stan",
+ "@tagPlaceholderState": {},
+ "chipActionShowCountryStates": "Pokaż stany",
+ "@chipActionShowCountryStates": {}
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 5bc7f5726..7e379a006 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -1271,6 +1271,28 @@
"@vaultLockTypePattern": {},
"settingsVideoEnablePip": "Picture-in-picture",
"@settingsVideoEnablePip": {},
- "settingsVideoBackgroundMode": "Modo background",
- "@settingsVideoBackgroundMode": {}
+ "settingsVideoBackgroundMode": "Modo de fundo",
+ "@settingsVideoBackgroundMode": {},
+ "settingsCollectionBurstPatternsTile": "Padrões de explosão",
+ "@settingsCollectionBurstPatternsTile": {},
+ "chipActionShowCountryStates": "Mostrar estados",
+ "@chipActionShowCountryStates": {},
+ "viewerActionLock": "Bloquear visualizador",
+ "@viewerActionLock": {},
+ "statePageTitle": "Estados",
+ "@statePageTitle": {},
+ "stateEmpty": "Nenhum estado",
+ "@stateEmpty": {},
+ "tagPlaceholderState": "Estado",
+ "@tagPlaceholderState": {},
+ "searchStatesSectionTitle": "Estados",
+ "@searchStatesSectionTitle": {},
+ "settingsCollectionBurstPatternsNone": "Nenhum",
+ "@settingsCollectionBurstPatternsNone": {},
+ "statsTopStatesSectionTitle": "Principais Estados",
+ "@statsTopStatesSectionTitle": {},
+ "viewerActionUnlock": "Desbloquear visualizador",
+ "@viewerActionUnlock": {},
+ "settingsVideoBackgroundModeDialogTitle": "Modo de fundo",
+ "@settingsVideoBackgroundModeDialogTitle": {}
}
diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb
index 38330dc18..b892b5a0f 100644
--- a/lib/l10n/app_ro.arb
+++ b/lib/l10n/app_ro.arb
@@ -1406,5 +1406,51 @@
"newVaultWarningDialogMessage": "Elementele din seifuri sunt disponibile doar pentru această aplicație și nu pentru altele.\n\nDacă dezinstalezi această aplicație sau ștergi datele acestei aplicații, vei pierde toate aceste elemente.",
"@newVaultWarningDialogMessage": {},
"settingsConfirmationVaultDataLoss": "Afișare avertisment privind pierderile de date din seif",
- "@settingsConfirmationVaultDataLoss": {}
+ "@settingsConfirmationVaultDataLoss": {},
+ "settingsVideoBackgroundModeDialogTitle": "Mod fundal",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "lengthUnitPixel": "px",
+ "@lengthUnitPixel": {},
+ "exportEntryDialogWriteMetadata": "Scrierea metadatelor",
+ "@exportEntryDialogWriteMetadata": {},
+ "drawerPlacePage": "Locații",
+ "@drawerPlacePage": {},
+ "placePageTitle": "Locații",
+ "@placePageTitle": {},
+ "lengthUnitPercent": "%",
+ "@lengthUnitPercent": {},
+ "settingsVideoBackgroundMode": "Mod fundal",
+ "@settingsVideoBackgroundMode": {},
+ "patternDialogEnter": "Introdu modelul",
+ "@patternDialogEnter": {},
+ "patternDialogConfirm": "Confirmă modelul",
+ "@patternDialogConfirm": {},
+ "placeEmpty": "Nu există locații",
+ "@placeEmpty": {},
+ "settingsVideoEnablePip": "Imagine în imagine",
+ "@settingsVideoEnablePip": {},
+ "vaultLockTypePattern": "Model",
+ "@vaultLockTypePattern": {},
+ "chipActionGoToPlacePage": "Arată în Locuri",
+ "@chipActionGoToPlacePage": {},
+ "settingsCollectionBurstPatternsNone": "Niciunul",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsCollectionBurstPatternsTile": "Modele de rafale",
+ "@settingsCollectionBurstPatternsTile": {},
+ "tagPlaceholderState": "Stat",
+ "@tagPlaceholderState": {},
+ "chipActionShowCountryStates": "Afișare state",
+ "@chipActionShowCountryStates": {},
+ "viewerActionLock": "Blocarea vizualizatorului",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "Deblocare vizualizator",
+ "@viewerActionUnlock": {},
+ "statePageTitle": "State",
+ "@statePageTitle": {},
+ "stateEmpty": "Nu există state",
+ "@stateEmpty": {},
+ "searchStatesSectionTitle": "State",
+ "@searchStatesSectionTitle": {},
+ "statsTopStatesSectionTitle": "Statele de top",
+ "@statsTopStatesSectionTitle": {}
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index aec8a7e4e..5e4fbf0e3 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1235,7 +1235,7 @@
"@filterLocatedLabel": {},
"filterTaggedLabel": "С тэгами",
"@filterTaggedLabel": {},
- "chipActionGoToPlacePage": "Показать в местах",
+ "chipActionGoToPlacePage": "Показать в локациях",
"@chipActionGoToPlacePage": {},
"settingsModificationWarningDialogMessage": "Другие настройки будут изменены.",
"@settingsModificationWarningDialogMessage": {},
@@ -1244,5 +1244,23 @@
"settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.",
"@settingsDisablingBinWarningDialogMessage": {},
"lengthUnitPixel": "пикс.",
- "@lengthUnitPixel": {}
+ "@lengthUnitPixel": {},
+ "chipActionLock": "Заблокировать",
+ "@chipActionLock": {},
+ "patternDialogEnter": "Введите ключ",
+ "@patternDialogEnter": {},
+ "patternDialogConfirm": "Подтвердите ключ",
+ "@patternDialogConfirm": {},
+ "vaultLockTypePattern": "Графический ключ",
+ "@vaultLockTypePattern": {},
+ "drawerPlacePage": "Локации",
+ "@drawerPlacePage": {},
+ "settingsVideoBackgroundMode": "Фоновый режим",
+ "@settingsVideoBackgroundMode": {},
+ "settingsVideoBackgroundModeDialogTitle": "Фоновый режим",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsVideoEnablePip": "Картинка в картинке",
+ "@settingsVideoEnablePip": {},
+ "placeEmpty": "Нет локаций",
+ "@placeEmpty": {}
}
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index 13b90185d..a09e87d79 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -1432,5 +1432,25 @@
"settingsVideoBackgroundMode": "Фоновий режим",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "tagPlaceholderState": "Штат",
+ "@tagPlaceholderState": {},
+ "chipActionShowCountryStates": "Показати штати",
+ "@chipActionShowCountryStates": {},
+ "viewerActionUnlock": "Розблокувати переглядач",
+ "@viewerActionUnlock": {},
+ "viewerActionLock": "Заблокувати переглядач",
+ "@viewerActionLock": {},
+ "stateEmpty": "Немає штатів",
+ "@stateEmpty": {},
+ "settingsCollectionBurstPatternsTile": "Вибух візерунків",
+ "@settingsCollectionBurstPatternsTile": {},
+ "settingsCollectionBurstPatternsNone": "Нічого",
+ "@settingsCollectionBurstPatternsNone": {},
+ "statsTopStatesSectionTitle": "Топ штатів",
+ "@statsTopStatesSectionTitle": {},
+ "searchStatesSectionTitle": "Штати",
+ "@searchStatesSectionTitle": {},
+ "statePageTitle": "Штати",
+ "@statePageTitle": {}
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 2acc34657..5db846beb 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -1192,5 +1192,13 @@
"filterNoAddressLabel": "无地址",
"@filterNoAddressLabel": {},
"settingsViewerShowRatingTags": "显示评分和标签",
- "@settingsViewerShowRatingTags": {}
+ "@settingsViewerShowRatingTags": {},
+ "chipActionLock": "锁定",
+ "@chipActionLock": {},
+ "chipActionConfigureVault": "配置保险库",
+ "@chipActionConfigureVault": {},
+ "chipActionCreateVault": "创建保险库",
+ "@chipActionCreateVault": {},
+ "chipActionShowCountryStates": "显示状态",
+ "@chipActionShowCountryStates": {}
}
diff --git a/lib/model/actions/move_type.dart b/lib/model/actions/move_type.dart
deleted file mode 100644
index cc7ff0c6b..000000000
--- a/lib/model/actions/move_type.dart
+++ /dev/null
@@ -1 +0,0 @@
-enum MoveType { copy, move, export, toBin, fromBin }
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
new file mode 100644
index 000000000..83a4ac43b
--- /dev/null
+++ b/lib/model/app/contributors.dart
@@ -0,0 +1,64 @@
+class Contributors {
+ static const translators = {
+ Contributor('D3ZOXY', 'its.ghost.message@gmail.com'),
+ Contributor('JanWaldhorn', 'weblate@jwh.anonaddy.com'),
+ Contributor('n-berenice', null),
+ Contributor('Jonatas de Almeida Barros', 'ajonatas56@gmail.com'),
+ Contributor('MeFinity', 'me.dot.finity@gmail.com'),
+ Contributor('Maki', null),
+ Contributor('HiSubway', 'shenyusoftware@gmail.com'),
+ Contributor('glemco', 'glemco@posteo.net'),
+ Contributor('Aerowolf', null),
+ Contributor('小默', 'duzhe163908@gmail.com'),
+ Contributor('metezd', 'itoldyouthat@protonmail.com'),
+ Contributor('Martijn Fabrie', null),
+ Contributor('Koen Koppens', 'koenkoppens@proton.me'),
+ Contributor('Emmanouil Papavergis', null),
+ Contributor('kha84', 'khalukhin@gmail.com'),
+ Contributor('gallegonovato', 'fran-carro@hotmail.es'),
+ Contributor('Havokdan', 'havokdan@yahoo.com.br'),
+ Contributor('Jean Mareilles', 'waged1266@tutanota.com'),
+ Contributor('이정희', 'daemul72@gmail.com'),
+ Contributor('Translator-3000', 'weblate.m1d0h@8shield.net'),
+ Contributor('Ralea Adrian Vicențiu', 'ralea.adrian@gmail.com'),
+ Contributor('Igor Sorocean', 'sorocean.igor@gmail.com'),
+ Contributor('JY3', 'GeeyunJY3@gmail.com'),
+ Contributor('Gediminas Murauskas', 'muziejusinfo@gmail.com'),
+ Contributor('Oğuz Ersen', 'oguz@ersen.moe'),
+ Contributor('Allan Nordhøy', 'epost@anotheragency.no'),
+ Contributor('pemibe', 'pemibe4634@dmonies.com'),
+ Contributor('Linerly', 'linerly@protonmail.com'),
+ Contributor('Skrripy', 'rozihrash.ya6w7@simplelogin.com'),
+ Contributor('vesp', 'vesp@post.cz'),
+ Contributor('Dan', 'denqwerta@gmail.com'),
+ Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'),
+ Contributor('Piotr K', '1337.kelt@gmail.com'),
+ Contributor('rehork', 'cooky@e.email'),
+ Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'),
+ Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
+ Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
+ Contributor('kaajjo', 'claymanoff@gmail.com'),
+ Contributor('Eduardo Malaspina', 'vaio0@swismail.com'),
+ Contributor('Evgeniy Khramov', 'thejenjagamertjg@gmail.com'),
+ Contributor('syu_pf_ssy', 'syu.pf.ssy@outlook.com'),
+ Contributor('Dick Pluim', 'github@dickpluim.com'),
+ // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
+ // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
+ // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
+ // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian
+ // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk
+ // Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
+ // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
+ // Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak
+ // Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
+ // Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
+ // Contributor('György Viktor', 'wickdj@gmail.com'), // Hungarian
+ };
+}
+
+class Contributor {
+ final String name;
+ final String? weblateEmail;
+
+ const Contributor(this.name, this.weblateEmail);
+}
diff --git a/lib/utils/dependencies.dart b/lib/model/app/dependencies.dart
similarity index 97%
rename from lib/utils/dependencies.dart
rename to lib/model/app/dependencies.dart
index 0b33a9f47..dedcba43d 100644
--- a/lib/utils/dependencies.dart
+++ b/lib/model/app/dependencies.dart
@@ -112,12 +112,6 @@ class Dependencies {
license: mit,
sourceUrl: 'https://github.com/aaassseee/screen_brightness',
),
- Dependency(
- name: 'Screen State',
- license: mit,
- licenseUrl: 'https://github.com/cph-cachet/flutter-plugins/blob/master/packages/screen_state/LICENSE',
- sourceUrl: 'https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state',
- ),
Dependency(
name: 'Shared Preferences',
license: bsd3,
diff --git a/lib/model/app/permissions.dart b/lib/model/app/permissions.dart
new file mode 100644
index 000000000..310427242
--- /dev/null
+++ b/lib/model/app/permissions.dart
@@ -0,0 +1,16 @@
+import 'package:permission_handler/permission_handler.dart';
+
+class Permissions {
+ static const storage = [
+ Permission.storage,
+ // for media access on Android >=13
+ Permission.photos,
+ Permission.videos,
+ ];
+
+ static const mediaAccess = [
+ ...storage,
+ // to access media with unredacted metadata with scoped storage (Android >=10)
+ Permission.accessMediaLocation,
+ ];
+}
diff --git a/lib/model/app/support.dart b/lib/model/app/support.dart
new file mode 100644
index 000000000..fbf2a29b4
--- /dev/null
+++ b/lib/model/app/support.dart
@@ -0,0 +1,96 @@
+import 'package:aves/ref/mime_types.dart';
+
+class AppSupport {
+ // TODO TLAD [codec] make it dynamic if it depends on OS/lib versions
+ static const Set undecodableImages = {
+ MimeTypes.art,
+ MimeTypes.cdr,
+ MimeTypes.crw,
+ MimeTypes.djvu,
+ MimeTypes.jpeg2000,
+ MimeTypes.jxl,
+ MimeTypes.pat,
+ MimeTypes.pcx,
+ MimeTypes.pnm,
+ MimeTypes.psdVnd,
+ MimeTypes.psdX,
+ MimeTypes.octetStream,
+ MimeTypes.zip,
+ };
+
+ static bool canDecode(String mimeType) => !undecodableImages.contains(mimeType);
+
+ // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
+ // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
+ // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
+ static bool _supportedByBitmapRegionDecoder(String mimeType) => [
+ MimeTypes.heic,
+ MimeTypes.heif,
+ MimeTypes.jpeg,
+ MimeTypes.png,
+ MimeTypes.webp,
+ MimeTypes.arw,
+ MimeTypes.cr2,
+ MimeTypes.nef,
+ MimeTypes.nrw,
+ MimeTypes.orf,
+ MimeTypes.pef,
+ MimeTypes.raf,
+ MimeTypes.rw2,
+ MimeTypes.srw,
+ ].contains(mimeType);
+
+ static bool canDecodeRegion(String mimeType) => _supportedByBitmapRegionDecoder(mimeType) || mimeType == MimeTypes.tiff;
+
+ // `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes,
+ // and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files.
+ static bool canEditExif(String mimeType) {
+ switch (mimeType.toLowerCase()) {
+ // as of androidx.exifinterface:exifinterface:1.3.4
+ case MimeTypes.jpeg:
+ case MimeTypes.png:
+ case MimeTypes.webp:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool canEditIptc(String mimeType) {
+ switch (mimeType.toLowerCase()) {
+ // as of latest PixyMeta
+ case MimeTypes.jpeg:
+ case MimeTypes.tiff:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool canEditXmp(String mimeType) {
+ switch (mimeType.toLowerCase()) {
+ // as of latest PixyMeta
+ case MimeTypes.gif:
+ case MimeTypes.jpeg:
+ case MimeTypes.png:
+ case MimeTypes.tiff:
+ return true;
+ // using `mp4parser`
+ case MimeTypes.mp4:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool canRemoveMetadata(String mimeType) {
+ switch (mimeType.toLowerCase()) {
+ // as of latest PixyMeta
+ case MimeTypes.jpeg:
+ case MimeTypes.tiff:
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/lib/model/apps.dart b/lib/model/apps.dart
new file mode 100644
index 000000000..b7bc6e944
--- /dev/null
+++ b/lib/model/apps.dart
@@ -0,0 +1,78 @@
+import 'package:aves/services/common/services.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter/foundation.dart';
+
+final AppInventory appInventory = AppInventory._private();
+
+class AppInventory {
+ Set _packages = {};
+ List _potentialAppDirs = [];
+
+ ValueNotifier areAppNamesReadyNotifier = ValueNotifier(false);
+
+ Iterable get _launcherPackages => _packages.where((v) => v.categoryLauncher);
+
+ AppInventory._private();
+
+ Future initAppNames() async {
+ if (_packages.isEmpty) {
+ debugPrint('Access installed app inventory');
+ _packages = await appService.getPackages();
+ _potentialAppDirs = _launcherPackages.expand((v) => v.potentialDirs).toList();
+ areAppNamesReadyNotifier.value = true;
+ }
+ }
+
+ Future resetAppNames() async {
+ _packages.clear();
+ _potentialAppDirs.clear();
+ areAppNamesReadyNotifier.value = false;
+ }
+
+ bool isPotentialAppDir(String dir) => _potentialAppDirs.contains(dir);
+
+ String? getAlbumAppPackageName(String albumPath) {
+ final dir = pContext.split(albumPath).last;
+ final package = _launcherPackages.firstWhereOrNull((v) => v.potentialDirs.contains(dir));
+ return package?.packageName;
+ }
+
+ String? getCurrentAppName(String packageName) {
+ final package = _packages.firstWhereOrNull((v) => v.packageName == packageName);
+ return package?.currentLabel;
+ }
+}
+
+class Package {
+ final String packageName;
+ final String? currentLabel, englishLabel;
+ final bool categoryLauncher, isSystem;
+ final Set ownedDirs = {};
+
+ Package({
+ required this.packageName,
+ required this.currentLabel,
+ required this.englishLabel,
+ required this.categoryLauncher,
+ required this.isSystem,
+ });
+
+ factory Package.fromMap(Map map) {
+ return Package(
+ packageName: map['packageName'] ?? '',
+ currentLabel: map['currentLabel'],
+ englishLabel: map['englishLabel'],
+ categoryLauncher: map['categoryLauncher'] ?? false,
+ isSystem: map['isSystem'] ?? false,
+ );
+ }
+
+ Set get potentialDirs => [
+ currentLabel,
+ englishLabel,
+ ...ownedDirs,
+ ].whereNotNull().toSet();
+
+ @override
+ String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}';
+}
diff --git a/lib/model/covers.dart b/lib/model/covers.dart
index 0eb13eb55..b36dee1a9 100644
--- a/lib/model/covers.dart
+++ b/lib/model/covers.dart
@@ -1,12 +1,14 @@
import 'dart:async';
+import 'package:aves/model/apps.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
-import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/model/source/collection_source.dart';
+import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
+import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
@@ -121,7 +123,7 @@ class Covers {
String? effectiveAlbumPackage(String albumPath) {
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
- return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath);
+ return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath);
}
// import/export
diff --git a/lib/model/device.dart b/lib/model/device.dart
index 22ce40c16..2b431ac87 100644
--- a/lib/model/device.dart
+++ b/lib/model/device.dart
@@ -10,7 +10,7 @@ final Device device = Device._private();
class Device {
late final String _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
- late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
+ late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
String get userAgent => _userAgent;
@@ -25,6 +25,8 @@ class Device {
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
+ bool get canRenderSubdivisionFlagEmojis => _canRenderSubdivisionFlagEmojis;
+
bool get canRequestManageMedia => _canRequestManageMedia;
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
@@ -71,6 +73,7 @@ class Device {
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canPrint = capabilities['canPrint'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
+ _canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
diff --git a/lib/model/entry/dirs.dart b/lib/model/entry/dirs.dart
index c500504a3..6db7918a4 100644
--- a/lib/model/entry/dirs.dart
+++ b/lib/model/entry/dirs.dart
@@ -52,7 +52,7 @@ class EntryDir {
}
String? _resolve() {
- final vrl = VolumeRelativeDirectory.fromPath(asIs!);
+ final vrl = androidFileUtils.relativeDirectoryFromPath(asIs!);
if (vrl == null || vrl.relativeDir.isEmpty) return asIs;
var resolved = vrl.volumePath;
diff --git a/lib/model/entry/entry.dart b/lib/model/entry/entry.dart
index 268357e2a..13b1a5b5b 100644
--- a/lib/model/entry/entry.dart
+++ b/lib/model/entry/entry.dart
@@ -1,5 +1,4 @@
import 'dart:async';
-import 'dart:io';
import 'dart:ui';
import 'package:aves/model/entry/cache.dart';
@@ -7,13 +6,12 @@ import 'package:aves/model/entry/dirs.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
-import 'package:aves/model/source/trash.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/format.dart';
-import 'package:aves_utils/aves_utils.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
+import 'package:aves_utils/aves_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
@@ -80,10 +78,6 @@ class AvesEntry with AvesEntryBase {
this.durationMillis = durationMillis;
}
- bool get canDecode => !MimeTypes.undecodableImages.contains(mimeType);
-
- bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
-
AvesEntry copyWith({
int? id,
String? uri,
@@ -225,15 +219,6 @@ class AvesEntry with AvesEntryBase {
return _extension;
}
- String? get storagePath => trashed ? trashDetails?.path : path;
-
- String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory;
-
- bool get isMissingAtPath {
- final _storagePath = storagePath;
- return _storagePath != null && !File(_storagePath).existsSync();
- }
-
// the MIME type reported by the Media Store is unreliable
// so we use the one found during cataloguing if possible
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
@@ -323,18 +308,6 @@ class AvesEntry with AvesEntryBase {
return _durationText!;
}
- bool get isExpiredTrash {
- final dateMillis = trashDetails?.dateMillis;
- if (dateMillis == null) return false;
- return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now());
- }
-
- int? get trashDaysLeft {
- final dateMillis = trashDetails?.dateMillis;
- if (dateMillis == null) return null;
- return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
- }
-
// returns whether this entry has GPS coordinates
// (0, 0) coordinates are considered invalid, as it is likely a default value
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
diff --git a/lib/model/entry/extensions/images.dart b/lib/model/entry/extensions/images.dart
index 46e18a337..ee9dbafd1 100644
--- a/lib/model/entry/extensions/images.dart
+++ b/lib/model/entry/extensions/images.dart
@@ -7,7 +7,7 @@ import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart';
-import 'package:flutter/widgets.dart';
+import 'package:flutter/painting.dart';
extension ExtraAvesEntryImages on AvesEntry {
bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent));
diff --git a/lib/model/entry/extensions/info.dart b/lib/model/entry/extensions/info.dart
index 46649a95f..04b064a1d 100644
--- a/lib/model/entry/extensions/info.dart
+++ b/lib/model/entry/extensions/info.dart
@@ -9,11 +9,11 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/colors.dart';
-import 'package:aves/utils/constants.dart';
+import 'package:aves/theme/text.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
-import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
extension ExtraAvesEntryInfo on AvesEntry {
@@ -115,7 +115,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
final dirName = [
'Stream ${index.toString().padLeft(indexDigits, '0')}',
typeText,
- ].join(Constants.separator);
+ ].join(AText.separator);
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
if (formattedStreamTags.isNotEmpty) {
final color = colors.fromString(typeText);
diff --git a/lib/model/entry/extensions/location.dart b/lib/model/entry/extensions/location.dart
index 97f477c15..e33c4ab72 100644
--- a/lib/model/entry/extensions/location.dart
+++ b/lib/model/entry/extensions/location.dart
@@ -12,6 +12,8 @@ import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
extension ExtraAvesEntryLocation on AvesEntry {
+ static final _invalidLocalityPattern = RegExp(r'^[-+\dA-Z]+$');
+
LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null;
Future locate({required bool background, required bool force, required Locale geocoderLocale}) async {
@@ -53,18 +55,17 @@ extension ExtraAvesEntryLocation on AvesEntry {
)
: call());
if (addresses.isNotEmpty) {
- final address = addresses.first;
- final cc = address.countryCode?.toUpperCase();
- final cn = address.countryName;
- final aa = address.adminArea;
+ final v = addresses.first;
+ var locality = v.locality ?? v.subLocality ?? v.featureName;
+ if (locality == null || _invalidLocalityPattern.hasMatch(locality) || {v.subThoroughfare, v.countryName}.contains(locality)) {
+ locality = v.subAdminArea;
+ }
addressDetails = AddressDetails(
id: id,
- countryCode: cc,
- countryName: cn,
- adminArea: aa,
- // if country & admin fields are null, it is likely the ocean,
- // which is identified by `featureName` but we default to the address line anyway
- locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null),
+ countryCode: v.countryCode?.toUpperCase(),
+ countryName: v.countryName,
+ adminArea: v.adminArea,
+ locality: locality,
);
}
} catch (error, stack) {
diff --git a/lib/model/entry/extensions/metadata_edition.dart b/lib/model/entry/extensions/metadata_edition.dart
index eaa156585..9d1f2f7c3 100644
--- a/lib/model/entry/extensions/metadata_edition.dart
+++ b/lib/model/entry/extensions/metadata_edition.dart
@@ -1,20 +1,20 @@
import 'dart:convert';
import 'dart:io';
+import 'package:aves/convert/convert.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/metadata/date_modifier.dart';
-import 'package:aves/model/metadata/enums/date_field_source.dart';
-import 'package:aves/model/metadata/enums/enums.dart';
-import 'package:aves/model/metadata/fields.dart';
-import 'package:aves/ref/exif.dart';
-import 'package:aves/ref/iptc.dart';
+import 'package:aves/ref/metadata/exif.dart';
+import 'package:aves/ref/metadata/iptc.dart';
import 'package:aves/ref/mime_types.dart';
+import 'package:aves/ref/metadata/xmp.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/xmp.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves/utils/xmp_utils.dart';
+import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
@@ -27,7 +27,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final appliedModifier = await _applyDateModifierToEntry(userModifier);
if (appliedModifier == null) {
- if (!isMissingAtPath && userModifier.action != DateEditAction.copyField) {
+ if (isValid && userModifier.action != DateEditAction.copyField) {
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null);
}
return {};
@@ -54,7 +54,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
break;
case DateEditAction.shift:
- final xmpDate = XMP.getString(descriptions, XMP.xmpCreateDate, namespace: Namespaces.xmp);
+ final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp);
if (xmpDate != null) {
final date = DateTime.tryParse(xmpDate);
if (date != null) {
@@ -262,18 +262,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
if (editTitle) {
modified |= XMP.setAttribute(
descriptions,
- XMP.dcTitle,
+ XmpElements.dcTitle,
title,
- namespace: Namespaces.dc,
+ namespace: XmpNamespaces.dc,
strat: XmpEditStrategy.always,
);
}
if (editDescription) {
modified |= XMP.setAttribute(
descriptions,
- XMP.dcDescription,
+ XmpElements.dcDescription,
description,
- namespace: Namespaces.dc,
+ namespace: XmpNamespaces.dc,
strat: XmpEditStrategy.always,
);
}
@@ -417,9 +417,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
static bool editCreateDateXmp(List descriptions, DateTime? date) {
return XMP.setAttribute(
descriptions,
- XMP.xmpCreateDate,
+ XmpAttributes.xmpCreateDate,
date != null ? XMP.toXmpDate(date) : null,
- namespace: Namespaces.xmp,
+ namespace: XmpNamespaces.xmp,
strat: XmpEditStrategy.always,
);
}
@@ -428,9 +428,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
static bool editTagsXmp(List descriptions, Set tags) {
return XMP.setStringBag(
descriptions,
- XMP.dcSubject,
+ XmpElements.dcSubject,
tags,
- namespace: Namespaces.dc,
+ namespace: XmpNamespaces.dc,
strat: XmpEditStrategy.always,
);
}
@@ -441,17 +441,17 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
modified |= XMP.setAttribute(
descriptions,
- XMP.xmpRating,
+ XmpElements.xmpRating,
(rating ?? 0) == 0 ? null : '$rating',
- namespace: Namespaces.xmp,
+ namespace: XmpNamespaces.xmp,
strat: XmpEditStrategy.always,
);
modified |= XMP.setAttribute(
descriptions,
- XMP.msPhotoRating,
+ XmpElements.msPhotoRating,
XMP.toMsPhotoRating(rating),
- namespace: Namespaces.microsoftPhoto,
+ namespace: XmpNamespaces.microsoftPhoto,
strat: XmpEditStrategy.updateIfPresent,
);
@@ -464,23 +464,23 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
modified |= XMP.removeElements(
descriptions,
- XMP.containerDirectory,
- Namespaces.gContainer,
+ XmpElements.containerDirectory,
+ XmpNamespaces.gContainer,
);
modified |= [
- XMP.gCameraMicroVideo,
- XMP.gCameraMicroVideoVersion,
- XMP.gCameraMicroVideoOffset,
- XMP.gCameraMicroVideoPresentationTimestampUs,
- XMP.gCameraMotionPhoto,
- XMP.gCameraMotionPhotoVersion,
- XMP.gCameraMotionPhotoPresentationTimestampUs,
+ XmpAttributes.gCameraMicroVideo,
+ XmpAttributes.gCameraMicroVideoVersion,
+ XmpAttributes.gCameraMicroVideoOffset,
+ XmpAttributes.gCameraMicroVideoPresentationTimestampUs,
+ XmpAttributes.gCameraMotionPhoto,
+ XmpAttributes.gCameraMotionPhotoVersion,
+ XmpAttributes.gCameraMotionPhotoPresentationTimestampUs,
].fold(modified, (prev, name) {
return prev |= XMP.removeElements(
descriptions,
name,
- Namespaces.gCamera,
+ XmpNamespaces.gCamera,
);
});
diff --git a/lib/model/entry/extensions/multipage.dart b/lib/model/entry/extensions/multipage.dart
index f77f252b1..977dd1733 100644
--- a/lib/model/entry/extensions/multipage.dart
+++ b/lib/model/entry/extensions/multipage.dart
@@ -7,8 +7,6 @@ import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
extension ExtraAvesEntryMultipage on AvesEntry {
- static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
-
bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst;
bool get isBurst => burstEntries?.isNotEmpty == true;
@@ -18,11 +16,13 @@ extension ExtraAvesEntryMultipage on AvesEntry {
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
- String? get burstKey {
+ String? getBurstKey(List patterns) {
if (filenameWithoutExtension != null) {
- final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
- if (match != null) {
- return '$directory/${match.group(1)}';
+ for (final pattern in patterns) {
+ final match = RegExp(pattern).firstMatch(filenameWithoutExtension!);
+ if (match != null) {
+ return '$directory/${match.group(1)}';
+ }
}
}
return null;
diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart
index ed1c3ef1c..f5d064c2f 100644
--- a/lib/model/entry/extensions/props.dart
+++ b/lib/model/entry/extensions/props.dart
@@ -1,63 +1,113 @@
+import 'dart:io';
import 'dart:ui';
+import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/settings/settings.dart';
+import 'package:aves/model/source/trash.dart';
import 'package:aves/ref/mime_types.dart';
+import 'package:aves/ref/unicode.dart';
+import 'package:aves/services/common/services.dart';
+import 'package:aves/theme/text.dart';
import 'package:aves/utils/android_file_utils.dart';
extension ExtraAvesEntryProps on AvesEntry {
+ bool get isValid => !isMissingAtPath && sizeBytes != 0 && width > 0 && height > 0;
+
+ // type
+
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
+ bool get canHaveAlpha => MimeTypes.canHaveAlpha(mimeType);
+
bool get isSvg => mimeType == MimeTypes.svg;
- // guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
- bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
-
- // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
- // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
- // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
- bool get _supportedByBitmapRegionDecoder =>
- [
- MimeTypes.heic,
- MimeTypes.heif,
- MimeTypes.jpeg,
- MimeTypes.png,
- MimeTypes.webp,
- MimeTypes.arw,
- MimeTypes.cr2,
- MimeTypes.nef,
- MimeTypes.nrw,
- MimeTypes.orf,
- MimeTypes.pef,
- MimeTypes.raf,
- MimeTypes.rw2,
- MimeTypes.srw,
- ].contains(mimeType) &&
- !isAnimated;
-
- bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
-
- bool get useTiles => supportTiling && (width > 4096 || height > 4096);
-
- bool get isRaw => MimeTypes.rawImages.contains(mimeType);
+ bool get isRaw => MimeTypes.isRaw(mimeType);
bool get isImage => MimeTypes.isImage(mimeType);
bool get isVideo => MimeTypes.isVideo(mimeType);
+ // size
+
+ bool get useTiles => canDecodeRegion && (width > 4096 || height > 4096);
+
+ bool get isSized => width > 0 && height > 0;
+
+ Size videoDisplaySize(double sar) {
+ final size = displaySize;
+ if (sar != 1) {
+ final dar = displayAspectRatio * sar;
+ final w = size.width;
+ final h = size.height;
+ if (w >= h) return Size(w, w / dar);
+ if (h > w) return Size(h * dar, h);
+ }
+ return size;
+ }
+
+ // text
+
+ String get resolutionText {
+ final ws = width;
+ final hs = height;
+ return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs';
+ }
+
+ String get aspectRatioText {
+ const separator = UniChars.ratio;
+ if (width > 0 && height > 0) {
+ final gcd = width.gcd(height);
+ final w = width ~/ gcd;
+ final h = height ~/ gcd;
+ return isRotated ? '$h$separator$w' : '$w$separator$h';
+ } else {
+ return '?$separator?';
+ }
+ }
+
+ // catalog
+
bool get isAnimated => catalogMetadata?.isAnimated ?? false;
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
bool get is360 => catalogMetadata?.is360 ?? false;
- bool get isMediaStoreContent => uri.startsWith('content://media/');
+ // trash
- bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
+ bool get isExpiredTrash {
+ final dateMillis = trashDetails?.dateMillis;
+ if (dateMillis == null) return false;
+ return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now());
+ }
- bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
+ int? get trashDaysLeft {
+ final dateMillis = trashDetails?.dateMillis;
+ if (dateMillis == null) return null;
+ return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
+ }
- bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
+ // storage
+
+ String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory;
+
+ bool get isMissingAtPath {
+ final _storagePath = trashed ? trashDetails?.path : path;
+ return _storagePath != null && !File(_storagePath).existsSync();
+ }
+
+ // providers
+
+ bool get _isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
+
+ bool get _isMediaStoreContent => uri.startsWith(AndroidFileUtils.mediaStoreUriRoot);
+
+ bool get isMediaStoreMediaContent => _isMediaStoreContent && AndroidFileUtils.mediaUriPathRoots.any(uri.contains);
+
+ // edition
+
+ bool get canEdit => !settings.isReadOnly && path != null && !trashed && (_isMediaStoreContent || _isVaultContent);
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
@@ -73,47 +123,17 @@ extension ExtraAvesEntryProps on AvesEntry {
bool get canFlip => canEdit && canEditExif;
- bool get canEditExif => MimeTypes.canEditExif(mimeType);
+ // app support
- bool get canEditIptc => MimeTypes.canEditIptc(mimeType);
+ bool get canDecode => AppSupport.canDecode(mimeType);
- bool get canEditXmp => MimeTypes.canEditXmp(mimeType);
+ bool get canDecodeRegion => AppSupport.canDecodeRegion(mimeType) && !isAnimated;
- bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType);
+ bool get canEditExif => AppSupport.canEditExif(mimeType);
- static const ratioSeparator = '\u2236';
- static const resolutionSeparator = ' \u00D7 ';
+ bool get canEditIptc => AppSupport.canEditIptc(mimeType);
- bool get isSized => width > 0 && height > 0;
+ bool get canEditXmp => AppSupport.canEditXmp(mimeType);
- String get resolutionText {
- final ws = width;
- final hs = height;
- return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
- }
-
- String get aspectRatioText {
- if (width > 0 && height > 0) {
- final gcd = width.gcd(height);
- final w = width ~/ gcd;
- final h = height ~/ gcd;
- return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
- } else {
- return '?$ratioSeparator?';
- }
- }
-
- Size videoDisplaySize(double sar) {
- final size = displaySize;
- if (sar != 1) {
- final dar = displayAspectRatio * sar;
- final w = size.width;
- final h = size.height;
- if (w >= h) return Size(w, w / dar);
- if (h > w) return Size(h * dar, h);
- }
- return size;
- }
-
- int get megaPixels => (width * height / 1000000).round();
+ bool get canRemoveMetadata => AppSupport.canRemoveMetadata(mimeType);
}
diff --git a/lib/model/actions/events.dart b/lib/model/events.dart
similarity index 100%
rename from lib/model/actions/events.dart
rename to lib/model/events.dart
diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart
index 8c5eeea57..5c56b8ad3 100644
--- a/lib/model/favourites.dart
+++ b/lib/model/favourites.dart
@@ -2,9 +2,10 @@ import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
+import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
-import 'package:flutter/widgets.dart';
+import 'package:flutter/foundation.dart';
final Favourites favourites = Favourites._private();
diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart
index 226436bbf..7f5f8aa77 100644
--- a/lib/model/filters/album.dart
+++ b/lib/model/filters/album.dart
@@ -3,8 +3,8 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
-import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart';
+import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart
index 2c822fecf..4a17edb2e 100644
--- a/lib/model/filters/coordinate.dart
+++ b/lib/model/filters/coordinate.dart
@@ -2,13 +2,11 @@ import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/enums/coordinate_format.dart';
-import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart';
-import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_map/aves_map.dart';
-import 'package:flutter/material.dart';
+import 'package:aves_model/aves_model.dart';
import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
@@ -61,7 +59,7 @@ class CoordinateFilter extends CollectionFilter {
bool get exclusiveProp => false;
@override
- String get universalLabel => _formatBounds(lookupAppLocalizations(AvesApp.supportedLocales.first), CoordinateFormat.decimal);
+ String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal);
@override
String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read().coordinateFormat);
diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart
index 413da4c86..3b5ff0333 100644
--- a/lib/model/filters/favourite.dart
+++ b/lib/model/filters/favourite.dart
@@ -5,7 +5,7 @@ import 'package:aves/theme/colors.dart';
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:flutter/widgets.dart';
import 'package:provider/provider.dart';
class FavouriteFilter extends CollectionFilter {
diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart
index aab90081e..0c547defd 100644
--- a/lib/model/filters/location.dart
+++ b/lib/model/filters/location.dart
@@ -1,6 +1,7 @@
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart';
+import 'package:aves/utils/emoji_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
@@ -11,23 +12,31 @@ class LocationFilter extends CoveredCollectionFilter {
final LocationLevel level;
late final String _location;
- late final String? _countryCode;
+ late final String? _code;
late final EntryFilter _test;
@override
- List