Merge branch 'develop'
This commit is contained in:
commit
03df8fbd26
499 changed files with 9111 additions and 4162 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||
Subproject commit f72efea43c3013323d1b95cff571f3c1caa37583
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -32,9 +32,6 @@ migrate_working_dir/
|
|||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
|
|
26
.metadata
26
.metadata
|
@ -1,10 +1,30 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: bc7bc940836f1f834699625426795fd6f07c18ec
|
||||
channel: beta
|
||||
revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
|
||||
base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
|
||||
- platform: android
|
||||
create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
|
||||
base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="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
2
android/.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -815,6 +815,8 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
callback.onFailure(e)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
return false
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun ByteBuffer.toByteArray(): ByteArray {
|
||||
val bytes = ByteArray(remaining())
|
||||
get(bytes, 0, bytes.size)
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() }
|
||||
|
||||
fun Byte.toHex(): String = "%02x".format(this)
|
|
@ -17,7 +17,12 @@ import kotlin.coroutines.suspendCoroutine
|
|||
object FlutterUtils {
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="@color/ic_shortcut_background"
|
||||
android:pathData="M0,24 A1,1 0 1,1 48,24 A1,1 0 1,1 0,24" />
|
||||
<group
|
||||
android:translateX="12"
|
||||
android:translateY="12">
|
||||
<path
|
||||
android:fillColor="@color/ic_shortcut_foreground"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z" />
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:tint="@color/ic_shortcut_foreground"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:scaleX="1.7226"
|
||||
android:scaleY="1.7226"
|
||||
android:translateX="33.3288"
|
||||
android:translateY="33.3288">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z" />
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_shortcut_background" />
|
||||
<foreground android:drawable="@drawable/ic_shortcut_safe_mode_foreground" />
|
||||
</adaptive-icon>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="analysis_notification_default_title">Prohledávání médií</string>
|
||||
<string name="analysis_notification_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>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="analysis_service_description">Bilder & 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>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="analysis_service_description">Explorar imágenes & 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>
|
|
@ -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>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="analysis_service_description">Analyse des images & vidéos</string>
|
||||
<string name="analysis_notification_default_title">Analyse des images</string>
|
||||
<string name="analysis_notification_action_stop">Annuler</string>
|
||||
<string name="safe_mode_shortcut_short_label">Mode sans échec</string>
|
||||
</resources>
|
12
android/app/src/main/res/values-hi/strings.xml
Normal file
12
android/app/src/main/res/values-hi/strings.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="analysis_notification_default_title">मीडिया जाँचा जा राहा है</string>
|
||||
<string name="analysis_notification_action_stop">रोके</string>
|
||||
<string name="app_widget_label">फोटो फ्रेम</string>
|
||||
<string name="wallpaper">वॉलपेपर</string>
|
||||
<string name="search_shortcut_short_label">खोजें</string>
|
||||
<string name="analysis_channel_name">मीडिया जाँचे</string>
|
||||
<string name="app_name">ऐवीज</string>
|
||||
<string name="videos_shortcut_short_label">वीडियो</string>
|
||||
<string name="analysis_service_description">छवि & वीडियो जाँचे</string>
|
||||
</resources>
|
8
android/app/src/main/res/values-hu/strings.xml
Normal file
8
android/app/src/main/res/values-hu/strings.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="wallpaper">Háttérkép</string>
|
||||
<string name="search_shortcut_short_label">Keresés</string>
|
||||
<string name="videos_shortcut_short_label">Videók</string>
|
||||
<string name="analysis_notification_action_stop">Állj</string>
|
||||
</resources>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="analysis_service_description">Pindai gambar & video</string>
|
||||
<string name="analysis_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>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="analysis_service_description">Scansione immagini & 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#Thu Oct 22 10:54:33 KST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -1 +1 @@
|
|||
Galerie und Metadata Explorer
|
||||
Galerie und Metadaten Explorer
|
5
fastlane/metadata/android/en-US/changelogs/96.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/96.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
In v1.8.5:
|
||||
- navigate states for some countries (requires rescan)
|
||||
- group Samsung and Sony bursts
|
||||
- lock viewer when watching videos
|
||||
Full changelog available on GitHub
|
5
fastlane/metadata/android/en-US/changelogs/9601.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/9601.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
In v1.8.5:
|
||||
- navigate states for some countries (requires rescan)
|
||||
- group Samsung and Sony bursts
|
||||
- lock viewer when watching videos
|
||||
Full changelog available on GitHub
|
5
fastlane/metadata/android/hi/full_description.txt
Normal file
5
fastlane/metadata/android/hi/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/hi/short_description.txt
Normal file
1
fastlane/metadata/android/hi/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
गैलरी और मोटाडेटा एक्स्प्लोरर
|
5
fastlane/metadata/android/hu/full_description.txt
Normal file
5
fastlane/metadata/android/hu/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/hu/short_description.txt
Normal file
1
fastlane/metadata/android/hu/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Gallery and metadata explorer
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
<b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
|
||||
|
||||
<i>Aves</i> integra com Android (de <b>API 19 para 33</b>, i.e. de KitKat para Android 13) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.
|
||||
<i>Aves</i> integra com Android (de KitKat até Android 13, incluindo TVs Android) com recursos como <b>widgets</b>, <b>atalhos de apps</b>, <b>protetor de tela</b> e <b>pesquisa global</b>. Também funciona como um <b>visualizador e selecionador de mídia</b>.
|
3
lib/convert/convert.dart
Normal file
3
lib/convert/convert.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'metadata/date_field_source.dart';
|
||||
export 'metadata/fields.dart';
|
||||
export 'metadata/metadata_type.dart';
|
18
lib/convert/metadata/date_field_source.dart
Normal file
18
lib/convert/metadata/date_field_source.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:aves_model/aves_model.dart';
|
||||
|
||||
extension ExtraDateFieldSourceConvert on DateFieldSource {
|
||||
MetadataField? toMetadataField() {
|
||||
switch (this) {
|
||||
case DateFieldSource.fileModifiedDate:
|
||||
return null;
|
||||
case DateFieldSource.exifDate:
|
||||
return MetadataField.exifDate;
|
||||
case DateFieldSource.exifDateOriginal:
|
||||
return MetadataField.exifDateOriginal;
|
||||
case DateFieldSource.exifDateDigitized:
|
||||
return MetadataField.exifDateDigitized;
|
||||
case DateFieldSource.exifGpsDate:
|
||||
return MetadataField.exifGpsDatestamp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,87 +1,6 @@
|
|||
import 'package:aves/model/metadata/enums/enums.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
|
||||
enum MetadataField {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
28
lib/convert/metadata/metadata_type.dart
Normal file
28
lib/convert/metadata/metadata_type.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
import 'package:aves_model/aves_model.dart';
|
||||
|
||||
extension ExtraMetadataTypeConvert on MetadataType {
|
||||
String get toPlatform {
|
||||
switch (this) {
|
||||
case MetadataType.comment:
|
||||
return 'comment';
|
||||
case MetadataType.exif:
|
||||
return 'exif';
|
||||
case MetadataType.iccProfile:
|
||||
return 'icc_profile';
|
||||
case MetadataType.iptc:
|
||||
return 'iptc';
|
||||
case MetadataType.jfif:
|
||||
return 'jfif';
|
||||
case MetadataType.jpegAdobe:
|
||||
return 'jpeg_adobe';
|
||||
case MetadataType.jpegDucky:
|
||||
return 'jpeg_ducky';
|
||||
case MetadataType.mp4:
|
||||
return 'mp4';
|
||||
case MetadataType.photoshopIrb:
|
||||
return 'photoshop_irb';
|
||||
case MetadataType.xmp:
|
||||
return 'xmp';
|
||||
}
|
||||
}
|
||||
}
|
140
lib/geo/states.dart
Normal file
140
lib/geo/states.dart
Normal file
|
@ -0,0 +1,140 @@
|
|||
import 'package:aves/ref/unicode.dart';
|
||||
import 'package:country_code/country_code.dart';
|
||||
|
||||
class GeoStates {
|
||||
static final aus = CountryCode.AU.alpha2;
|
||||
static final gbr = CountryCode.GB.alpha2;
|
||||
static final ind = CountryCode.IN.alpha2;
|
||||
static final usa = CountryCode.US.alpha2;
|
||||
|
||||
static final Set<String> stateCountryCodes = {
|
||||
aus,
|
||||
gbr,
|
||||
ind,
|
||||
usa,
|
||||
};
|
||||
|
||||
static final stateCodesByCountryCode = {
|
||||
aus: EmojiStateCodes.aus,
|
||||
gbr: EmojiStateCodes.gbr,
|
||||
ind: EmojiStateCodes.ind,
|
||||
usa: EmojiStateCodes.usa,
|
||||
};
|
||||
|
||||
static const stateCodeByName = {
|
||||
..._australiaEnglish,
|
||||
..._indiaEnglish,
|
||||
..._unitedKingdomEnglish,
|
||||
..._unitedStatesEnglish,
|
||||
};
|
||||
|
||||
static const _australiaEnglish = {
|
||||
'Australian Capital Territory': EmojiStateCodes.auAustralianCapitalTerritory,
|
||||
'New South Wales': EmojiStateCodes.auNewSouthWales,
|
||||
'Northern Territory': EmojiStateCodes.auNorthernTerritory,
|
||||
'Queensland': EmojiStateCodes.auQueensland,
|
||||
'South Australia': EmojiStateCodes.auSouthAustralia,
|
||||
'Tasmania': EmojiStateCodes.auTasmania,
|
||||
'Victoria': EmojiStateCodes.auVictoria,
|
||||
'Western Australia': EmojiStateCodes.auWesternAustralia,
|
||||
};
|
||||
|
||||
static const _indiaEnglish = {
|
||||
'Andaman and Nicobar Islands': EmojiStateCodes.inAndamanAndNicobarIslands,
|
||||
'Andhra Pradesh': EmojiStateCodes.inAndhraPradesh,
|
||||
'Arunachal Pradesh': EmojiStateCodes.inArunachalPradesh,
|
||||
'Assam': EmojiStateCodes.inAssam,
|
||||
'Bihar': EmojiStateCodes.inBihar,
|
||||
'Chandigarh': EmojiStateCodes.inChandigarh,
|
||||
'Chhattisgarh': EmojiStateCodes.inChhattisgarh,
|
||||
'Daman and Diu': EmojiStateCodes.inDamanAndDiu,
|
||||
'Delhi': EmojiStateCodes.inDelhi,
|
||||
'Dadra and Nagar Haveli': EmojiStateCodes.inDadraAndNagarHaveli,
|
||||
'Goa': EmojiStateCodes.inGoa,
|
||||
'Gujarat': EmojiStateCodes.inGujarat,
|
||||
'Himachal Pradesh': EmojiStateCodes.inHimachalPradesh,
|
||||
'Haryana': EmojiStateCodes.inHaryana,
|
||||
'Jharkhand': EmojiStateCodes.inJharkhand,
|
||||
'Jammu and Kashmir': EmojiStateCodes.inJammuAndKashmir,
|
||||
'Karnataka': EmojiStateCodes.inKarnataka,
|
||||
'Kerala': EmojiStateCodes.inKerala,
|
||||
'Lakshadweep': EmojiStateCodes.inLakshadweep,
|
||||
'Maharashtra': EmojiStateCodes.inMaharashtra,
|
||||
'Meghalaya': EmojiStateCodes.inMeghalaya,
|
||||
'Manipur': EmojiStateCodes.inManipur,
|
||||
'Madhya Pradesh': EmojiStateCodes.inMadhyaPradesh,
|
||||
'Mizoram': EmojiStateCodes.inMizoram,
|
||||
'Nagaland': EmojiStateCodes.inNagaland,
|
||||
'Odisha': EmojiStateCodes.inOdisha,
|
||||
'Punjab': EmojiStateCodes.inPunjab,
|
||||
'Puducherry': EmojiStateCodes.inPuducherry,
|
||||
'Rajasthan': EmojiStateCodes.inRajasthan,
|
||||
'Sikkim': EmojiStateCodes.inSikkim,
|
||||
'Telangana': EmojiStateCodes.inTelangana,
|
||||
'Tamil Nadu': EmojiStateCodes.inTamilNadu,
|
||||
'Tripura': EmojiStateCodes.inTripura,
|
||||
'Uttar Pradesh': EmojiStateCodes.inUttarPradesh,
|
||||
'Uttarakhand': EmojiStateCodes.inUttarakhand,
|
||||
'West Bengal': EmojiStateCodes.inWestBengal,
|
||||
};
|
||||
|
||||
static const _unitedKingdomEnglish = {
|
||||
'England': EmojiStateCodes.gbEngland,
|
||||
'Northern Ireland': EmojiStateCodes.gbNorthernIreland,
|
||||
'Scotland': EmojiStateCodes.gbScotland,
|
||||
'Wales': EmojiStateCodes.gbWales,
|
||||
};
|
||||
|
||||
static const _unitedStatesEnglish = {
|
||||
'Alabama': EmojiStateCodes.usAlabama,
|
||||
'Alaska': EmojiStateCodes.usAlaska,
|
||||
'Arizona': EmojiStateCodes.usArizona,
|
||||
'Arkansas': EmojiStateCodes.usArkansas,
|
||||
'California': EmojiStateCodes.usCalifornia,
|
||||
'Colorado': EmojiStateCodes.usColorado,
|
||||
'Connecticut': EmojiStateCodes.usConnecticut,
|
||||
'Delaware': EmojiStateCodes.usDelaware,
|
||||
'Florida': EmojiStateCodes.usFlorida,
|
||||
'Georgia': EmojiStateCodes.usGeorgia,
|
||||
'Hawaii': EmojiStateCodes.usHawaii,
|
||||
'Idaho': EmojiStateCodes.usIdaho,
|
||||
'Illinois': EmojiStateCodes.usIllinois,
|
||||
'Indiana': EmojiStateCodes.usIndiana,
|
||||
'Iowa': EmojiStateCodes.usIowa,
|
||||
'Kansas': EmojiStateCodes.usKansas,
|
||||
'Kentucky': EmojiStateCodes.usKentucky,
|
||||
'Louisiana': EmojiStateCodes.usLouisiana,
|
||||
'Maine': EmojiStateCodes.usMaine,
|
||||
'Maryland': EmojiStateCodes.usMaryland,
|
||||
'Massachusetts': EmojiStateCodes.usMassachusetts,
|
||||
'Michigan': EmojiStateCodes.usMichigan,
|
||||
'Minnesota': EmojiStateCodes.usMinnesota,
|
||||
'Mississippi': EmojiStateCodes.usMississippi,
|
||||
'Missouri': EmojiStateCodes.usMissouri,
|
||||
'Montana': EmojiStateCodes.usMontana,
|
||||
'Nebraska': EmojiStateCodes.usNebraska,
|
||||
'Nevada': EmojiStateCodes.usNevada,
|
||||
'New Hampshire': EmojiStateCodes.usNewHampshire,
|
||||
'New Jersey': EmojiStateCodes.usNewJersey,
|
||||
'New Mexico': EmojiStateCodes.usNewMexico,
|
||||
'New York': EmojiStateCodes.usNewYork,
|
||||
'North Carolina': EmojiStateCodes.usNorthCarolina,
|
||||
'North Dakota': EmojiStateCodes.usNorthDakota,
|
||||
'Ohio': EmojiStateCodes.usOhio,
|
||||
'Oklahoma': EmojiStateCodes.usOklahoma,
|
||||
'Oregon': EmojiStateCodes.usOregon,
|
||||
'Pennsylvania': EmojiStateCodes.usPennsylvania,
|
||||
'Rhode Island': EmojiStateCodes.usRhodeIsland,
|
||||
'South Carolina': EmojiStateCodes.usSouthCarolina,
|
||||
'South Dakota': EmojiStateCodes.usSouthDakota,
|
||||
'Tennessee': EmojiStateCodes.usTennessee,
|
||||
'Utah': EmojiStateCodes.usUtah,
|
||||
'Vermont': EmojiStateCodes.usVermont,
|
||||
'Virginia': EmojiStateCodes.usVirginia,
|
||||
'Washington': EmojiStateCodes.usWashington,
|
||||
'Washington DC': EmojiStateCodes.usWashingtonDC,
|
||||
'West Virginia': EmojiStateCodes.usWestVirginia,
|
||||
'Wisconsin': EmojiStateCodes.usWisconsin,
|
||||
'Wyoming': EmojiStateCodes.usWyoming,
|
||||
};
|
||||
}
|
|
@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
|
|||
|
||||
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
|
||||
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) {
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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
77
lib/l10n/app_hi.arb
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"welcomeOptional": "वैकल्पिक",
|
||||
"@welcomeOptional": {},
|
||||
"welcomeTermsToggle": "मैं नियमों और शर्तों पर सहमत हुं",
|
||||
"@welcomeTermsToggle": {},
|
||||
"columnCount": "{count, plural, =1{१ कॉलम} other{{count} कॉलम}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"timeSeconds": "{seconds, plural, =1{ १ सेकंड} other{{seconds} सेकंडस}}",
|
||||
"@timeSeconds": {
|
||||
"placeholders": {
|
||||
"seconds": {}
|
||||
}
|
||||
},
|
||||
"timeDays": "{days, plural, =1{ १ दिन} other{{days} दिन}}",
|
||||
"@timeDays": {
|
||||
"placeholders": {
|
||||
"days": {}
|
||||
}
|
||||
},
|
||||
"applyButtonLabel": "लगाऐ",
|
||||
"@applyButtonLabel": {},
|
||||
"nextButtonLabel": "आगे",
|
||||
"@nextButtonLabel": {},
|
||||
"showButtonLabel": "देखे",
|
||||
"@showButtonLabel": {},
|
||||
"hideButtonLabel": "छिपाए",
|
||||
"@hideButtonLabel": {},
|
||||
"continueButtonLabel": "जारी रखे",
|
||||
"@continueButtonLabel": {},
|
||||
"clearTooltip": "मिटाएं",
|
||||
"@clearTooltip": {},
|
||||
"actionRemove": "हटाएं",
|
||||
"@actionRemove": {},
|
||||
"itemCount": "{count, plural, =1{१ चीज} other{{count} चीजे}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteButtonLabel": "डिलीट",
|
||||
"@deleteButtonLabel": {},
|
||||
"timeMinutes": "{minutes, plural, =1{ १ मिनट} other{{minutes} मिनट}}",
|
||||
"@timeMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"focalLength": "{length} एम एम",
|
||||
"@focalLength": {
|
||||
"placeholders": {
|
||||
"length": {
|
||||
"type": "String",
|
||||
"example": "5.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nextTooltip": "आगे",
|
||||
"@nextTooltip": {},
|
||||
"appName": "ऐवीज",
|
||||
"@appName": {},
|
||||
"welcomeMessage": "ऐवीज मे आपका स्वागत है",
|
||||
"@welcomeMessage": {},
|
||||
"previousTooltip": "पिछे",
|
||||
"@previousTooltip": {},
|
||||
"hideTooltip": "छिपाए",
|
||||
"@hideTooltip": {},
|
||||
"cancelTooltip": "कैंसिल",
|
||||
"@cancelTooltip": {},
|
||||
"changeTooltip": "बदलें",
|
||||
"@changeTooltip": {},
|
||||
"showTooltip": "देखें",
|
||||
"@showTooltip": {}
|
||||
}
|
186
lib/l10n/app_hu.arb
Normal file
186
lib/l10n/app_hu.arb
Normal file
|
@ -0,0 +1,186 @@
|
|||
{
|
||||
"applyButtonLabel": "ALKALMAZ",
|
||||
"@applyButtonLabel": {},
|
||||
"deleteButtonLabel": "TÖRLÉS",
|
||||
"@deleteButtonLabel": {},
|
||||
"nextButtonLabel": "KÖVETKEZŐ",
|
||||
"@nextButtonLabel": {},
|
||||
"continueButtonLabel": "FOLYTAT",
|
||||
"@continueButtonLabel": {},
|
||||
"previousTooltip": "Előző",
|
||||
"@previousTooltip": {},
|
||||
"nextTooltip": "Következő",
|
||||
"@nextTooltip": {},
|
||||
"saveTooltip": "Mentés",
|
||||
"@saveTooltip": {},
|
||||
"sourceStateLoading": "Betöltés",
|
||||
"@sourceStateLoading": {},
|
||||
"doNotAskAgain": "Ne kérdezd újra",
|
||||
"@doNotAskAgain": {},
|
||||
"chipActionDelete": "Törlés",
|
||||
"@chipActionDelete": {},
|
||||
"appName": "Aves",
|
||||
"@appName": {},
|
||||
"welcomeMessage": "Üdvözöl az Aves",
|
||||
"@welcomeMessage": {},
|
||||
"cancelTooltip": "Mégse",
|
||||
"@cancelTooltip": {},
|
||||
"chipActionCreateAlbum": "Új album",
|
||||
"@chipActionCreateAlbum": {},
|
||||
"entryActionCopyToClipboard": "Vágolapra másolás",
|
||||
"@entryActionCopyToClipboard": {},
|
||||
"entryActionDelete": "Törlés",
|
||||
"@entryActionDelete": {},
|
||||
"entryActionExport": "Exportálás",
|
||||
"@entryActionExport": {},
|
||||
"entryActionInfo": "Infó",
|
||||
"@entryActionInfo": {},
|
||||
"entryActionShare": "Megosztás",
|
||||
"@entryActionShare": {},
|
||||
"entryActionPrint": "Nyomtatás",
|
||||
"@entryActionPrint": {},
|
||||
"entryActionEdit": "Szerkesztés",
|
||||
"@entryActionEdit": {},
|
||||
"entryActionRotateScreen": "Képernyő forgatása",
|
||||
"@entryActionRotateScreen": {},
|
||||
"entryActionAddFavourite": "Kedvencekhez adás",
|
||||
"@entryActionAddFavourite": {},
|
||||
"videoActionMute": "Némítás",
|
||||
"@videoActionMute": {},
|
||||
"viewerActionSettings": "Beállítások",
|
||||
"@viewerActionSettings": {},
|
||||
"entryInfoActionEditDate": "Dátum és idő szerkesztése",
|
||||
"@entryInfoActionEditDate": {},
|
||||
"filterNoTitleLabel": "Névtelen",
|
||||
"@filterNoTitleLabel": {},
|
||||
"filterOnThisDayLabel": "Ezen a napon",
|
||||
"@filterOnThisDayLabel": {},
|
||||
"filterRecentlyAddedLabel": "Nemrég hozzáadva",
|
||||
"@filterRecentlyAddedLabel": {},
|
||||
"filterTypePanoramaLabel": "Panoráma",
|
||||
"@filterTypePanoramaLabel": {},
|
||||
"filterMimeVideoLabel": "Videó",
|
||||
"@filterMimeVideoLabel": {},
|
||||
"albumTierNew": "Új",
|
||||
"@albumTierNew": {},
|
||||
"themeBrightnessDark": "Sötét",
|
||||
"@themeBrightnessDark": {},
|
||||
"vaultLockTypePassword": "Jelszó",
|
||||
"@vaultLockTypePassword": {},
|
||||
"videoControlsPlay": "Lejátszás",
|
||||
"@videoControlsPlay": {},
|
||||
"videoControlsNone": "Nincs",
|
||||
"@videoControlsNone": {},
|
||||
"videoLoopModeAlways": "Mindig",
|
||||
"@videoLoopModeAlways": {},
|
||||
"viewerTransitionNone": "Nincs",
|
||||
"@viewerTransitionNone": {},
|
||||
"storageVolumeDescriptionFallbackPrimary": "Belső tárhely",
|
||||
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||
"storageVolumeDescriptionFallbackNonPrimary": "SD kártya",
|
||||
"@storageVolumeDescriptionFallbackNonPrimary": {},
|
||||
"newAlbumDialogTitle": "Új album",
|
||||
"@newAlbumDialogTitle": {},
|
||||
"newAlbumDialogNameLabel": "Album neve",
|
||||
"@newAlbumDialogNameLabel": {},
|
||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "A mappa már létezik",
|
||||
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||
"newAlbumDialogStorageLabel": "Tárhely:",
|
||||
"@newAlbumDialogStorageLabel": {},
|
||||
"renameAlbumDialogLabel": "Új név",
|
||||
"@renameAlbumDialogLabel": {},
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "A mappa már létezik",
|
||||
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||
"renameEntrySetPageTitle": "Átnevezés",
|
||||
"@renameEntrySetPageTitle": {},
|
||||
"renameProcessorName": "Név",
|
||||
"@renameProcessorName": {},
|
||||
"renameEntryDialogLabel": "Új név",
|
||||
"@renameEntryDialogLabel": {},
|
||||
"editEntryDateDialogTitle": "Dátum és idő",
|
||||
"@editEntryDateDialogTitle": {},
|
||||
"videoStreamSelectionDialogText": "Feliratok",
|
||||
"@videoStreamSelectionDialogText": {},
|
||||
"videoStreamSelectionDialogOff": "Ki",
|
||||
"@videoStreamSelectionDialogOff": {},
|
||||
"genericSuccessFeedback": "Kész!",
|
||||
"@genericSuccessFeedback": {},
|
||||
"genericFailureFeedback": "Sikertelen",
|
||||
"@genericFailureFeedback": {},
|
||||
"genericDangerWarningDialogMessage": "Biztos benne?",
|
||||
"@genericDangerWarningDialogMessage": {},
|
||||
"menuActionSlideshow": "Diavetités",
|
||||
"@menuActionSlideshow": {},
|
||||
"coverDialogTabCover": "Borító",
|
||||
"@coverDialogTabCover": {},
|
||||
"appPickDialogNone": "Nincs",
|
||||
"@appPickDialogNone": {},
|
||||
"aboutPageTitle": "Névjegy",
|
||||
"@aboutPageTitle": {},
|
||||
"aboutLinkLicense": "Licensz",
|
||||
"@aboutLinkLicense": {},
|
||||
"aboutBugSectionTitle": "Hiba jelentés",
|
||||
"@aboutBugSectionTitle": {},
|
||||
"aboutBugCopyInfoButton": "Másolás",
|
||||
"@aboutBugCopyInfoButton": {},
|
||||
"aboutTranslatorsSectionTitle": "Fordítók",
|
||||
"@aboutTranslatorsSectionTitle": {},
|
||||
"collectionActionEdit": "Szerkesztés",
|
||||
"@collectionActionEdit": {},
|
||||
"dateToday": "Ma",
|
||||
"@dateToday": {},
|
||||
"dateThisMonth": "Ebben a hónapban",
|
||||
"@dateThisMonth": {},
|
||||
"drawerAboutButton": "Névjegy",
|
||||
"@drawerAboutButton": {},
|
||||
"drawerSettingsButton": "Beállítások",
|
||||
"@drawerSettingsButton": {},
|
||||
"drawerCollectionFavourites": "Kedvencek",
|
||||
"@drawerCollectionFavourites": {},
|
||||
"drawerCollectionImages": "Képek",
|
||||
"@drawerCollectionImages": {},
|
||||
"drawerCollectionVideos": "Videók",
|
||||
"@drawerCollectionVideos": {},
|
||||
"drawerCollectionPanoramas": "Panorámák",
|
||||
"@drawerCollectionPanoramas": {},
|
||||
"albumDownload": "Letöltés",
|
||||
"@albumDownload": {},
|
||||
"albumScreenshots": "Képernyő képek",
|
||||
"@albumScreenshots": {},
|
||||
"albumPageTitle": "Albumok",
|
||||
"@albumPageTitle": {},
|
||||
"newFilterBanner": "új",
|
||||
"@newFilterBanner": {},
|
||||
"chipActionRename": "Átnevez",
|
||||
"@chipActionRename": {},
|
||||
"entryActionRename": "Átnevezés",
|
||||
"@entryActionRename": {},
|
||||
"keepScreenOnNever": "Soha",
|
||||
"@keepScreenOnNever": {},
|
||||
"videoLoopModeNever": "Soha",
|
||||
"@videoLoopModeNever": {},
|
||||
"videoActionPlay": "Lejátszás",
|
||||
"@videoActionPlay": {},
|
||||
"entryInfoActionRemoveMetadata": "Metaadat eltávolítása",
|
||||
"@entryInfoActionRemoveMetadata": {},
|
||||
"albumTierRegular": "Egyebek",
|
||||
"@albumTierRegular": {},
|
||||
"keepScreenOnAlways": "Mindig",
|
||||
"@keepScreenOnAlways": {},
|
||||
"nameConflictStrategyRename": "Átnevezés",
|
||||
"@nameConflictStrategyRename": {},
|
||||
"themeBrightnessBlack": "Fekete",
|
||||
"@themeBrightnessBlack": {},
|
||||
"menuActionMap": "Térkép",
|
||||
"@menuActionMap": {},
|
||||
"collectionPageTitle": "Gyűjtemény",
|
||||
"@collectionPageTitle": {},
|
||||
"sectionUnknown": "Ismeretlen",
|
||||
"@sectionUnknown": {},
|
||||
"dateYesterday": "Tegnap",
|
||||
"@dateYesterday": {},
|
||||
"drawerAlbumPage": "Albumok",
|
||||
"@drawerAlbumPage": {},
|
||||
"albumCamera": "Kamera",
|
||||
"@albumCamera": {}
|
||||
}
|
|
@ -1274,5 +1274,25 @@
|
|||
"settingsVideoBackgroundMode": "Mode latar belakang",
|
||||
"@settingsVideoBackgroundMode": {},
|
||||
"settingsVideoBackgroundModeDialogTitle": "Mode Latar Belakang",
|
||||
"@settingsVideoBackgroundModeDialogTitle": {}
|
||||
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||
"settingsCollectionBurstPatternsTile": "Pola semburan",
|
||||
"@settingsCollectionBurstPatternsTile": {},
|
||||
"settingsCollectionBurstPatternsNone": "Tidak ada",
|
||||
"@settingsCollectionBurstPatternsNone": {},
|
||||
"chipActionShowCountryStates": "Tampilkan wilayah",
|
||||
"@chipActionShowCountryStates": {},
|
||||
"viewerActionUnlock": "Buka kunci penampil",
|
||||
"@viewerActionUnlock": {},
|
||||
"statePageTitle": "Wilayah",
|
||||
"@statePageTitle": {},
|
||||
"stateEmpty": "Tidak ada wilayah",
|
||||
"@stateEmpty": {},
|
||||
"tagPlaceholderState": "Wilayah",
|
||||
"@tagPlaceholderState": {},
|
||||
"viewerActionLock": "Kunci penampil",
|
||||
"@viewerActionLock": {},
|
||||
"searchStatesSectionTitle": "Wilayah",
|
||||
"@searchStatesSectionTitle": {},
|
||||
"statsTopStatesSectionTitle": "Wilayah Teratas",
|
||||
"@statsTopStatesSectionTitle": {}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"@sourceStateLocatingPlaces": {},
|
||||
"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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1192,5 +1192,13 @@
|
|||
"filterNoAddressLabel": "无地址",
|
||||
"@filterNoAddressLabel": {},
|
||||
"settingsViewerShowRatingTags": "显示评分和标签",
|
||||
"@settingsViewerShowRatingTags": {}
|
||||
"@settingsViewerShowRatingTags": {},
|
||||
"chipActionLock": "锁定",
|
||||
"@chipActionLock": {},
|
||||
"chipActionConfigureVault": "配置保险库",
|
||||
"@chipActionConfigureVault": {},
|
||||
"chipActionCreateVault": "创建保险库",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionShowCountryStates": "显示状态",
|
||||
"@chipActionShowCountryStates": {}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
enum MoveType { copy, move, export, toBin, fromBin }
|
64
lib/model/app/contributors.dart
Normal file
64
lib/model/app/contributors.dart
Normal file
|
@ -0,0 +1,64 @@
|
|||
class Contributors {
|
||||
static const translators = {
|
||||
Contributor('D3ZOXY', 'its.ghost.message@gmail.com'),
|
||||
Contributor('JanWaldhorn', 'weblate@jwh.anonaddy.com'),
|
||||
Contributor('n-berenice', null),
|
||||
Contributor('Jonatas de Almeida Barros', 'ajonatas56@gmail.com'),
|
||||
Contributor('MeFinity', 'me.dot.finity@gmail.com'),
|
||||
Contributor('Maki', null),
|
||||
Contributor('HiSubway', 'shenyusoftware@gmail.com'),
|
||||
Contributor('glemco', 'glemco@posteo.net'),
|
||||
Contributor('Aerowolf', null),
|
||||
Contributor('小默', 'duzhe163908@gmail.com'),
|
||||
Contributor('metezd', 'itoldyouthat@protonmail.com'),
|
||||
Contributor('Martijn Fabrie', null),
|
||||
Contributor('Koen Koppens', 'koenkoppens@proton.me'),
|
||||
Contributor('Emmanouil Papavergis', null),
|
||||
Contributor('kha84', 'khalukhin@gmail.com'),
|
||||
Contributor('gallegonovato', 'fran-carro@hotmail.es'),
|
||||
Contributor('Havokdan', 'havokdan@yahoo.com.br'),
|
||||
Contributor('Jean Mareilles', 'waged1266@tutanota.com'),
|
||||
Contributor('이정희', 'daemul72@gmail.com'),
|
||||
Contributor('Translator-3000', 'weblate.m1d0h@8shield.net'),
|
||||
Contributor('Ralea Adrian Vicențiu', 'ralea.adrian@gmail.com'),
|
||||
Contributor('Igor Sorocean', 'sorocean.igor@gmail.com'),
|
||||
Contributor('JY3', 'GeeyunJY3@gmail.com'),
|
||||
Contributor('Gediminas Murauskas', 'muziejusinfo@gmail.com'),
|
||||
Contributor('Oğuz Ersen', 'oguz@ersen.moe'),
|
||||
Contributor('Allan Nordhøy', 'epost@anotheragency.no'),
|
||||
Contributor('pemibe', 'pemibe4634@dmonies.com'),
|
||||
Contributor('Linerly', 'linerly@protonmail.com'),
|
||||
Contributor('Skrripy', 'rozihrash.ya6w7@simplelogin.com'),
|
||||
Contributor('vesp', 'vesp@post.cz'),
|
||||
Contributor('Dan', 'denqwerta@gmail.com'),
|
||||
Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'),
|
||||
Contributor('Piotr K', '1337.kelt@gmail.com'),
|
||||
Contributor('rehork', 'cooky@e.email'),
|
||||
Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'),
|
||||
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
|
||||
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
|
||||
Contributor('kaajjo', 'claymanoff@gmail.com'),
|
||||
Contributor('Eduardo Malaspina', 'vaio0@swismail.com'),
|
||||
Contributor('Evgeniy Khramov', 'thejenjagamertjg@gmail.com'),
|
||||
Contributor('syu_pf_ssy', 'syu.pf.ssy@outlook.com'),
|
||||
Contributor('Dick Pluim', 'github@dickpluim.com'),
|
||||
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
|
||||
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
|
||||
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
|
||||
// Contributor('slasb37', 'p84haghi@gmail.com'), // Persian
|
||||
// Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk
|
||||
// Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
|
||||
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
||||
// Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak
|
||||
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
|
||||
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
||||
// Contributor('György Viktor', 'wickdj@gmail.com'), // Hungarian
|
||||
};
|
||||
}
|
||||
|
||||
class Contributor {
|
||||
final String name;
|
||||
final String? weblateEmail;
|
||||
|
||||
const Contributor(this.name, this.weblateEmail);
|
||||
}
|
|
@ -112,12 +112,6 @@ class Dependencies {
|
|||
license: mit,
|
||||
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,
|
16
lib/model/app/permissions.dart
Normal file
16
lib/model/app/permissions.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class Permissions {
|
||||
static const storage = [
|
||||
Permission.storage,
|
||||
// for media access on Android >=13
|
||||
Permission.photos,
|
||||
Permission.videos,
|
||||
];
|
||||
|
||||
static const mediaAccess = [
|
||||
...storage,
|
||||
// to access media with unredacted metadata with scoped storage (Android >=10)
|
||||
Permission.accessMediaLocation,
|
||||
];
|
||||
}
|
96
lib/model/app/support.dart
Normal file
96
lib/model/app/support.dart
Normal file
|
@ -0,0 +1,96 @@
|
|||
import 'package:aves/ref/mime_types.dart';
|
||||
|
||||
class AppSupport {
|
||||
// TODO TLAD [codec] make it dynamic if it depends on OS/lib versions
|
||||
static const Set<String> undecodableImages = {
|
||||
MimeTypes.art,
|
||||
MimeTypes.cdr,
|
||||
MimeTypes.crw,
|
||||
MimeTypes.djvu,
|
||||
MimeTypes.jpeg2000,
|
||||
MimeTypes.jxl,
|
||||
MimeTypes.pat,
|
||||
MimeTypes.pcx,
|
||||
MimeTypes.pnm,
|
||||
MimeTypes.psdVnd,
|
||||
MimeTypes.psdX,
|
||||
MimeTypes.octetStream,
|
||||
MimeTypes.zip,
|
||||
};
|
||||
|
||||
static bool canDecode(String mimeType) => !undecodableImages.contains(mimeType);
|
||||
|
||||
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
|
||||
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
|
||||
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
|
||||
static bool _supportedByBitmapRegionDecoder(String mimeType) => [
|
||||
MimeTypes.heic,
|
||||
MimeTypes.heif,
|
||||
MimeTypes.jpeg,
|
||||
MimeTypes.png,
|
||||
MimeTypes.webp,
|
||||
MimeTypes.arw,
|
||||
MimeTypes.cr2,
|
||||
MimeTypes.nef,
|
||||
MimeTypes.nrw,
|
||||
MimeTypes.orf,
|
||||
MimeTypes.pef,
|
||||
MimeTypes.raf,
|
||||
MimeTypes.rw2,
|
||||
MimeTypes.srw,
|
||||
].contains(mimeType);
|
||||
|
||||
static bool canDecodeRegion(String mimeType) => _supportedByBitmapRegionDecoder(mimeType) || mimeType == MimeTypes.tiff;
|
||||
|
||||
// `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes,
|
||||
// and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files.
|
||||
static bool canEditExif(String mimeType) {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
// as of androidx.exifinterface:exifinterface:1.3.4
|
||||
case MimeTypes.jpeg:
|
||||
case MimeTypes.png:
|
||||
case MimeTypes.webp:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool canEditIptc(String mimeType) {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
// as of latest PixyMeta
|
||||
case MimeTypes.jpeg:
|
||||
case MimeTypes.tiff:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool canEditXmp(String mimeType) {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
// as of latest PixyMeta
|
||||
case MimeTypes.gif:
|
||||
case MimeTypes.jpeg:
|
||||
case MimeTypes.png:
|
||||
case MimeTypes.tiff:
|
||||
return true;
|
||||
// using `mp4parser`
|
||||
case MimeTypes.mp4:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool canRemoveMetadata(String mimeType) {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
// as of latest PixyMeta
|
||||
case MimeTypes.jpeg:
|
||||
case MimeTypes.tiff:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
78
lib/model/apps.dart
Normal file
78
lib/model/apps.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
final AppInventory appInventory = AppInventory._private();
|
||||
|
||||
class AppInventory {
|
||||
Set<Package> _packages = {};
|
||||
List<String> _potentialAppDirs = [];
|
||||
|
||||
ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false);
|
||||
|
||||
Iterable<Package> get _launcherPackages => _packages.where((v) => v.categoryLauncher);
|
||||
|
||||
AppInventory._private();
|
||||
|
||||
Future<void> initAppNames() async {
|
||||
if (_packages.isEmpty) {
|
||||
debugPrint('Access installed app inventory');
|
||||
_packages = await appService.getPackages();
|
||||
_potentialAppDirs = _launcherPackages.expand((v) => v.potentialDirs).toList();
|
||||
areAppNamesReadyNotifier.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetAppNames() async {
|
||||
_packages.clear();
|
||||
_potentialAppDirs.clear();
|
||||
areAppNamesReadyNotifier.value = false;
|
||||
}
|
||||
|
||||
bool isPotentialAppDir(String dir) => _potentialAppDirs.contains(dir);
|
||||
|
||||
String? getAlbumAppPackageName(String albumPath) {
|
||||
final dir = pContext.split(albumPath).last;
|
||||
final package = _launcherPackages.firstWhereOrNull((v) => v.potentialDirs.contains(dir));
|
||||
return package?.packageName;
|
||||
}
|
||||
|
||||
String? getCurrentAppName(String packageName) {
|
||||
final package = _packages.firstWhereOrNull((v) => v.packageName == packageName);
|
||||
return package?.currentLabel;
|
||||
}
|
||||
}
|
||||
|
||||
class Package {
|
||||
final String packageName;
|
||||
final String? currentLabel, englishLabel;
|
||||
final bool categoryLauncher, isSystem;
|
||||
final Set<String> ownedDirs = {};
|
||||
|
||||
Package({
|
||||
required this.packageName,
|
||||
required this.currentLabel,
|
||||
required this.englishLabel,
|
||||
required this.categoryLauncher,
|
||||
required this.isSystem,
|
||||
});
|
||||
|
||||
factory Package.fromMap(Map map) {
|
||||
return Package(
|
||||
packageName: map['packageName'] ?? '',
|
||||
currentLabel: map['currentLabel'],
|
||||
englishLabel: map['englishLabel'],
|
||||
categoryLauncher: map['categoryLauncher'] ?? false,
|
||||
isSystem: map['isSystem'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Set<String> get potentialDirs => [
|
||||
currentLabel,
|
||||
englishLabel,
|
||||
...ownedDirs,
|
||||
].whereNotNull().toSet();
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}';
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import '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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue