Merge branch 'develop'
This commit is contained in:
commit
03df8fbd26
499 changed files with 9111 additions and 4162 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
||||||
Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da
|
Subproject commit f72efea43c3013323d1b95cff571f3c1caa37583
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -32,9 +32,6 @@ migrate_working_dir/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Web related
|
|
||||||
lib/generated_plugin_registrant.dart
|
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
|
|
26
.metadata
26
.metadata
|
@ -1,10 +1,30 @@
|
||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# 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:
|
version:
|
||||||
revision: bc7bc940836f1f834699625426795fd6f07c18ec
|
revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
|
||||||
channel: beta
|
channel: stable
|
||||||
|
|
||||||
project_type: app
|
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'
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
## <a id="v1.8.5"></a>[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
|
||||||
|
|
||||||
## <a id="v1.8.4"></a>[v1.8.4] - 2023-03-17
|
## <a id="v1.8.4"></a>[v1.8.4] - 2023-03-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -115,7 +139,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Changed
|
### 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
|
- in the tag editor, tapping on applied tag applies it to all items instead of removing it
|
||||||
- pin app bar when selecting items
|
- pin app bar when selecting items
|
||||||
|
|
||||||
|
|
2
android/.gitignore
vendored
2
android/.gitignore
vendored
|
@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java
|
||||||
# Remember to never publicly share your keystore.
|
# Remember to never publicly share your keystore.
|
||||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||||
key.properties
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
|
|
|
@ -46,6 +46,16 @@ if (keystorePropertiesFile.exists()) {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 33
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
@ -148,6 +158,7 @@ android {
|
||||||
// which lead to: UnsatisfiedLinkError...couldn't find "libflutter.so"
|
// which lead to: UnsatisfiedLinkError...couldn't find "libflutter.so"
|
||||||
// cf https://github.com/flutter/flutter/issues/37566#issuecomment-640879500
|
// cf https://github.com/flutter/flutter/issues/37566#issuecomment-640879500
|
||||||
ndk {
|
ndk {
|
||||||
|
//noinspection ChromeOsAbiSupport
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,9 +194,10 @@ repositories {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
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.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.media:media:1.6.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
|
||||||
|
@ -193,9 +205,9 @@ dependencies {
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.18.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`
|
// SLF4J implementation for `mp4parser`
|
||||||
implementation 'org.slf4j:slf4j-simple:2.0.6'
|
implementation 'org.slf4j:slf4j-simple:2.0.7'
|
||||||
|
|
||||||
// forked, built by JitPack:
|
// forked, built by JitPack:
|
||||||
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||||
|
@ -210,7 +222,7 @@ dependencies {
|
||||||
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300'
|
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300'
|
||||||
|
|
||||||
kapt 'androidx.annotation:annotation:1.6.0'
|
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')
|
compileOnly rootProject.findProject(':streams_channel')
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
Gradle v7.4 / Android Gradle Plugin v7.3.0 recommend:
|
Gradle v7.4 / Android Gradle Plugin v7.3.0 recommend:
|
||||||
- removing "package" from AndroidManifest.xml
|
- removing "package" from AndroidManifest.xml
|
||||||
- adding it as "namespace" in app/build.gradle
|
- 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.
|
||||||
-->
|
-->
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
|
@ -251,6 +251,11 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||||
when (val action = intent?.action) {
|
when (val action = intent?.action) {
|
||||||
Intent.ACTION_MAIN -> {
|
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 ->
|
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
||||||
val filters = extractFiltersFromIntent(intent)
|
val filters = extractFiltersFromIntent(intent)
|
||||||
return hashMapOf(
|
return hashMapOf(
|
||||||
|
@ -393,7 +398,16 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
)
|
)
|
||||||
.build()
|
.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() {
|
private fun onAnalysisCompleted() {
|
||||||
|
@ -428,12 +442,14 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
|
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
|
||||||
const val INTENT_DATA_KEY_PAGE = "page"
|
const val INTENT_DATA_KEY_PAGE = "page"
|
||||||
const val INTENT_DATA_KEY_QUERY = "query"
|
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_URI = "uri"
|
||||||
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
|
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
|
||||||
|
|
||||||
const val EXTRA_KEY_PAGE = "page"
|
const val EXTRA_KEY_PAGE = "page"
|
||||||
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
|
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
|
||||||
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
|
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
|
||||||
|
const val EXTRA_KEY_SAFE_MODE = "safeMode"
|
||||||
const val EXTRA_KEY_WIDGET_ID = "widgetId"
|
const val EXTRA_KEY_WIDGET_ID = "widgetId"
|
||||||
|
|
||||||
// request code to pending runnable
|
// request code to pending runnable
|
||||||
|
|
|
@ -38,10 +38,6 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
import org.mp4parser.IsoFile
|
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.FileInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
@ -341,23 +337,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
||||||
pfd.use {
|
pfd.use {
|
||||||
FileInputStream(it.fileDescriptor).use { stream ->
|
FileInputStream(it.fileDescriptor).use { stream ->
|
||||||
stream.channel.use { channel ->
|
stream.channel.use { channel ->
|
||||||
val boxParser = PropertyBoxParserImpl().apply {
|
IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile ->
|
||||||
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.dumpBoxes(sb)
|
isoFile.dumpBoxes(sb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
||||||
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
|
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
|
||||||
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
|
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
|
||||||
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
|
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
|
||||||
|
"canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
|
||||||
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
|
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
|
||||||
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
|
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
|
||||||
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
||||||
|
|
|
@ -160,9 +160,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
thisDirName = "Spherical Video"
|
thisDirName = "Spherical Video"
|
||||||
metadataMap[thisDirName] = HashMap(GSpherical(bytes).describe())
|
metadataMap[thisDirName] = HashMap(GSpherical(bytes).describe())
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickTimeMetadata.PROF_UUID -> {
|
QuickTimeMetadata.PROF_UUID -> {
|
||||||
// redundant with info derived on the Dart side
|
// redundant with info derived on the Dart side
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickTimeMetadata.USMT_UUID -> {
|
QuickTimeMetadata.USMT_UUID -> {
|
||||||
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
||||||
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
|
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
|
||||||
|
@ -187,6 +189,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val uuidPart = uuid.substringBefore('-')
|
val uuidPart = uuid.substringBefore('-')
|
||||||
thisDirName = "${dir.name} $uuidPart"
|
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
|
// skip `Geo double/ascii params`, as their content is split and presented through various GeoTIFF keys
|
||||||
ExifGeoTiffTags.TAG_GEO_DOUBLE_PARAMS,
|
ExifGeoTiffTags.TAG_GEO_DOUBLE_PARAMS,
|
||||||
ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> ArrayList()
|
ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> ArrayList()
|
||||||
|
|
||||||
else -> listOf(exifTagMapper(tag))
|
else -> listOf(exifTagMapper(tag))
|
||||||
}
|
}
|
||||||
}?.let { geoTiffDirMap.putAll(it) }
|
}?.let { geoTiffDirMap.putAll(it) }
|
||||||
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
|
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeType == MimeTypes.DNG -> {
|
mimeType == MimeTypes.DNG -> {
|
||||||
// split DNG tags in their own directory
|
// split DNG tags in their own directory
|
||||||
val dngDirMap = metadataMap[DIR_DNG] ?: HashMap()
|
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[true]?.map { exifTagMapper(it) }?.let { dngDirMap.putAll(it) }
|
||||||
byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
|
byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> dirMap.putAll(tags.map { exifTagMapper(it) })
|
else -> dirMap.putAll(tags.map { exifTagMapper(it) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dir.isPngTextDir() -> {
|
dir.isPngTextDir() -> {
|
||||||
metadataMap.remove(thisDirName)
|
metadataMap.remove(thisDirName)
|
||||||
dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap()
|
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) })
|
else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -406,6 +414,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(mimeType)) {
|
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
|
// 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
|
// and to identify whether there is an accessible cover image
|
||||||
// do not include HEIC here
|
// do not include HEIC here
|
||||||
|
@ -641,12 +655,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MimeTypes.GIF -> {
|
MimeTypes.GIF -> {
|
||||||
// identification of animated GIF
|
// identification of animated GIF
|
||||||
if (metadata.containsDirectoryOfType(GifAnimationDirectory::class.java)) {
|
if (metadata.containsDirectoryOfType(GifAnimationDirectory::class.java)) {
|
||||||
flags = flags or MASK_IS_ANIMATED
|
flags = flags or MASK_IS_ANIMATED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MimeTypes.WEBP -> {
|
MimeTypes.WEBP -> {
|
||||||
// identification of animated WEBP
|
// identification of animated WEBP
|
||||||
for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) {
|
||||||
|
@ -655,6 +671,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MimeTypes.TIFF -> {
|
MimeTypes.TIFF -> {
|
||||||
// identification of GeoTIFF
|
// identification of GeoTIFF
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||||
|
@ -1119,16 +1136,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED -> {
|
ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED -> {
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||||
dir.getDateDigitizedMillis { dateMillis = it }
|
dir.getDateDigitizedMillis { dateMillis = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL -> {
|
ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL -> {
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||||
dir.getDateOriginalMillis { dateMillis = it }
|
dir.getDateOriginalMillis { dateMillis = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GpsDirectory.TAG_DATE_STAMP -> {
|
GpsDirectory.TAG_DATE_STAMP -> {
|
||||||
for (dir in metadata.getDirectoriesOfType(GpsDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(GpsDirectory::class.java)) {
|
||||||
dir.gpsDate?.let { dateMillis = it.time }
|
dir.gpsDate?.let { dateMillis = it.time }
|
||||||
|
|
|
@ -101,7 +101,17 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
endOfStream()
|
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")
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
|
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
|
||||||
|
@ -116,12 +126,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
fun onGranted(uri: Uri) {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = mimeType
|
|
||||||
putExtra(Intent.EXTRA_TITLE, name)
|
|
||||||
}
|
|
||||||
MainActivity.pendingStorageAccessResultHandlers[MainActivity.CREATE_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
|
|
||||||
ioScope.launch {
|
ioScope.launch {
|
||||||
try {
|
try {
|
||||||
// truncate is necessary when overwriting a longer file
|
// truncate is necessary when overwriting a longer file
|
||||||
|
@ -134,13 +139,20 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
}
|
}
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
}, {
|
}
|
||||||
|
|
||||||
|
fun onDenied() {
|
||||||
success(null)
|
success(null)
|
||||||
endOfStream()
|
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() {
|
private suspend fun openFile() {
|
||||||
@SuppressLint("ObsoleteSdkInt")
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
@ -178,13 +190,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
|
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
|
||||||
}
|
}
|
||||||
if (intent.resolveActivity(activity.packageManager) != null) {
|
safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickCollectionFilters() {
|
private fun pickCollectionFilters() {
|
||||||
|
|
|
@ -33,6 +33,7 @@ object Metadata {
|
||||||
const val DIR_DNG = "DNG" // custom
|
const val DIR_DNG = "DNG" // custom
|
||||||
const val DIR_EXIF_GEOTIFF = "GeoTIFF" // custom
|
const val DIR_EXIF_GEOTIFF = "GeoTIFF" // custom
|
||||||
const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom
|
const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom
|
||||||
|
const val DIR_MP4_USER_DATA = "User Data" // custom
|
||||||
|
|
||||||
// types of metadata
|
// types of metadata
|
||||||
const val TYPE_COMMENT = "comment"
|
const val TYPE_COMMENT = "comment"
|
||||||
|
|
|
@ -2,11 +2,22 @@ package deckers.thibault.aves.metadata
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
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.StorageUtils
|
||||||
|
import deckers.thibault.aves.utils.toByteArray
|
||||||
|
import deckers.thibault.aves.utils.toHex
|
||||||
import org.mp4parser.*
|
import org.mp4parser.*
|
||||||
|
import org.mp4parser.boxes.UnknownBox
|
||||||
import org.mp4parser.boxes.UserBox
|
import org.mp4parser.boxes.UserBox
|
||||||
|
import org.mp4parser.boxes.apple.AppleCoverBox
|
||||||
import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox
|
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.iso14496.part12.*
|
||||||
|
import org.mp4parser.boxes.threegpp.ts26244.AuthorBox
|
||||||
import org.mp4parser.support.AbstractBox
|
import org.mp4parser.support.AbstractBox
|
||||||
import org.mp4parser.support.Matrix
|
import org.mp4parser.support.Matrix
|
||||||
import org.mp4parser.tools.Path
|
import org.mp4parser.tools.Path
|
||||||
|
@ -15,8 +26,10 @@ import java.io.FileInputStream
|
||||||
import java.nio.channels.Channels
|
import java.nio.channels.Channels
|
||||||
|
|
||||||
object Mp4ParserHelper {
|
object Mp4ParserHelper {
|
||||||
|
private val LOG_TAG = LogUtils.createTag<Mp4ParserHelper>()
|
||||||
|
|
||||||
// arbitrary size to detect boxes that may yield an OOM
|
// 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<Pair<Long, ByteArray>> {
|
fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List<Pair<Long, ByteArray>> {
|
||||||
// we can skip uninteresting boxes with a seekable data source
|
// 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}")
|
sb.appendLine("${"\t".repeat(indent)}[$boxType] ${box.javaClass.simpleName}")
|
||||||
box.dumpBoxes(sb, indent + 1)
|
box.dumpBoxes(sb, indent + 1)
|
||||||
}
|
}
|
||||||
is UserBox -> {
|
|
||||||
val userTypeHex = box.userType.joinToString("") { "%02x".format(it) }
|
is UserBox -> sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=${box.userType.toHex()} $box")
|
||||||
sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=$userTypeHex $box")
|
|
||||||
}
|
|
||||||
else -> sb.appendLine("${"\t".repeat(indent)}[$boxType] $box")
|
else -> sb.appendLine("${"\t".repeat(indent)}[$boxType] $box")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -227,10 +238,127 @@ object Mp4ParserHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Box.toBytes(): ByteArray {
|
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())
|
val stream = ByteArrayOutputStream(size.toInt())
|
||||||
Channels.newChannel(stream).use { getBox(it) }
|
Channels.newChannel(stream).use { getBox(it) }
|
||||||
return stream.toByteArray()
|
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<String, String> {
|
||||||
|
val fields = HashMap<String, String>()
|
||||||
|
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<UserDataBox>(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<String, String> {
|
||||||
|
val fields = HashMap<String, String>()
|
||||||
|
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<HandlerBox>(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)
|
class Mp4TooLargeException(val type: String, message: String) : RuntimeException(message)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package deckers.thibault.aves.metadata
|
package deckers.thibault.aves.metadata
|
||||||
|
|
||||||
|
import deckers.thibault.aves.utils.toHex
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -51,7 +52,7 @@ object QuickTimeMetadata {
|
||||||
// 0x01: string
|
// 0x01: string
|
||||||
0x01 -> String(payload, Charset.forName("UTF-16BE")).trim()
|
0x01 -> String(payload, Charset.forName("UTF-16BE")).trim()
|
||||||
// 0x101: artwork/icon
|
// 0x101: artwork/icon
|
||||||
else -> "0x${payload.joinToString("") { "%02x".format(it) }}"
|
else -> "0x${payload.toHex()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val blockTypeString = when (blockType) {
|
val blockTypeString = when (blockType) {
|
||||||
|
@ -61,7 +62,7 @@ object QuickTimeMetadata {
|
||||||
0x0A -> "Track property"
|
0x0A -> "Track property"
|
||||||
0x0B -> "Time zone"
|
0x0B -> "Time zone"
|
||||||
0x0C -> "Modification Time"
|
0x0C -> "Modification Time"
|
||||||
else -> "0x${"%02x".format(blockType)}"
|
else -> "0x${blockType.toByte().toHex()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
blocks.add(
|
blocks.add(
|
||||||
|
|
|
@ -21,13 +21,9 @@ import deckers.thibault.aves.utils.MemoryUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import org.mp4parser.IsoFile
|
import org.mp4parser.IsoFile
|
||||||
import org.mp4parser.PropertyBoxParserImpl
|
|
||||||
import org.mp4parser.boxes.UserBox
|
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.io.FileInputStream
|
||||||
import java.util.*
|
import java.util.TimeZone
|
||||||
|
|
||||||
object XMP {
|
object XMP {
|
||||||
private val LOG_TAG = LogUtils.createTag<XMP>()
|
private val LOG_TAG = LogUtils.createTag<XMP>()
|
||||||
|
@ -156,26 +152,12 @@ object XMP {
|
||||||
pfd.use {
|
pfd.use {
|
||||||
FileInputStream(it.fileDescriptor).use { stream ->
|
FileInputStream(it.fileDescriptor).use { stream ->
|
||||||
stream.channel.use { channel ->
|
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,
|
// 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`,
|
// 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.
|
// 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, _ ->
|
isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
|
||||||
val boxSize = box.size
|
val boxSize = box.size
|
||||||
if (MemoryUtils.canAllocate(boxSize)) {
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get XMP by MP4 parser for mimeType=$mimeType uri=$uri", e)
|
Log.w(LOG_TAG, "failed to get XMP by MP4 parser for mimeType=$mimeType uri=$uri", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -815,6 +815,8 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: NoClassDefFoundError) {
|
||||||
|
callback.onFailure(e)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
callback.onFailure(e)
|
callback.onFailure(e)
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -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)
|
|
@ -17,7 +17,12 @@ import kotlin.coroutines.suspendCoroutine
|
||||||
object FlutterUtils {
|
object FlutterUtils {
|
||||||
private val LOG_TAG = LogUtils.createTag<FlutterUtils>()
|
private val LOG_TAG = LogUtils.createTag<FlutterUtils>()
|
||||||
|
|
||||||
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)
|
val callbackHandle = context.getSharedPreferences(sharedPreferencesKey, Context.MODE_PRIVATE).getLong(callbackHandleKey, 0)
|
||||||
if (callbackHandle == 0L) {
|
if (callbackHandle == 0L) {
|
||||||
Log.e(LOG_TAG, "failed to retrieve registered callback handle for sharedPreferencesKey=$sharedPreferencesKey callbackHandleKey=$callbackHandleKey")
|
Log.e(LOG_TAG, "failed to retrieve registered callback handle for sharedPreferencesKey=$sharedPreferencesKey callbackHandleKey=$callbackHandleKey")
|
||||||
|
|
|
@ -195,11 +195,8 @@ object PermissionManager {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
// cf https://developer.android.com/about/versions/11/privacy/storage#directory-access
|
// cf https://developer.android.com/about/versions/11/privacy/storage#directory-access
|
||||||
dirs.add(Environment.DIRECTORY_DOWNLOADS)
|
dirs.add(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
// depends on device, no documentation
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
dirs.add("Android")
|
||||||
// by observation, no documentation
|
|
||||||
dirs.add("Android")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dirs
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,23 @@ import java.util.regex.Pattern
|
||||||
object StorageUtils {
|
object StorageUtils {
|
||||||
private val LOG_TAG = LogUtils.createTag<StorageUtils>()
|
private val LOG_TAG = LogUtils.createTag<StorageUtils>()
|
||||||
|
|
||||||
// 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"
|
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 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 UUID_PATTERN = Regex("[A-Fa-f\\d-]+")
|
||||||
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
|
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
|
// fallback when UUID does not appear in the SD card volume path
|
||||||
val primaryVolumePath = getPrimaryVolumePath(context)
|
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")
|
Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid")
|
||||||
return null
|
return null
|
||||||
|
@ -535,7 +557,7 @@ object StorageUtils {
|
||||||
uri ?: return false
|
uri ?: return false
|
||||||
// a URI's authority is [userinfo@]host[:port]
|
// a URI's authority is [userinfo@]host[:port]
|
||||||
// but we only want the host when comparing to Media Store's "authority"
|
// 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 {
|
fun getOriginalUri(context: Context, uri: Uri): Uri {
|
||||||
|
@ -544,7 +566,7 @@ object StorageUtils {
|
||||||
val path = uri.path
|
val path = uri.path
|
||||||
path ?: return uri
|
path ?: return uri
|
||||||
// from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException`
|
// 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"
|
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
|
||||||
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
return MediaStore.setRequireOriginal(uri)
|
return MediaStore.setRequireOriginal(uri)
|
||||||
|
@ -601,7 +623,7 @@ object StorageUtils {
|
||||||
return uri
|
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
|
// 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.
|
// by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI.
|
||||||
private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? {
|
private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/ic_shortcut_background"
|
||||||
|
android:pathData="M0,24 A1,1 0 1,1 48,24 A1,1 0 1,1 0,24" />
|
||||||
|
<group
|
||||||
|
android:translateX="12"
|
||||||
|
android:translateY="12">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/ic_shortcut_foreground"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:tint="@color/ic_shortcut_foreground"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group
|
||||||
|
android:scaleX="1.7226"
|
||||||
|
android:scaleY="1.7226"
|
||||||
|
android:translateX="33.3288"
|
||||||
|
android:translateY="33.3288">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_shortcut_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_shortcut_safe_mode_foreground" />
|
||||||
|
</adaptive-icon>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_notification_default_title">Prohledávání médií</string>
|
<string name="analysis_notification_default_title">Prohledávání médií</string>
|
||||||
<string name="analysis_notification_action_stop">Zastavit</string>
|
<string name="analysis_notification_action_stop">Zastavit</string>
|
||||||
<string name="app_widget_label">Fotorámeček</string>
|
<string name="app_widget_label">Fotorámeček</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Bezpečný režim</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">Bilder & Videos scannen</string>
|
<string name="analysis_service_description">Bilder & Videos scannen</string>
|
||||||
<string name="analysis_notification_default_title">Medien scannen</string>
|
<string name="analysis_notification_default_title">Medien scannen</string>
|
||||||
<string name="analysis_notification_action_stop">Abbrechen</string>
|
<string name="analysis_notification_action_stop">Abbrechen</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Sicherer Modus</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">Explorar imágenes & videos</string>
|
<string name="analysis_service_description">Explorar imágenes & videos</string>
|
||||||
<string name="analysis_notification_default_title">Explorando medios</string>
|
<string name="analysis_notification_default_title">Explorando medios</string>
|
||||||
<string name="analysis_notification_action_stop">Anular</string>
|
<string name="analysis_notification_action_stop">Anular</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Modo seguro</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_notification_action_stop">Gelditu</string>
|
<string name="analysis_notification_action_stop">Gelditu</string>
|
||||||
<string name="analysis_notification_default_title">Media eskaneatzen</string>
|
<string name="analysis_notification_default_title">Media eskaneatzen</string>
|
||||||
<string name="app_name">Aves</string>
|
<string name="app_name">Aves</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Modu segurua</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">Analyse des images & vidéos</string>
|
<string name="analysis_service_description">Analyse des images & vidéos</string>
|
||||||
<string name="analysis_notification_default_title">Analyse des images</string>
|
<string name="analysis_notification_default_title">Analyse des images</string>
|
||||||
<string name="analysis_notification_action_stop">Annuler</string>
|
<string name="analysis_notification_action_stop">Annuler</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Mode sans échec</string>
|
||||||
</resources>
|
</resources>
|
12
android/app/src/main/res/values-hi/strings.xml
Normal file
12
android/app/src/main/res/values-hi/strings.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="analysis_notification_default_title">मीडिया जाँचा जा राहा है</string>
|
||||||
|
<string name="analysis_notification_action_stop">रोके</string>
|
||||||
|
<string name="app_widget_label">फोटो फ्रेम</string>
|
||||||
|
<string name="wallpaper">वॉलपेपर</string>
|
||||||
|
<string name="search_shortcut_short_label">खोजें</string>
|
||||||
|
<string name="analysis_channel_name">मीडिया जाँचे</string>
|
||||||
|
<string name="app_name">ऐवीज</string>
|
||||||
|
<string name="videos_shortcut_short_label">वीडियो</string>
|
||||||
|
<string name="analysis_service_description">छवि & वीडियो जाँचे</string>
|
||||||
|
</resources>
|
8
android/app/src/main/res/values-hu/strings.xml
Normal file
8
android/app/src/main/res/values-hu/strings.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Aves</string>
|
||||||
|
<string name="wallpaper">Háttérkép</string>
|
||||||
|
<string name="search_shortcut_short_label">Keresés</string>
|
||||||
|
<string name="videos_shortcut_short_label">Videók</string>
|
||||||
|
<string name="analysis_notification_action_stop">Állj</string>
|
||||||
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">Pindai gambar & video</string>
|
<string name="analysis_service_description">Pindai gambar & video</string>
|
||||||
<string name="analysis_notification_default_title">Memindai media</string>
|
<string name="analysis_notification_default_title">Memindai media</string>
|
||||||
<string name="analysis_notification_action_stop">Berhenti</string>
|
<string name="analysis_notification_action_stop">Berhenti</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Mode aman</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">Scansione immagini & videos</string>
|
<string name="analysis_service_description">Scansione immagini & videos</string>
|
||||||
<string name="analysis_notification_default_title">Scansione in corso</string>
|
<string name="analysis_notification_default_title">Scansione in corso</string>
|
||||||
<string name="analysis_notification_action_stop">Annulla</string>
|
<string name="analysis_notification_action_stop">Annulla</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Modalità provvisoria</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">画像と動画をスキャン</string>
|
<string name="analysis_service_description">画像と動画をスキャン</string>
|
||||||
<string name="analysis_notification_default_title">メディアをスキャン中</string>
|
<string name="analysis_notification_default_title">メディアをスキャン中</string>
|
||||||
<string name="analysis_notification_action_stop">停止</string>
|
<string name="analysis_notification_action_stop">停止</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">セーフモード</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">사진과 동영상 분석</string>
|
<string name="analysis_service_description">사진과 동영상 분석</string>
|
||||||
<string name="analysis_notification_default_title">미디어 분석</string>
|
<string name="analysis_notification_default_title">미디어 분석</string>
|
||||||
<string name="analysis_notification_action_stop">취소</string>
|
<string name="analysis_notification_action_stop">취소</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">안전 모드</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="wallpaper">Bakgrunnsbilde</string>
|
<string name="wallpaper">Bakgrunnsbilde</string>
|
||||||
<string name="search_shortcut_short_label">Søk</string>
|
<string name="search_shortcut_short_label">Søk</string>
|
||||||
<string name="analysis_notification_action_stop">Stopp</string>
|
<string name="analysis_notification_action_stop">Stopp</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Trygt modus</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_notification_action_stop">Zatrzymaj</string>
|
<string name="analysis_notification_action_stop">Zatrzymaj</string>
|
||||||
<string name="app_name">Aves</string>
|
<string name="app_name">Aves</string>
|
||||||
<string name="wallpaper">Tapeta</string>
|
<string name="wallpaper">Tapeta</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Tryb bezpieczny</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_notification_default_title">Scanarea suporturilor</string>
|
<string name="analysis_notification_default_title">Scanarea suporturilor</string>
|
||||||
<string name="analysis_notification_action_stop">Stop</string>
|
<string name="analysis_notification_action_stop">Stop</string>
|
||||||
<string name="search_shortcut_short_label">Căutare</string>
|
<string name="search_shortcut_short_label">Căutare</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Modul de siguranță</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_service_description">Сканировать изображения и видео</string>
|
<string name="analysis_service_description">Сканировать изображения и видео</string>
|
||||||
<string name="analysis_notification_default_title">Сканирование медиа</string>
|
<string name="analysis_notification_default_title">Сканирование медиа</string>
|
||||||
<string name="analysis_notification_action_stop">Стоп</string>
|
<string name="analysis_notification_action_stop">Стоп</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Безопасный режим</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,5 @@
|
||||||
<string name="analysis_notification_action_stop">Стоп</string>
|
<string name="analysis_notification_action_stop">Стоп</string>
|
||||||
<string name="app_widget_label">Фоторамка</string>
|
<string name="app_widget_label">Фоторамка</string>
|
||||||
<string name="analysis_notification_default_title">Сканування медіа</string>
|
<string name="analysis_notification_default_title">Сканування медіа</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Безпечний режим</string>
|
||||||
</resources>
|
</resources>
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="app_name">Aves</string>
|
<string name="app_name">Aves</string>
|
||||||
<string name="app_widget_label">Photo Frame</string>
|
<string name="app_widget_label">Photo Frame</string>
|
||||||
<string name="wallpaper">Wallpaper</string>
|
<string name="wallpaper">Wallpaper</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Safe mode</string>
|
||||||
<string name="search_shortcut_short_label">Search</string>
|
<string name="search_shortcut_short_label">Search</string>
|
||||||
<string name="videos_shortcut_short_label">Videos</string>
|
<string name="videos_shortcut_short_label">Videos</string>
|
||||||
<string name="analysis_channel_name">Media scan</string>
|
<string name="analysis_channel_name">Media scan</string>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
kotlin_version = '1.7.20'
|
kotlin_version = '1.8.0'
|
||||||
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||||
useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") }
|
useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") }
|
||||||
useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") }
|
useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") }
|
||||||
|
@ -18,8 +17,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.4.2'
|
||||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
if (useCrashlytics) {
|
if (useCrashlytics) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#Thu Oct 22 10:54:33 KST 2020
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Galerie und Metadata Explorer
|
Galerie und Metadaten Explorer
|
5
fastlane/metadata/android/en-US/changelogs/96.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/96.txt
Normal file
|
@ -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
|
5
fastlane/metadata/android/en-US/changelogs/9601.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/9601.txt
Normal file
|
@ -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
|
5
fastlane/metadata/android/hi/full_description.txt
Normal file
5
fastlane/metadata/android/hi/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||||
|
|
||||||
|
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||||
|
|
||||||
|
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/hi/short_description.txt
Normal file
1
fastlane/metadata/android/hi/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
गैलरी और मोटाडेटा एक्स्प्लोरर
|
5
fastlane/metadata/android/hu/full_description.txt
Normal file
5
fastlane/metadata/android/hu/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||||
|
|
||||||
|
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||||
|
|
||||||
|
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/hu/short_description.txt
Normal file
1
fastlane/metadata/android/hu/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Gallery and metadata explorer
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
<b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
|
<b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
|
||||||
|
|
||||||
<i>Aves</i> integra com Android (de <b>API 19 para 33</b>, i.e. de KitKat para Android 13) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.
|
<i>Aves</i> integra com Android (de KitKat até Android 13, incluindo TVs Android) com recursos como <b>widgets</b>, <b>atalhos de apps</b>, <b>protetor de tela</b> e <b>pesquisa global</b>. Também funciona como um <b>visualizador e selecionador de mídia</b>.
|
3
lib/convert/convert.dart
Normal file
3
lib/convert/convert.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export 'metadata/date_field_source.dart';
|
||||||
|
export 'metadata/fields.dart';
|
||||||
|
export 'metadata/metadata_type.dart';
|
18
lib/convert/metadata/date_field_source.dart
Normal file
18
lib/convert/metadata/date_field_source.dart
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,87 +1,6 @@
|
||||||
import 'package:aves/model/metadata/enums/enums.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
|
||||||
enum MetadataField {
|
extension ExtraMetadataFieldConvert on 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<MetadataField> 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 {
|
|
||||||
MetadataType get type {
|
MetadataType get type {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case MetadataField.exifDate:
|
case MetadataField.exifDate:
|
||||||
|
@ -228,21 +147,4 @@ extension ExtraMetadataField on MetadataField {
|
||||||
return null;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
28
lib/convert/metadata/metadata_type.dart
Normal file
28
lib/convert/metadata/metadata_type.dart
Normal file
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
lib/geo/states.dart
Normal file
140
lib/geo/states.dart
Normal file
|
@ -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<String> 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
|
||||||
|
|
||||||
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
|
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
|
||||||
try {
|
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);
|
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
|
||||||
return await decode(buffer);
|
return await decode(buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1426,5 +1426,31 @@
|
||||||
"vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje",
|
"vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje",
|
||||||
"@vaultDialogLockModeWhenScreenOff": {},
|
"@vaultDialogLockModeWhenScreenOff": {},
|
||||||
"vaultBinUsageDialogMessage": "Některé trezory používají koš.",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1208,5 +1208,91 @@
|
||||||
"settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.",
|
"settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.",
|
||||||
"@settingsModificationWarningDialogMessage": {},
|
"@settingsModificationWarningDialogMessage": {},
|
||||||
"settingsViewerShowDescription": "Beschreibung anzeigen",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -723,7 +723,7 @@
|
||||||
"@searchAlbumsSectionTitle": {},
|
"@searchAlbumsSectionTitle": {},
|
||||||
"searchCountriesSectionTitle": "Χωρες",
|
"searchCountriesSectionTitle": "Χωρες",
|
||||||
"@searchCountriesSectionTitle": {},
|
"@searchCountriesSectionTitle": {},
|
||||||
"searchPlacesSectionTitle": "Τοποθεσιες",
|
"searchPlacesSectionTitle": "Μερη",
|
||||||
"@searchPlacesSectionTitle": {},
|
"@searchPlacesSectionTitle": {},
|
||||||
"searchTagsSectionTitle": "Ετικετες",
|
"searchTagsSectionTitle": "Ετικετες",
|
||||||
"@searchTagsSectionTitle": {},
|
"@searchTagsSectionTitle": {},
|
||||||
|
@ -1252,5 +1252,47 @@
|
||||||
"lengthUnitPercent": "%",
|
"lengthUnitPercent": "%",
|
||||||
"@lengthUnitPercent": {},
|
"@lengthUnitPercent": {},
|
||||||
"lengthUnitPixel": "px",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
"chipActionUnpin": "Unpin from top",
|
"chipActionUnpin": "Unpin from top",
|
||||||
"chipActionRename": "Rename",
|
"chipActionRename": "Rename",
|
||||||
"chipActionSetCover": "Set cover",
|
"chipActionSetCover": "Set cover",
|
||||||
|
"chipActionShowCountryStates": "Show states",
|
||||||
"chipActionCreateAlbum": "Create album",
|
"chipActionCreateAlbum": "Create album",
|
||||||
"chipActionCreateVault": "Create vault",
|
"chipActionCreateVault": "Create vault",
|
||||||
"chipActionConfigureVault": "Configure vault",
|
"chipActionConfigureVault": "Configure vault",
|
||||||
|
@ -125,6 +126,8 @@
|
||||||
"videoActionSetSpeed": "Playback speed",
|
"videoActionSetSpeed": "Playback speed",
|
||||||
|
|
||||||
"viewerActionSettings": "Settings",
|
"viewerActionSettings": "Settings",
|
||||||
|
"viewerActionLock": "Lock viewer",
|
||||||
|
"viewerActionUnlock": "Unlock viewer",
|
||||||
|
|
||||||
"slideshowActionResume": "Resume",
|
"slideshowActionResume": "Resume",
|
||||||
"slideshowActionShowInCollection": "Show in Collection",
|
"slideshowActionShowInCollection": "Show in Collection",
|
||||||
|
@ -677,6 +680,9 @@
|
||||||
"countryPageTitle": "Countries",
|
"countryPageTitle": "Countries",
|
||||||
"countryEmpty": "No countries",
|
"countryEmpty": "No countries",
|
||||||
|
|
||||||
|
"statePageTitle": "States",
|
||||||
|
"stateEmpty": "No states",
|
||||||
|
|
||||||
"placePageTitle": "Places",
|
"placePageTitle": "Places",
|
||||||
"placeEmpty": "No places",
|
"placeEmpty": "No places",
|
||||||
|
|
||||||
|
@ -690,6 +696,7 @@
|
||||||
"searchDateSectionTitle": "Date",
|
"searchDateSectionTitle": "Date",
|
||||||
"searchAlbumsSectionTitle": "Albums",
|
"searchAlbumsSectionTitle": "Albums",
|
||||||
"searchCountriesSectionTitle": "Countries",
|
"searchCountriesSectionTitle": "Countries",
|
||||||
|
"searchStatesSectionTitle": "States",
|
||||||
"searchPlacesSectionTitle": "Places",
|
"searchPlacesSectionTitle": "Places",
|
||||||
"searchTagsSectionTitle": "Tags",
|
"searchTagsSectionTitle": "Tags",
|
||||||
"searchRatingSectionTitle": "Ratings",
|
"searchRatingSectionTitle": "Ratings",
|
||||||
|
@ -754,6 +761,9 @@
|
||||||
"settingsCollectionBrowsingQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when browsing items.",
|
"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.",
|
"settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.",
|
||||||
|
|
||||||
|
"settingsCollectionBurstPatternsTile": "Burst patterns",
|
||||||
|
"settingsCollectionBurstPatternsNone": "None",
|
||||||
|
|
||||||
"settingsViewerSectionTitle": "Viewer",
|
"settingsViewerSectionTitle": "Viewer",
|
||||||
"settingsViewerGestureSideTapNext": "Tap on screen edges to show previous/next item",
|
"settingsViewerGestureSideTapNext": "Tap on screen edges to show previous/next item",
|
||||||
"settingsViewerUseCutout": "Use cutout area",
|
"settingsViewerUseCutout": "Use cutout area",
|
||||||
|
@ -892,6 +902,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"statsTopCountriesSectionTitle": "Top Countries",
|
"statsTopCountriesSectionTitle": "Top Countries",
|
||||||
|
"statsTopStatesSectionTitle": "Top States",
|
||||||
"statsTopPlacesSectionTitle": "Top Places",
|
"statsTopPlacesSectionTitle": "Top Places",
|
||||||
"statsTopTagsSectionTitle": "Top Tags",
|
"statsTopTagsSectionTitle": "Top Tags",
|
||||||
"statsTopAlbumsSectionTitle": "Top Albums",
|
"statsTopAlbumsSectionTitle": "Top Albums",
|
||||||
|
@ -948,6 +959,7 @@
|
||||||
"tagEditorSectionPlaceholders": "Placeholders",
|
"tagEditorSectionPlaceholders": "Placeholders",
|
||||||
|
|
||||||
"tagPlaceholderCountry": "Country",
|
"tagPlaceholderCountry": "Country",
|
||||||
|
"tagPlaceholderState": "State",
|
||||||
"tagPlaceholderPlace": "Place",
|
"tagPlaceholderPlace": "Place",
|
||||||
|
|
||||||
"panoramaEnableSensorControl": "Enable sensor control",
|
"panoramaEnableSensorControl": "Enable sensor control",
|
||||||
|
|
|
@ -1273,6 +1273,26 @@
|
||||||
"@settingsVideoEnablePip": {},
|
"@settingsVideoEnablePip": {},
|
||||||
"settingsVideoBackgroundMode": "Reproducción de fondo",
|
"settingsVideoBackgroundMode": "Reproducción de fondo",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Background mode",
|
"settingsVideoBackgroundModeDialogTitle": "Reproducción de fondo",
|
||||||
"@settingsVideoBackgroundModeDialogTitle": {}
|
"@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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1432,5 +1432,25 @@
|
||||||
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
|
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1274,5 +1274,25 @@
|
||||||
"settingsVideoBackgroundMode": "Lecture en arrière-plan",
|
"settingsVideoBackgroundMode": "Lecture en arrière-plan",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Arrière-plan",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
77
lib/l10n/app_hi.arb
Normal file
77
lib/l10n/app_hi.arb
Normal file
|
@ -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": {}
|
||||||
|
}
|
186
lib/l10n/app_hu.arb
Normal file
186
lib/l10n/app_hu.arb
Normal file
|
@ -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": {}
|
||||||
|
}
|
|
@ -1274,5 +1274,25 @@
|
||||||
"settingsVideoBackgroundMode": "Mode latar belakang",
|
"settingsVideoBackgroundMode": "Mode latar belakang",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Mode Latar Belakang",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
"@sourceStateLocatingPlaces": {},
|
"@sourceStateLocatingPlaces": {},
|
||||||
"chipActionDelete": "Elimina",
|
"chipActionDelete": "Elimina",
|
||||||
"@chipActionDelete": {},
|
"@chipActionDelete": {},
|
||||||
"chipActionGoToAlbumPage": "Mostra negli album",
|
"chipActionGoToAlbumPage": "Mostra negli Album",
|
||||||
"@chipActionGoToAlbumPage": {},
|
"@chipActionGoToAlbumPage": {},
|
||||||
"chipActionGoToCountryPage": "Mostra nei Paesi",
|
"chipActionGoToCountryPage": "Mostra nei Paesi",
|
||||||
"@chipActionGoToCountryPage": {},
|
"@chipActionGoToCountryPage": {},
|
||||||
|
@ -1101,7 +1101,7 @@
|
||||||
"@viewerInfoOpenLinkText": {},
|
"@viewerInfoOpenLinkText": {},
|
||||||
"viewerInfoViewXmlLinkText": "Visualizza XML",
|
"viewerInfoViewXmlLinkText": "Visualizza XML",
|
||||||
"@viewerInfoViewXmlLinkText": {},
|
"@viewerInfoViewXmlLinkText": {},
|
||||||
"viewerInfoSearchFieldLabel": "Metadati di ricerca",
|
"viewerInfoSearchFieldLabel": "Ricerca metadati",
|
||||||
"@viewerInfoSearchFieldLabel": {},
|
"@viewerInfoSearchFieldLabel": {},
|
||||||
"viewerInfoSearchEmpty": "Nessuna chiave corrispondente",
|
"viewerInfoSearchEmpty": "Nessuna chiave corrispondente",
|
||||||
"@viewerInfoSearchEmpty": {},
|
"@viewerInfoSearchEmpty": {},
|
||||||
|
@ -1248,5 +1248,49 @@
|
||||||
"settingsDisablingBinWarningDialogMessage": "Gli elementi nel cestino verranno eliminati permanentemente.",
|
"settingsDisablingBinWarningDialogMessage": "Gli elementi nel cestino verranno eliminati permanentemente.",
|
||||||
"@settingsDisablingBinWarningDialogMessage": {},
|
"@settingsDisablingBinWarningDialogMessage": {},
|
||||||
"configureVaultDialogTitle": "Configura Cassaforte",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1170,5 +1170,69 @@
|
||||||
"settingsSubtitleThemeTextPositionTile": "テキストの位置",
|
"settingsSubtitleThemeTextPositionTile": "テキストの位置",
|
||||||
"@settingsSubtitleThemeTextPositionTile": {},
|
"@settingsSubtitleThemeTextPositionTile": {},
|
||||||
"entryInfoActionExportMetadata": "メタデータをエクスポート",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1274,5 +1274,25 @@
|
||||||
"settingsVideoBackgroundMode": "백그라운드 재생",
|
"settingsVideoBackgroundMode": "백그라운드 재생",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
|
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
|
||||||
"@settingsVideoBackgroundModeDialogTitle": {}
|
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||||
|
"settingsCollectionBurstPatternsNone": "없음",
|
||||||
|
"@settingsCollectionBurstPatternsNone": {},
|
||||||
|
"settingsCollectionBurstPatternsTile": "연속 촬영 양식",
|
||||||
|
"@settingsCollectionBurstPatternsTile": {},
|
||||||
|
"tagPlaceholderState": "주",
|
||||||
|
"@tagPlaceholderState": {},
|
||||||
|
"chipActionShowCountryStates": "주 보기",
|
||||||
|
"@chipActionShowCountryStates": {},
|
||||||
|
"stateEmpty": "주가 없습니다",
|
||||||
|
"@stateEmpty": {},
|
||||||
|
"searchStatesSectionTitle": "주",
|
||||||
|
"@searchStatesSectionTitle": {},
|
||||||
|
"statsTopStatesSectionTitle": "주 랭킹",
|
||||||
|
"@statsTopStatesSectionTitle": {},
|
||||||
|
"statePageTitle": "주",
|
||||||
|
"@statePageTitle": {},
|
||||||
|
"viewerActionLock": "뷰어 잠금",
|
||||||
|
"@viewerActionLock": {},
|
||||||
|
"viewerActionUnlock": "뷰어 잠금 해제",
|
||||||
|
"@viewerActionUnlock": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -517,7 +517,7 @@
|
||||||
"@aboutCreditsWorldAtlas1": {},
|
"@aboutCreditsWorldAtlas1": {},
|
||||||
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
|
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
|
||||||
"@aboutCreditsWorldAtlas2": {},
|
"@aboutCreditsWorldAtlas2": {},
|
||||||
"aboutTranslatorsSectionTitle": "Vdertalers",
|
"aboutTranslatorsSectionTitle": "Vertalers",
|
||||||
"@aboutTranslatorsSectionTitle": {},
|
"@aboutTranslatorsSectionTitle": {},
|
||||||
"aboutLicensesSectionTitle": "Open-Source Licenties",
|
"aboutLicensesSectionTitle": "Open-Source Licenties",
|
||||||
"@aboutLicensesSectionTitle": {},
|
"@aboutLicensesSectionTitle": {},
|
||||||
|
@ -1154,5 +1154,11 @@
|
||||||
"settingsAllowMediaManagement": "Mediabeheer toestaan",
|
"settingsAllowMediaManagement": "Mediabeheer toestaan",
|
||||||
"@settingsAllowMediaManagement": {},
|
"@settingsAllowMediaManagement": {},
|
||||||
"editEntryLocationDialogSetCustom": "Aangepaste locatie instellen",
|
"editEntryLocationDialogSetCustom": "Aangepaste locatie instellen",
|
||||||
"@editEntryLocationDialogSetCustom": {}
|
"@editEntryLocationDialogSetCustom": {},
|
||||||
|
"entryInfoActionExportMetadata": "Metagegevens exporteren",
|
||||||
|
"@entryInfoActionExportMetadata": {},
|
||||||
|
"lengthUnitPercent": "%",
|
||||||
|
"@lengthUnitPercent": {},
|
||||||
|
"vaultLockTypePin": "PIN",
|
||||||
|
"@vaultLockTypePin": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1432,5 +1432,25 @@
|
||||||
"settingsVideoBackgroundMode": "Tryb tła",
|
"settingsVideoBackgroundMode": "Tryb tła",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Tryb tła",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1271,6 +1271,28 @@
|
||||||
"@vaultLockTypePattern": {},
|
"@vaultLockTypePattern": {},
|
||||||
"settingsVideoEnablePip": "Picture-in-picture",
|
"settingsVideoEnablePip": "Picture-in-picture",
|
||||||
"@settingsVideoEnablePip": {},
|
"@settingsVideoEnablePip": {},
|
||||||
"settingsVideoBackgroundMode": "Modo background",
|
"settingsVideoBackgroundMode": "Modo de fundo",
|
||||||
"@settingsVideoBackgroundMode": {}
|
"@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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": "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": {},
|
"@newVaultWarningDialogMessage": {},
|
||||||
"settingsConfirmationVaultDataLoss": "Afișare avertisment privind pierderile de date din seif",
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1235,7 +1235,7 @@
|
||||||
"@filterLocatedLabel": {},
|
"@filterLocatedLabel": {},
|
||||||
"filterTaggedLabel": "С тэгами",
|
"filterTaggedLabel": "С тэгами",
|
||||||
"@filterTaggedLabel": {},
|
"@filterTaggedLabel": {},
|
||||||
"chipActionGoToPlacePage": "Показать в местах",
|
"chipActionGoToPlacePage": "Показать в локациях",
|
||||||
"@chipActionGoToPlacePage": {},
|
"@chipActionGoToPlacePage": {},
|
||||||
"settingsModificationWarningDialogMessage": "Другие настройки будут изменены.",
|
"settingsModificationWarningDialogMessage": "Другие настройки будут изменены.",
|
||||||
"@settingsModificationWarningDialogMessage": {},
|
"@settingsModificationWarningDialogMessage": {},
|
||||||
|
@ -1244,5 +1244,23 @@
|
||||||
"settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.",
|
"settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.",
|
||||||
"@settingsDisablingBinWarningDialogMessage": {},
|
"@settingsDisablingBinWarningDialogMessage": {},
|
||||||
"lengthUnitPixel": "пикс.",
|
"lengthUnitPixel": "пикс.",
|
||||||
"@lengthUnitPixel": {}
|
"@lengthUnitPixel": {},
|
||||||
|
"chipActionLock": "Заблокировать",
|
||||||
|
"@chipActionLock": {},
|
||||||
|
"patternDialogEnter": "Введите ключ",
|
||||||
|
"@patternDialogEnter": {},
|
||||||
|
"patternDialogConfirm": "Подтвердите ключ",
|
||||||
|
"@patternDialogConfirm": {},
|
||||||
|
"vaultLockTypePattern": "Графический ключ",
|
||||||
|
"@vaultLockTypePattern": {},
|
||||||
|
"drawerPlacePage": "Локации",
|
||||||
|
"@drawerPlacePage": {},
|
||||||
|
"settingsVideoBackgroundMode": "Фоновый режим",
|
||||||
|
"@settingsVideoBackgroundMode": {},
|
||||||
|
"settingsVideoBackgroundModeDialogTitle": "Фоновый режим",
|
||||||
|
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||||
|
"settingsVideoEnablePip": "Картинка в картинке",
|
||||||
|
"@settingsVideoEnablePip": {},
|
||||||
|
"placeEmpty": "Нет локаций",
|
||||||
|
"@placeEmpty": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1432,5 +1432,25 @@
|
||||||
"settingsVideoBackgroundMode": "Фоновий режим",
|
"settingsVideoBackgroundMode": "Фоновий режим",
|
||||||
"@settingsVideoBackgroundMode": {},
|
"@settingsVideoBackgroundMode": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
|
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
|
||||||
"@settingsVideoBackgroundModeDialogTitle": {}
|
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||||
|
"tagPlaceholderState": "Штат",
|
||||||
|
"@tagPlaceholderState": {},
|
||||||
|
"chipActionShowCountryStates": "Показати штати",
|
||||||
|
"@chipActionShowCountryStates": {},
|
||||||
|
"viewerActionUnlock": "Розблокувати переглядач",
|
||||||
|
"@viewerActionUnlock": {},
|
||||||
|
"viewerActionLock": "Заблокувати переглядач",
|
||||||
|
"@viewerActionLock": {},
|
||||||
|
"stateEmpty": "Немає штатів",
|
||||||
|
"@stateEmpty": {},
|
||||||
|
"settingsCollectionBurstPatternsTile": "Вибух візерунків",
|
||||||
|
"@settingsCollectionBurstPatternsTile": {},
|
||||||
|
"settingsCollectionBurstPatternsNone": "Нічого",
|
||||||
|
"@settingsCollectionBurstPatternsNone": {},
|
||||||
|
"statsTopStatesSectionTitle": "Топ штатів",
|
||||||
|
"@statsTopStatesSectionTitle": {},
|
||||||
|
"searchStatesSectionTitle": "Штати",
|
||||||
|
"@searchStatesSectionTitle": {},
|
||||||
|
"statePageTitle": "Штати",
|
||||||
|
"@statePageTitle": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1192,5 +1192,13 @@
|
||||||
"filterNoAddressLabel": "无地址",
|
"filterNoAddressLabel": "无地址",
|
||||||
"@filterNoAddressLabel": {},
|
"@filterNoAddressLabel": {},
|
||||||
"settingsViewerShowRatingTags": "显示评分和标签",
|
"settingsViewerShowRatingTags": "显示评分和标签",
|
||||||
"@settingsViewerShowRatingTags": {}
|
"@settingsViewerShowRatingTags": {},
|
||||||
|
"chipActionLock": "锁定",
|
||||||
|
"@chipActionLock": {},
|
||||||
|
"chipActionConfigureVault": "配置保险库",
|
||||||
|
"@chipActionConfigureVault": {},
|
||||||
|
"chipActionCreateVault": "创建保险库",
|
||||||
|
"@chipActionCreateVault": {},
|
||||||
|
"chipActionShowCountryStates": "显示状态",
|
||||||
|
"@chipActionShowCountryStates": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
enum MoveType { copy, move, export, toBin, fromBin }
|
|
64
lib/model/app/contributors.dart
Normal file
64
lib/model/app/contributors.dart
Normal file
|
@ -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);
|
||||||
|
}
|
|
@ -112,12 +112,6 @@ class Dependencies {
|
||||||
license: mit,
|
license: mit,
|
||||||
sourceUrl: 'https://github.com/aaassseee/screen_brightness',
|
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(
|
Dependency(
|
||||||
name: 'Shared Preferences',
|
name: 'Shared Preferences',
|
||||||
license: bsd3,
|
license: bsd3,
|
16
lib/model/app/permissions.dart
Normal file
16
lib/model/app/permissions.dart
Normal file
|
@ -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,
|
||||||
|
];
|
||||||
|
}
|
96
lib/model/app/support.dart
Normal file
96
lib/model/app/support.dart
Normal file
|
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
lib/model/apps.dart
Normal file
78
lib/model/apps.dart
Normal file
|
@ -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<Package> _packages = {};
|
||||||
|
List<String> _potentialAppDirs = [];
|
||||||
|
|
||||||
|
ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
Iterable<Package> get _launcherPackages => _packages.where((v) => v.categoryLauncher);
|
||||||
|
|
||||||
|
AppInventory._private();
|
||||||
|
|
||||||
|
Future<void> 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<void> 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<String> 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<String> 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}';
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/apps.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.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/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -121,7 +123,7 @@ class Covers {
|
||||||
|
|
||||||
String? effectiveAlbumPackage(String albumPath) {
|
String? effectiveAlbumPackage(String albumPath) {
|
||||||
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
|
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
|
||||||
return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath);
|
return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// import/export
|
// import/export
|
||||||
|
|
|
@ -10,7 +10,7 @@ final Device device = Device._private();
|
||||||
class Device {
|
class Device {
|
||||||
late final String _userAgent;
|
late final String _userAgent;
|
||||||
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
|
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;
|
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
|
||||||
|
|
||||||
String get userAgent => _userAgent;
|
String get userAgent => _userAgent;
|
||||||
|
@ -25,6 +25,8 @@ class Device {
|
||||||
|
|
||||||
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
|
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
|
||||||
|
|
||||||
|
bool get canRenderSubdivisionFlagEmojis => _canRenderSubdivisionFlagEmojis;
|
||||||
|
|
||||||
bool get canRequestManageMedia => _canRequestManageMedia;
|
bool get canRequestManageMedia => _canRequestManageMedia;
|
||||||
|
|
||||||
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
|
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
|
||||||
|
@ -71,6 +73,7 @@ class Device {
|
||||||
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
||||||
_canPrint = capabilities['canPrint'] ?? false;
|
_canPrint = capabilities['canPrint'] ?? false;
|
||||||
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
|
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
|
||||||
|
_canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false;
|
||||||
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
|
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
|
||||||
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
|
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
|
||||||
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
|
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
|
||||||
|
|
|
@ -52,7 +52,7 @@ class EntryDir {
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _resolve() {
|
String? _resolve() {
|
||||||
final vrl = VolumeRelativeDirectory.fromPath(asIs!);
|
final vrl = androidFileUtils.relativeDirectoryFromPath(asIs!);
|
||||||
if (vrl == null || vrl.relativeDir.isEmpty) return asIs;
|
if (vrl == null || vrl.relativeDir.isEmpty) return asIs;
|
||||||
|
|
||||||
var resolved = vrl.volumePath;
|
var resolved = vrl.volumePath;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/entry/cache.dart';
|
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/address.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/metadata/trash.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/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves_utils/aves_utils.dart';
|
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
import 'package:aves_utils/aves_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
@ -80,10 +78,6 @@ class AvesEntry with AvesEntryBase {
|
||||||
this.durationMillis = durationMillis;
|
this.durationMillis = durationMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get canDecode => !MimeTypes.undecodableImages.contains(mimeType);
|
|
||||||
|
|
||||||
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
|
|
||||||
|
|
||||||
AvesEntry copyWith({
|
AvesEntry copyWith({
|
||||||
int? id,
|
int? id,
|
||||||
String? uri,
|
String? uri,
|
||||||
|
@ -225,15 +219,6 @@ class AvesEntry with AvesEntryBase {
|
||||||
return _extension;
|
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
|
// the MIME type reported by the Media Store is unreliable
|
||||||
// so we use the one found during cataloguing if possible
|
// so we use the one found during cataloguing if possible
|
||||||
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
||||||
|
@ -323,18 +308,6 @@ class AvesEntry with AvesEntryBase {
|
||||||
return _durationText!;
|
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
|
// returns whether this entry has GPS coordinates
|
||||||
// (0, 0) coordinates are considered invalid, as it is likely a default value
|
// (0, 0) coordinates are considered invalid, as it is likely a default value
|
||||||
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
|
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:aves/model/entry/cache.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
extension ExtraAvesEntryImages on AvesEntry {
|
extension ExtraAvesEntryImages on AvesEntry {
|
||||||
bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent));
|
bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent));
|
||||||
|
|
|
@ -9,11 +9,11 @@ import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||||
import 'package:aves/theme/colors.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/widgets/viewer/info/metadata/metadata_dir.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
extension ExtraAvesEntryInfo on AvesEntry {
|
extension ExtraAvesEntryInfo on AvesEntry {
|
||||||
|
@ -115,7 +115,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
|
||||||
final dirName = [
|
final dirName = [
|
||||||
'Stream ${index.toString().padLeft(indexDigits, '0')}',
|
'Stream ${index.toString().padLeft(indexDigits, '0')}',
|
||||||
typeText,
|
typeText,
|
||||||
].join(Constants.separator);
|
].join(AText.separator);
|
||||||
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
|
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
|
||||||
if (formattedStreamTags.isNotEmpty) {
|
if (formattedStreamTags.isNotEmpty) {
|
||||||
final color = colors.fromString(typeText);
|
final color = colors.fromString(typeText);
|
||||||
|
|
|
@ -12,6 +12,8 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
extension ExtraAvesEntryLocation on AvesEntry {
|
extension ExtraAvesEntryLocation on AvesEntry {
|
||||||
|
static final _invalidLocalityPattern = RegExp(r'^[-+\dA-Z]+$');
|
||||||
|
|
||||||
LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null;
|
LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null;
|
||||||
|
|
||||||
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
|
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
|
||||||
|
@ -53,18 +55,17 @@ extension ExtraAvesEntryLocation on AvesEntry {
|
||||||
)
|
)
|
||||||
: call());
|
: call());
|
||||||
if (addresses.isNotEmpty) {
|
if (addresses.isNotEmpty) {
|
||||||
final address = addresses.first;
|
final v = addresses.first;
|
||||||
final cc = address.countryCode?.toUpperCase();
|
var locality = v.locality ?? v.subLocality ?? v.featureName;
|
||||||
final cn = address.countryName;
|
if (locality == null || _invalidLocalityPattern.hasMatch(locality) || {v.subThoroughfare, v.countryName}.contains(locality)) {
|
||||||
final aa = address.adminArea;
|
locality = v.subAdminArea;
|
||||||
|
}
|
||||||
addressDetails = AddressDetails(
|
addressDetails = AddressDetails(
|
||||||
id: id,
|
id: id,
|
||||||
countryCode: cc,
|
countryCode: v.countryCode?.toUpperCase(),
|
||||||
countryName: cn,
|
countryName: v.countryName,
|
||||||
adminArea: aa,
|
adminArea: v.adminArea,
|
||||||
// if country & admin fields are null, it is likely the ocean,
|
locality: locality,
|
||||||
// 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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:aves/convert/convert.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/catalog.dart';
|
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/metadata/enums/date_field_source.dart';
|
import 'package:aves/ref/metadata/exif.dart';
|
||||||
import 'package:aves/model/metadata/enums/enums.dart';
|
import 'package:aves/ref/metadata/iptc.dart';
|
||||||
import 'package:aves/model/metadata/fields.dart';
|
|
||||||
import 'package:aves/ref/exif.dart';
|
|
||||||
import 'package:aves/ref/iptc.dart';
|
|
||||||
import 'package:aves/ref/mime_types.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/common/services.dart';
|
||||||
import 'package:aves/services/metadata/xmp.dart';
|
import 'package:aves/services/metadata/xmp.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:aves/utils/xmp_utils.dart';
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
@ -27,7 +27,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
|
|
||||||
final appliedModifier = await _applyDateModifierToEntry(userModifier);
|
final appliedModifier = await _applyDateModifierToEntry(userModifier);
|
||||||
if (appliedModifier == null) {
|
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);
|
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
@ -54,7 +54,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
||||||
break;
|
break;
|
||||||
case DateEditAction.shift:
|
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) {
|
if (xmpDate != null) {
|
||||||
final date = DateTime.tryParse(xmpDate);
|
final date = DateTime.tryParse(xmpDate);
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
|
@ -262,18 +262,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
if (editTitle) {
|
if (editTitle) {
|
||||||
modified |= XMP.setAttribute(
|
modified |= XMP.setAttribute(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.dcTitle,
|
XmpElements.dcTitle,
|
||||||
title,
|
title,
|
||||||
namespace: Namespaces.dc,
|
namespace: XmpNamespaces.dc,
|
||||||
strat: XmpEditStrategy.always,
|
strat: XmpEditStrategy.always,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (editDescription) {
|
if (editDescription) {
|
||||||
modified |= XMP.setAttribute(
|
modified |= XMP.setAttribute(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.dcDescription,
|
XmpElements.dcDescription,
|
||||||
description,
|
description,
|
||||||
namespace: Namespaces.dc,
|
namespace: XmpNamespaces.dc,
|
||||||
strat: XmpEditStrategy.always,
|
strat: XmpEditStrategy.always,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -417,9 +417,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
static bool editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
|
static bool editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
|
||||||
return XMP.setAttribute(
|
return XMP.setAttribute(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.xmpCreateDate,
|
XmpAttributes.xmpCreateDate,
|
||||||
date != null ? XMP.toXmpDate(date) : null,
|
date != null ? XMP.toXmpDate(date) : null,
|
||||||
namespace: Namespaces.xmp,
|
namespace: XmpNamespaces.xmp,
|
||||||
strat: XmpEditStrategy.always,
|
strat: XmpEditStrategy.always,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -428,9 +428,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
static bool editTagsXmp(List<XmlNode> descriptions, Set<String> tags) {
|
static bool editTagsXmp(List<XmlNode> descriptions, Set<String> tags) {
|
||||||
return XMP.setStringBag(
|
return XMP.setStringBag(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.dcSubject,
|
XmpElements.dcSubject,
|
||||||
tags,
|
tags,
|
||||||
namespace: Namespaces.dc,
|
namespace: XmpNamespaces.dc,
|
||||||
strat: XmpEditStrategy.always,
|
strat: XmpEditStrategy.always,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -441,17 +441,17 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
|
|
||||||
modified |= XMP.setAttribute(
|
modified |= XMP.setAttribute(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.xmpRating,
|
XmpElements.xmpRating,
|
||||||
(rating ?? 0) == 0 ? null : '$rating',
|
(rating ?? 0) == 0 ? null : '$rating',
|
||||||
namespace: Namespaces.xmp,
|
namespace: XmpNamespaces.xmp,
|
||||||
strat: XmpEditStrategy.always,
|
strat: XmpEditStrategy.always,
|
||||||
);
|
);
|
||||||
|
|
||||||
modified |= XMP.setAttribute(
|
modified |= XMP.setAttribute(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.msPhotoRating,
|
XmpElements.msPhotoRating,
|
||||||
XMP.toMsPhotoRating(rating),
|
XMP.toMsPhotoRating(rating),
|
||||||
namespace: Namespaces.microsoftPhoto,
|
namespace: XmpNamespaces.microsoftPhoto,
|
||||||
strat: XmpEditStrategy.updateIfPresent,
|
strat: XmpEditStrategy.updateIfPresent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -464,23 +464,23 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
|
|
||||||
modified |= XMP.removeElements(
|
modified |= XMP.removeElements(
|
||||||
descriptions,
|
descriptions,
|
||||||
XMP.containerDirectory,
|
XmpElements.containerDirectory,
|
||||||
Namespaces.gContainer,
|
XmpNamespaces.gContainer,
|
||||||
);
|
);
|
||||||
|
|
||||||
modified |= [
|
modified |= [
|
||||||
XMP.gCameraMicroVideo,
|
XmpAttributes.gCameraMicroVideo,
|
||||||
XMP.gCameraMicroVideoVersion,
|
XmpAttributes.gCameraMicroVideoVersion,
|
||||||
XMP.gCameraMicroVideoOffset,
|
XmpAttributes.gCameraMicroVideoOffset,
|
||||||
XMP.gCameraMicroVideoPresentationTimestampUs,
|
XmpAttributes.gCameraMicroVideoPresentationTimestampUs,
|
||||||
XMP.gCameraMotionPhoto,
|
XmpAttributes.gCameraMotionPhoto,
|
||||||
XMP.gCameraMotionPhotoVersion,
|
XmpAttributes.gCameraMotionPhotoVersion,
|
||||||
XMP.gCameraMotionPhotoPresentationTimestampUs,
|
XmpAttributes.gCameraMotionPhotoPresentationTimestampUs,
|
||||||
].fold<bool>(modified, (prev, name) {
|
].fold<bool>(modified, (prev, name) {
|
||||||
return prev |= XMP.removeElements(
|
return prev |= XMP.removeElements(
|
||||||
descriptions,
|
descriptions,
|
||||||
name,
|
name,
|
||||||
Namespaces.gCamera,
|
XmpNamespaces.gCamera,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,6 @@ import 'package:aves/services/common/services.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
extension ExtraAvesEntryMultipage on AvesEntry {
|
extension ExtraAvesEntryMultipage on AvesEntry {
|
||||||
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
|
|
||||||
|
|
||||||
bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst;
|
bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst;
|
||||||
|
|
||||||
bool get isBurst => burstEntries?.isNotEmpty == true;
|
bool get isBurst => burstEntries?.isNotEmpty == true;
|
||||||
|
@ -18,11 +16,13 @@ extension ExtraAvesEntryMultipage on AvesEntry {
|
||||||
|
|
||||||
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
|
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
|
||||||
|
|
||||||
String? get burstKey {
|
String? getBurstKey(List<String> patterns) {
|
||||||
if (filenameWithoutExtension != null) {
|
if (filenameWithoutExtension != null) {
|
||||||
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
|
for (final pattern in patterns) {
|
||||||
if (match != null) {
|
final match = RegExp(pattern).firstMatch(filenameWithoutExtension!);
|
||||||
return '$directory/${match.group(1)}';
|
if (match != null) {
|
||||||
|
return '$directory/${match.group(1)}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,63 +1,113 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:aves/model/app/support.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/settings/settings.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/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';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
|
||||||
extension ExtraAvesEntryProps on AvesEntry {
|
extension ExtraAvesEntryProps on AvesEntry {
|
||||||
|
bool get isValid => !isMissingAtPath && sizeBytes != 0 && width > 0 && height > 0;
|
||||||
|
|
||||||
|
// type
|
||||||
|
|
||||||
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
||||||
|
|
||||||
|
bool get canHaveAlpha => MimeTypes.canHaveAlpha(mimeType);
|
||||||
|
|
||||||
bool get isSvg => mimeType == MimeTypes.svg;
|
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 isRaw => MimeTypes.isRaw(mimeType);
|
||||||
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 isImage => MimeTypes.isImage(mimeType);
|
bool get isImage => MimeTypes.isImage(mimeType);
|
||||||
|
|
||||||
bool get isVideo => MimeTypes.isVideo(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 isAnimated => catalogMetadata?.isAnimated ?? false;
|
||||||
|
|
||||||
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
|
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
|
||||||
|
|
||||||
bool get is360 => catalogMetadata?.is360 ?? 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);
|
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||||
|
|
||||||
|
@ -73,47 +123,17 @@ extension ExtraAvesEntryProps on AvesEntry {
|
||||||
|
|
||||||
bool get canFlip => canEdit && canEditExif;
|
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';
|
bool get canEditIptc => AppSupport.canEditIptc(mimeType);
|
||||||
static const resolutionSeparator = ' \u00D7 ';
|
|
||||||
|
|
||||||
bool get isSized => width > 0 && height > 0;
|
bool get canEditXmp => AppSupport.canEditXmp(mimeType);
|
||||||
|
|
||||||
String get resolutionText {
|
bool get canRemoveMetadata => AppSupport.canRemoveMetadata(mimeType);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@ import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
final Favourites favourites = Favourites._private();
|
final Favourites favourites = Favourites._private();
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.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/widgets/common/identity/aves_icons.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -2,13 +2,11 @@ import 'package:aves/l10n/l10n.dart';
|
||||||
import 'package:aves/model/entry/extensions/location.dart';
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.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/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/icons.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/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves_map/aves_map.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:flutter/widgets.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -61,7 +59,7 @@ class CoordinateFilter extends CollectionFilter {
|
||||||
bool get exclusiveProp => false;
|
bool get exclusiveProp => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get universalLabel => _formatBounds(lookupAppLocalizations(AvesApp.supportedLocales.first), CoordinateFormat.decimal);
|
String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat);
|
String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class FavouriteFilter extends CollectionFilter {
|
class FavouriteFilter extends CollectionFilter {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.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:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -11,23 +12,31 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
|
|
||||||
final LocationLevel level;
|
final LocationLevel level;
|
||||||
late final String _location;
|
late final String _location;
|
||||||
late final String? _countryCode;
|
late final String? _code;
|
||||||
late final EntryFilter _test;
|
late final EntryFilter _test;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [level, _location, _countryCode, reversed];
|
List<Object?> get props => [level, _location, _code, reversed];
|
||||||
|
|
||||||
LocationFilter(this.level, String location, {super.reversed = false}) {
|
LocationFilter(this.level, String location, {super.reversed = false}) {
|
||||||
final split = location.split(locationSeparator);
|
final split = location.split(locationSeparator);
|
||||||
_location = split.isNotEmpty ? split[0] : location;
|
_location = split.isNotEmpty ? split[0] : location;
|
||||||
_countryCode = split.length > 1 ? split[1] : null;
|
_code = split.length > 1 ? split[1] : null;
|
||||||
|
|
||||||
if (_location.isEmpty) {
|
if (_location.isEmpty) {
|
||||||
_test = (entry) => !entry.hasGps;
|
_test = (entry) => !entry.hasGps;
|
||||||
} else if (level == LocationLevel.country) {
|
} else {
|
||||||
_test = (entry) => entry.addressDetails?.countryCode == _countryCode;
|
switch (level) {
|
||||||
} else if (level == LocationLevel.place) {
|
case LocationLevel.country:
|
||||||
_test = (entry) => entry.addressDetails?.place == _location;
|
_test = (entry) => entry.addressDetails?.countryCode == _code;
|
||||||
|
break;
|
||||||
|
case LocationLevel.state:
|
||||||
|
_test = (entry) => entry.addressDetails?.stateCode == _code;
|
||||||
|
break;
|
||||||
|
case LocationLevel.place:
|
||||||
|
_test = (entry) => entry.addressDetails?.place == _location;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,16 +49,29 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() {
|
||||||
'type': type,
|
String location = _location;
|
||||||
'level': level.toString(),
|
switch (level) {
|
||||||
'location': _countryCode != null ? countryNameAndCode : _location,
|
case LocationLevel.country:
|
||||||
'reversed': reversed,
|
case LocationLevel.state:
|
||||||
};
|
if (_code != null) {
|
||||||
|
location = _nameAndCode;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LocationLevel.place:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'type': type,
|
||||||
|
'level': level.toString(),
|
||||||
|
'location': location,
|
||||||
|
'reversed': reversed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
String get countryNameAndCode => '$_location$locationSeparator$_countryCode';
|
String get _nameAndCode => '$_location$locationSeparator$_code';
|
||||||
|
|
||||||
String? get countryCode => _countryCode;
|
String? get code => _code;
|
||||||
|
|
||||||
String get place => _location;
|
String get place => _location;
|
||||||
|
|
||||||
|
@ -71,11 +93,9 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
return Icon(AIcons.locationUnlocated, size: size);
|
return Icon(AIcons.locationUnlocated, size: size);
|
||||||
}
|
}
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case LocationLevel.place:
|
|
||||||
return Icon(AIcons.place, size: size);
|
|
||||||
case LocationLevel.country:
|
case LocationLevel.country:
|
||||||
if (_countryCode != null && device.canRenderFlagEmojis) {
|
if (_code != null && device.canRenderFlagEmojis) {
|
||||||
final flag = countryCodeToFlag(_countryCode);
|
final flag = EmojiUtils.countryCodeToFlag(_code);
|
||||||
if (flag != null) {
|
if (flag != null) {
|
||||||
return Text(
|
return Text(
|
||||||
flag,
|
flag,
|
||||||
|
@ -85,6 +105,20 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Icon(AIcons.country, size: size);
|
return Icon(AIcons.country, size: size);
|
||||||
|
case LocationLevel.state:
|
||||||
|
if (_code != null && device.canRenderSubdivisionFlagEmojis) {
|
||||||
|
final flag = EmojiUtils.stateCodeToFlag(_code);
|
||||||
|
if (flag != null) {
|
||||||
|
return Text(
|
||||||
|
flag,
|
||||||
|
style: TextStyle(fontSize: size),
|
||||||
|
textScaleFactor: 1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Icon(AIcons.state, size: size);
|
||||||
|
case LocationLevel.place:
|
||||||
|
return Icon(AIcons.place, size: size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,16 +126,7 @@ class LocationFilter extends CoveredCollectionFilter {
|
||||||
String get category => type;
|
String get category => type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get key => '$type-$reversed-$level-$_location';
|
String get key => '$type-$reversed-$level-$code-$place';
|
||||||
|
|
||||||
// U+0041 Latin Capital letter A
|
|
||||||
// U+1F1E6 🇦 REGIONAL INDICATOR SYMBOL LETTER A
|
|
||||||
static const _countryCodeToFlagDiff = 0x1F1E6 - 0x0041;
|
|
||||||
|
|
||||||
static String? countryCodeToFlag(String? code) {
|
|
||||||
if (code == null || code.length != 2) return null;
|
|
||||||
return String.fromCharCodes(code.toUpperCase().codeUnits.map((letter) => letter += _countryCodeToFlagDiff));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LocationLevel { place, country }
|
enum LocationLevel { place, state, country }
|
||||||
|
|
|
@ -11,12 +11,14 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
static const type = 'placeholder';
|
static const type = 'placeholder';
|
||||||
|
|
||||||
static const _country = 'country';
|
static const _country = 'country';
|
||||||
|
static const _state = 'state';
|
||||||
static const _place = 'place';
|
static const _place = 'place';
|
||||||
|
|
||||||
final String placeholder;
|
final String placeholder;
|
||||||
late final IconData _icon;
|
late final IconData _icon;
|
||||||
|
|
||||||
static final country = PlaceholderFilter._private(_country);
|
static final country = PlaceholderFilter._private(_country);
|
||||||
|
static final state = PlaceholderFilter._private(_state);
|
||||||
static final place = PlaceholderFilter._private(_place);
|
static final place = PlaceholderFilter._private(_place);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -27,6 +29,9 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
case _country:
|
case _country:
|
||||||
_icon = AIcons.country;
|
_icon = AIcons.country;
|
||||||
break;
|
break;
|
||||||
|
case _state:
|
||||||
|
_icon = AIcons.state;
|
||||||
|
break;
|
||||||
case _place:
|
case _place:
|
||||||
_icon = AIcons.place;
|
_icon = AIcons.place;
|
||||||
break;
|
break;
|
||||||
|
@ -48,6 +53,7 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
Future<String?> toTag(AvesEntry entry) async {
|
Future<String?> toTag(AvesEntry entry) async {
|
||||||
switch (placeholder) {
|
switch (placeholder) {
|
||||||
case _country:
|
case _country:
|
||||||
|
case _state:
|
||||||
case _place:
|
case _place:
|
||||||
if (!entry.isCatalogued) {
|
if (!entry.isCatalogued) {
|
||||||
await entry.catalog(background: false, force: false, persist: true);
|
await entry.catalog(background: false, force: false, persist: true);
|
||||||
|
@ -60,8 +66,14 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
final address = entry.addressDetails;
|
final address = entry.addressDetails;
|
||||||
if (address == null) return null;
|
if (address == null) return null;
|
||||||
|
|
||||||
if (placeholder == _country) return address.countryName;
|
switch (placeholder) {
|
||||||
if (placeholder == _place) return address.place;
|
case _country:
|
||||||
|
return address.countryName;
|
||||||
|
case _state:
|
||||||
|
return address.stateName;
|
||||||
|
case _place:
|
||||||
|
return address.place;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -81,6 +93,8 @@ class PlaceholderFilter extends CollectionFilter {
|
||||||
switch (placeholder) {
|
switch (placeholder) {
|
||||||
case _country:
|
case _country:
|
||||||
return context.l10n.tagPlaceholderCountry;
|
return context.l10n.tagPlaceholderCountry;
|
||||||
|
case _state:
|
||||||
|
return context.l10n.tagPlaceholderState;
|
||||||
case _place:
|
case _place:
|
||||||
return context.l10n.tagPlaceholderPlace;
|
return context.l10n.tagPlaceholderPlace;
|
||||||
default:
|
default:
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue