Merge branch 'develop'
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 2663184aa79047d0a33a14a3b607954f8fdd8730
|
||||
Subproject commit 603104015dd692ea3403755b55d07813d5cf8965
|
4
.github/workflows/dependency-review.yml
vendored
|
@ -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
|
||||
|
|
10
.github/workflows/quality-check.yml
vendored
|
@ -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}}"
|
||||
|
|
13
.github/workflows/release.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/scorecards.yml
vendored
|
@ -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
|
||||
|
|
20
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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 '../..'
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
12
android/app/src/main/res/values-b+en+Shaw/strings.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -20,8 +20,8 @@ android {
|
|||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
5
fastlane/metadata/android/en-Shaw/full_description.txt
Normal 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>.
|
BIN
fastlane/metadata/android/en-Shaw/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 280 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 496 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 194 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 325 KiB |
BIN
fastlane/metadata/android/en-Shaw/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 336 KiB |
1
fastlane/metadata/android/en-Shaw/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
𐑜𐑨𐑤𐑼𐑦 𐑯 𐑥𐑧𐑑𐑩𐑛𐑱𐑑𐑩 𐑦𐑒𐑕𐑐𐑤𐑹𐑼
|
5
fastlane/metadata/android/en-US/changelogs/136.txt
Normal 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
|
5
fastlane/metadata/android/en-US/changelogs/13601.txt
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": "УСТАНАВІЦЬ ШПАЛЕРЫ",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "جێبەجێ کردن",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {},
|
||||
|
|
|
@ -229,14 +229,8 @@
|
|||
"@videoLoopModeShortOnly": {},
|
||||
"videoLoopModeAlways": "Πάντα",
|
||||
"@videoLoopModeAlways": {},
|
||||
"videoControlsPlay": "Αναπαραγωγή",
|
||||
"@videoControlsPlay": {},
|
||||
"videoControlsPlaySeek": "Αναπαραγωγή & πίσω/μπροστά",
|
||||
"@videoControlsPlaySeek": {},
|
||||
"videoControlsPlayOutside": "Άνοιγμα με άλλη εφαρμογή",
|
||||
"@videoControlsPlayOutside": {},
|
||||
"videoControlsNone": "Καμία επιλογή",
|
||||
"@videoControlsNone": {},
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
|
|
|
@ -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
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 l’image suivante",
|
||||
"@videoActionShowNextFrame": {},
|
||||
"videoActionShowPreviousFrame": "Montrer l’image précédente",
|
||||
"@videoActionShowPreviousFrame": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ध्वनि के साथ चलाए",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -217,14 +217,8 @@
|
|||
"@videoLoopModeShortOnly": {},
|
||||
"videoLoopModeAlways": "常にループ再生",
|
||||
"@videoLoopModeAlways": {},
|
||||
"videoControlsPlay": "再生",
|
||||
"@videoControlsPlay": {},
|
||||
"videoControlsPlaySeek": "再生&早送り/早戻し",
|
||||
"@videoControlsPlaySeek": {},
|
||||
"videoControlsPlayOutside": "他のプレイヤーで開く",
|
||||
"@videoControlsPlayOutside": {},
|
||||
"videoControlsNone": "なし",
|
||||
"@videoControlsNone": {},
|
||||
"mapStyleGoogleNormal": "Google マップ",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google マップ(ハイブリッド)",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "မှိန်သွားခြင်း",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -167,14 +167,8 @@
|
|||
"@unitSystemImperial": {},
|
||||
"videoLoopModeShortOnly": "เฉพาะคลิปสั้น",
|
||||
"@videoLoopModeShortOnly": {},
|
||||
"videoControlsPlay": "เล่น",
|
||||
"@videoControlsPlay": {},
|
||||
"videoControlsPlaySeek": "เล่นและกรอหลัง/หน้า",
|
||||
"@videoControlsPlaySeek": {},
|
||||
"videoControlsPlayOutside": "เปิดด้วยตัวเล่นอื่น",
|
||||
"@videoControlsPlayOutside": {},
|
||||
"videoControlsNone": "ไม่มี",
|
||||
"@videoControlsNone": {},
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -229,14 +229,8 @@
|
|||
"@videoLoopModeShortOnly": {},
|
||||
"videoLoopModeAlways": "始终",
|
||||
"@videoLoopModeAlways": {},
|
||||
"videoControlsPlay": "播放",
|
||||
"@videoControlsPlay": {},
|
||||
"videoControlsPlaySeek": "播放和步进/步退",
|
||||
"@videoControlsPlaySeek": {},
|
||||
"videoControlsPlayOutside": "用其他播放器打开",
|
||||
"@videoControlsPlayOutside": {},
|
||||
"videoControlsNone": "无",
|
||||
"@videoControlsNone": {},
|
||||
"mapStyleGoogleNormal": "Google 地图",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google 地图 (卫星图像)",
|
||||
|
|
|
@ -205,14 +205,8 @@
|
|||
"@videoLoopModeShortOnly": {},
|
||||
"videoLoopModeAlways": "總是",
|
||||
"@videoLoopModeAlways": {},
|
||||
"videoControlsPlay": "播放",
|
||||
"@videoControlsPlay": {},
|
||||
"videoControlsPlaySeek": "播放和後退/前進",
|
||||
"@videoControlsPlaySeek": {},
|
||||
"videoControlsPlayOutside": "用其他播放器打開",
|
||||
"@videoControlsPlayOutside": {},
|
||||
"videoControlsNone": "無",
|
||||
"@videoControlsNone": {},
|
||||
"mapStyleGoogleNormal": "Google 地圖",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleTerrain": "Google 地圖 (地形)",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -146,7 +146,7 @@ class Covers {
|
|||
if (colorValue != null) 'color': colorValue,
|
||||
};
|
||||
})
|
||||
.whereNotNull()
|
||||
.nonNulls
|
||||
.toList();
|
||||
return jsonList.isNotEmpty ? jsonList : null;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|