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/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols

View file

@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
# This file should be version controlled.
version:
revision: bc7bc940836f1f834699625426795fd6f07c18ec
channel: beta
revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
- platform: android
create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

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="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
### Added
@ -115,7 +139,8 @@ All notable changes to this project will be documented in this file.
### Changed
- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment`
- editing description writes XMP `dc:description`, and clears Exif `ImageDescription`
/ `UserComment`
- in the tag editor, tapping on applied tag applies it to all items instead of removing it
- pin app bar when selecting items

2
android/.gitignore vendored
View file

@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View file

@ -46,6 +46,16 @@ if (keystorePropertiesFile.exists()) {
android {
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -148,6 +158,7 @@ android {
// which lead to: UnsatisfiedLinkError...couldn't find "libflutter.so"
// cf https://github.com/flutter/flutter/issues/37566#issuecomment-640879500
ndk {
//noinspection ChromeOsAbiSupport
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
@ -183,9 +194,10 @@ repositories {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
@ -193,9 +205,9 @@ dependencies {
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
implementation 'com.github.bumptech.glide:glide:4.15.0'
implementation 'com.github.bumptech.glide:glide:4.15.1'
// SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.6'
implementation 'org.slf4j:slf4j-simple:2.0.7'
// forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
@ -210,7 +222,7 @@ dependencies {
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300'
kapt 'androidx.annotation:annotation:1.6.0'
kapt 'com.github.bumptech.glide:compiler:4.15.0'
kapt 'com.github.bumptech.glide:compiler:4.15.1'
compileOnly rootProject.findProject(':streams_channel')
}

View file

@ -4,7 +4,7 @@
Gradle v7.4 / Android Gradle Plugin v7.3.0 recommend:
- removing "package" from AndroidManifest.xml
- adding it as "namespace" in app/build.gradle
This change eventually prevents building the app with Flutter v3.3.3.
This change eventually prevents building the app with Flutter v3.7.11.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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?> {
when (val action = intent?.action) {
Intent.ACTION_MAIN -> {
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
return hashMapOf(
INTENT_DATA_KEY_SAFE_MODE to true,
)
}
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
val filters = extractFiltersFromIntent(intent)
return hashMapOf(
@ -393,7 +398,16 @@ open class MainActivity : FlutterFragmentActivity() {
)
.build()
ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search))
val safeMode = ShortcutInfoCompat.Builder(this, "safeMode")
.setShortLabel(getString(R.string.safe_mode_shortcut_short_label))
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_safe_mode else R.drawable.ic_shortcut_safe_mode))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(EXTRA_KEY_SAFE_MODE, true)
)
.build()
ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search, safeMode))
}
private fun onAnalysisCompleted() {
@ -428,12 +442,14 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_SAFE_MODE = "safeMode"
const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_SAFE_MODE = "safeMode"
const val EXTRA_KEY_WIDGET_ID = "widgetId"
// request code to pending runnable

View file

@ -38,10 +38,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import org.mp4parser.IsoFile
import org.mp4parser.PropertyBoxParserImpl
import org.mp4parser.boxes.iso14496.part12.FreeBox
import org.mp4parser.boxes.iso14496.part12.MediaDataBox
import org.mp4parser.boxes.iso14496.part12.SampleTableBox
import java.io.FileInputStream
import java.io.IOException
@ -341,23 +337,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
pfd.use {
FileInputStream(it.fileDescriptor).use { stream ->
stream.channel.use { channel ->
val boxParser = PropertyBoxParserImpl().apply {
val skippedTypes = listOf(
// parsing `MediaDataBox` can take a long time
MediaDataBox.TYPE,
// parsing `SampleTableBox` or `FreeBox` may yield OOM
SampleTableBox.TYPE, FreeBox.TYPE,
// some files are padded with `0` but the parser does not stop, reads type "0000",
// then a large size from following "0000", which may yield OOM
"0000",
)
setBoxSkipper { type, size ->
if (skippedTypes.contains(type)) return@setBoxSkipper true
if (size > Mp4ParserHelper.BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
false
}
}
IsoFile(channel, boxParser).use { isoFile ->
IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile ->
isoFile.dumpBoxes(sb)
}
}

View file

@ -42,6 +42,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
"canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),

View file

@ -160,9 +160,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
thisDirName = "Spherical Video"
metadataMap[thisDirName] = HashMap(GSpherical(bytes).describe())
}
QuickTimeMetadata.PROF_UUID -> {
// redundant with info derived on the Dart side
}
QuickTimeMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
@ -187,6 +189,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
else -> {
val uuidPart = uuid.substringBefore('-')
thisDirName = "${dir.name} $uuidPart"
@ -268,11 +271,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// skip `Geo double/ascii params`, as their content is split and presented through various GeoTIFF keys
ExifGeoTiffTags.TAG_GEO_DOUBLE_PARAMS,
ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> ArrayList()
else -> listOf(exifTagMapper(tag))
}
}?.let { geoTiffDirMap.putAll(it) }
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
}
mimeType == MimeTypes.DNG -> {
// split DNG tags in their own directory
val dngDirMap = metadataMap[DIR_DNG] ?: HashMap()
@ -281,9 +286,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
byDng[true]?.map { exifTagMapper(it) }?.let { dngDirMap.putAll(it) }
byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
}
else -> dirMap.putAll(tags.map { exifTagMapper(it) })
}
}
dir.isPngTextDir() -> {
metadataMap.remove(thisDirName)
dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap()
@ -332,6 +339,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
}
}
@ -406,6 +414,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
if (isVideo(mimeType)) {
// `metadata-extractor` do not extract custom tags in user data box
val userDataDir = Mp4ParserHelper.getUserData(context, mimeType, uri)
if (userDataDir.isNotEmpty()) {
metadataMap[Metadata.DIR_MP4_USER_DATA] = userDataDir
}
// this is used as fallback when the video metadata cannot be found on the Dart side
// and to identify whether there is an accessible cover image
// do not include HEIC here
@ -641,12 +655,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
MimeTypes.GIF -> {
// identification of animated GIF
if (metadata.containsDirectoryOfType(GifAnimationDirectory::class.java)) {
flags = flags or MASK_IS_ANIMATED
}
}
MimeTypes.WEBP -> {
// identification of animated WEBP
for (dir in metadata.getDirectoriesOfType(WebpDirectory::class.java)) {
@ -655,6 +671,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
MimeTypes.TIFF -> {
// identification of GeoTIFF
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
@ -1119,16 +1136,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateDigitizedMillis { dateMillis = it }
}
}
ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateOriginalMillis { dateMillis = it }
}
}
GpsDirectory.TAG_DATE_STAMP -> {
for (dir in metadata.getDirectoriesOfType(GpsDirectory::class.java)) {
dir.gpsDate?.let { dateMillis = it.time }

View file

@ -101,7 +101,17 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
endOfStream()
}
private fun createFile() {
private suspend fun safeStartActivityForResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) {
if (intent.resolveActivity(activity.packageManager) != null) {
MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied)
activity.startActivityForResult(intent, requestCode)
} else {
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
onDenied()
}
}
private suspend fun createFile() {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
@ -116,12 +126,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
return
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, name)
}
MainActivity.pendingStorageAccessResultHandlers[MainActivity.CREATE_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
fun onGranted(uri: Uri) {
ioScope.launch {
try {
// truncate is necessary when overwriting a longer file
@ -134,13 +139,20 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
}
endOfStream()
}
}, {
}
fun onDenied() {
success(null)
endOfStream()
})
activity.startActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST)
}
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, name)
}
safeStartActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
}
private suspend fun openFile() {
@SuppressLint("ObsoleteSdkInt")
@ -178,13 +190,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
addCategory(Intent.CATEGORY_OPENABLE)
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
}
if (intent.resolveActivity(activity.packageManager) != null) {
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied)
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
} else {
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
onDenied()
}
safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
}
private fun pickCollectionFilters() {

View file

@ -33,6 +33,7 @@ object Metadata {
const val DIR_DNG = "DNG" // custom
const val DIR_EXIF_GEOTIFF = "GeoTIFF" // custom
const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom
const val DIR_MP4_USER_DATA = "User Data" // custom
// types of metadata
const val TYPE_COMMENT = "comment"

View file

@ -2,11 +2,22 @@ package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import android.util.Log
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.toByteArray
import deckers.thibault.aves.utils.toHex
import org.mp4parser.*
import org.mp4parser.boxes.UnknownBox
import org.mp4parser.boxes.UserBox
import org.mp4parser.boxes.apple.AppleCoverBox
import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox
import org.mp4parser.boxes.apple.AppleItemListBox
import org.mp4parser.boxes.apple.AppleVariableSignedIntegerBox
import org.mp4parser.boxes.apple.Utf8AppleDataBox
import org.mp4parser.boxes.iso14496.part12.*
import org.mp4parser.boxes.threegpp.ts26244.AuthorBox
import org.mp4parser.support.AbstractBox
import org.mp4parser.support.Matrix
import org.mp4parser.tools.Path
@ -15,8 +26,10 @@ import java.io.FileInputStream
import java.nio.channels.Channels
object Mp4ParserHelper {
private val LOG_TAG = LogUtils.createTag<Mp4ParserHelper>()
// 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>> {
// we can skip uninteresting boxes with a seekable data source
@ -214,10 +227,8 @@ object Mp4ParserHelper {
sb.appendLine("${"\t".repeat(indent)}[$boxType] ${box.javaClass.simpleName}")
box.dumpBoxes(sb, indent + 1)
}
is UserBox -> {
val userTypeHex = box.userType.joinToString("") { "%02x".format(it) }
sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=$userTypeHex $box")
}
is UserBox -> sb.appendLine("${"\t".repeat(indent)}[$boxType] userType=${box.userType.toHex()} $box")
else -> sb.appendLine("${"\t".repeat(indent)}[$boxType] $box")
}
} catch (e: Exception) {
@ -227,10 +238,127 @@ object Mp4ParserHelper {
}
fun Box.toBytes(): ByteArray {
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
val stream = ByteArrayOutputStream(size.toInt())
Channels.newChannel(stream).use { getBox(it) }
return stream.toByteArray()
}
fun metadataBoxParser() = PropertyBoxParserImpl().apply {
val skippedTypes = listOf(
// parsing `MediaDataBox` can take a long time
MediaDataBox.TYPE,
// parsing `SampleTableBox` or `FreeBox` may yield OOM
SampleTableBox.TYPE, FreeBox.TYPE,
// some files are padded with `0` but the parser does not stop, reads type "0000",
// then a large size from following "0000", which may yield OOM
"0000",
)
setBoxSkipper { type, size ->
if (skippedTypes.contains(type)) return@setBoxSkipper true
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
false
}
}
fun getUserData(
context: Context,
mimeType: String,
uri: Uri,
): MutableMap<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)

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.metadata
import deckers.thibault.aves.utils.toHex
import java.math.BigInteger
import java.nio.charset.Charset
import java.util.*
@ -51,7 +52,7 @@ object QuickTimeMetadata {
// 0x01: string
0x01 -> String(payload, Charset.forName("UTF-16BE")).trim()
// 0x101: artwork/icon
else -> "0x${payload.joinToString("") { "%02x".format(it) }}"
else -> "0x${payload.toHex()}"
}
val blockTypeString = when (blockType) {
@ -61,7 +62,7 @@ object QuickTimeMetadata {
0x0A -> "Track property"
0x0B -> "Time zone"
0x0C -> "Modification Time"
else -> "0x${"%02x".format(blockType)}"
else -> "0x${blockType.toByte().toHex()}"
}
blocks.add(

View file

@ -21,13 +21,9 @@ import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import org.mp4parser.IsoFile
import org.mp4parser.PropertyBoxParserImpl
import org.mp4parser.boxes.UserBox
import org.mp4parser.boxes.iso14496.part12.FreeBox
import org.mp4parser.boxes.iso14496.part12.MediaDataBox
import org.mp4parser.boxes.iso14496.part12.SampleTableBox
import java.io.FileInputStream
import java.util.*
import java.util.TimeZone
object XMP {
private val LOG_TAG = LogUtils.createTag<XMP>()
@ -156,26 +152,12 @@ object XMP {
pfd.use {
FileInputStream(it.fileDescriptor).use { stream ->
stream.channel.use { channel ->
val boxParser = PropertyBoxParserImpl().apply {
val skippedTypes = listOf(
// parsing `MediaDataBox` can take a long time
MediaDataBox.TYPE,
// parsing `SampleTableBox` or `FreeBox` may yield OOM
SampleTableBox.TYPE, FreeBox.TYPE,
)
setBoxSkipper { type, size ->
if (skippedTypes.contains(type)) return@setBoxSkipper true
if (size > Mp4ParserHelper.BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
false
}
}
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
// TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent,
// because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`,
// so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes.
IsoFile(channel, boxParser).use { isoFile ->
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
IsoFile(channel, Mp4ParserHelper.metadataBoxParser()).use { isoFile ->
isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
val boxSize = box.size
if (MemoryUtils.canAllocate(boxSize)) {
@ -193,6 +175,8 @@ object XMP {
}
}
}
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to parse MP4 for mimeType=$mimeType uri=$uri", e)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get XMP by MP4 parser for mimeType=$mimeType uri=$uri", e)
}

View file

@ -815,6 +815,8 @@ abstract class ImageProvider {
}
}
}
} catch (e: NoClassDefFoundError) {
callback.onFailure(e)
} catch (e: Exception) {
callback.onFailure(e)
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 {
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)
if (callbackHandle == 0L) {
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) {
// cf https://developer.android.com/about/versions/11/privacy/storage#directory-access
dirs.add(Environment.DIRECTORY_DOWNLOADS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// by observation, no documentation
dirs.add("Android")
}
// depends on device, no documentation
dirs.add("Android")
}
return dirs
}

View file

@ -33,11 +33,23 @@ import java.util.regex.Pattern
object 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"
// cf DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
private const val EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary"
private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
private const val TREE_URI_ROOT = "$SCHEME_CONTENT://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
private val MEDIA_STORE_VOLUME_EXTERNAL = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.VOLUME_EXTERNAL else "external"
// TODO TLAD get it from `MediaStore.Images.Media.EXTERNAL_CONTENT_URI`?
private val IMAGE_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/images/"
// TODO TLAD get it from `MediaStore.Video.Media.EXTERNAL_CONTENT_URI`?
private val VIDEO_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/video/"
private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+")
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
@ -348,7 +360,17 @@ object StorageUtils {
// fallback when UUID does not appear in the SD card volume path
val primaryVolumePath = getPrimaryVolumePath(context)
getVolumePaths(context).firstOrNull { it != primaryVolumePath }?.let { return it }
getVolumePaths(context).firstOrNull { volumePath ->
if (volumePath == primaryVolumePath) {
false
} else {
// exclude volumes that use regular naming scheme with UUID in them
// to prevent returning path with the UUID of a new volume
// when the argument is the UUID of an obsolete volume
val volumeUuid = volumePath.split(File.separator).lastOrNull { it.isNotEmpty() }
!(volumeUuid == null || volumeUuid.matches(UUID_PATTERN))
}
}?.let { return it }
Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid")
return null
@ -535,7 +557,7 @@ object StorageUtils {
uri ?: return false
// a URI's authority is [userinfo@]host[:port]
// but we only want the host when comparing to Media Store's "authority"
return ContentResolver.SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)
return SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)
}
fun getOriginalUri(context: Context, uri: Uri): Uri {
@ -544,7 +566,7 @@ object StorageUtils {
val path = uri.path
path ?: return uri
// from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException`
if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) {
if (path.startsWith(IMAGE_PATH_ROOT) || path.startsWith(VIDEO_PATH_ROOT)) {
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return MediaStore.setRequireOriginal(uri)
@ -601,7 +623,7 @@ object StorageUtils {
return uri
}
// Build a typical `images` or `videos` content URI from the original content ID.
// Build a typical `images` or `video` content URI from the original content ID.
// We cannot safely apply this to a `file` content URI, as it may point to a file not indexed
// by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI.
private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? {

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_action_stop">Zastavit</string>
<string name="app_widget_label">Fotorámeček</string>
<string name="safe_mode_shortcut_short_label">Bezpečný režim</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Bilder &amp; Videos scannen</string>
<string name="analysis_notification_default_title">Medien scannen</string>
<string name="analysis_notification_action_stop">Abbrechen</string>
<string name="safe_mode_shortcut_short_label">Sicherer Modus</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Explorar imágenes &amp; videos</string>
<string name="analysis_notification_default_title">Explorando medios</string>
<string name="analysis_notification_action_stop">Anular</string>
<string name="safe_mode_shortcut_short_label">Modo seguro</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_notification_action_stop">Gelditu</string>
<string name="analysis_notification_default_title">Media eskaneatzen</string>
<string name="app_name">Aves</string>
<string name="safe_mode_shortcut_short_label">Modu segurua</string>
</resources>

View file

@ -9,4 +9,5 @@
<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_action_stop">Annuler</string>
<string name="safe_mode_shortcut_short_label">Mode sans échec</string>
</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_notification_default_title">Memindai media</string>
<string name="analysis_notification_action_stop">Berhenti</string>
<string name="safe_mode_shortcut_short_label">Mode aman</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Scansione immagini &amp; videos</string>
<string name="analysis_notification_default_title">Scansione in corso</string>
<string name="analysis_notification_action_stop">Annulla</string>
<string name="safe_mode_shortcut_short_label">Modalità provvisoria</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">画像と動画をスキャン</string>
<string name="analysis_notification_default_title">メディアをスキャン中</string>
<string name="analysis_notification_action_stop">停止</string>
<string name="safe_mode_shortcut_short_label">セーフモード</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">사진과 동영상 분석</string>
<string name="analysis_notification_default_title">미디어 분석</string>
<string name="analysis_notification_action_stop">취소</string>
<string name="safe_mode_shortcut_short_label">안전 모드</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="wallpaper">Bakgrunnsbilde</string>
<string name="search_shortcut_short_label">Søk</string>
<string name="analysis_notification_action_stop">Stopp</string>
<string name="safe_mode_shortcut_short_label">Trygt modus</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_notification_action_stop">Zatrzymaj</string>
<string name="app_name">Aves</string>
<string name="wallpaper">Tapeta</string>
<string name="safe_mode_shortcut_short_label">Tryb bezpieczny</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_notification_default_title">Scanarea suporturilor</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="search_shortcut_short_label">Căutare</string>
<string name="safe_mode_shortcut_short_label">Modul de siguranță</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_service_description">Сканировать изображения и видео</string>
<string name="analysis_notification_default_title">Сканирование медиа</string>
<string name="analysis_notification_action_stop">Стоп</string>
<string name="safe_mode_shortcut_short_label">Безопасный режим</string>
</resources>

View file

@ -9,4 +9,5 @@
<string name="analysis_notification_action_stop">Стоп</string>
<string name="app_widget_label">Фоторамка</string>
<string name="analysis_notification_default_title">Сканування медіа</string>
<string name="safe_mode_shortcut_short_label">Безпечний режим</string>
</resources>

View file

@ -3,6 +3,7 @@
<string name="app_name">Aves</string>
<string name="app_widget_label">Photo Frame</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="videos_shortcut_short_label">Videos</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 {
ext {
kotlin_version = '1.7.20'
kotlin_version = '1.8.0'
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") }
useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") }
@ -18,8 +17,7 @@ buildscript {
}
dependencies {
// TODO TLAD upgrade Android Gradle plugin >=7.3 when this is fixed: https://github.com/flutter/flutter/issues/115100
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
if (useCrashlytics) {

View file

@ -1,4 +1,3 @@
#Thu Oct 22 10:54:33 KST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
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.
<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 {
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 {
extension ExtraMetadataFieldConvert on MetadataField {
MetadataType get type {
switch (this) {
case MetadataField.exifDate:
@ -228,21 +147,4 @@ extension ExtraMetadataField on MetadataField {
return null;
}
}
String get title {
switch (this) {
case MetadataField.exifDate:
return 'Exif date';
case MetadataField.exifDateOriginal:
return 'Exif original date';
case MetadataField.exifDateDigitized:
return 'Exif digitized date';
case MetadataField.exifGpsDatestamp:
return 'Exif GPS date';
case MetadataField.xmpXmpCreateDate:
return 'XMP xmp:CreateDate';
default:
return name;
}
}
}

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 {
try {
final bytes = await androidAppService.getAppIcon(key.packageName, key.size);
final bytes = await appService.getAppIcon(key.packageName, key.size);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
return await decode(buffer);
} catch (error) {

View file

@ -1426,5 +1426,31 @@
"vaultDialogLockModeWhenScreenOff": "Uzamknout při vypnutí displeje",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultBinUsageDialogMessage": "Některé trezory používají koš.",
"@vaultBinUsageDialogMessage": {}
"@vaultBinUsageDialogMessage": {},
"settingsVideoBackgroundMode": "Režim na pozadí",
"@settingsVideoBackgroundMode": {},
"settingsCollectionBurstPatternsNone": "Žádný",
"@settingsCollectionBurstPatternsNone": {},
"chipActionShowCountryStates": "Zobrazit země",
"@chipActionShowCountryStates": {},
"viewerActionLock": "Uzamknout prohlížení",
"@viewerActionLock": {},
"viewerActionUnlock": "Odemknout prohlížení",
"@viewerActionUnlock": {},
"settingsVideoEnablePip": "Obraz v obraze",
"@settingsVideoEnablePip": {},
"statePageTitle": "Státy",
"@statePageTitle": {},
"stateEmpty": "Žádné státy",
"@stateEmpty": {},
"searchStatesSectionTitle": "Státy",
"@searchStatesSectionTitle": {},
"settingsCollectionBurstPatternsTile": "Vzory dávek",
"@settingsCollectionBurstPatternsTile": {},
"statsTopStatesSectionTitle": "Nejčastější státy",
"@statsTopStatesSectionTitle": {},
"tagPlaceholderState": "Stát",
"@tagPlaceholderState": {},
"settingsVideoBackgroundModeDialogTitle": "Režim na pozadí",
"@settingsVideoBackgroundModeDialogTitle": {}
}

View file

@ -1208,5 +1208,91 @@
"settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.",
"@settingsModificationWarningDialogMessage": {},
"settingsViewerShowDescription": "Beschreibung anzeigen",
"@settingsViewerShowDescription": {}
"@settingsViewerShowDescription": {},
"chipActionGoToPlacePage": "In Orten anzeigen",
"@chipActionGoToPlacePage": {},
"chipActionLock": "Sperren",
"@chipActionLock": {},
"chipActionCreateVault": "Tresor anlegen",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Tresor konfigurieren",
"@chipActionConfigureVault": {},
"settingsCollectionBurstPatternsTile": "Berstmuster",
"@settingsCollectionBurstPatternsTile": {},
"settingsVideoEnablePip": "Bild-in-Bild",
"@settingsVideoEnablePip": {},
"patternDialogEnter": "Muster eingeben",
"@patternDialogEnter": {},
"tagPlaceholderState": "Staat",
"@tagPlaceholderState": {},
"settingsDisablingBinWarningDialogMessage": "Die Elemente im Papierkorb werden für immer gelöscht.",
"@settingsDisablingBinWarningDialogMessage": {},
"chipActionShowCountryStates": "Staaten anzeigen",
"@chipActionShowCountryStates": {},
"viewerActionLock": "Anzeige sperren",
"@viewerActionLock": {},
"viewerActionUnlock": "Anzeige entsperren",
"@viewerActionUnlock": {},
"albumTierVaults": "Tresore",
"@albumTierVaults": {},
"patternDialogConfirm": "Muster bestätigen",
"@patternDialogConfirm": {},
"exportEntryDialogWriteMetadata": "Metadaten schreiben",
"@exportEntryDialogWriteMetadata": {},
"drawerPlacePage": "Orte",
"@drawerPlacePage": {},
"statePageTitle": "Staaten",
"@statePageTitle": {},
"stateEmpty": "Keine Staaten",
"@stateEmpty": {},
"placePageTitle": "Orte",
"@placePageTitle": {},
"placeEmpty": "Keine Orte",
"@placeEmpty": {},
"settingsCollectionBurstPatternsNone": "Nichts",
"@settingsCollectionBurstPatternsNone": {},
"settingsVideoBackgroundMode": "Hintergrund-Modus",
"@settingsVideoBackgroundMode": {},
"searchStatesSectionTitle": "Staaten",
"@searchStatesSectionTitle": {},
"settingsConfirmationVaultDataLoss": "Warnung vor Tresordatenverlust anzeigen",
"@settingsConfirmationVaultDataLoss": {},
"settingsVideoBackgroundModeDialogTitle": "Hintergrund-Modus",
"@settingsVideoBackgroundModeDialogTitle": {},
"statsTopStatesSectionTitle": "Top Staaten",
"@statsTopStatesSectionTitle": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"vaultLockTypePattern": "Muster",
"@vaultLockTypePattern": {},
"vaultLockTypePassword": "Passwort",
"@vaultLockTypePassword": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"passwordDialogEnter": "Passwort eingeben",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Passwort bestätigen",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Authentifizierung zum Konfigurieren des Tresors",
"@authenticateToConfigureVault": {},
"newVaultWarningDialogMessage": "Elemente in Tresoren sind nur für diese App verfügbar und nicht in anderen.\n\nWenn Sie diese App deinstallieren oder die Daten dieser App löschen, gehen alle diese Elemente verloren.",
"@newVaultWarningDialogMessage": {},
"newVaultDialogTitle": "Neuer Tresor",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Tresor konfigurieren",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Sperren beim Ausschalten des Bildschirms",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Schloss-Typ",
"@vaultDialogLockTypeLabel": {},
"pinDialogConfirm": "PIN bestätigen",
"@pinDialogConfirm": {},
"authenticateToUnlockVault": "Authentifizierung zum Entsperren des Tresors",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Einige Tresore verwenden den Papierkorb.",
"@vaultBinUsageDialogMessage": {},
"pinDialogEnter": "PIN eingeben",
"@pinDialogEnter": {}
}

View file

@ -723,7 +723,7 @@
"@searchAlbumsSectionTitle": {},
"searchCountriesSectionTitle": "Χωρες",
"@searchCountriesSectionTitle": {},
"searchPlacesSectionTitle": "Τοποθεσιες",
"searchPlacesSectionTitle": "Μερη",
"@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Ετικετες",
"@searchTagsSectionTitle": {},
@ -1252,5 +1252,47 @@
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {}
"@lengthUnitPixel": {},
"chipActionGoToPlacePage": "Εμφάνιση στα μέρη",
"@chipActionGoToPlacePage": {},
"patternDialogConfirm": "Επιβεβαιώστε το μοτίβο",
"@patternDialogConfirm": {},
"drawerPlacePage": "Μέρη",
"@drawerPlacePage": {},
"settingsVideoBackgroundMode": "Αναπαραγωγή στο παρασκήνιο",
"@settingsVideoBackgroundMode": {},
"chipActionShowCountryStates": "Εμφάνιση πολιτειών",
"@chipActionShowCountryStates": {},
"viewerActionLock": "Κλείδωμα προβολής",
"@viewerActionLock": {},
"patternDialogEnter": "Εισάγετε το μοτίβο",
"@patternDialogEnter": {},
"statePageTitle": "Πολιτειες",
"@statePageTitle": {},
"stateEmpty": "Χωρίς πολιτεία",
"@stateEmpty": {},
"searchStatesSectionTitle": "Πολιτειες",
"@searchStatesSectionTitle": {},
"settingsCollectionBurstPatternsTile": "Εμφάνιση μοτίβων",
"@settingsCollectionBurstPatternsTile": {},
"settingsCollectionBurstPatternsNone": "Χωρίς",
"@settingsCollectionBurstPatternsNone": {},
"settingsVideoBackgroundModeDialogTitle": "Αναπαραγωγη στο παρασκηνιο",
"@settingsVideoBackgroundModeDialogTitle": {},
"statsTopStatesSectionTitle": "Κορυφαιες Πολιτειες",
"@statsTopStatesSectionTitle": {},
"tagPlaceholderState": "Πολιτεία",
"@tagPlaceholderState": {},
"exportEntryDialogWriteMetadata": "Εγγραφή μεταδεδομένων",
"@exportEntryDialogWriteMetadata": {},
"placePageTitle": "Μερη",
"@placePageTitle": {},
"placeEmpty": "Χωρίς μέρος",
"@placeEmpty": {},
"settingsVideoEnablePip": "Picture-in-picture",
"@settingsVideoEnablePip": {},
"viewerActionUnlock": "Ξεκλείδωμα προβολής",
"@viewerActionUnlock": {},
"vaultLockTypePattern": "Μοτίβο",
"@vaultLockTypePattern": {}
}

View file

@ -84,6 +84,7 @@
"chipActionUnpin": "Unpin from top",
"chipActionRename": "Rename",
"chipActionSetCover": "Set cover",
"chipActionShowCountryStates": "Show states",
"chipActionCreateAlbum": "Create album",
"chipActionCreateVault": "Create vault",
"chipActionConfigureVault": "Configure vault",
@ -125,6 +126,8 @@
"videoActionSetSpeed": "Playback speed",
"viewerActionSettings": "Settings",
"viewerActionLock": "Lock viewer",
"viewerActionUnlock": "Unlock viewer",
"slideshowActionResume": "Resume",
"slideshowActionShowInCollection": "Show in Collection",
@ -677,6 +680,9 @@
"countryPageTitle": "Countries",
"countryEmpty": "No countries",
"statePageTitle": "States",
"stateEmpty": "No states",
"placePageTitle": "Places",
"placeEmpty": "No places",
@ -690,6 +696,7 @@
"searchDateSectionTitle": "Date",
"searchAlbumsSectionTitle": "Albums",
"searchCountriesSectionTitle": "Countries",
"searchStatesSectionTitle": "States",
"searchPlacesSectionTitle": "Places",
"searchTagsSectionTitle": "Tags",
"searchRatingSectionTitle": "Ratings",
@ -754,6 +761,9 @@
"settingsCollectionBrowsingQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when browsing items.",
"settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.",
"settingsCollectionBurstPatternsTile": "Burst patterns",
"settingsCollectionBurstPatternsNone": "None",
"settingsViewerSectionTitle": "Viewer",
"settingsViewerGestureSideTapNext": "Tap on screen edges to show previous/next item",
"settingsViewerUseCutout": "Use cutout area",
@ -892,6 +902,7 @@
}
},
"statsTopCountriesSectionTitle": "Top Countries",
"statsTopStatesSectionTitle": "Top States",
"statsTopPlacesSectionTitle": "Top Places",
"statsTopTagsSectionTitle": "Top Tags",
"statsTopAlbumsSectionTitle": "Top Albums",
@ -948,6 +959,7 @@
"tagEditorSectionPlaceholders": "Placeholders",
"tagPlaceholderCountry": "Country",
"tagPlaceholderState": "State",
"tagPlaceholderPlace": "Place",
"panoramaEnableSensorControl": "Enable sensor control",

View file

@ -1273,6 +1273,26 @@
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Reproducción de fondo",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Background mode",
"@settingsVideoBackgroundModeDialogTitle": {}
"settingsVideoBackgroundModeDialogTitle": "Reproducción de fondo",
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsTile": "Modelos de ráfaga",
"@settingsCollectionBurstPatternsTile": {},
"settingsCollectionBurstPatternsNone": "Ninguno",
"@settingsCollectionBurstPatternsNone": {},
"tagPlaceholderState": "Estado",
"@tagPlaceholderState": {},
"viewerActionUnlock": "Desbloquear visor",
"@viewerActionUnlock": {},
"stateEmpty": "Sin estados",
"@stateEmpty": {},
"chipActionShowCountryStates": "Mostrar los estados",
"@chipActionShowCountryStates": {},
"statePageTitle": "Estados",
"@statePageTitle": {},
"viewerActionLock": "Bloquear visor",
"@viewerActionLock": {},
"searchStatesSectionTitle": "Estados",
"@searchStatesSectionTitle": {},
"statsTopStatesSectionTitle": "Estados principales",
"@statsTopStatesSectionTitle": {}
}

View file

@ -1432,5 +1432,25 @@
"settingsVideoBackgroundMode": "Erreprodukzioa atzeko planoan",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Atzeko planoko modua",
"@settingsVideoBackgroundModeDialogTitle": {}
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsNone": "Bat ere ez",
"@settingsCollectionBurstPatternsNone": {},
"settingsCollectionBurstPatternsTile": "Segida moduak",
"@settingsCollectionBurstPatternsTile": {},
"tagPlaceholderState": "Egoera",
"@tagPlaceholderState": {},
"viewerActionUnlock": "Deskblokeatu bisorea",
"@viewerActionUnlock": {},
"stateEmpty": "Egoerarik ez",
"@stateEmpty": {},
"chipActionShowCountryStates": "Erakutsi egoerak",
"@chipActionShowCountryStates": {},
"statePageTitle": "Egoerak",
"@statePageTitle": {},
"viewerActionLock": "Blokeatu bisorea",
"@viewerActionLock": {},
"searchStatesSectionTitle": "Egoerak",
"@searchStatesSectionTitle": {},
"statsTopStatesSectionTitle": "Egoera Nagusiak",
"@statsTopStatesSectionTitle": {}
}

View file

@ -1274,5 +1274,25 @@
"settingsVideoBackgroundMode": "Lecture en arrière-plan",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Arrière-plan",
"@settingsVideoBackgroundModeDialogTitle": {}
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsNone": "Aucun",
"@settingsCollectionBurstPatternsNone": {},
"settingsCollectionBurstPatternsTile": "Modèles de rafale",
"@settingsCollectionBurstPatternsTile": {},
"tagPlaceholderState": "État",
"@tagPlaceholderState": {},
"chipActionShowCountryStates": "Afficher les États",
"@chipActionShowCountryStates": {},
"stateEmpty": "Aucun État",
"@stateEmpty": {},
"searchStatesSectionTitle": "États",
"@searchStatesSectionTitle": {},
"statePageTitle": "États",
"@statePageTitle": {},
"statsTopStatesSectionTitle": "Top États",
"@statsTopStatesSectionTitle": {},
"viewerActionLock": "Verrouiller la visionneuse",
"@viewerActionLock": {},
"viewerActionUnlock": "Déverrouiller la visionneuse",
"@viewerActionUnlock": {}
}

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": {},
"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": {},
"chipActionDelete": "Elimina",
"@chipActionDelete": {},
"chipActionGoToAlbumPage": "Mostra negli album",
"chipActionGoToAlbumPage": "Mostra negli Album",
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Mostra nei Paesi",
"@chipActionGoToCountryPage": {},
@ -1101,7 +1101,7 @@
"@viewerInfoOpenLinkText": {},
"viewerInfoViewXmlLinkText": "Visualizza XML",
"@viewerInfoViewXmlLinkText": {},
"viewerInfoSearchFieldLabel": "Metadati di ricerca",
"viewerInfoSearchFieldLabel": "Ricerca metadati",
"@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "Nessuna chiave corrispondente",
"@viewerInfoSearchEmpty": {},
@ -1248,5 +1248,49 @@
"settingsDisablingBinWarningDialogMessage": "Gli elementi nel cestino verranno eliminati permanentemente.",
"@settingsDisablingBinWarningDialogMessage": {},
"configureVaultDialogTitle": "Configura Cassaforte",
"@configureVaultDialogTitle": {}
"@configureVaultDialogTitle": {},
"exportEntryDialogWriteMetadata": "Scrivi metadati",
"@exportEntryDialogWriteMetadata": {},
"chipActionGoToPlacePage": "Mostra nei Luoghi",
"@chipActionGoToPlacePage": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"patternDialogEnter": "Inserisci sequenza",
"@patternDialogEnter": {},
"patternDialogConfirm": "Conferma sequenza",
"@patternDialogConfirm": {},
"drawerPlacePage": "Luoghi",
"@drawerPlacePage": {},
"placeEmpty": "Nessun luogo",
"@placeEmpty": {},
"placePageTitle": "Luoghi",
"@placePageTitle": {},
"settingsVideoBackgroundMode": "Modalità sottofondo",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Modalità Sottofondo",
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsVideoEnablePip": "Picture-in-picture",
"@settingsVideoEnablePip": {},
"vaultLockTypePattern": "Sequenza",
"@vaultLockTypePattern": {},
"viewerActionLock": "Blocca visualizzazione",
"@viewerActionLock": {},
"viewerActionUnlock": "Sblocca visualizzazione",
"@viewerActionUnlock": {},
"statsTopStatesSectionTitle": "Stati più frequenti",
"@statsTopStatesSectionTitle": {},
"tagPlaceholderState": "Stato",
"@tagPlaceholderState": {},
"settingsCollectionBurstPatternsNone": "Nessuno",
"@settingsCollectionBurstPatternsNone": {},
"chipActionShowCountryStates": "Mostra stati",
"@chipActionShowCountryStates": {},
"statePageTitle": "Stati",
"@statePageTitle": {},
"stateEmpty": "Nessuno stato",
"@stateEmpty": {},
"searchStatesSectionTitle": "Stati",
"@searchStatesSectionTitle": {}
}

View file

@ -1170,5 +1170,69 @@
"settingsSubtitleThemeTextPositionTile": "テキストの位置",
"@settingsSubtitleThemeTextPositionTile": {},
"entryInfoActionExportMetadata": "メタデータをエクスポート",
"@entryInfoActionExportMetadata": {}
"@entryInfoActionExportMetadata": {},
"subtitlePositionTop": "トップ",
"@subtitlePositionTop": {},
"configureVaultDialogTitle": "保管庫の設定",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "画面オフ時にロック",
"@vaultDialogLockModeWhenScreenOff": {},
"newVaultDialogTitle": "新しい保管庫",
"@newVaultDialogTitle": {},
"authenticateToConfigureVault": "保管庫を設定するための認証",
"@authenticateToConfigureVault": {},
"vaultDialogLockTypeLabel": "ロックの種類",
"@vaultDialogLockTypeLabel": {},
"pinDialogEnter": "PINを入力",
"@pinDialogEnter": {},
"patternDialogEnter": "パターンを入力",
"@patternDialogEnter": {},
"pinDialogConfirm": "PINの確認",
"@pinDialogConfirm": {},
"passwordDialogEnter": "パスワードを入力",
"@passwordDialogEnter": {},
"authenticateToUnlockVault": "認証して保管庫のロックを解除する",
"@authenticateToUnlockVault": {},
"passwordDialogConfirm": "パスワードの確認",
"@passwordDialogConfirm": {},
"chipActionFilterIn": "フィルター",
"@chipActionFilterIn": {},
"filterAspectRatioPortraitLabel": "縦向き",
"@filterAspectRatioPortraitLabel": {},
"filterNoAddressLabel": "位置情報なし",
"@filterNoAddressLabel": {},
"keepScreenOnVideoPlayback": "動画再生時",
"@keepScreenOnVideoPlayback": {},
"chipActionGoToPlacePage": "場所別に表示",
"@chipActionGoToPlacePage": {},
"tagPlaceholderState": "州",
"@tagPlaceholderState": {},
"vaultLockTypePassword": "パスワード",
"@vaultLockTypePassword": {},
"tooManyItemsErrorDialogMessage": "少ないアイテムで再度試してください。",
"@tooManyItemsErrorDialogMessage": {},
"statePageTitle": "州",
"@statePageTitle": {},
"drawerPlacePage": "場所",
"@drawerPlacePage": {},
"chipActionLock": "ロック",
"@chipActionLock": {},
"filterAspectRatioLandscapeLabel": "横向き",
"@filterAspectRatioLandscapeLabel": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"newVaultWarningDialogMessage": "保管庫のアイテムはアプリ内のみで保存しているため、他のアプリでは利用できません。\n\nこのアプリをアンインストールしたり、データを消去したりすると、これらのアイテムはすべて失われます。",
"@newVaultWarningDialogMessage": {},
"patternDialogConfirm": "パターンの確認",
"@patternDialogConfirm": {},
"placePageTitle": "場所",
"@placePageTitle": {},
"settingsVideoEnablePip": "ピクチャインピクチャ",
"@settingsVideoEnablePip": {},
"vaultLockTypePattern": "パターン",
"@vaultLockTypePattern": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {}
}

View file

@ -1274,5 +1274,25 @@
"settingsVideoBackgroundMode": "백그라운드 재생",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "백그라운드 재생",
"@settingsVideoBackgroundModeDialogTitle": {}
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsNone": "없음",
"@settingsCollectionBurstPatternsNone": {},
"settingsCollectionBurstPatternsTile": "연속 촬영 양식",
"@settingsCollectionBurstPatternsTile": {},
"tagPlaceholderState": "주",
"@tagPlaceholderState": {},
"chipActionShowCountryStates": "주 보기",
"@chipActionShowCountryStates": {},
"stateEmpty": "주가 없습니다",
"@stateEmpty": {},
"searchStatesSectionTitle": "주",
"@searchStatesSectionTitle": {},
"statsTopStatesSectionTitle": "주 랭킹",
"@statsTopStatesSectionTitle": {},
"statePageTitle": "주",
"@statePageTitle": {},
"viewerActionLock": "뷰어 잠금",
"@viewerActionLock": {},
"viewerActionUnlock": "뷰어 잠금 해제",
"@viewerActionUnlock": {}
}

View file

@ -517,7 +517,7 @@
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
"@aboutCreditsWorldAtlas2": {},
"aboutTranslatorsSectionTitle": "Vdertalers",
"aboutTranslatorsSectionTitle": "Vertalers",
"@aboutTranslatorsSectionTitle": {},
"aboutLicensesSectionTitle": "Open-Source Licenties",
"@aboutLicensesSectionTitle": {},
@ -1154,5 +1154,11 @@
"settingsAllowMediaManagement": "Mediabeheer toestaan",
"@settingsAllowMediaManagement": {},
"editEntryLocationDialogSetCustom": "Aangepaste locatie instellen",
"@editEntryLocationDialogSetCustom": {}
"@editEntryLocationDialogSetCustom": {},
"entryInfoActionExportMetadata": "Metagegevens exporteren",
"@entryInfoActionExportMetadata": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {}
}

View file

@ -1432,5 +1432,25 @@
"settingsVideoBackgroundMode": "Tryb tła",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Tryb tła",
"@settingsVideoBackgroundModeDialogTitle": {}
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsNone": "Brak",
"@settingsCollectionBurstPatternsNone": {},
"settingsCollectionBurstPatternsTile": "Wzory wybuchowe",
"@settingsCollectionBurstPatternsTile": {},
"viewerActionUnlock": "Odblokuj przeglądarkę",
"@viewerActionUnlock": {},
"viewerActionLock": "Zablokuj przeglądarkę",
"@viewerActionLock": {},
"statePageTitle": "Stany",
"@statePageTitle": {},
"stateEmpty": "Brak stanów",
"@stateEmpty": {},
"searchStatesSectionTitle": "Stany",
"@searchStatesSectionTitle": {},
"statsTopStatesSectionTitle": "Najpopularniejsze stany",
"@statsTopStatesSectionTitle": {},
"tagPlaceholderState": "Stan",
"@tagPlaceholderState": {},
"chipActionShowCountryStates": "Pokaż stany",
"@chipActionShowCountryStates": {}
}

View file

@ -1271,6 +1271,28 @@
"@vaultLockTypePattern": {},
"settingsVideoEnablePip": "Picture-in-picture",
"@settingsVideoEnablePip": {},
"settingsVideoBackgroundMode": "Modo background",
"@settingsVideoBackgroundMode": {}
"settingsVideoBackgroundMode": "Modo de fundo",
"@settingsVideoBackgroundMode": {},
"settingsCollectionBurstPatternsTile": "Padrões de explosão",
"@settingsCollectionBurstPatternsTile": {},
"chipActionShowCountryStates": "Mostrar estados",
"@chipActionShowCountryStates": {},
"viewerActionLock": "Bloquear visualizador",
"@viewerActionLock": {},
"statePageTitle": "Estados",
"@statePageTitle": {},
"stateEmpty": "Nenhum estado",
"@stateEmpty": {},
"tagPlaceholderState": "Estado",
"@tagPlaceholderState": {},
"searchStatesSectionTitle": "Estados",
"@searchStatesSectionTitle": {},
"settingsCollectionBurstPatternsNone": "Nenhum",
"@settingsCollectionBurstPatternsNone": {},
"statsTopStatesSectionTitle": "Principais Estados",
"@statsTopStatesSectionTitle": {},
"viewerActionUnlock": "Desbloquear visualizador",
"@viewerActionUnlock": {},
"settingsVideoBackgroundModeDialogTitle": "Modo de fundo",
"@settingsVideoBackgroundModeDialogTitle": {}
}

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": {},
"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": {},
"filterTaggedLabel": "С тэгами",
"@filterTaggedLabel": {},
"chipActionGoToPlacePage": "Показать в местах",
"chipActionGoToPlacePage": "Показать в локациях",
"@chipActionGoToPlacePage": {},
"settingsModificationWarningDialogMessage": "Другие настройки будут изменены.",
"@settingsModificationWarningDialogMessage": {},
@ -1244,5 +1244,23 @@
"settingsDisablingBinWarningDialogMessage": "Элементы в корзине будут удалены навсегда.",
"@settingsDisablingBinWarningDialogMessage": {},
"lengthUnitPixel": "пикс.",
"@lengthUnitPixel": {}
"@lengthUnitPixel": {},
"chipActionLock": "Заблокировать",
"@chipActionLock": {},
"patternDialogEnter": "Введите ключ",
"@patternDialogEnter": {},
"patternDialogConfirm": "Подтвердите ключ",
"@patternDialogConfirm": {},
"vaultLockTypePattern": "Графический ключ",
"@vaultLockTypePattern": {},
"drawerPlacePage": "Локации",
"@drawerPlacePage": {},
"settingsVideoBackgroundMode": "Фоновый режим",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Фоновый режим",
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsVideoEnablePip": "Картинка в картинке",
"@settingsVideoEnablePip": {},
"placeEmpty": "Нет локаций",
"@placeEmpty": {}
}

View file

@ -1432,5 +1432,25 @@
"settingsVideoBackgroundMode": "Фоновий режим",
"@settingsVideoBackgroundMode": {},
"settingsVideoBackgroundModeDialogTitle": "Фоновий режим",
"@settingsVideoBackgroundModeDialogTitle": {}
"@settingsVideoBackgroundModeDialogTitle": {},
"tagPlaceholderState": "Штат",
"@tagPlaceholderState": {},
"chipActionShowCountryStates": "Показати штати",
"@chipActionShowCountryStates": {},
"viewerActionUnlock": "Розблокувати переглядач",
"@viewerActionUnlock": {},
"viewerActionLock": "Заблокувати переглядач",
"@viewerActionLock": {},
"stateEmpty": "Немає штатів",
"@stateEmpty": {},
"settingsCollectionBurstPatternsTile": "Вибух візерунків",
"@settingsCollectionBurstPatternsTile": {},
"settingsCollectionBurstPatternsNone": "Нічого",
"@settingsCollectionBurstPatternsNone": {},
"statsTopStatesSectionTitle": "Топ штатів",
"@statsTopStatesSectionTitle": {},
"searchStatesSectionTitle": "Штати",
"@searchStatesSectionTitle": {},
"statePageTitle": "Штати",
"@statePageTitle": {}
}

View file

@ -1192,5 +1192,13 @@
"filterNoAddressLabel": "无地址",
"@filterNoAddressLabel": {},
"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,
sourceUrl: 'https://github.com/aaassseee/screen_brightness',
),
Dependency(
name: 'Screen State',
license: mit,
licenseUrl: 'https://github.com/cph-cachet/flutter-plugins/blob/master/packages/screen_state/LICENSE',
sourceUrl: 'https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state',
),
Dependency(
name: 'Shared Preferences',
license: bsd3,

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 'package:aves/model/apps.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
@ -121,7 +123,7 @@ class Covers {
String? effectiveAlbumPackage(String albumPath) {
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath);
return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath);
}
// import/export

View file

@ -10,7 +10,7 @@ final Device device = Device._private();
class Device {
late final String _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
String get userAgent => _userAgent;
@ -25,6 +25,8 @@ class Device {
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
bool get canRenderSubdivisionFlagEmojis => _canRenderSubdivisionFlagEmojis;
bool get canRequestManageMedia => _canRequestManageMedia;
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
@ -71,6 +73,7 @@ class Device {
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canPrint = capabilities['canPrint'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_canUseCrypto = capabilities['canUseCrypto'] ?? false;

View file

@ -52,7 +52,7 @@ class EntryDir {
}
String? _resolve() {
final vrl = VolumeRelativeDirectory.fromPath(asIs!);
final vrl = androidFileUtils.relativeDirectoryFromPath(asIs!);
if (vrl == null || vrl.relativeDir.isEmpty) return asIs;
var resolved = vrl.volumePath;

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:aves/model/entry/cache.dart';
@ -7,13 +6,12 @@ import 'package:aves/model/entry/dirs.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/source/trash.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/format.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
@ -80,10 +78,6 @@ class AvesEntry with AvesEntryBase {
this.durationMillis = durationMillis;
}
bool get canDecode => !MimeTypes.undecodableImages.contains(mimeType);
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
AvesEntry copyWith({
int? id,
String? uri,
@ -225,15 +219,6 @@ class AvesEntry with AvesEntryBase {
return _extension;
}
String? get storagePath => trashed ? trashDetails?.path : path;
String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory;
bool get isMissingAtPath {
final _storagePath = storagePath;
return _storagePath != null && !File(_storagePath).existsSync();
}
// the MIME type reported by the Media Store is unreliable
// so we use the one found during cataloguing if possible
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
@ -323,18 +308,6 @@ class AvesEntry with AvesEntryBase {
return _durationText!;
}
bool get isExpiredTrash {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return false;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now());
}
int? get trashDaysLeft {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return null;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
}
// returns whether this entry has GPS coordinates
// (0, 0) coordinates are considered invalid, as it is likely a default value
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;

View file

@ -7,7 +7,7 @@ import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/painting.dart';
extension ExtraAvesEntryImages on AvesEntry {
bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent));

View file

@ -9,11 +9,11 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/theme/text.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
extension ExtraAvesEntryInfo on AvesEntry {
@ -115,7 +115,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
final dirName = [
'Stream ${index.toString().padLeft(indexDigits, '0')}',
typeText,
].join(Constants.separator);
].join(AText.separator);
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
if (formattedStreamTags.isNotEmpty) {
final color = colors.fromString(typeText);

View file

@ -12,6 +12,8 @@ import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
extension ExtraAvesEntryLocation on AvesEntry {
static final _invalidLocalityPattern = RegExp(r'^[-+\dA-Z]+$');
LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null;
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
@ -53,18 +55,17 @@ extension ExtraAvesEntryLocation on AvesEntry {
)
: call());
if (addresses.isNotEmpty) {
final address = addresses.first;
final cc = address.countryCode?.toUpperCase();
final cn = address.countryName;
final aa = address.adminArea;
final v = addresses.first;
var locality = v.locality ?? v.subLocality ?? v.featureName;
if (locality == null || _invalidLocalityPattern.hasMatch(locality) || {v.subThoroughfare, v.countryName}.contains(locality)) {
locality = v.subAdminArea;
}
addressDetails = AddressDetails(
id: id,
countryCode: cc,
countryName: cn,
adminArea: aa,
// if country & admin fields are null, it is likely the ocean,
// which is identified by `featureName` but we default to the address line anyway
locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null),
countryCode: v.countryCode?.toUpperCase(),
countryName: v.countryName,
adminArea: v.adminArea,
locality: locality,
);
}
} catch (error, stack) {

View file

@ -1,20 +1,20 @@
import 'dart:convert';
import 'dart:io';
import 'package:aves/convert/convert.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/model/metadata/enums/date_field_source.dart';
import 'package:aves/model/metadata/enums/enums.dart';
import 'package:aves/model/metadata/fields.dart';
import 'package:aves/ref/exif.dart';
import 'package:aves/ref/iptc.dart';
import 'package:aves/ref/metadata/exif.dart';
import 'package:aves/ref/metadata/iptc.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/ref/metadata/xmp.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/xmp.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves/utils/xmp_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
@ -27,7 +27,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final appliedModifier = await _applyDateModifierToEntry(userModifier);
if (appliedModifier == null) {
if (!isMissingAtPath && userModifier.action != DateEditAction.copyField) {
if (isValid && userModifier.action != DateEditAction.copyField) {
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null);
}
return {};
@ -54,7 +54,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
break;
case DateEditAction.shift:
final xmpDate = XMP.getString(descriptions, XMP.xmpCreateDate, namespace: Namespaces.xmp);
final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp);
if (xmpDate != null) {
final date = DateTime.tryParse(xmpDate);
if (date != null) {
@ -262,18 +262,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
if (editTitle) {
modified |= XMP.setAttribute(
descriptions,
XMP.dcTitle,
XmpElements.dcTitle,
title,
namespace: Namespaces.dc,
namespace: XmpNamespaces.dc,
strat: XmpEditStrategy.always,
);
}
if (editDescription) {
modified |= XMP.setAttribute(
descriptions,
XMP.dcDescription,
XmpElements.dcDescription,
description,
namespace: Namespaces.dc,
namespace: XmpNamespaces.dc,
strat: XmpEditStrategy.always,
);
}
@ -417,9 +417,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
static bool editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
return XMP.setAttribute(
descriptions,
XMP.xmpCreateDate,
XmpAttributes.xmpCreateDate,
date != null ? XMP.toXmpDate(date) : null,
namespace: Namespaces.xmp,
namespace: XmpNamespaces.xmp,
strat: XmpEditStrategy.always,
);
}
@ -428,9 +428,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
static bool editTagsXmp(List<XmlNode> descriptions, Set<String> tags) {
return XMP.setStringBag(
descriptions,
XMP.dcSubject,
XmpElements.dcSubject,
tags,
namespace: Namespaces.dc,
namespace: XmpNamespaces.dc,
strat: XmpEditStrategy.always,
);
}
@ -441,17 +441,17 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
modified |= XMP.setAttribute(
descriptions,
XMP.xmpRating,
XmpElements.xmpRating,
(rating ?? 0) == 0 ? null : '$rating',
namespace: Namespaces.xmp,
namespace: XmpNamespaces.xmp,
strat: XmpEditStrategy.always,
);
modified |= XMP.setAttribute(
descriptions,
XMP.msPhotoRating,
XmpElements.msPhotoRating,
XMP.toMsPhotoRating(rating),
namespace: Namespaces.microsoftPhoto,
namespace: XmpNamespaces.microsoftPhoto,
strat: XmpEditStrategy.updateIfPresent,
);
@ -464,23 +464,23 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
modified |= XMP.removeElements(
descriptions,
XMP.containerDirectory,
Namespaces.gContainer,
XmpElements.containerDirectory,
XmpNamespaces.gContainer,
);
modified |= [
XMP.gCameraMicroVideo,
XMP.gCameraMicroVideoVersion,
XMP.gCameraMicroVideoOffset,
XMP.gCameraMicroVideoPresentationTimestampUs,
XMP.gCameraMotionPhoto,
XMP.gCameraMotionPhotoVersion,
XMP.gCameraMotionPhotoPresentationTimestampUs,
XmpAttributes.gCameraMicroVideo,
XmpAttributes.gCameraMicroVideoVersion,
XmpAttributes.gCameraMicroVideoOffset,
XmpAttributes.gCameraMicroVideoPresentationTimestampUs,
XmpAttributes.gCameraMotionPhoto,
XmpAttributes.gCameraMotionPhotoVersion,
XmpAttributes.gCameraMotionPhotoPresentationTimestampUs,
].fold<bool>(modified, (prev, name) {
return prev |= XMP.removeElements(
descriptions,
name,
Namespaces.gCamera,
XmpNamespaces.gCamera,
);
});

View file

@ -7,8 +7,6 @@ import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
extension ExtraAvesEntryMultipage on AvesEntry {
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst;
bool get isBurst => burstEntries?.isNotEmpty == true;
@ -18,11 +16,13 @@ extension ExtraAvesEntryMultipage on AvesEntry {
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
String? get burstKey {
String? getBurstKey(List<String> patterns) {
if (filenameWithoutExtension != null) {
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
if (match != null) {
return '$directory/${match.group(1)}';
for (final pattern in patterns) {
final match = RegExp(pattern).firstMatch(filenameWithoutExtension!);
if (match != null) {
return '$directory/${match.group(1)}';
}
}
}
return null;

View file

@ -1,63 +1,113 @@
import 'dart:io';
import 'dart:ui';
import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/trash.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/ref/unicode.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/text.dart';
import 'package:aves/utils/android_file_utils.dart';
extension ExtraAvesEntryProps on AvesEntry {
bool get isValid => !isMissingAtPath && sizeBytes != 0 && width > 0 && height > 0;
// type
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
bool get canHaveAlpha => MimeTypes.canHaveAlpha(mimeType);
bool get isSvg => mimeType == MimeTypes.svg;
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
bool get _supportedByBitmapRegionDecoder =>
[
MimeTypes.heic,
MimeTypes.heif,
MimeTypes.jpeg,
MimeTypes.png,
MimeTypes.webp,
MimeTypes.arw,
MimeTypes.cr2,
MimeTypes.nef,
MimeTypes.nrw,
MimeTypes.orf,
MimeTypes.pef,
MimeTypes.raf,
MimeTypes.rw2,
MimeTypes.srw,
].contains(mimeType) &&
!isAnimated;
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
bool get isRaw => MimeTypes.isRaw(mimeType);
bool get isImage => MimeTypes.isImage(mimeType);
bool get isVideo => MimeTypes.isVideo(mimeType);
// size
bool get useTiles => canDecodeRegion && (width > 4096 || height > 4096);
bool get isSized => width > 0 && height > 0;
Size videoDisplaySize(double sar) {
final size = displaySize;
if (sar != 1) {
final dar = displayAspectRatio * sar;
final w = size.width;
final h = size.height;
if (w >= h) return Size(w, w / dar);
if (h > w) return Size(h * dar, h);
}
return size;
}
// text
String get resolutionText {
final ws = width;
final hs = height;
return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs';
}
String get aspectRatioText {
const separator = UniChars.ratio;
if (width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isRotated ? '$h$separator$w' : '$w$separator$h';
} else {
return '?$separator?';
}
}
// catalog
bool get isAnimated => catalogMetadata?.isAnimated ?? false;
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
bool get is360 => catalogMetadata?.is360 ?? false;
bool get isMediaStoreContent => uri.startsWith('content://media/');
// trash
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
bool get isExpiredTrash {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return false;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now());
}
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
int? get trashDaysLeft {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return null;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
}
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
// storage
String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory;
bool get isMissingAtPath {
final _storagePath = trashed ? trashDetails?.path : path;
return _storagePath != null && !File(_storagePath).existsSync();
}
// providers
bool get _isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
bool get _isMediaStoreContent => uri.startsWith(AndroidFileUtils.mediaStoreUriRoot);
bool get isMediaStoreMediaContent => _isMediaStoreContent && AndroidFileUtils.mediaUriPathRoots.any(uri.contains);
// edition
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (_isMediaStoreContent || _isVaultContent);
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
@ -73,47 +123,17 @@ extension ExtraAvesEntryProps on AvesEntry {
bool get canFlip => canEdit && canEditExif;
bool get canEditExif => MimeTypes.canEditExif(mimeType);
// app support
bool get canEditIptc => MimeTypes.canEditIptc(mimeType);
bool get canDecode => AppSupport.canDecode(mimeType);
bool get canEditXmp => MimeTypes.canEditXmp(mimeType);
bool get canDecodeRegion => AppSupport.canDecodeRegion(mimeType) && !isAnimated;
bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType);
bool get canEditExif => AppSupport.canEditExif(mimeType);
static const ratioSeparator = '\u2236';
static const resolutionSeparator = ' \u00D7 ';
bool get canEditIptc => AppSupport.canEditIptc(mimeType);
bool get isSized => width > 0 && height > 0;
bool get canEditXmp => AppSupport.canEditXmp(mimeType);
String get resolutionText {
final ws = width;
final hs = height;
return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
}
String get aspectRatioText {
if (width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
} else {
return '?$ratioSeparator?';
}
}
Size videoDisplaySize(double sar) {
final size = displaySize;
if (sar != 1) {
final dar = displayAspectRatio * sar;
final w = size.width;
final h = size.height;
if (w >= h) return Size(w, w / dar);
if (h > w) return Size(h * dar, h);
}
return size;
}
int get megaPixels => (width * height / 1000000).round();
bool get canRemoveMetadata => AppSupport.canRemoveMetadata(mimeType);
}

View file

@ -2,9 +2,10 @@ import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
final Favourites favourites = Favourites._private();

View file

@ -3,8 +3,8 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

View file

@ -2,13 +2,11 @@ import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/enums/coordinate_format.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
@ -61,7 +59,7 @@ class CoordinateFilter extends CollectionFilter {
bool get exclusiveProp => false;
@override
String get universalLabel => _formatBounds(lookupAppLocalizations(AvesApp.supportedLocales.first), CoordinateFormat.decimal);
String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal);
@override
String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat);

View file

@ -5,7 +5,7 @@ import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class FavouriteFilter extends CollectionFilter {

View file

@ -1,6 +1,7 @@
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/emoji_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
@ -11,23 +12,31 @@ class LocationFilter extends CoveredCollectionFilter {
final LocationLevel level;
late final String _location;
late final String? _countryCode;
late final String? _code;
late final EntryFilter _test;
@override
List<Object?> get props => [level, _location, _countryCode, reversed];
List<Object?> get props => [level, _location, _code, reversed];
LocationFilter(this.level, String location, {super.reversed = false}) {
final split = location.split(locationSeparator);
_location = split.isNotEmpty ? split[0] : location;
_countryCode = split.length > 1 ? split[1] : null;
_code = split.length > 1 ? split[1] : null;
if (_location.isEmpty) {
_test = (entry) => !entry.hasGps;
} else if (level == LocationLevel.country) {
_test = (entry) => entry.addressDetails?.countryCode == _countryCode;
} else if (level == LocationLevel.place) {
_test = (entry) => entry.addressDetails?.place == _location;
} else {
switch (level) {
case LocationLevel.country:
_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
Map<String, dynamic> toMap() => {
'type': type,
'level': level.toString(),
'location': _countryCode != null ? countryNameAndCode : _location,
'reversed': reversed,
};
Map<String, dynamic> toMap() {
String location = _location;
switch (level) {
case LocationLevel.country:
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;
@ -71,11 +93,9 @@ class LocationFilter extends CoveredCollectionFilter {
return Icon(AIcons.locationUnlocated, size: size);
}
switch (level) {
case LocationLevel.place:
return Icon(AIcons.place, size: size);
case LocationLevel.country:
if (_countryCode != null && device.canRenderFlagEmojis) {
final flag = countryCodeToFlag(_countryCode);
if (_code != null && device.canRenderFlagEmojis) {
final flag = EmojiUtils.countryCodeToFlag(_code);
if (flag != null) {
return Text(
flag,
@ -85,6 +105,20 @@ class LocationFilter extends CoveredCollectionFilter {
}
}
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;
@override
String get key => '$type-$reversed-$level-$_location';
// 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));
}
String get key => '$type-$reversed-$level-$code-$place';
}
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 _country = 'country';
static const _state = 'state';
static const _place = 'place';
final String placeholder;
late final IconData _icon;
static final country = PlaceholderFilter._private(_country);
static final state = PlaceholderFilter._private(_state);
static final place = PlaceholderFilter._private(_place);
@override
@ -27,6 +29,9 @@ class PlaceholderFilter extends CollectionFilter {
case _country:
_icon = AIcons.country;
break;
case _state:
_icon = AIcons.state;
break;
case _place:
_icon = AIcons.place;
break;
@ -48,6 +53,7 @@ class PlaceholderFilter extends CollectionFilter {
Future<String?> toTag(AvesEntry entry) async {
switch (placeholder) {
case _country:
case _state:
case _place:
if (!entry.isCatalogued) {
await entry.catalog(background: false, force: false, persist: true);
@ -60,8 +66,14 @@ class PlaceholderFilter extends CollectionFilter {
final address = entry.addressDetails;
if (address == null) return null;
if (placeholder == _country) return address.countryName;
if (placeholder == _place) return address.place;
switch (placeholder) {
case _country:
return address.countryName;
case _state:
return address.stateName;
case _place:
return address.place;
}
break;
}
return null;
@ -81,6 +93,8 @@ class PlaceholderFilter extends CollectionFilter {
switch (placeholder) {
case _country:
return context.l10n.tagPlaceholderCountry;
case _state:
return context.l10n.tagPlaceholderState;
case _place:
return context.l10n.tagPlaceholderPlace;
default:

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