Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-10-30 23:14:23 +01:00
commit 530bd1a8b0
273 changed files with 3847 additions and 1620 deletions

@ -1 +1 @@
Subproject commit 2663184aa79047d0a33a14a3b607954f8fdd8730
Subproject commit 603104015dd692ea3403755b55d07813d5cf8965

View file

@ -22,6 +22,6 @@ jobs:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0

View file

@ -23,7 +23,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get Flutter packages
run: scripts/pub_get_all.sh
@ -59,17 +59,17 @@ jobs:
# Building relies on the Android Gradle plugin,
# which requires a modern Java version (not the default one).
- name: Set up JDK for Android Gradle plugin
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: 'temurin'
java-version: '21'
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@ -83,6 +83,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
category: "/language:${{matrix.language}}"

View file

@ -13,7 +13,9 @@ jobs:
name: GitHub release
runs-on: ubuntu-latest
permissions:
attestations: write
contents: write
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
@ -23,13 +25,13 @@ jobs:
# Building relies on the Android Gradle plugin,
# which requires a modern Java version (not the default one).
- name: Set up JDK for Android Gradle plugin
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: 'temurin'
java-version: '21'
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get Flutter packages
run: scripts/pub_get_all.sh
@ -72,6 +74,11 @@ jobs:
AVES_KEY_PASSWORD: ${{ secrets.AVES_KEY_PASSWORD }}
AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
with:
subject-path: 'outputs/*'
- name: Create GitHub release
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
with:
@ -96,7 +103,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get appbundle from artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8

View file

@ -36,7 +36,7 @@ jobs:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
sarif_file: results.sarif

View file

@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.17"></a>[v1.11.17] - 2024-10-30
### Added
- Map: create shortcut to custom region and filters
- Video: frame stepping forward/backward
- Video: custom playback buttons
- English (Shavian) translation (thanks Paranoid Android)
### Changed
- upgraded Flutter to stable v3.24.4
### Fixed
- crash when loading large collection
- Viewer: copying content URI item
- Albums: creating album with same name as existing empty directory
- Privacy: tagging while vaults are unlocked does not yield recent tags visible when vaults are locked
## <a id="v1.11.16"></a>[v1.11.16] - 2024-10-10
### Fixed

View file

@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'com.android.application'
id 'com.google.devtools.ksp'
@ -30,16 +28,15 @@ if (keystorePropertiesFile.exists()) {
keystoreProperties["googleApiKey"] = System.getenv("AVES_GOOGLE_API_KEY") ?: "<NONE>"
}
kotlin {
jvmToolchain 17
}
android {
namespace 'deckers.thibault.aves'
compileSdk 35
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
ndkVersion '26.1.10909125'
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
ndkVersion '27.0.12077973'
defaultConfig {
applicationId packageName
@ -133,15 +130,6 @@ android {
}
}
tasks.withType(KotlinCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
jvmToolchain(21)
}
flutter {
source '../..'
}

View file

@ -332,6 +332,9 @@
<!--
Impeller is not supported by `media_kit` v1.1.10+1:
https://github.com/media-kit/media-kit/issues/707
Screenshot driver scenario is not supported by Impeller:
"Compressed screenshots not supported for Impeller"
-->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"

View file

@ -56,6 +56,7 @@ import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.channel.streams.SettingsChangeStreamHandler
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.anyCauseIs
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
@ -314,6 +315,7 @@ open class MainActivity : FlutterFragmentActivity() {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW_GEO,
INTENT_DATA_KEY_URI to uri.toString(),
INTENT_DATA_KEY_FILTERS to extractFiltersFromIntent(intent),
)
}
@ -370,7 +372,8 @@ open class MainActivity : FlutterFragmentActivity() {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS,
INTENT_DATA_KEY_MIME_TYPE to intent.type,
INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false),
INTENT_DATA_KEY_MIME_TYPES to intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)?.toList(),
INTENT_DATA_KEY_ALLOW_MULTIPLE to intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false),
)
}
@ -432,33 +435,50 @@ open class MainActivity : FlutterFragmentActivity() {
open fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
val pickedUris = call.argument<List<String>>("uris")
try {
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this@MainActivity, Uri.parse(uriString)) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {
data = firstUri
} else {
clipData = ClipData.newUri(contentResolver, null, firstUri).apply {
pickedUris.drop(1).forEach {
addItem(ClipData.Item(toUri(it)))
}
}
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
setResult(RESULT_OK, intent)
} else {
setResult(RESULT_CANCELED)
}
if (pickedUris.isNullOrEmpty()) {
setResult(RESULT_CANCELED)
// move code triggering `Binder` call off the main thread
defaultScope.launch { finish() }
} catch (e: Exception) {
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) {
result.error("submitPickedItems-large", "transaction too large with ${pickedUris?.size} URIs", e)
return
}
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this@MainActivity, Uri.parse(uriString)) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {
data = firstUri
} else {
result.error("submitPickedItems-exception", "failed to pick ${pickedUris?.size} URIs", e)
clipData = ClipData.newUri(contentResolver, null, firstUri).apply {
pickedUris.drop(1).forEach {
addItem(ClipData.Item(toUri(it)))
}
}
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
// move code triggering `Binder` call off the main thread
defaultScope.launch {
submitPickedItemsIntent(intent, result)
}
}
private fun submitPickedItemsIntent(intent: Intent, result: MethodChannel.Result) {
try {
setResult(RESULT_OK, intent)
finish()
} catch (e: Exception) {
setResult(RESULT_CANCELED)
if (e is SecurityException && intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) {
// in some environments, providing the write flag yields a `SecurityException`:
// "UID XXXX does not have permission to content://XXXX"
// so we retry without it
Log.i(LOG_TAG, "retry submitting picked items without FLAG_GRANT_WRITE_URI_PERMISSION")
intent.flags = intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
submitPickedItemsIntent(intent, result)
} else if (e.anyCauseIs<TransactionTooLargeException>()) {
result.error("submitPickedItems-large", "transaction too large with ${intent.clipData?.itemCount} URIs", e)
} else {
result.error("submitPickedItems-exception", "failed to pick ${intent.clipData?.itemCount} URIs", e)
}
}
}
@ -552,6 +572,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_EXPLORER_PATH = "explorerPath"
const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_MIME_TYPES = "mimeTypes"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_SECURE_URIS = "secureUris"
@ -566,6 +587,8 @@ open class MainActivity : FlutterFragmentActivity() {
// dart page routes
const val COLLECTION_PAGE_ROUTE_NAME = "/collection"
const val ENTRY_VIEWER_PAGE_ROUTE_NAME = "/viewer"
const val EXPLORER_PAGE_ROUTE_NAME = "/explorer"
const val MAP_PAGE_ROUTE_NAME = "/map"
const val SEARCH_PAGE_ROUTE_NAME = "/search"

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls
import android.app.ActivityManager
import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
@ -51,6 +52,17 @@ class AnalysisHandler(private val activity: FlutterFragmentActivity, private val
return
}
val activityManager: ActivityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningAppProcesses = activityManager.runningAppProcesses
if (runningAppProcesses != null) {
val importance = runningAppProcesses[0].importance
if (importance < ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
// the app is in the background
result.error("startAnalysis-background", "app is in the background (process importance=$importance)", null)
return
}
}
// can be null or empty
val allEntryIds = call.argument<List<Int>>("entryIds")

View file

@ -23,11 +23,15 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.COLLECTION_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.ENTRY_VIEWER_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.EXPLORER_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR
import deckers.thibault.aves.MainActivity.Companion.MAP_PAGE_ROUTE_NAME
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@ -35,6 +39,7 @@ import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.anyCauseIs
import deckers.thibault.aves.utils.getApplicationInfoCompat
import deckers.thibault.aves.utils.queryIntentActivitiesCompat
import io.flutter.plugin.common.MethodCall
@ -303,7 +308,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val started = safeStartActivityChooser(title, intent)
result.success(started)
} catch (e: Exception) {
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) {
if (e.anyCauseIs<TransactionTooLargeException>()) {
result.error("share-large", "transaction too large with ${uriList.size} URIs", e)
} else {
result.error("share-exception", "failed to share ${uriList.size} URIs", e)
@ -354,12 +359,17 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// shortcuts
private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) {
// common arguments
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val route = call.argument<String>("route")
// route dependent arguments
val filters = call.argument<List<String>>("filters")
val explorerPath = call.argument<String>("explorerPath")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (label == null) {
val explorerPath = call.argument<String>("path")
val viewUri = call.argument<String>("viewUri")?.let { Uri.parse(it) }
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
if (label == null || route == null) {
result.error("pin-args", "missing arguments", null)
return
}
@ -383,24 +393,60 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection)
val resId = when (route) {
MAP_PAGE_ROUTE_NAME -> if (supportAdaptiveIcon) R.mipmap.ic_shortcut_map else R.drawable.ic_shortcut_map
else -> if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection
}
icon = IconCompat.createWithResource(context, resId)
}
val intent = when {
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
val intent: Intent = when (route) {
COLLECTION_PAGE_ROUTE_NAME -> {
if (filters == null) {
result.error("pin-filters", "collection shortcut requires filters", null)
return
}
Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, route)
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
}
explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/explorer")
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
ENTRY_VIEWER_PAGE_ROUTE_NAME -> {
if (viewUri == null) {
result.error("pin-viewUri", "viewer shortcut requires URI", null)
return
}
Intent(Intent.ACTION_VIEW, viewUri, context, MainActivity::class.java)
}
EXPLORER_PAGE_ROUTE_NAME -> {
Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, route)
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
}
MAP_PAGE_ROUTE_NAME -> {
if (geoUri == null) {
result.error("pin-geoUri", "map shortcut requires URI", null)
return
}
Intent(Intent.ACTION_VIEW, geoUri, context, MainActivity::class.java).apply {
putExtra(EXTRA_KEY_PAGE, route)
// filters are optional
filters?.let {
putExtra(EXTRA_KEY_FILTERS_ARRAY, it.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
putExtra(EXTRA_KEY_FILTERS_STRING, it.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
}
}
}
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
else -> {
result.error("pin-intent", "failed to build intent", null)
result.error("pin-route", "unsupported shortcut route=$route", null)
return
}
}

View file

@ -16,6 +16,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.math.roundToInt
class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
@ -44,7 +45,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
val defaultSizeDip = call.argument<Number>("defaultSizeDip")?.toDouble()
val quality = call.argument<Int>("quality")
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null || quality == null) {
if (uri == null || mimeType == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null || quality == null) {
result.error("getThumbnail-args", "missing arguments", null)
return
}
@ -54,7 +55,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
context = context,
uri = uri,
mimeType = mimeType,
dateModifiedSecs = dateModifiedSecs,
dateModifiedSecs = dateModifiedSecs ?: (Date().time / 1000),
rotationDegrees = rotationDegrees,
isFlipped = isFlipped,
width = (widthDip * density).roundToInt(),

View file

@ -29,6 +29,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
when (call.method) {
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
"getCacheDirectory" -> ioScope.launch { safe(call, result, ::getCacheDirectory) }
"getUntrackedTrashPaths" -> ioScope.launch { safe(call, result, ::getUntrackedTrashPaths) }
"getUntrackedVaultPaths" -> ioScope.launch { safe(call, result, ::getUntrackedVaultPaths) }
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
@ -122,6 +123,18 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(volumes)
}
private fun getCacheDirectory(call: MethodCall, result: MethodChannel.Result) {
val external = call.argument<Boolean>("external")
if (external == null) {
result.error("getCacheDirectory-args", "missing arguments", null)
return
}
val dir = (if (external) context.externalCacheDir else context.cacheDir)
result.success(dir!!.path)
}
private fun getUntrackedTrashPaths(call: MethodCall, result: MethodChannel.Result) {
val knownPaths = call.argument<List<String>>("knownPaths")
if (knownPaths == null) {

View file

@ -137,8 +137,7 @@ abstract class ImageProvider {
"success" to false,
)
// prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store
if (sourcePath != null && !desiredName.startsWith('.')) {
if (sourcePath != null) {
try {
var newFields: FieldMap = skippedFieldMap
if (!isCancelledOp()) {
@ -570,6 +569,20 @@ abstract class ImageProvider {
}
}
fun createTimeStampFileName() = Date().time.toString()
private fun sanitizeDesiredFileName(desiredName: String): String {
var name = desiredName
// prevent creating hidden files
while (name.isNotEmpty() && name.startsWith(".")) {
name = name.substring(1)
}
if (name.isEmpty()) {
name = createTimeStampFileName()
}
return name
}
// returns available name to use, or `null` to skip it
suspend fun resolveTargetFileNameWithoutExtension(
contextWrapper: ContextWrapper,
@ -578,18 +591,19 @@ abstract class ImageProvider {
mimeType: String,
conflictStrategy: NameConflictStrategy,
): NameConflictResolution {
var resolvedName: String? = desiredNameWithoutExtension
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
var resolvedName: String? = sanitizedNameWithoutExtension
var replacementFile: File? = null
val extension = extensionFor(mimeType)
val targetFile = File(dir, "$desiredNameWithoutExtension$extension")
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) {
NameConflictStrategy.RENAME -> {
var nameWithoutExtension = desiredNameWithoutExtension
var nameWithoutExtension = sanitizedNameWithoutExtension
var i = 0
while (File(dir, "$nameWithoutExtension$extension").exists()) {
i++
nameWithoutExtension = "$desiredNameWithoutExtension ($i)"
nameWithoutExtension = "$sanitizedNameWithoutExtension ($i)"
}
resolvedName = nameWithoutExtension
}

View file

@ -40,6 +40,7 @@ import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.io.SyncFailedException
import java.util.Date
import java.util.Locale
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation
@ -478,64 +479,62 @@ class MediaStoreImageProvider : ImageProvider() {
"success" to false,
)
if (sourcePath != null) {
// on API 30 we cannot get access granted directly to a volume root from its document tree,
// but it is still less constraining to use tree document files than to rely on the Media Store
//
// Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but:
// - we need to scan the file to get the Media Store content URI
// - the underlying document provider controls the new file name
//
// Relying on the Media Store, we can create an item via `ContentResolver.insert()`
// with a path, and retrieve its content URI, but:
// - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`)
// - the volume name should be lower case, not exactly as the `StorageVolume` UUID
// cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()`
// - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?)
// - there is no documentation regarding support for usage with removable storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try {
val appDir = when {
toBin -> StorageUtils.trashDirFor(activity, sourcePath)
toVault -> File(targetDir)
else -> null
}
if (appDir != null) {
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
if (toVault) {
appDir.mkdirs()
}
}
if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = File(sourcePath)
if (!sourceFile.exists() && toBin) {
delete(activity, sourceUri, sourcePath, mimeType = mimeType)
deletedFieldMap
} else {
moveSingle(
activity = activity,
sourceFile = sourceFile,
sourceUri = sourceUri,
targetDir = effectiveTargetDir,
targetDirDocFile = targetDirDocFile,
desiredName = desiredName ?: sourceFile.name,
nameConflictStrategy = nameConflictStrategy,
mimeType = mimeType,
copy = copy,
toBin = toBin,
)
}
}
result["newFields"] = newFields
result["success"] = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
// on API 30 we cannot get access granted directly to a volume root from its document tree,
// but it is still less constraining to use tree document files than to rely on the Media Store
//
// Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but:
// - we need to scan the file to get the Media Store content URI
// - the underlying document provider controls the new file name
//
// Relying on the Media Store, we can create an item via `ContentResolver.insert()`
// with a path, and retrieve its content URI, but:
// - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`)
// - the volume name should be lower case, not exactly as the `StorageVolume` UUID
// cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()`
// - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?)
// - there is no documentation regarding support for usage with removable storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try {
val appDir = when {
toBin -> StorageUtils.trashDirFor(activity, sourcePath ?: StorageUtils.getPrimaryVolumePath(activity))
toVault -> File(targetDir)
else -> null
}
if (appDir != null) {
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
if (toVault) {
appDir.mkdirs()
}
}
if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = if (sourcePath != null) File(sourcePath) else null
if (sourceFile != null && !sourceFile.exists() && toBin) {
delete(activity, sourceUri, sourcePath, mimeType = mimeType)
deletedFieldMap
} else {
moveSingle(
activity = activity,
sourceFile = sourceFile,
sourceUri = sourceUri,
targetDir = effectiveTargetDir,
targetDirDocFile = targetDirDocFile,
desiredName = desiredName ?: sourceFile?.name ?: sourceUri.lastPathSegment ?: createTimeStampFileName(),
nameConflictStrategy = nameConflictStrategy,
mimeType = mimeType,
copy = copy,
toBin = toBin,
)
}
}
result["newFields"] = newFields
result["success"] = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
}
callback.onSuccess(result)
}
@ -544,7 +543,7 @@ class MediaStoreImageProvider : ImageProvider() {
private suspend fun moveSingle(
activity: Activity,
sourceFile: File,
sourceFile: File?,
sourceUri: Uri,
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
@ -554,8 +553,8 @@ class MediaStoreImageProvider : ImageProvider() {
copy: Boolean,
toBin: Boolean,
): FieldMap {
val sourcePath = sourceFile.path
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
val sourcePath = sourceFile?.path
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy
return skippedFieldMap

View file

@ -0,0 +1,10 @@
package deckers.thibault.aves.utils
inline fun <reified T : Throwable> Exception.anyCauseIs(): Boolean {
var cause: Throwable? = this
while (cause != null) {
if (cause is T) return true
cause = cause.cause
}
return false
}

View file

@ -8,4 +8,5 @@
<string name="analysis_notification_default_title">يتم فحص الوسائط</string>
<string name="analysis_notification_action_stop">إيقاف</string>
<string name="app_name">Aves</string>
<string name="map_shortcut_short_label">خريطة</string>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">𐑱𐑝𐑰𐑟</string>
<string name="app_widget_label">𐑓𐑴𐑑𐑴 𐑓𐑮𐑱𐑥</string>
<string name="wallpaper">𐑢𐑷𐑤𐑐𐑱𐑐𐑼</string>
<string name="map_shortcut_short_label">𐑥𐑨𐑐</string>
<string name="search_shortcut_short_label">𐑕𐑻𐑗</string>
<string name="videos_shortcut_short_label">𐑝𐑦𐑛𐑦𐑴𐑟</string>
<string name="analysis_channel_name">𐑥𐑰𐑛𐑾 𐑕𐑒𐑨𐑯</string>
<string name="analysis_notification_default_title">𐑕𐑒𐑨𐑯𐑦𐑙 𐑥𐑰𐑛𐑾</string>
<string name="analysis_notification_action_stop">𐑕𐑑𐑪𐑐</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">Analyse von Medien</string>
<string name="analysis_notification_default_title">Medien scannen</string>
<string name="analysis_notification_action_stop">Abbrechen</string>
<string name="map_shortcut_short_label">Karte</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="app_name">Aves</string>
<string name="analysis_channel_name">Median skannaus</string>
<string name="search_shortcut_short_label">Hae</string>
<string name="map_shortcut_short_label">Kartta</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">Digitalização de mídia</string>
<string name="analysis_notification_default_title">Digitalizando mídia</string>
<string name="analysis_notification_action_stop">Pare</string>
<string name="map_shortcut_short_label">Mapa</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_notification_action_stop">Zastaviť</string>
<string name="analysis_channel_name">Skenovanie médií</string>
<string name="analysis_notification_default_title">Skenovanie média</string>
<string name="map_shortcut_short_label">Mapa</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">Medya tarama</string>
<string name="analysis_notification_default_title">Medya taranıyor</string>
<string name="analysis_notification_action_stop">Durdur</string>
<string name="map_shortcut_short_label">Harita</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_notification_action_stop">Стоп</string>
<string name="app_widget_label">Фоторамка</string>
<string name="analysis_notification_default_title">Сканування медіа</string>
<string name="map_shortcut_short_label">Мапа</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="app_name">Aves</string>
<string name="analysis_channel_name">Quét phương tiện</string>
<string name="search_shortcut_short_label">Tìm kiếm</string>
<string name="map_shortcut_short_label">Bản đồ</string>
</resources>

View file

@ -20,8 +20,8 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip

View file

@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20"
settings.ext.agp_version = '8.6.1'
settings.ext.agp_version = '8.7.0'
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

View file

@ -0,0 +1,5 @@
¡<i>·𐑱𐑝𐑰𐑟</i> 𐑒𐑨𐑯 𐑣𐑨𐑯𐑛𐑩𐑤 𐑷𐑤 𐑕𐑹𐑑𐑕 𐑝 𐑦𐑥𐑦𐑡𐑩𐑟 𐑯 𐑝𐑦𐑛𐑦𐑴𐑟, 𐑦𐑯𐑒𐑤𐑵𐑛𐑦𐑙 𐑘𐑹 𐑑𐑦𐑐𐑦𐑒𐑩𐑤 ⸰𐑡𐑓𐑧𐑜'𐑟 𐑯 ⸰𐑥𐑐4'𐑟, 𐑚𐑳𐑑 𐑷𐑤𐑕𐑴 𐑥𐑹 𐑦𐑜𐑟𐑪𐑑𐑦𐑒 𐑔𐑦𐑙𐑟 𐑤𐑲𐑒 <b>𐑥𐑳𐑤𐑑𐑦-𐑐𐑱𐑡 ⸰𐑑𐑦𐑓𐑓'𐑕, ⸰𐑕𐑝𐑜'𐑟, 𐑴𐑤𐑛 ⸰𐑷𐑝𐑦'𐑟 𐑯 𐑥𐑹</b>! 𐑦𐑑 𐑕𐑒𐑨𐑯𐑟 𐑘𐑹 𐑥𐑰𐑛𐑾 𐑒𐑩𐑤𐑧𐑒𐑖𐑩𐑯 𐑑 𐑲𐑛𐑧𐑯𐑑𐑦𐑓𐑲 <b>𐑥𐑴𐑖𐑩𐑯 𐑓𐑴𐑑𐑴𐑟</b>, <b>𐑐𐑨𐑯𐑼𐑭𐑥𐑩𐑟</b> (⸰𐑷𐑯𐑨 𐑓𐑴𐑑𐑴 𐑕𐑓𐑽𐑟), <b>360° 𐑝𐑦𐑛𐑦𐑴𐑟</b>, 𐑨𐑟 𐑢𐑧𐑤 𐑨𐑟 <b>⸰𐑡𐑰𐑴𐑑𐑦𐑓𐑓</b> 𐑓𐑲𐑤𐑟.
<b>𐑯𐑨𐑝𐑦𐑜𐑱𐑖𐑩𐑯 𐑯 𐑕𐑻𐑗</b> 𐑦𐑟 𐑩𐑯 𐑦𐑥𐑐𐑹𐑑𐑩𐑯𐑑 𐑐𐑸𐑑 𐑝 <i>·𐑱𐑝𐑰𐑟</i>. 𐑞 𐑜𐑴𐑤 𐑦𐑟 𐑓 𐑿𐑟𐑼𐑟 𐑑 𐑰𐑟𐑦𐑤𐑦 𐑓𐑤𐑴 𐑓𐑮𐑪𐑥 𐑨𐑤𐑚𐑩𐑥𐑟 𐑑 𐑓𐑴𐑑𐑴𐑟 𐑑 𐑑𐑨𐑜𐑟 𐑑 𐑥𐑨𐑐𐑕, 𐑯𐑯𐑯.
<i>·𐑱𐑝𐑰𐑟</i> 𐑦𐑯𐑑𐑦𐑜𐑮𐑱𐑑𐑕 𐑢𐑦𐑞 ·𐑨𐑯𐑛𐑮𐑶𐑛 (𐑓𐑮𐑪𐑥 ·𐑒𐑦𐑑𐑒𐑨𐑑 𐑑 ·𐑨𐑯𐑛𐑮𐑶𐑛 14, 𐑦𐑯𐑒𐑤𐑵𐑛𐑦𐑙 ·𐑨𐑯𐑛𐑮𐑶𐑛 ⸰𐑑𐑝) 𐑢𐑦𐑞 𐑓𐑰𐑗𐑼𐑟 𐑕𐑳𐑗 𐑨𐑟 <b>𐑢𐑦𐑡𐑩𐑑𐑕</b>, <b>𐑨𐑐 𐑖𐑹𐑑𐑒𐑳𐑑𐑕</b>, <b>𐑕𐑒𐑮𐑰𐑯 𐑕𐑱𐑝𐑼</b> 𐑯 <b>𐑜𐑤𐑴𐑚𐑩𐑤 𐑕𐑻𐑗</b> 𐑣𐑨𐑯𐑛𐑤𐑦𐑙. 𐑦𐑑 𐑷𐑤𐑕𐑴 𐑢𐑻𐑒𐑕 𐑨𐑟 𐑩 <b>𐑥𐑰𐑛𐑾 𐑝𐑿𐑼 𐑯 𐑐𐑦𐑒𐑼</b>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

View file

@ -0,0 +1 @@
𐑜𐑨𐑤𐑼𐑦 𐑯 𐑥𐑧𐑑𐑩𐑛𐑱𐑑𐑩 𐑦𐑒𐑕𐑐𐑤𐑹𐑼

View file

@ -0,0 +1,5 @@
In v1.11.17:
- peruse your videos frame by frame
- create map shortcuts to filtered collections
- enjoy the app in Shavian
Full changelog available on GitHub

View file

@ -0,0 +1,5 @@
In v1.11.17:
- peruse your videos frame by frame
- create map shortcuts to filtered collections
- enjoy the app in Shavian
Full changelog available on GitHub

View file

@ -41,7 +41,7 @@ class CountryTopology {
return Map.fromEntries(numericMap.entries.map((kv) {
final code = _countryOfNumeric(kv.key);
return code != null ? MapEntry(code, kv.value) : null;
}).whereNotNull());
}).nonNulls);
}
// returns a map of the given positions by the ISO 3166-1 numeric code of the country containing them

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
// cf https://github.com/topojson/topojson-specification
@ -60,7 +59,7 @@ class Topology extends TopologyJsonObject {
final name = kv.key;
final geometry = Geometry.build(kv.value);
return geometry != null ? MapEntry(name, geometry) : null;
}).whereNotNull()),
}).nonNulls),
arcs = (data['arcs'] as List).cast<List>().map((arc) => arc.cast<List>().map((position) => position.cast<num>()).toList()).toList(),
transform = data.containsKey('transform') ? Transform.parse((data['transform'] as Map).cast<String, dynamic>()) : null,
super.parse();
@ -238,7 +237,7 @@ class GeometryCollection extends Geometry {
final List<Geometry> geometries;
GeometryCollection.parse(super.data)
: geometries = (data['geometries'] as List).cast<Map<String, dynamic>>().map(Geometry.build).whereNotNull().toList(),
: geometries = (data['geometries'] as List).cast<Map<String, dynamic>>().map(Geometry.build).nonNulls.toList(),
super.parse();
@override

View file

@ -1,3 +1,4 @@
import 'package:aves/utils/math_utils.dart';
import 'package:latlong2/latlong.dart';
// e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)`
@ -24,3 +25,13 @@ import 'package:latlong2/latlong.dart';
}
return null;
}
String toGeoUri(LatLng latLng, {double? zoom}) {
final latitude = roundToPrecision(latLng.latitude, decimals: 6);
final longitude = roundToPrecision(latLng.longitude, decimals: 6);
var uri = 'geo:$latitude,$longitude?q=$latitude,$longitude';
if (zoom != null) {
uri += '&z=$zoom';
}
return uri;
}

View file

@ -541,8 +541,6 @@
"@settingsEnableBin": {},
"entryActionViewMotionPhotoVideo": "فتح الفيديو",
"@entryActionViewMotionPhotoVideo": {},
"videoControlsNone": "لا شيء",
"@videoControlsNone": {},
"otherDirectoryDescription": "دليل «{name}»",
"@otherDirectoryDescription": {
"placeholders": {
@ -757,8 +755,6 @@
"@drawerCollectionFavourites": {},
"filterTypeRawLabel": "خام",
"@filterTypeRawLabel": {},
"videoControlsPlaySeek": "تشغيل وتقدم للأمام/ للخلف",
"@videoControlsPlaySeek": {},
"settingsSubtitleThemeTextAlignmentCenter": "وسط",
"@settingsSubtitleThemeTextAlignmentCenter": {},
"keepScreenOnVideoPlayback": "أثناء تشغيل الفيديو",
@ -1031,8 +1027,6 @@
"@viewerErrorDoesNotExist": {},
"albumCamera": "الكاميرا",
"@albumCamera": {},
"videoControlsPlay": "تشغيل",
"@videoControlsPlay": {},
"settingsNavigationSectionTitle": "التنقل",
"@settingsNavigationSectionTitle": {},
"settingsDisplayRefreshRateModeDialogTitle": "معدل التحديث",
@ -1544,5 +1538,11 @@
"mapStyleOpenTopoMap": "الخريطة الطبوغرافية المفتوحة",
"@mapStyleOpenTopoMap": {},
"mapAttributionOsmData": "بيانات الخريطة © [OpenStreetMap](https://www.openstreetmap.org/copyright) المساهمين",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | البلاط بواسطة [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapStyleOsmLiberty": "حرية خرائط OSM",
"@mapStyleOsmLiberty": {},
"mapAttributionOsmLiberty": "البلاط بواسطة [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • مُستضاف بواسطة [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {}
}

View file

@ -257,8 +257,6 @@
"@coordinateFormatDecimal": {},
"subtitlePositionBottom": "Ніз",
"@subtitlePositionBottom": {},
"videoControlsPlaySeek": "Прайграванне і пераход на пазіцыю",
"@videoControlsPlaySeek": {},
"nameConflictStrategyReplace": "Замяніць",
"@nameConflictStrategyReplace": {},
"filterAspectRatioLandscapeLabel": "Ландшафтныя",
@ -361,8 +359,6 @@
"@settingsVideoEnablePip": {},
"videoControlsPlayOutside": "Адкрыць у іншым прайгравальніку",
"@videoControlsPlayOutside": {},
"videoControlsPlay": "Прайграванне",
"@videoControlsPlay": {},
"videoLoopModeNever": "Ніколі",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Толькі для кароткіх відэа",
@ -593,8 +589,6 @@
"@viewerInfoSearchSuggestionResolution": {},
"viewerInfoSearchSuggestionDimensions": "Памеры",
"@viewerInfoSearchSuggestionDimensions": {},
"videoControlsNone": "Нічога",
"@videoControlsNone": {},
"viewerErrorUnknown": "Ой!",
"@viewerErrorUnknown": {},
"viewerSetWallpaperButtonLabel": "УСТАНАВІЦЬ ШПАЛЕРЫ",

View file

@ -240,12 +240,6 @@
"@vaultLockTypePassword": {},
"settingsVideoEnablePip": "Imatge-en-imatge",
"@settingsVideoEnablePip": {},
"videoControlsPlay": "Reproduir",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Reproduir i retrocedeix/avança",
"@videoControlsPlaySeek": {},
"videoControlsNone": "Cap",
"@videoControlsNone": {},
"viewerTransitionZoomIn": "Ampliar",
"@viewerTransitionZoomIn": {},
"viewerTransitionNone": "Cap",

View file

@ -218,8 +218,6 @@
"@albumTierApps": {},
"entryActionViewMotionPhotoVideo": "کردنەوەی ڤیدیۆ",
"@entryActionViewMotionPhotoVideo": {},
"videoControlsNone": "هیچیان",
"@videoControlsNone": {},
"filterNoTitleLabel": "بێ سەرناو",
"@filterNoTitleLabel": {},
"videoPlaybackMuted": "بەبێ دەنگی لێبدە",
@ -244,8 +242,6 @@
"@addShortcutButtonLabel": {},
"entryActionOpenMap": "لە نەرمەواڵەی نەخشە پیشانی بدە",
"@entryActionOpenMap": {},
"videoControlsPlaySeek": "لێدان و بردنە پێش/پاش",
"@videoControlsPlaySeek": {},
"entryActionRotateCCW": "سوڕاندن بە پێچەوانەی میلی کاتژمێر",
"@entryActionRotateCCW": {},
"viewerActionSettings": "ڕێکخستنەکان",
@ -274,8 +270,6 @@
"@themeBrightnessBlack": {},
"videoPlaybackSkip": "بازدان",
"@videoPlaybackSkip": {},
"videoControlsPlay": "لێدان",
"@videoControlsPlay": {},
"entryActionShareVideoOnly": "بە تەنیا ڤیدیۆ هاوبەش بکە",
"@entryActionShareVideoOnly": {},
"applyTooltip": "جێبەجێ کردن",

View file

@ -250,14 +250,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Vždy",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Přehrát",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Přehrávat a vyhledávat vzad/vpřed",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Otevřít jiným přehrávačem",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Žádný",
"@videoControlsNone": {},
"coordinateFormatDms": "Stupně, minuty, vteřiny",
"@coordinateFormatDms": {},
"coordinateFormatDecimal": "Stupně s desetinnými místy",

View file

@ -101,9 +101,9 @@
"@entryActionRename": {},
"entryActionRestore": "Wiederherstellen",
"@entryActionRestore": {},
"entryActionRotateCCW": "Drehen gegen den Uhrzeigersinn",
"entryActionRotateCCW": "Gegen den Uhrzeigersinn drehen",
"@entryActionRotateCCW": {},
"entryActionRotateCW": "Drehen im Uhrzeigersinn",
"entryActionRotateCW": "Im Uhrzeigersinn drehen",
"@entryActionRotateCW": {},
"entryActionFlip": "Horizontal spiegeln",
"@entryActionFlip": {},
@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Immer",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Abspielen/Pausieren",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Abspielen/Pausieren & Sprung-Schaltflächen",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Mit anderem Video-Player öffnen",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Keine Schaltflächen",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
@ -1361,7 +1355,7 @@
"@videoActionABRepeat": {},
"videoRepeatActionSetStart": "Start festlegen",
"@videoRepeatActionSetStart": {},
"stopTooltip": "Stop",
"stopTooltip": "Stoppen",
"@stopTooltip": {},
"videoRepeatActionSetEnd": "Ende festlegen",
"@videoRepeatActionSetEnd": {},

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Πάντα",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Αναπαραγωγή",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Αναπαραγωγή & πίσω/μπροστά",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Άνοιγμα με άλλη εφαρμογή",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Καμία επιλογή",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",

View file

@ -138,6 +138,8 @@
"videoActionPlay": "Play",
"videoActionReplay10": "Seek backward 10 seconds",
"videoActionSkip10": "Seek forward 10 seconds",
"videoActionShowPreviousFrame": "Show previous frame",
"videoActionShowNextFrame": "Show next frame",
"videoActionSelectStreams": "Select tracks",
"videoActionSetSpeed": "Playback speed",
"videoActionABRepeat": "A-B repeat",
@ -270,10 +272,7 @@
"settingsVideoEnablePip": "Picture-in-picture",
"videoControlsPlay": "Play",
"videoControlsPlaySeek": "Play & seek backward/forward",
"videoControlsPlayOutside": "Open with other player",
"videoControlsNone": "None",
"videoLoopModeNever": "Never",
"videoLoopModeShortOnly": "Short videos only",

1593
lib/l10n/app_en_Shaw.arb Normal file

File diff suppressed because it is too large Load diff

View file

@ -217,12 +217,6 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Siempre",
"@videoLoopModeAlways": {},
"videoControlsNone": "Ninguno",
"@videoControlsNone": {},
"videoControlsPlay": "Reproducir",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Reproducir y buscar",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Reproducir externamente",
"@videoControlsPlayOutside": {},
"mapStyleGoogleNormal": "Google Maps",
@ -1392,5 +1386,9 @@
"mapAttributionOsmLiberty": "Mosaicos por [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Alojado por [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {},
"mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {}
"@mapStyleOsmLiberty": {},
"videoActionShowPreviousFrame": "Mostrar fotograma anterior",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "Mostrar fotograma siguiente",
"@videoActionShowNextFrame": {}
}

View file

@ -236,8 +236,6 @@
"@entryActionViewSource": {},
"unitSystemMetric": "Metriko",
"@unitSystemMetric": {},
"videoControlsPlay": "Erreproduzitu",
"@videoControlsPlay": {},
"entryActionShowGeoTiffOnMap": "Erakutsi gainjarritako mapa bezala",
"@entryActionShowGeoTiffOnMap": {},
"coordinateFormatDms": "DMS (Dokumentuak kudeatzeko sistema)",
@ -271,12 +269,8 @@
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Bideo laburrak soilik",
"@videoLoopModeShortOnly": {},
"videoControlsPlaySeek": "Erreproduzitu eta aurrera edo atzera egin",
"@videoControlsPlaySeek": {},
"nameConflictStrategySkip": "Jauzi",
"@nameConflictStrategySkip": {},
"videoControlsNone": "Bat ere ez",
"@videoControlsNone": {},
"keepScreenOnNever": "Inoiz",
"@keepScreenOnNever": {},
"nameConflictStrategyReplace": "Ordezkatu",

View file

@ -232,8 +232,6 @@
"@filterTypeMotionPhotoLabel": {},
"unitSystemImperial": "مایلی",
"@unitSystemImperial": {},
"videoControlsPlaySeek": "پخش، برگشت به عقب، جلو رفتن",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "باز کردن با برنامه‌ای دیگر",
"@videoControlsPlayOutside": {},
"videoLoopModeShortOnly": "فقط برای ویدئو های کوتاه",
@ -252,8 +250,6 @@
"@menuActionStats": {},
"exportEntryDialogWidth": "عرض",
"@exportEntryDialogWidth": {},
"videoControlsPlay": "پخش",
"@videoControlsPlay": {},
"mapZoomInTooltip": "بزرگ نمایی",
"@mapZoomInTooltip": {},
"chipActionFilterOut": "پاک‌کردن از پالایش",
@ -417,8 +413,6 @@
"@filterAspectRatioPortraitLabel": {},
"filterTypeGeotiffLabel": "GeoTIFF",
"@filterTypeGeotiffLabel": {},
"videoControlsNone": "هیچ‌کدام",
"@videoControlsNone": {},
"otherDirectoryDescription": "شاخهٔ «{name}»",
"@otherDirectoryDescription": {
"placeholders": {

View file

@ -376,14 +376,8 @@
"@vaultLockTypePassword": {},
"settingsVideoEnablePip": "Kuva kuvassa",
"@settingsVideoEnablePip": {},
"videoControlsPlay": "Toista",
"@videoControlsPlay": {},
"videoControlsPlayOutside": "Avaa toisella soittimella",
"@videoControlsPlayOutside": {},
"videoControlsPlaySeek": "Toista & selaa eteen/taakse",
"@videoControlsPlaySeek": {},
"videoControlsNone": "Ei mitään",
"@videoControlsNone": {},
"videoLoopModeNever": "Ei koskaan",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Vain lyhyissä videoissa",

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Toujours",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Lecture",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Lecture & déplacement",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Ouvrir avec un autre lecteur",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Aucun",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Satellite)",
@ -1392,5 +1386,9 @@
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Fond de carte par [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapAttributionOsmData": "Données © les contributeurs d[OpenStreetMap](https://www.openstreetmap.org/copyright)",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"videoActionShowNextFrame": "Montrer limage suivante",
"@videoActionShowNextFrame": {},
"videoActionShowPreviousFrame": "Montrer limage précédente",
"@videoActionShowPreviousFrame": {}
}

View file

@ -273,18 +273,12 @@
"@unitSystemMetric": {},
"unitSystemImperial": "Imperial",
"@unitSystemImperial": {},
"videoControlsPlay": "Reproducir",
"@videoControlsPlay": {},
"videoLoopModeNever": "Nunca",
"@videoLoopModeNever": {},
"videoLoopModeAlways": "Sempre",
"@videoLoopModeAlways": {},
"videoControlsPlaySeek": "Reprroduce e busca cara atrás/adelante",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Abrir con outro xogador",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Ningún",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"videoLoopModeShortOnly": "Só vídeos curtos",

View file

@ -266,8 +266,6 @@
"@themeBrightnessDark": {},
"themeBrightnessBlack": "काला",
"@themeBrightnessBlack": {},
"videoControlsPlaySeek": "पिछड़े / आगे की तलाश करें",
"@videoControlsPlaySeek": {},
"mapStyleOsmHot": "Humanitarian OSM",
"@mapStyleOsmHot": {},
"filterAspectRatioPortraitLabel": "पोर्ट्रेट",
@ -439,12 +437,8 @@
"@authenticateToUnlockVault": {},
"settingsVideoEnablePip": "पिक्चर-इन-पिक्चर",
"@settingsVideoEnablePip": {},
"videoControlsPlay": "चलाएं",
"@videoControlsPlay": {},
"videoControlsPlayOutside": "अन्य प्लेयर के साथ खोलें",
"@videoControlsPlayOutside": {},
"videoControlsNone": "कोई नहीं",
"@videoControlsNone": {},
"videoLoopModeShortOnly": "केवल लघु वीडियो",
"@videoLoopModeShortOnly": {},
"videoPlaybackWithSound": "ध्वनि के साथ चलाए",

View file

@ -67,10 +67,6 @@
"@themeBrightnessDark": {},
"vaultLockTypePassword": "Jelszó",
"@vaultLockTypePassword": {},
"videoControlsPlay": "Lejátszás",
"@videoControlsPlay": {},
"videoControlsNone": "Nincs",
"@videoControlsNone": {},
"videoLoopModeAlways": "Mindig",
"@videoLoopModeAlways": {},
"viewerTransitionNone": "Nincs",
@ -913,8 +909,6 @@
"@accessibilityAnimationsKeep": {},
"keepScreenOnViewerOnly": "Csak a megtekintőnél",
"@keepScreenOnViewerOnly": {},
"videoControlsPlaySeek": "Lejátszás és ugrás vissza/előre",
"@videoControlsPlaySeek": {},
"viewerTransitionSlide": "Csúsztatás",
"@viewerTransitionSlide": {},
"viewerTransitionFade": "Áttünés",

View file

@ -225,14 +225,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Selalu",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Putar",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Putar dan cari",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Buka dengan pemutar lain",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Tidak ada",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
@ -1392,5 +1386,9 @@
"mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {},
"mapAttributionOsmLiberty": "Ubin oleh [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Disediakan oleh [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {}
"@mapAttributionOsmLiberty": {},
"videoActionShowPreviousFrame": "Tampilkan bingkai sebelumnya",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "Tampilkan bingkai berikutnya",
"@videoActionShowNextFrame": {}
}

View file

@ -463,8 +463,6 @@
"@settingsEnableBin": {},
"entryActionViewMotionPhotoVideo": "Opna myndskeið",
"@entryActionViewMotionPhotoVideo": {},
"videoControlsNone": "Ekkert",
"@videoControlsNone": {},
"otherDirectoryDescription": "„{name}“ möppu",
"@otherDirectoryDescription": {
"placeholders": {
@ -673,8 +671,6 @@
"@drawerCollectionFavourites": {},
"filterTypeRawLabel": "RAW",
"@filterTypeRawLabel": {},
"videoControlsPlaySeek": "Spila og leita afturábak/áfram",
"@videoControlsPlaySeek": {},
"settingsSubtitleThemeTextAlignmentCenter": "Miðjað",
"@settingsSubtitleThemeTextAlignmentCenter": {},
"keepScreenOnVideoPlayback": "Á meðan myndskeið er í spilun",
@ -918,8 +914,6 @@
"@viewerErrorDoesNotExist": {},
"albumCamera": "Myndavél",
"@albumCamera": {},
"videoControlsPlay": "Afspilun",
"@videoControlsPlay": {},
"settingsNavigationSectionTitle": "Flakk",
"@settingsNavigationSectionTitle": {},
"settingsDisplayRefreshRateModeDialogTitle": "Uppfærslutíðni",

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Sempre",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Riproduci",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Riproduci e cerca avanti/indietro",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Apri con un altro lettore",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Nessuno",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Ibrido)",

View file

@ -217,14 +217,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "常にループ再生",
"@videoLoopModeAlways": {},
"videoControlsPlay": "再生",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "再生&早送り/早戻し",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "他のプレイヤーで開く",
"@videoControlsPlayOutside": {},
"videoControlsNone": "なし",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google マップ",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google マップ(ハイブリッド)",

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "항상 반복",
"@videoLoopModeAlways": {},
"videoControlsPlay": "재생",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "재생 및 앞뒤로 가기",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "다른 앱에서 열기",
"@videoControlsPlayOutside": {},
"videoControlsNone": "없음",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google 지도",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google 지도 (위성)",
@ -1392,5 +1386,9 @@
"mapAttributionOsmLiberty": "타일 [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • 호스팅 [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {},
"mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {}
"@mapStyleOsmLiberty": {},
"videoActionShowNextFrame": "다음 프레임 보기",
"@videoActionShowNextFrame": {},
"videoActionShowPreviousFrame": "이전 프레임 보기",
"@videoActionShowPreviousFrame": {}
}

View file

@ -139,8 +139,6 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Visada",
"@videoLoopModeAlways": {},
"videoControlsNone": "Jokie",
"@videoControlsNone": {},
"keepScreenOnNever": "Niekada",
"@keepScreenOnNever": {},
"keepScreenOnAlways": "Visada",
@ -935,10 +933,6 @@
"@filterNoLocationLabel": {},
"filterMimeImageLabel": "Paveikslėlis",
"@filterMimeImageLabel": {},
"videoControlsPlay": "Groti",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Groti ir peršokti pirmyn/atgal",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Atidaryti su kitu grotuvu",
"@videoControlsPlayOutside": {},
"mapStyleGoogleNormal": "Google Maps",

View file

@ -464,8 +464,6 @@
"@vaultLockTypePattern": {},
"videoControlsPlayOutside": "တခြား player နဲ့ဖွင့်တဲ့ခလုတ်",
"@videoControlsPlayOutside": {},
"videoControlsNone": "ဘာမှမထား",
"@videoControlsNone": {},
"videoLoopModeNever": "ဘယ်တော့မှမလုပ်ပါနှင့်",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "ဗီဒီယိုအတိုလေးတွေတွင်သာ",
@ -494,10 +492,6 @@
"@keepScreenOnNever": {},
"keepScreenOnVideoPlayback": "ဗီဒီယိုကြည့်နေသည့်အချိန်အတွင်း",
"@keepScreenOnVideoPlayback": {},
"videoControlsPlay": "ဖွင့်တဲ့ခလုတ်",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "ဖွင့်တဲ့ခလုတ်နဲ့ အရှေ့/အနောက်သွားတဲ့ ခလုတ်",
"@videoControlsPlaySeek": {},
"videoPlaybackSkip": "ကျော်လိုက်ပါ",
"@videoPlaybackSkip": {},
"viewerTransitionFade": "မှိန်သွားခြင်း",

View file

@ -457,14 +457,8 @@
"@albumTierApps": {},
"videoLoopModeShortOnly": "Kun korte videoer",
"@videoLoopModeShortOnly": {},
"videoControlsPlay": "Spill",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Spill og blafre forover/bakover",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Åpne med annen avspiller",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Ingen",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"videoPlaybackSkip": "Hopp over",

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Altijd",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Afspelen",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Speel & zoek terug/vooruit",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Met andere speler openen",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Geen",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybride)",
@ -1001,7 +995,7 @@
"@settingsUnitSystemDialogTitle": {},
"settingsScreenSaverPageTitle": "Schermbeveiliging",
"@settingsScreenSaverPageTitle": {},
"settingsWidgetPageTitle": "Foto Lijstje",
"settingsWidgetPageTitle": "Fotolijst",
"@settingsWidgetPageTitle": {},
"settingsWidgetShowOutline": "Contour",
"@settingsWidgetShowOutline": {},
@ -1394,5 +1388,9 @@
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Tegels van [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapAttributionOsmData": "Kaartgegevens © [OpenStreetMap](https://www.openstreetmap.org/copyright) bijdragers",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"videoActionShowNextFrame": "Volgend frame weergeven",
"@videoActionShowNextFrame": {},
"videoActionShowPreviousFrame": "Vorig frame weergeven",
"@videoActionShowPreviousFrame": {}
}

View file

@ -236,12 +236,8 @@
"@videoLoopModeNever": {},
"videoLoopModeAlways": "På",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Spel av",
"@videoControlsPlay": {},
"videoControlsPlayOutside": "Opne med ein ytre avspelar",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Ingen",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (hybrid)",
@ -329,8 +325,6 @@
},
"unitSystemMetric": "Metrisk",
"@unitSystemMetric": {},
"videoControlsPlaySeek": "Spel av, spol framover/bakover",
"@videoControlsPlaySeek": {},
"mapStyleStamenWatercolor": "Stamen Watercolor (vassfarge)",
"@mapStyleStamenWatercolor": {},
"viewerTransitionNone": "Ingen",

View file

@ -211,8 +211,6 @@
"@filterAspectRatioPortraitLabel": {},
"filterNoAddressLabel": "Brak adresu",
"@filterNoAddressLabel": {},
"videoControlsPlaySeek": "Odtwórz oraz przeszukuj",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Odtwórz innym odtwarzaczem",
"@videoControlsPlayOutside": {},
"mapStyleGoogleNormal": "Mapy Google",
@ -325,8 +323,6 @@
"@unitSystemImperial": {},
"videoLoopModeNever": "Nigdy",
"@videoLoopModeNever": {},
"videoControlsNone": "Brak",
"@videoControlsNone": {},
"accessibilityAnimationsRemove": "Zapobiegaj efektom ekranu",
"@accessibilityAnimationsRemove": {},
"accessibilityAnimationsKeep": "Zachowaj efekty ekranu",
@ -355,8 +351,6 @@
"@coordinateDmsWest": {},
"unitSystemMetric": "Metryczne",
"@unitSystemMetric": {},
"videoControlsPlay": "Odtwórz",
"@videoControlsPlay": {},
"albumTierPinned": "Przypięty",
"@albumTierPinned": {},
"videoResumeButtonLabel": "WZNÓW",
@ -1550,5 +1544,9 @@
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Kafelki od [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapAttributionOsmData": "Dane mapy © [OpenStreetMap](https://www.openstreetmap.org/copyright) współtwórcy",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"videoActionShowPreviousFrame": "Pokaż poprzednią klatkę",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "Pokaż kolejną klatkę",
"@videoActionShowNextFrame": {}
}

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Sempre",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Começar",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Começar e procurar",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Abrir com outro player",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Nenhum",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Híbrido)",
@ -993,9 +987,9 @@
"@settingsDisplayRefreshRateModeDialogTitle": {},
"settingsLanguageSectionTitle": "Idioma e Formatos",
"@settingsLanguageSectionTitle": {},
"settingsLanguageTile": "Língua",
"settingsLanguageTile": "Idioma",
"@settingsLanguageTile": {},
"settingsLanguagePageTitle": "Língua",
"settingsLanguagePageTitle": "Idioma",
"@settingsLanguagePageTitle": {},
"settingsCoordinateFormatTile": "Formato de coordenadas",
"@settingsCoordinateFormatTile": {},
@ -1384,5 +1378,13 @@
"setHomeCustom": "Personalizada",
"@setHomeCustom": {},
"mapAttributionOsmData": "Dados do mapa © [OpenStreetMap](https://www.openstreetmap.org/copyright) colaboradores",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"mapStyleOpenTopoMap": "OpenTopoMap",
"@mapStyleOpenTopoMap": {},
"mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {},
"mapAttributionOsmLiberty": "Blocos por [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Hospedado por [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {},
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Blocos por [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {}
}

View file

@ -237,14 +237,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Mereu",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Redă",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Redați și căutați înapoi/înainte",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Deschide cu alt player",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Nici unul",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Hărți Google",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Hărți Google (hibrid)",

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Всегда",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Воспроизведение",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Воспроизведение и переход на позицию",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Открыть в другом видеоплеере",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Ничего",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Карты",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Карты (Гибридный)",
@ -1388,5 +1382,13 @@
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Плитки от [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapAttributionOsmData": "Данные карты © [openstreetmap](https://www.openstreetmap.org/copyright) участники",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"videoActionShowPreviousFrame": "Показать предыдущий кадр",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "Показать следующий кадр",
"@videoActionShowNextFrame": {},
"mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {},
"mapAttributionOsmLiberty": "Tiles by [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Hosted by [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {}
}

View file

@ -270,10 +270,6 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Vždy",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Spustiť",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Spustiť & pretočiť dozadu/dopredu",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Otvoriť v inom prehrávači",
"@videoControlsPlayOutside": {},
"mapStyleGoogleNormal": "Google mapy",
@ -286,8 +282,6 @@
"@mapStyleStamenWatercolor": {},
"nameConflictStrategyRename": "Premenovať",
"@nameConflictStrategyRename": {},
"videoControlsNone": "Žiadne",
"@videoControlsNone": {},
"nameConflictStrategyReplace": "Nahradiť",
"@nameConflictStrategyReplace": {},
"keepScreenOnNever": "Nikdy",
@ -1544,5 +1538,11 @@
"explorerPageTitle": "Prieskumník",
"@explorerPageTitle": {},
"mapAttributionOsmData": "Údaje máp © [OpenStreetMap](https://www.openstreetmap.org/copyright) prispievatelia",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) Dlaždice podľa [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapStyleOsmLiberty": "OSM Slobody",
"@mapStyleOsmLiberty": {},
"mapAttributionOsmLiberty": "Dlaždice podľa [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Hostovaný [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {}
}

View file

@ -375,14 +375,8 @@
"@vaultLockTypePassword": {},
"settingsVideoEnablePip": "Bild-i-bild",
"@settingsVideoEnablePip": {},
"videoControlsPlay": "Spela",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Spela & spola bakåt/framåt",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Öppna med annan spelare",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Inga",
"@videoControlsNone": {},
"videoLoopModeNever": "Aldrig",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Bara korta videor",
@ -1572,5 +1566,9 @@
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Brickor av [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"mapAttributionOsmData": "Kartdata © [OpenStreetMap](https://www.openstreetmap.org/copyright) bidragsgivare",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"videoActionShowPreviousFrame": "Visa föregående bildruta",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "Visa nästa bildruta",
"@videoActionShowNextFrame": {}
}

View file

@ -167,14 +167,8 @@
"@unitSystemImperial": {},
"videoLoopModeShortOnly": "เฉพาะคลิปสั้น",
"@videoLoopModeShortOnly": {},
"videoControlsPlay": "เล่น",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "เล่นและกรอหลัง/หน้า",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "เปิดด้วยตัวเล่นอื่น",
"@videoControlsPlayOutside": {},
"videoControlsNone": "ไม่มี",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",

View file

@ -211,14 +211,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Her zaman",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Oynat",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "Oynat ve ileri/geri git",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "Başka bir oynatıcı ile aç",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Hiçbiri",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Haritalar",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Haritalar (Hibrit)",

View file

@ -209,12 +209,8 @@
"@videoLoopModeNever": {},
"videoLoopModeAlways": "Завжди",
"@videoLoopModeAlways": {},
"videoControlsPlay": "Відтворити",
"@videoControlsPlay": {},
"videoControlsPlayOutside": "Відкрити в іншому відеоплеєрі",
"@videoControlsPlayOutside": {},
"videoControlsNone": "Нічого",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google Карти",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Карти (Гібрид)",
@ -715,8 +711,6 @@
"@filterTypeAnimatedLabel": {},
"videoLoopModeShortOnly": "Тільки короткі відео",
"@videoLoopModeShortOnly": {},
"videoControlsPlaySeek": "Відтворити та перемотати назад/вперед",
"@videoControlsPlaySeek": {},
"mapStyleStamenWatercolor": "Stamen Watercolor",
"@mapStyleStamenWatercolor": {},
"widgetOpenPageCollection": "Відкрити колекцію",
@ -1542,5 +1536,17 @@
"sortOrderLongestFirst": "Спершу найдовше",
"@sortOrderLongestFirst": {},
"mapAttributionOsmData": "Картографічні дані © [OpenStreetMap](https://www.openstreetmap.org/copyright) помічники",
"@mapAttributionOsmData": {}
"@mapAttributionOsmData": {},
"mapStyleOsmLiberty": "OSM Liberty",
"@mapStyleOsmLiberty": {},
"mapStyleOpenTopoMap": "OpenTopoMap",
"@mapStyleOpenTopoMap": {},
"mapAttributionOsmLiberty": "Плитки від [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Розміщено на [OSM Americana](https://tile.ourmap.us)",
"@mapAttributionOsmLiberty": {},
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Плитки від [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
"@mapAttributionOpenTopoMap": {},
"videoActionShowPreviousFrame": "Показати попередній кадр",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "Показати наступний кадр",
"@videoActionShowNextFrame": {}
}

View file

@ -633,8 +633,6 @@
"@patternDialogEnter": {},
"settingsEnableBin": "Dùng thùng rác",
"@settingsEnableBin": {},
"videoControlsNone": "Không có",
"@videoControlsNone": {},
"otherDirectoryDescription": "“{name}” thư mục",
"@otherDirectoryDescription": {
"placeholders": {
@ -833,8 +831,6 @@
"@drawerCollectionFavourites": {},
"filterTypeRawLabel": "RAW",
"@filterTypeRawLabel": {},
"videoControlsPlaySeek": "Phát & kéo lùi lại/tiến tới",
"@videoControlsPlaySeek": {},
"settingsSubtitleThemeTextAlignmentCenter": "Trung tâm",
"@settingsSubtitleThemeTextAlignmentCenter": {},
"keepScreenOnVideoPlayback": "Khi phát lại video",
@ -1077,8 +1073,6 @@
"@viewerErrorDoesNotExist": {},
"albumCamera": "Máy Ảnh",
"@albumCamera": {},
"videoControlsPlay": "Phát",
"@videoControlsPlay": {},
"settingsNavigationSectionTitle": "Điều hướng",
"@settingsNavigationSectionTitle": {},
"settingsDisplayRefreshRateModeDialogTitle": "Tốc độ làm mới",

View file

@ -229,14 +229,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "始终",
"@videoLoopModeAlways": {},
"videoControlsPlay": "播放",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "播放和步进/步退",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "用其他播放器打开",
"@videoControlsPlayOutside": {},
"videoControlsNone": "无",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google 地图",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google 地图 (卫星图像)",

View file

@ -205,14 +205,8 @@
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "總是",
"@videoLoopModeAlways": {},
"videoControlsPlay": "播放",
"@videoControlsPlay": {},
"videoControlsPlaySeek": "播放和後退/前進",
"@videoControlsPlaySeek": {},
"videoControlsPlayOutside": "用其他播放器打開",
"@videoControlsPlayOutside": {},
"videoControlsNone": "無",
"@videoControlsNone": {},
"mapStyleGoogleNormal": "Google 地圖",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleTerrain": "Google 地圖 (地形)",

View file

@ -69,7 +69,7 @@ class Contributors {
Contributor('Henning Bunk', 'henningtbunk@gmail.com'),
Contributor('Samirah Ail', 'samiratalzahrani@gmail.com'),
Contributor('Salih Ail', 'rrrfff444@gmail.com'),
Contributor('nasreddineloukriz', 'nasreddineloukriz@gmail.com'),
Contributor('Nasreddine Loukriz', 'nasreddineloukriz@gmail.com'),
Contributor('Mohamed Zeroug', 'mzeroug19@gmail.com'),
Contributor('ssantos', 'ssantos@web.de'),
Contributor('Сергій', 'sergiy.goncharuk.1@gmail.com'),
@ -102,8 +102,14 @@ class Contributors {
Contributor('Taufan', 'taufanxxx@gmail.com'),
Contributor('Leo Aaua Felix', 'g00g7el@gmail.com'),
Contributor('-J-', 'heyj0e@tuta.io'),
Contributor('bittin1ddc447d824349b2', 'bittin@reimu.nl'),
Contributor('bittin', 'bittin@reimu.nl'),
Contributor('splice11', 'trenchedgrandpa@protonmail.com'),
Contributor('Ihor Hordiichuk', 'igor_ck@outlook.com'),
Contributor('João Palmeiro', 'joaommpalmeiro@gmail.com'),
Contributor('Whoever4976', 'wolffjonas47@gmail.com'),
Contributor('Your Average Code', 'neumeiersi91358@th-nuernberg.de'),
Contributor('Paranoid Android', 'f.cherdzhiev@innopolis.university'),
Contributor('Noah Kenzie Rodriguez-Beus', 'noahbeus@protonmail.com'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese

View file

@ -71,7 +71,7 @@ class Package {
currentLabel,
englishLabel,
...ownedDirs,
].whereNotNull().map(normalizePotentialDir).toSet();
].nonNulls.map(normalizePotentialDir).toSet();
static String normalizePotentialDir(String dir) {
return dir.replaceAll('_', ' ').trim().toLowerCase();

View file

@ -146,7 +146,7 @@ class Covers {
if (colorValue != null) 'color': colorValue,
};
})
.whereNotNull()
.nonNulls
.toList();
return jsonList.isNotEmpty ? jsonList : null;
}

View file

@ -115,7 +115,7 @@ abstract class LocalMediaDb {
Future<Set<VideoPlaybackRow>> loadAllVideoPlayback();
Future<VideoPlaybackRow?> loadVideoPlayback(int? id);
Future<VideoPlaybackRow?> loadVideoPlayback(int id);
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows);

View file

@ -31,6 +31,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
static const trashTable = 'trash';
static const videoPlaybackTable = 'videoPlayback';
static const _queryCursorBufferSize = 1000;
static int _lastId = 0;
@override
@ -180,6 +181,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
whereArgs.add(origin);
}
final entries = <AvesEntry>{};
if (directory != null) {
final separator = pContext.separator;
if (!directory.endsWith(separator)) {
@ -188,21 +190,25 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
where = '${where != null ? '$where AND ' : ''}path LIKE ?';
whereArgs.add('$directory%');
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
final dirLength = directory.length;
return rows
.whereNot((row) {
// skip entries in subfolders
final path = row['path'] as String?;
return path == null || path.substring(dirLength).contains(separator);
})
.map(AvesEntry.fromMap)
.toSet();
while (await cursor.moveNext()) {
final row = cursor.current;
// skip entries in subfolders
final path = row['path'] as String?;
if (path != null && !path.substring(dirLength).contains(separator)) {
entries.add(AvesEntry.fromMap(row));
}
}
} else {
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
entries.add(AvesEntry.fromMap(cursor.current));
}
}
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
return rows.map(AvesEntry.fromMap).toSet();
return entries;
}
@override
@ -278,8 +284,13 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Map<int?, int?>> loadDates() async {
final rows = await _db.query(dateTakenTable);
return Map.fromEntries(rows.map((map) => MapEntry(map['id'] as int, (map['dateMillis'] ?? 0) as int)));
final result = <int?, int?>{};
final cursor = await _db.queryCursor(dateTakenTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
final row = cursor.current;
result[row['id'] as int] = row['dateMillis'] as int? ?? 0;
}
return result;
}
// catalog metadata
@ -292,8 +303,12 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<CatalogMetadata>> loadCatalogMetadata() async {
final rows = await _db.query(metadataTable);
return rows.map(CatalogMetadata.fromMap).toSet();
final result = <CatalogMetadata>{};
final cursor = await _db.queryCursor(metadataTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(CatalogMetadata.fromMap(cursor.current));
}
return result;
}
@override
@ -351,8 +366,12 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<AddressDetails>> loadAddresses() async {
final rows = await _db.query(addressTable);
return rows.map(AddressDetails.fromMap).toSet();
final result = <AddressDetails>{};
final cursor = await _db.queryCursor(addressTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(AddressDetails.fromMap(cursor.current));
}
return result;
}
@override
@ -395,8 +414,12 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<VaultDetails>> loadAllVaults() async {
final rows = await _db.query(vaultTable);
return rows.map(VaultDetails.fromMap).toSet();
final result = <VaultDetails>{};
final cursor = await _db.queryCursor(vaultTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(VaultDetails.fromMap(cursor.current));
}
return result;
}
@override
@ -443,8 +466,12 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<TrashDetails>> loadAllTrashDetails() async {
final rows = await _db.query(trashTable);
return rows.map(TrashDetails.fromMap).toSet();
final result = <TrashDetails>{};
final cursor = await _db.queryCursor(trashTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(TrashDetails.fromMap(cursor.current));
}
return result;
}
@override
@ -474,8 +501,12 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<FavouriteRow>> loadAllFavourites() async {
final rows = await _db.query(favouriteTable);
return rows.map(FavouriteRow.fromMap).toSet();
final result = <FavouriteRow>{};
final cursor = await _db.queryCursor(favouriteTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(FavouriteRow.fromMap(cursor.current));
}
return result;
}
@override
@ -524,8 +555,15 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<CoverRow>> loadAllCovers() async {
final rows = await _db.query(coverTable);
return rows.map(CoverRow.fromMap).whereNotNull().toSet();
final result = <CoverRow>{};
final cursor = await _db.queryCursor(coverTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
final row = CoverRow.fromMap(cursor.current);
if (row != null) {
result.add(row);
}
}
return result;
}
@override
@ -587,14 +625,19 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<VideoPlaybackRow>> loadAllVideoPlayback() async {
final rows = await _db.query(videoPlaybackTable);
return rows.map(VideoPlaybackRow.fromMap).whereNotNull().toSet();
final result = <VideoPlaybackRow>{};
final cursor = await _db.queryCursor(videoPlaybackTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
final row = VideoPlaybackRow.fromMap(cursor.current);
if (row != null) {
result.add(row);
}
}
return result;
}
@override
Future<VideoPlaybackRow?> loadVideoPlayback(int? id) async {
if (id == null) return null;
Future<VideoPlaybackRow?> loadVideoPlayback(int id) async {
final rows = await _db.query(videoPlaybackTable, where: 'id = ?', whereArgs: [id]);
if (rows.isEmpty) return null;
@ -631,11 +674,13 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
// convenience methods
Future<Set<T>> _getByIds<T>(Set<int> ids, String table, T Function(Map<String, Object?> row) mapRow) async {
if (ids.isEmpty) return {};
final rows = await _db.query(
table,
where: 'id IN (${ids.join(',')})',
);
return rows.map(mapRow).toSet();
final result = <T>{};
if (ids.isNotEmpty) {
final cursor = await _db.queryCursor(table, where: 'id IN (${ids.join(',')})', bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(mapRow(cursor.current));
}
}
return result;
}
}

View file

@ -1,5 +1,4 @@
import 'package:aves/model/db/db_sqflite.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
@ -338,11 +337,11 @@ class LocalMediaDbUpgrader {
// clean duplicates introduced before Aves v1.7.1
final duplicatedContentIdRows = await db.query(entryTable, columns: ['contentId'], groupBy: 'contentId', having: 'COUNT(id) > 1 AND contentId IS NOT NULL');
final duplicatedContentIds = duplicatedContentIdRows.map((row) => row['contentId'] as int?).whereNotNull().toSet();
final duplicatedContentIds = duplicatedContentIdRows.map((row) => row['contentId'] as int?).nonNulls.toSet();
final duplicateIds = <int>{};
await Future.forEach(duplicatedContentIds, (contentId) async {
final rows = await db.query(entryTable, columns: ['id'], where: 'contentId = ?', whereArgs: [contentId]);
final ids = rows.map((row) => row['id'] as int?).whereNotNull().toList()..sort();
final ids = rows.map((row) => row['id'] as int?).nonNulls.toList()..sort();
if (ids.length > 1) {
ids.removeAt(0);
duplicateIds.addAll(ids);

View file

@ -12,8 +12,8 @@ import 'package:aves/theme/format.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';
import 'package:leak_tracker/leak_tracker.dart';
enum EntryDataType { basic, aspectRatio, catalog, address, references }
@ -73,7 +73,7 @@ class AvesEntry with AvesEntryBase {
this.stackedEntries,
}) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
LeakTracking.dispatchObjectCreated(
library: 'aves',
className: '$AvesEntry',
object: this,
@ -189,7 +189,7 @@ class AvesEntry with AvesEntryBase {
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
LeakTracking.dispatchObjectDisposed(object: this);
}
visualChangeNotifier.dispose();
metadataChangeNotifier.dispose();
@ -392,7 +392,7 @@ class AvesEntry with AvesEntryBase {
_addressDetails?.countryName,
_addressDetails?.adminArea,
_addressDetails?.locality,
}.whereNotNull().where((v) => v.isNotEmpty).join(', ');
}.nonNulls.where((v) => v.isNotEmpty).join(', ');
}
Future<void> applyNewFields(Map newFields, {required bool persist}) async {
@ -461,17 +461,17 @@ class AvesEntry with AvesEntryBase {
}
Future<bool> delete() {
final completer = Completer<bool>();
final opCompleter = Completer<bool>();
mediaEditService.delete(entries: {this}).listen(
(event) => completer.complete(event.success && !event.skipped),
onError: completer.completeError,
(event) => opCompleter.complete(event.success && !event.skipped),
onError: opCompleter.completeError,
onDone: () {
if (!completer.isCompleted) {
completer.complete(false);
if (!opCompleter.isCompleted) {
opCompleter.complete(false);
}
},
);
return completer.future;
return opCompleter.future;
}
// when the MIME type or the image itself changed (e.g. after rotation)

View file

@ -141,7 +141,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
final rawTags = formatCount.map((key, value) {
final count = value.length;
// remove duplicate names, so number of displayed names may not match displayed count
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
final names = value.nonNulls.toSet().toList()..sort(compareAsciiUpperCase);
return MapEntry(key, '$count items: ${names.join(', ')}');
});
directories.add(MetadataDirectory('Attachments', _toSortedTags(rawTags)));
@ -157,7 +157,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
if (value.isEmpty) return null;
final tagName = tagKV.key as String;
return MapEntry(tagName, value);
}).whereNotNull()));
}).nonNulls));
return tags;
}
}

View file

@ -12,7 +12,9 @@ final Favourites favourites = Favourites._private();
class Favourites with ChangeNotifier {
Set<FavouriteRow> _rows = {};
Favourites._private();
Favourites._private() {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
}
Future<void> init() async {
_rows = await localMediaDb.loadAllFavourites();
@ -58,7 +60,7 @@ class Favourites with ChangeNotifier {
Map<String, List<String>>? export(CollectionSource source) {
final visibleEntries = source.visibleEntries;
final ids = favourites.all;
final paths = visibleEntries.where((entry) => ids.contains(entry.id)).map((entry) => entry.path).whereNotNull().toSet();
final paths = visibleEntries.where((entry) => ids.contains(entry.id)).map((entry) => entry.path).nonNulls.toSet();
final byVolume = groupBy<String, StorageVolume?>(paths, androidFileUtils.getStorageVolume);
final jsonMap = Map.fromEntries(byVolume.entries.map((kv) {
final volume = kv.key?.path;
@ -66,7 +68,7 @@ class Favourites with ChangeNotifier {
final rootLength = volume.length;
final relativePaths = kv.value.map((v) => v.substring(rootLength)).toList();
return MapEntry(volume, relativePaths);
}).whereNotNull());
}).nonNulls);
return jsonMap.isNotEmpty ? jsonMap : null;
}

View file

@ -35,7 +35,7 @@ class OrFilter extends CollectionFilter {
factory OrFilter.fromMap(Map<String, dynamic> json) {
return OrFilter(
(json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet(),
(json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(),
reversed: json['reversed'] ?? false,
);
}

View file

@ -5,6 +5,10 @@ import 'package:flutter/painting.dart';
class HighlightInfo extends ChangeNotifier {
final EventBus eventBus = EventBus();
HighlightInfo() {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
}
void trackItem<T>(
T? item, {
TrackPredicate? predicate,

View file

@ -4,6 +4,7 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';
class MultiPageInfo {
final AvesEntry mainEntry;
@ -18,7 +19,7 @@ class MultiPageInfo {
required List<SinglePageInfo> pages,
}) : _pages = pages {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
LeakTracking.dispatchObjectCreated(
library: 'aves',
className: '$MultiPageInfo',
object: this,
@ -44,7 +45,7 @@ class MultiPageInfo {
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
LeakTracking.dispatchObjectDisposed(object: this);
}
_transientEntries.forEach((entry) => entry.dispose());
}

View file

@ -210,7 +210,7 @@ class MetadataFieldNamingProcessor extends NamingProcessor {
}
@override
Set<MetadataField> getRequiredFields() => {field}.whereNotNull().toSet();
Set<MetadataField> getRequiredFields() => {field}.nonNulls.toSet();
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
@ -268,7 +268,7 @@ class HashNamingProcessor extends NamingProcessor {
}
@override
Set<MetadataField> getRequiredFields() => {function}.whereNotNull().toSet();
Set<MetadataField> getRequiredFields() => {function}.nonNulls.toSet();
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {

View file

@ -9,6 +9,7 @@ class Query extends ChangeNotifier {
final StreamController<bool> _enabledStreamController = StreamController.broadcast();
Query({required bool enabled, required String? initialValue}) {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
_enabled = enabled;
if (initialValue != null && initialValue.isNotEmpty) {
_enabled = true;

View file

@ -9,6 +9,10 @@ class Selection<T> extends ChangeNotifier {
Set<T> get selectedItems => _selectedItems;
Selection() {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
}
void browse() {
if (!_isSelecting) return;
_isSelecting = false;

View file

@ -1,13 +1,17 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
mixin AppSettings on SettingsAccess {
static const int recentFilterHistoryMax = 20;
void initAppSettings() {
vaults.addListener(_onVaultsChanged);
}
bool get hasAcceptedTerms => getBool(SettingKeys.hasAcceptedTermsKey) ?? SettingsDefaults.hasAcceptedTerms;
set hasAcceptedTerms(bool newValue) => set(SettingKeys.hasAcceptedTermsKey, newValue);
@ -99,15 +103,32 @@ mixin AppSettings on SettingsAccess {
set entryRenamingPattern(String newValue) => set(SettingKeys.entryRenamingPatternKey, newValue);
List<int>? get topEntryIds => getStringList(SettingKeys.topEntryIdsKey)?.map(int.tryParse).whereNotNull().toList();
List<int>? get topEntryIds => getStringList(SettingKeys.topEntryIdsKey)?.map(int.tryParse).nonNulls.toList();
set topEntryIds(List<int>? newValue) => set(SettingKeys.topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList());
set topEntryIds(List<int>? newValue) => set(SettingKeys.topEntryIdsKey, newValue?.map((id) => id.toString()).nonNulls.toList());
List<String> get recentDestinationAlbums => getStringList(SettingKeys.recentDestinationAlbumsKey) ?? [];
set recentDestinationAlbums(List<String> newValue) => set(SettingKeys.recentDestinationAlbumsKey, newValue.take(recentFilterHistoryMax).toList());
List<CollectionFilter> get recentTags => (getStringList(SettingKeys.recentTagsKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList();
// recent tags
set recentTags(List<CollectionFilter> newValue) => set(SettingKeys.recentTagsKey, newValue.take(recentFilterHistoryMax).map((filter) => filter.toJson()).toList());
List<CollectionFilter> get _recentTags => (getStringList(SettingKeys.recentTagsKey) ?? []).map(CollectionFilter.fromJson).nonNulls.toList();
set _recentTags(List<CollectionFilter> newValue) => set(SettingKeys.recentTagsKey, newValue.take(recentFilterHistoryMax).map((filter) => filter.toJson()).toList());
// when vaults are unlocked, recent tags are transient and not persisted
List<CollectionFilter>? _protectedRecentTags;
List<CollectionFilter> get recentTags => vaults.needProtection ? _protectedRecentTags ?? List.of(_recentTags) : _recentTags;
set recentTags(List<CollectionFilter> newValue) {
if (vaults.needProtection) {
_protectedRecentTags = newValue;
} else {
_recentTags = newValue;
}
}
void _onVaultsChanged() => _protectedRecentTags = null;
}

View file

@ -1,7 +1,6 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
mixin FilterGridsSettings on SettingsAccess {
AlbumChipGroupFactor get albumGroupFactor => getEnumOrDefault(SettingKeys.albumGroupFactorKey, SettingsDefaults.albumGroupFactor, AlbumChipGroupFactor.values);
@ -48,7 +47,7 @@ mixin FilterGridsSettings on SettingsAccess {
set tagSortReverse(bool newValue) => set(SettingKeys.tagSortReverseKey, newValue);
Set<CollectionFilter> get pinnedFilters => (getStringList(SettingKeys.pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
Set<CollectionFilter> get pinnedFilters => (getStringList(SettingKeys.pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).nonNulls.toSet();
set pinnedFilters(Set<CollectionFilter> newValue) => set(SettingKeys.pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());

View file

@ -1,7 +1,6 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
mixin NavigationSettings on SettingsAccess {
bool get mustBackTwiceToExit => getBool(SettingKeys.mustBackTwiceToExitKey) ?? SettingsDefaults.mustBackTwiceToExit;
@ -14,7 +13,7 @@ mixin NavigationSettings on SettingsAccess {
HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).nonNulls.toSet();
String? get homeCustomExplorerPath => getString(SettingKeys.homeCustomExplorerPathKey);

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