Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2023-04-18 19:52:55 +02:00
commit 03df8fbd26
499 changed files with 9111 additions and 4162 deletions

@ -1 +1 @@
Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da Subproject commit f72efea43c3013323d1b95cff571f3c1caa37583

3
.gitignore vendored
View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Bilder &amp; Videos scannen</string> <string name="analysis_service_description">Bilder &amp; 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>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Explorar imágenes &amp; videos</string> <string name="analysis_service_description">Explorar imágenes &amp; 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>

View file

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

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Analyse des images &amp; vidéos</string> <string name="analysis_service_description">Analyse des images &amp; 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>

View 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">छवि &amp; वीडियो जाँचे</string>
</resources>

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

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Pindai gambar &amp; video</string> <string name="analysis_service_description">Pindai gambar &amp; 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>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Scansione immagini &amp; videos</string> <string name="analysis_service_description">Scansione immagini &amp; 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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
Galerie und Metadata Explorer Galerie und Metadaten Explorer

View 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

View 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

View 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>.

View file

@ -0,0 +1 @@
गैलरी और मोटाडेटा एक्स्प्लोरर

View 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>.

View file

@ -0,0 +1 @@
Gallery and metadata explorer

View file

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

@ -0,0 +1,3 @@
export 'metadata/date_field_source.dart';
export 'metadata/fields.dart';
export 'metadata/metadata_type.dart';

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

View file

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

View 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
View 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,
};
}

View file

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

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

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

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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
View 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
View 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": {}
}

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -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": {}
} }

View file

@ -1192,5 +1192,13 @@
"filterNoAddressLabel": "无地址", "filterNoAddressLabel": "无地址",
"@filterNoAddressLabel": {}, "@filterNoAddressLabel": {},
"settingsViewerShowRatingTags": "显示评分和标签", "settingsViewerShowRatingTags": "显示评分和标签",
"@settingsViewerShowRatingTags": {} "@settingsViewerShowRatingTags": {},
"chipActionLock": "锁定",
"@chipActionLock": {},
"chipActionConfigureVault": "配置保险库",
"@chipActionConfigureVault": {},
"chipActionCreateVault": "创建保险库",
"@chipActionCreateVault": {},
"chipActionShowCountryStates": "显示状态",
"@chipActionShowCountryStates": {}
} }

View file

@ -1 +0,0 @@
enum MoveType { copy, move, export, toBin, fromBin }

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

View file

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

View 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,
];
}

View 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
View 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}';
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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