Merge branch 'develop'
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 135454af32477f815a7525073027a3ff9eff1bfd
|
||||
Subproject commit 9944297138845a94256f1cf37beb88ff9a8e811a
|
24
CHANGELOG.md
|
@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="v1.8.0"></a>[v1.8.0] - 2023-02-20
|
||||
|
||||
### Added
|
||||
|
||||
- Vaults
|
||||
- Viewer: overlay details expand/collapse on tap
|
||||
- Viewer: export actions available as quick actions
|
||||
- Slideshow: added settings quick action
|
||||
- TV: improved support for Info
|
||||
- Basque translation (thanks Aitor Salaberria)
|
||||
|
||||
### Changed
|
||||
|
||||
- disabling the recycle bin will delete forever items in it
|
||||
- remember pin status of albums becoming empty
|
||||
- allow setting dates before 1970/01/01
|
||||
- upgraded Flutter to stable v3.7.3
|
||||
|
||||
### Fixed
|
||||
|
||||
- SD card access grant on Android Lollipop
|
||||
- copying to SD card in some cases
|
||||
- sharing SD card files referred by `file` URI
|
||||
|
||||
## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18
|
||||
|
||||
### Added
|
||||
|
|
|
@ -18,6 +18,9 @@ if (localPropertiesFile.exists()) {
|
|||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
// Keys
|
||||
|
@ -181,10 +184,11 @@ dependencies {
|
|||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.5'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha04'
|
||||
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||
|
|
|
@ -19,7 +19,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
android:required="false" />
|
||||
|
||||
<!--
|
||||
Scoped storage on Android 10 is inconvenient because users need to confirm edition on each individual file.
|
||||
Scoped storage on Android 10 (API 29) is inconvenient because users need to confirm edition on each individual file.
|
||||
So we request `WRITE_EXTERNAL_STORAGE` until Android 10 (API 29), and enable `requestLegacyExternalStorage`
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
@ -32,28 +32,35 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
android:maxSdkVersion="29"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<!-- to analyze media in a service -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- to fetch map tiles -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_MEDIA"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- to show foreground service progress via notification -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- to access media with original metadata with scoped storage (Android >=10) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
|
||||
<!-- to change wallpaper -->
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||
<!-- to unlock vaults (API >=28) -->
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<!-- TODO TLAD remove this permission when this is fixed: https://github.com/flutter/flutter/issues/42451 -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- for API <26 -->
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
<uses-permission
|
||||
android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
|
||||
android:maxSdkVersion="25" />
|
||||
|
||||
<!-- allow install on API 19, but Google Maps is from API 20 -->
|
||||
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps" />
|
||||
<!--
|
||||
allow install on API 19, despite the `minSdkVersion` declared in dependencies:
|
||||
- Google Maps is from API 20
|
||||
- the Security library is from API 21
|
||||
-->
|
||||
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps, androidx.security:security-crypto" />
|
||||
|
||||
<!-- from Android 11, we should define <queries> to make other apps visible to this app -->
|
||||
<queries>
|
||||
|
@ -75,12 +82,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
android:allowBackup="true"
|
||||
android:appCategory="image"
|
||||
android:banner="@drawable/banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/full_backup_content"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
tools:targetApi="o">
|
||||
tools:targetApi="s">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
|
|
@ -37,12 +37,16 @@ class AnalysisService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
initChannels(this)
|
||||
try {
|
||||
initChannels(this)
|
||||
|
||||
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
serviceHandler = ServiceHandler(looper)
|
||||
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
serviceHandler = ServiceHandler(looper)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "failed to initialize service", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +83,10 @@ class AnalysisService : Service() {
|
|||
}
|
||||
|
||||
private fun initChannels(context: Context) {
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
val engine = flutterEngine
|
||||
engine ?: throw Exception("Flutter engine is not initialized")
|
||||
|
||||
val messenger = engine.dartExecutor
|
||||
|
||||
// channels for analysis
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class HomeWidgetSettingsActivity : MainActivity() {
|
||||
|
@ -28,8 +29,12 @@ class HomeWidgetSettingsActivity : MainActivity() {
|
|||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor
|
||||
MethodChannel(messenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"configure" -> {
|
||||
|
@ -42,9 +47,9 @@ class HomeWidgetSettingsActivity : MainActivity() {
|
|||
}
|
||||
|
||||
private fun saveWidget() {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||
val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId)
|
||||
HomeWidgetProvider().onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, widgetInfo)
|
||||
HomeWidgetProvider().onAppWidgetOptionsChanged(this, appWidgetManager, appWidgetId, widgetInfo)
|
||||
|
||||
val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, intent)
|
||||
|
|
|
@ -90,7 +90,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
val messenger = flutterEngine!!.dartExecutor
|
||||
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
|
||||
try {
|
||||
val bytes = suspendCoroutine { cont ->
|
||||
val bytes = suspendCoroutine<Any?> { cont ->
|
||||
defaultScope.launch {
|
||||
FlutterUtils.runOnUiThread {
|
||||
channel.invokeMethod("drawWidget", hashMapOf(
|
||||
|
@ -194,7 +194,10 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
}
|
||||
|
||||
private fun initChannels(context: Context) {
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
val engine = flutterEngine
|
||||
engine ?: throw Exception("Flutter engine is not initialized")
|
||||
|
||||
val messenger = engine.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
|
|
|
@ -25,14 +25,15 @@ import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
|||
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
open class MainActivity : FlutterActivity() {
|
||||
open class MainActivity : FlutterFragmentActivity() {
|
||||
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
||||
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
|
||||
private lateinit var intentStreamHandler: IntentStreamHandler
|
||||
|
@ -68,8 +69,12 @@ open class MainActivity : FlutterActivity() {
|
|||
// .build()
|
||||
// )
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor
|
||||
|
||||
// notification: platform -> dart
|
||||
analysisStreamHandler = AnalysisStreamHandler().apply {
|
||||
|
@ -99,6 +104,7 @@ open class MainActivity : FlutterActivity() {
|
|||
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
MethodChannel(messenger, SecurityHandler.CHANNEL).setMethodCallHandler(SecurityHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
|
@ -172,7 +178,18 @@ open class MainActivity : FlutterActivity() {
|
|||
mediaSessionHandler.dispose()
|
||||
mediaStoreChangeStreamHandler.dispose()
|
||||
settingsChangeStreamHandler.dispose()
|
||||
super.onDestroy()
|
||||
try {
|
||||
super.onDestroy()
|
||||
} catch (e: Exception) {
|
||||
// on Android 11, app may crash as follows:
|
||||
// `Fatal Exception:`
|
||||
// `java.lang.RuntimeException: Unable to destroy activity {deckers.thibault.aves/deckers.thibault.aves.MainActivity}:`
|
||||
// `java.lang.IllegalArgumentException: NetworkCallback was not registered`
|
||||
// related to this error:
|
||||
// `Package android does not belong to 10162`
|
||||
// cf https://issuetracker.google.com/issues/175055271
|
||||
Log.e(LOG_TAG, "failed while destroying activity", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
|
@ -182,6 +199,7 @@ open class MainActivity : FlutterActivity() {
|
|||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
|
||||
DELETE_SINGLE_PERMISSION_REQUEST,
|
||||
|
@ -244,7 +262,7 @@ open class MainActivity : FlutterActivity() {
|
|||
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
|
||||
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
|
||||
// MIME type is optional
|
||||
val type = intent.type ?: intent.resolveType(context)
|
||||
val type = intent.type ?: intent.resolveType(this)
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
|
||||
INTENT_DATA_KEY_MIME_TYPE to type,
|
||||
|
@ -314,7 +332,7 @@ open class MainActivity : FlutterActivity() {
|
|||
private fun submitPickedItems(call: MethodCall) {
|
||||
val pickedUris = call.argument<List<String>>("uris")
|
||||
if (pickedUris != null && pickedUris.isNotEmpty()) {
|
||||
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) }
|
||||
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
|
||||
val intent = Intent().apply {
|
||||
val firstUri = toUri(pickedUris.first())
|
||||
if (pickedUris.size == 1) {
|
||||
|
|
|
@ -72,7 +72,10 @@ class SearchSuggestionsProvider : ContentProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
val engine = flutterEngine
|
||||
engine ?: throw Exception("Flutter engine is not initialized")
|
||||
|
||||
val messenger = engine.dartExecutor
|
||||
val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
@ -18,11 +17,12 @@ import deckers.thibault.aves.utils.FlutterUtils
|
|||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class WallpaperActivity : FlutterActivity() {
|
||||
class WallpaperActivity : FlutterFragmentActivity() {
|
||||
private lateinit var intentDataMap: MutableMap<String, Any?>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -36,8 +36,33 @@ class WallpaperActivity : FlutterActivity() {
|
|||
Log.i(LOG_TAG, "onCreate intent extras=$it")
|
||||
}
|
||||
intentDataMap = extractIntentData(intent)
|
||||
}
|
||||
|
||||
initChannels(this)
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
val messenger = flutterEngine.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
|
||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
|
||||
|
||||
// intent handling
|
||||
// detail fetch: dart -> platform
|
||||
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -54,32 +79,6 @@ class WallpaperActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun initChannels(activity: Activity) {
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(activity))
|
||||
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(activity))
|
||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(activity))
|
||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(activity))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(activity))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(activity))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(activity))
|
||||
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(activity))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(activity))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(activity, args) }
|
||||
|
||||
// intent handling
|
||||
// detail fetch: dart -> platform
|
||||
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
|
||||
}
|
||||
|
||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
|
@ -94,7 +93,7 @@ class WallpaperActivity : FlutterActivity() {
|
|||
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
|
||||
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
|
||||
// MIME type is optional
|
||||
val type = intent.type ?: intent.resolveType(context)
|
||||
val type = intent.type ?: intent.resolveType(this)
|
||||
return hashMapOf(
|
||||
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
|
||||
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,
|
||||
|
|
|
@ -21,7 +21,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
|||
when (call.method) {
|
||||
"canManageMedia" -> safe(call, result, ::canManageMedia)
|
||||
"getCapabilities" -> safe(call, result, ::getCapabilities)
|
||||
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
|
||||
"getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
|
||||
"getLocales" -> safe(call, result, ::getLocales)
|
||||
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
|
||||
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
|
||||
|
@ -41,9 +41,10 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
|||
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
||||
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
|
||||
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
|
||||
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
||||
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
|
||||
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
|
||||
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
|
||||
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
||||
"hasGeocoder" to Geocoder.isPresent(),
|
||||
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S),
|
||||
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
|
||||
|
@ -52,8 +53,8 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
|||
)
|
||||
}
|
||||
|
||||
private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(TimeZone.getDefault().id)
|
||||
private fun getDefaultTimeZoneRawOffsetMillis(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(TimeZone.getDefault().rawOffset)
|
||||
}
|
||||
|
||||
private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
|
|
|
@ -35,6 +35,15 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
|||
}
|
||||
|
||||
fun dispose() {
|
||||
unregisterNoisyAudioReceiver()
|
||||
}
|
||||
|
||||
private fun registerNoisyAudioReceiver() {
|
||||
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
|
||||
isNoisyAudioReceiverRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterNoisyAudioReceiver() {
|
||||
if (isNoisyAudioReceiverRegistered) {
|
||||
context.unregisterReceiver(noisyAudioReceiver)
|
||||
isNoisyAudioReceiverRegistered = false
|
||||
|
@ -51,14 +60,17 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
|||
|
||||
private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val title = call.argument<String>("title")
|
||||
val title = call.argument<String>("title") ?: uri?.toString()
|
||||
val durationMillis = call.argument<Number>("durationMillis")?.toLong()
|
||||
val stateString = call.argument<String>("state")
|
||||
val positionMillis = call.argument<Number>("positionMillis")?.toLong()
|
||||
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
|
||||
|
||||
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) {
|
||||
result.error("update-args", "missing arguments", null)
|
||||
result.error(
|
||||
"updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" +
|
||||
", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed", null
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -67,7 +79,7 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
|||
STATE_PAUSED -> PlaybackStateCompat.STATE_PAUSED
|
||||
STATE_PLAYING -> PlaybackStateCompat.STATE_PLAYING
|
||||
else -> {
|
||||
result.error("update-state", "unknown state=$stateString", null)
|
||||
result.error("updateSession-state", "unknown state=$stateString", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -90,39 +102,41 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
|||
.build()
|
||||
|
||||
FlutterUtils.runOnUiThread {
|
||||
if (session == null) {
|
||||
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
||||
val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
|
||||
session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
|
||||
setCallback(mediaCommandHandler)
|
||||
try {
|
||||
if (session == null) {
|
||||
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
||||
val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
|
||||
session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
|
||||
setCallback(mediaCommandHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
session!!.apply {
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
||||
.build()
|
||||
setMetadata(metadata)
|
||||
setPlaybackState(playbackState)
|
||||
if (!isActive) {
|
||||
isActive = true
|
||||
session!!.apply {
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
||||
.build()
|
||||
setMetadata(metadata)
|
||||
setPlaybackState(playbackState)
|
||||
if (!isActive) {
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
|
||||
if (!wasPlaying && isPlaying) {
|
||||
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
|
||||
isNoisyAudioReceiverRegistered = true
|
||||
} else if (wasPlaying && !isPlaying) {
|
||||
context.unregisterReceiver(noisyAudioReceiver)
|
||||
isNoisyAudioReceiverRegistered = false
|
||||
val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
|
||||
if (!wasPlaying && isPlaying) {
|
||||
registerNoisyAudioReceiver()
|
||||
} else if (wasPlaying && !isPlaying) {
|
||||
unregisterNoisyAudioReceiver()
|
||||
}
|
||||
wasPlaying = isPlaying
|
||||
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("updateSession-exception", e.message, e.stackTraceToString())
|
||||
}
|
||||
wasPlaying = isPlaying
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun releaseSession(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
|
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
@ -195,6 +196,8 @@ private class MetadataOpCallback(
|
|||
} else {
|
||||
"$errorCodeBase-mp4largeother"
|
||||
}
|
||||
} else if (throwable is FileNotFoundException) {
|
||||
"$errorCodeBase-filenotfound"
|
||||
} else {
|
||||
"$errorCodeBase-failure"
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
|
|||
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
|
||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp
|
||||
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
|
||||
|
@ -104,8 +104,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
||||
"getIptc" -> ioScope.launch { safe(call, result, ::getIptc) }
|
||||
"getXmp" -> ioScope.launch { safe(call, result, ::getXmp) }
|
||||
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentResolverProp) }
|
||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentResolverProp) }
|
||||
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
|
||||
else -> result.notImplemented()
|
||||
|
@ -1047,10 +1047,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(xmpStrings)
|
||||
}
|
||||
|
||||
private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) {
|
||||
private fun hasContentProp(call: MethodCall, result: MethodChannel.Result) {
|
||||
val prop = call.argument<String>("prop")
|
||||
if (prop == null) {
|
||||
result.error("hasContentResolverProp-args", "missing arguments", null)
|
||||
result.error("hasContentProp-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1058,27 +1058,27 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
when (prop) {
|
||||
"owner_package_name" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
else -> {
|
||||
result.error("hasContentResolverProp-unknown", "unknown property=$prop", null)
|
||||
result.error("hasContentProp-unknown", "unknown property=$prop", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) {
|
||||
private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val prop = call.argument<String>("prop")
|
||||
if (mimeType == null || uri == null || prop == null) {
|
||||
result.error("getContentResolverProp-args", "missing arguments", null)
|
||||
result.error("getContentPropValue-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val value = context.queryContentResolverProp(uri, mimeType, prop)
|
||||
val value = context.queryContentPropValue(uri, mimeType, prop)
|
||||
result.success(value?.toString())
|
||||
} catch (e: Exception) {
|
||||
result.error("getContentResolverProp-query", "failed to query prop for uri=$uri", e.message)
|
||||
result.error("getContentPropValue-query", "failed to query prop for uri=$uri", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
|
||||
class SecurityHandler(private val context: Context) : MethodCallHandler {
|
||||
private var sharedPreferences: SharedPreferences? = null
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"writeValue" -> safe(call, result, ::writeValue)
|
||||
"readValue" -> safe(call, result, ::readValue)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStore(): SharedPreferences {
|
||||
if (sharedPreferences == null) {
|
||||
val mainKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
sharedPreferences = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
FILENAME,
|
||||
mainKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
return sharedPreferences!!
|
||||
}
|
||||
|
||||
private fun writeValue(call: MethodCall, result: MethodChannel.Result) {
|
||||
val key = call.argument<String>("key")
|
||||
val value = call.argument<Any?>("value")
|
||||
if (key == null) {
|
||||
result.error("writeValue-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
with(getStore().edit()) {
|
||||
when (value) {
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
is Int -> putInt(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
is String -> putString(key, value)
|
||||
null -> remove(key)
|
||||
else -> {
|
||||
result.error("writeValue-type", "unsupported type for value=$value", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
apply()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun readValue(call: MethodCall, result: MethodChannel.Result) {
|
||||
val key = call.argument<String>("key")
|
||||
if (key == null) {
|
||||
result.error("readValue-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
result.success(getStore().all[key])
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL = "deckers.thibault/aves/security"
|
||||
const val FILENAME = "secret_shared_prefs"
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import androidx.core.os.EnvironmentCompat
|
|||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.PermissionManager
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import deckers.thibault.aves.utils.StorageUtils.getPrimaryVolumePath
|
||||
import deckers.thibault.aves.utils.StorageUtils.getVolumePaths
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
|
@ -25,6 +26,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
|
||||
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
|
||||
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
|
||||
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
|
||||
"getInaccessibleDirectories" -> ioScope.launch { safe(call, result, ::getInaccessibleDirectories) }
|
||||
|
@ -88,6 +90,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(volumes)
|
||||
}
|
||||
|
||||
private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(StorageUtils.getVaultRoot(context))
|
||||
}
|
||||
|
||||
private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
|
||||
val path = call.argument<String>("path")
|
||||
if (path == null) {
|
||||
|
|
|
@ -109,7 +109,7 @@ class ThumbnailFetcher internal constructor(
|
|||
} else {
|
||||
@Suppress("deprecation")
|
||||
var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null)
|
||||
// from Android 10, returned thumbnail is already rotated according to EXIF orientation
|
||||
// from Android 10 (API 29), returned thumbnail is already rotated according to EXIF orientation
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
|
||||
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
|
|||
result.success(true)
|
||||
}
|
||||
|
||||
override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
|
||||
private fun setWindowFlag(call: MethodCall, result: MethodChannel.Result, flag: Int) {
|
||||
val on = call.argument<Boolean>("on")
|
||||
if (on == null) {
|
||||
result.error("keepOn-args", "missing arguments", null)
|
||||
|
@ -20,8 +20,6 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
|
|||
}
|
||||
|
||||
val window = activity.window
|
||||
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
|
||||
val old = (window.attributes.flags and flag) != 0
|
||||
if (old != on) {
|
||||
if (on) {
|
||||
|
@ -33,6 +31,14 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
|
|||
result.success(null)
|
||||
}
|
||||
|
||||
override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
|
||||
setWindowFlag(call, result, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
override fun secureScreen(call: MethodCall, result: MethodChannel.Result) {
|
||||
setWindowFlag(call, result, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
|
||||
val orientation = call.argument<Int>("orientation")
|
||||
if (orientation == null) {
|
||||
|
|
|
@ -13,6 +13,10 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
|
|||
result.success(null)
|
||||
}
|
||||
|
||||
override fun secureScreen(call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(false)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
|
|||
when (call.method) {
|
||||
"isActivity" -> Coresult.safe(call, result, ::isActivity)
|
||||
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
|
||||
"secureScreen" -> Coresult.safe(call, result, ::secureScreen)
|
||||
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
|
||||
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
|
||||
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
|
||||
|
@ -25,6 +26,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
|
|||
|
||||
abstract fun keepScreenOn(call: MethodCall, result: MethodChannel.Result)
|
||||
|
||||
abstract fun secureScreen(call: MethodCall, result: MethodChannel.Result)
|
||||
|
||||
private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
var locked = false
|
||||
try {
|
||||
|
|
|
@ -139,8 +139,8 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
var destinationDir = arguments["destinationPath"] as String?
|
||||
val mimeType = arguments["mimeType"] as String?
|
||||
val width = arguments["width"] as Int?
|
||||
val height = arguments["height"] as Int?
|
||||
val width = (arguments["width"] as Number?)?.toInt()
|
||||
val height = (arguments["height"] as Number?)?.toInt()
|
||||
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
|
||||
error("export-args", "missing arguments", null)
|
||||
|
|
|
@ -15,7 +15,7 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
|
|||
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
|
||||
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
|
||||
import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader
|
||||
import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp
|
||||
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
|
@ -130,7 +130,7 @@ object XMP {
|
|||
) {
|
||||
if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
val xmpBytes = context.queryContentResolverProp(uri, mimeType, MediaStore.MediaColumns.XMP)
|
||||
val xmpBytes = context.queryContentPropValue(uri, mimeType, MediaStore.MediaColumns.XMP)
|
||||
if (xmpBytes is ByteArray && xmpBytes.size > 0) {
|
||||
val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS)
|
||||
processXmp(xmpMeta)
|
||||
|
@ -170,6 +170,11 @@ object XMP {
|
|||
}
|
||||
}
|
||||
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
|
||||
|
||||
// TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent,
|
||||
// because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`,
|
||||
// so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes.
|
||||
|
||||
IsoFile(channel, boxParser).use { isoFile ->
|
||||
isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
|
||||
val boxSize = box.size
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
|||
import java.io.IOException
|
||||
|
||||
class SourceEntry {
|
||||
private val origin: Int
|
||||
val uri: Uri // content or file URI
|
||||
var path: String? = null // best effort to get local path
|
||||
private val sourceMimeType: String
|
||||
|
@ -48,12 +49,14 @@ class SourceEntry {
|
|||
|
||||
private var foundExif: Boolean = false
|
||||
|
||||
constructor(uri: Uri, sourceMimeType: String) {
|
||||
constructor(origin: Int, uri: Uri, sourceMimeType: String) {
|
||||
this.origin = origin
|
||||
this.uri = uri
|
||||
this.sourceMimeType = sourceMimeType
|
||||
}
|
||||
|
||||
constructor(map: FieldMap) {
|
||||
origin = map["origin"] as Int
|
||||
uri = Uri.parse(map["uri"] as String)
|
||||
path = map["path"] as String?
|
||||
sourceMimeType = map["sourceMimeType"] as String
|
||||
|
@ -77,6 +80,7 @@ class SourceEntry {
|
|||
|
||||
fun toMap(): FieldMap {
|
||||
return hashMapOf(
|
||||
"origin" to origin,
|
||||
"uri" to uri.toString(),
|
||||
"path" to path,
|
||||
"sourceMimeType" to sourceMimeType,
|
||||
|
@ -249,13 +253,15 @@ class SourceEntry {
|
|||
|
||||
private fun fillByTiffDecode(context: Context) {
|
||||
try {
|
||||
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() ?: return
|
||||
val options = TiffBitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
|
||||
val fd = pfd.detachFd()
|
||||
val options = TiffBitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||
width = options.outWidth
|
||||
height = options.outHeight
|
||||
}
|
||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||
width = options.outWidth
|
||||
height = options.outHeight
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
|
@ -267,5 +273,11 @@ class SourceEntry {
|
|||
is Int -> o.toLong()
|
||||
else -> o as? Long
|
||||
}
|
||||
|
||||
// should match `EntryOrigins` on the Dart side
|
||||
const val ORIGIN_MEDIA_STORE_CONTENT = 0
|
||||
const val ORIGIN_UNKNOWN_CONTENT = 1
|
||||
const val ORIGIN_FILE = 2
|
||||
const val ORIGIN_VAULT = 3
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ internal class ContentImageProvider : ImageProvider() {
|
|||
}
|
||||
|
||||
val fields: FieldMap = hashMapOf(
|
||||
"origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
|
||||
"uri" to uri.toString(),
|
||||
"sourceMimeType" to mimeType,
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.SourceEntry
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import java.io.File
|
||||
|
@ -15,7 +16,7 @@ internal class FileImageProvider : ImageProvider() {
|
|||
return
|
||||
}
|
||||
|
||||
val entry = SourceEntry(uri, sourceMimeType)
|
||||
val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType)
|
||||
|
||||
val path = uri.path
|
||||
if (path != null) {
|
||||
|
@ -52,6 +53,19 @@ internal class FileImageProvider : ImageProvider() {
|
|||
throw Exception("failed to delete entry with uri=$uri path=$path")
|
||||
}
|
||||
|
||||
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
|
||||
try {
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
newFields["dateModifiedSecs"] = file.lastModified() / 1000
|
||||
newFields["sizeBytes"] = file.length()
|
||||
}
|
||||
callback.onSuccess(newFields)
|
||||
} catch (e: SecurityException) {
|
||||
callback.onFailure(e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>()
|
||||
}
|
||||
|
|
|
@ -811,11 +811,6 @@ abstract class ImageProvider {
|
|||
fields: List<String>,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
if (dateMillis != null && dateMillis < 0) {
|
||||
callback.onFailure(Exception("dateMillis=$dateMillis cannot be negative"))
|
||||
return
|
||||
}
|
||||
|
||||
val success = editExif(context, path, uri, mimeType, callback) { exif ->
|
||||
when {
|
||||
dateMillis != null -> {
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
|
||||
|
@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.io.SyncFailedException
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.coroutines.Continuation
|
||||
|
@ -219,6 +221,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
|
||||
} else {
|
||||
var entryMap: FieldMap = hashMapOf(
|
||||
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
|
||||
"uri" to itemUri.toString(),
|
||||
"path" to cursor.getString(pathColumn),
|
||||
"sourceMimeType" to mimeType,
|
||||
|
@ -349,7 +352,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
} catch (securityException: SecurityException) {
|
||||
// even if the app has access permission granted on the containing directory,
|
||||
// the delete request may yield a `RecoverableSecurityException` on Android >=10
|
||||
// the delete request may yield a `RecoverableSecurityException` on API >=29
|
||||
// when the underlying file no longer exists and this is an orphaned entry in the Media Store
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) {
|
||||
Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException)
|
||||
|
@ -386,10 +389,12 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val entries = kv.value
|
||||
|
||||
val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER
|
||||
val toVault = StorageUtils.isInVault(activity, targetDir)
|
||||
val toAppDir = toBin || toVault
|
||||
|
||||
var effectiveTargetDir: String? = null
|
||||
var targetDirDocFile: DocumentFileCompat? = null
|
||||
if (!toBin) {
|
||||
if (!toAppDir) {
|
||||
effectiveTargetDir = targetDir
|
||||
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
|
||||
if (!File(targetDir).exists()) {
|
||||
|
@ -437,13 +442,20 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// - 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 {
|
||||
if (toBin) {
|
||||
val trashDir = StorageUtils.trashDirFor(activity, sourcePath)
|
||||
if (trashDir != null) {
|
||||
effectiveTargetDir = ensureTrailingSeparator(trashDir.path)
|
||||
targetDirDocFile = DocumentFileCompat.fromFile(trashDir)
|
||||
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)
|
||||
|
@ -462,6 +474,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType = mimeType,
|
||||
copy = copy,
|
||||
toBin = toBin,
|
||||
toVault = toVault,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -488,6 +501,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType: String,
|
||||
copy: Boolean,
|
||||
toBin: Boolean,
|
||||
toVault: Boolean,
|
||||
): FieldMap {
|
||||
val sourcePath = sourceFile.path
|
||||
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
|
||||
|
@ -512,7 +526,16 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
) { output: OutputStream -> sourceDocFile.copyTo(output) }
|
||||
) { output: OutputStream ->
|
||||
try {
|
||||
sourceDocFile.copyTo(output)
|
||||
} catch (e: SyncFailedException) {
|
||||
// The copied file is synced after writing, but it consistently fails in some cases
|
||||
// (e.g. copying to SD card on Xiaomi 2201117PG with Android 11).
|
||||
// It seems this failure can be safely ignored, as the new file is complete.
|
||||
Log.w(LOG_TAG, "sync failure after copying from uri=$sourceUri, path=$sourcePath to targetDir=$targetDir", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!copy) {
|
||||
// delete original entry
|
||||
|
@ -522,13 +545,21 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
|
||||
}
|
||||
}
|
||||
if (toBin) {
|
||||
return hashMapOf(
|
||||
return if (toBin) {
|
||||
hashMapOf(
|
||||
"trashed" to true,
|
||||
"trashPath" to targetPath,
|
||||
)
|
||||
} else if (toVault) {
|
||||
hashMapOf(
|
||||
"uri" to File(targetPath).toUri().toString(),
|
||||
"contentId" to null,
|
||||
"path" to targetPath,
|
||||
"origin" to SourceEntry.ORIGIN_VAULT,
|
||||
)
|
||||
} else {
|
||||
scanNewPath(activity, targetPath, mimeType)
|
||||
}
|
||||
return scanNewPath(activity, targetPath, mimeType)
|
||||
}
|
||||
|
||||
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
||||
|
@ -910,7 +941,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
private val VIDEO_PROJECTION = arrayOf(
|
||||
*BASE_PROJECTION,
|
||||
MediaColumns.DURATION,
|
||||
// `ORIENTATION` was only available for images before Android 10
|
||||
// `ORIENTATION` was only available for images before Android 10 (API 29)
|
||||
*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(
|
||||
MediaStore.MediaColumns.ORIENTATION,
|
||||
) else emptyArray()
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.content.ContentUris
|
|||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||
|
@ -30,7 +31,13 @@ object ContextUtils {
|
|||
return am.getRunningServices(Integer.MAX_VALUE).any { it.service.className == serviceClass.name }
|
||||
}
|
||||
|
||||
fun Context.queryContentResolverProp(uri: Uri, mimeType: String, prop: String): Any? {
|
||||
// `flag`: `DocumentsContract.Document.FLAG_SUPPORTS_COPY`, etc.
|
||||
fun Context.queryDocumentProviderFlag(docUri: Uri, flag: Int): Boolean {
|
||||
val flags = queryContentPropValue(docUri, "", DocumentsContract.Document.COLUMN_FLAGS) as Long?
|
||||
return if (flags != null) (flags.toInt() and flag) == flag else false
|
||||
}
|
||||
|
||||
fun Context.queryContentPropValue(uri: Uri, mimeType: String, column: String): Any? {
|
||||
var contentUri: Uri = uri
|
||||
if (StorageUtils.isMediaStoreContentUri(uri)) {
|
||||
uri.tryParseId()?.let { id ->
|
||||
|
@ -43,26 +50,26 @@ object ContextUtils {
|
|||
}
|
||||
}
|
||||
|
||||
// throws SQLiteException when the requested prop is not a known column
|
||||
val cursor = contentResolver.query(contentUri, arrayOf(prop), null, null, null)
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
throw Exception("failed to get cursor for contentUri=$contentUri")
|
||||
}
|
||||
|
||||
var value: Any? = null
|
||||
try {
|
||||
value = when (cursor.getType(0)) {
|
||||
Cursor.FIELD_TYPE_NULL -> null
|
||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
|
||||
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
|
||||
Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
|
||||
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
|
||||
else -> null
|
||||
val cursor = contentResolver.query(contentUri, arrayOf(column), null, null, null)
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
Log.w(LOG_TAG, "failed to get cursor for contentUri=$contentUri column=$column")
|
||||
} else {
|
||||
value = when (cursor.getType(0)) {
|
||||
Cursor.FIELD_TYPE_NULL -> null
|
||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
|
||||
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
|
||||
Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
|
||||
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
|
||||
else -> null
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri key=$prop", e)
|
||||
// throws SQLiteException/IllegalArgumentException when the requested prop is not a known column
|
||||
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri column=$column", e)
|
||||
}
|
||||
cursor.close()
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
|
||||
object MimeTypes {
|
||||
|
@ -153,47 +154,11 @@ object MimeTypes {
|
|||
// among other refs:
|
||||
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
|
||||
fun extensionFor(mimeType: String): String? = when (mimeType) {
|
||||
ARW -> ".arw"
|
||||
AVI, AVI_VND -> ".avi"
|
||||
AVIF -> ".avif"
|
||||
BMP -> ".bmp"
|
||||
CR2 -> ".cr2"
|
||||
CRW -> ".crw"
|
||||
DCR -> ".dcr"
|
||||
DJVU -> ".djvu"
|
||||
DNG -> ".dng"
|
||||
ERF -> ".erf"
|
||||
GIF -> ".gif"
|
||||
HEIC, HEIF -> ".heif"
|
||||
ICO -> ".ico"
|
||||
JPEG -> ".jpg"
|
||||
K25 -> ".k25"
|
||||
KDC -> ".kdc"
|
||||
MKV -> ".mkv"
|
||||
MOV -> ".mov"
|
||||
MP2T, MP2TS -> ".m2ts"
|
||||
MP4 -> ".mp4"
|
||||
MRW -> ".mrw"
|
||||
NEF -> ".nef"
|
||||
NRW -> ".nrw"
|
||||
OGV -> ".ogv"
|
||||
ORF -> ".orf"
|
||||
PEF -> ".pef"
|
||||
PNG -> ".png"
|
||||
PSD_VND, PSD_X -> ".psd"
|
||||
RAF -> ".raf"
|
||||
RAW -> ".raw"
|
||||
RW2 -> ".rw2"
|
||||
SR2 -> ".sr2"
|
||||
SRF -> ".srf"
|
||||
SRW -> ".srw"
|
||||
SVG -> ".svg"
|
||||
TIFF -> ".tiff"
|
||||
WBMP -> ".wbmp"
|
||||
WEBM -> ".webm"
|
||||
WEBP -> ".webp"
|
||||
X3F -> ".x3f"
|
||||
else -> null
|
||||
else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
|
||||
}
|
||||
|
||||
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
|
||||
|
|
|
@ -119,7 +119,7 @@ object PermissionManager {
|
|||
dirSet.add("")
|
||||
}
|
||||
} else {
|
||||
// request volume root until Android 10
|
||||
// request volume root until Android 10 (API 29)
|
||||
dirSet.add("")
|
||||
}
|
||||
dirsPerVolume[volumePath] = dirSet
|
||||
|
|
|
@ -39,28 +39,29 @@ object StorageUtils {
|
|||
|
||||
private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
|
||||
|
||||
private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+")
|
||||
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
|
||||
|
||||
const val TRASH_PATH_PLACEHOLDER = "#trash"
|
||||
|
||||
private fun isAppFile(context: Context, path: String): Boolean {
|
||||
val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
|
||||
return filesDirs.any { path.startsWith(it.path) }
|
||||
val dirs = context.getExternalFilesDirs(null).filterNotNull()
|
||||
return dirs.any { path.startsWith(it.path) }
|
||||
}
|
||||
|
||||
private fun appExternalFilesDirFor(context: Context, path: String): File? {
|
||||
val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
|
||||
val dirs = context.getExternalFilesDirs(null).filterNotNull()
|
||||
val volumePath = getVolumePath(context, path)
|
||||
return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.firstOrNull()
|
||||
return volumePath?.let { dirs.firstOrNull { it.startsWith(volumePath) } } ?: dirs.firstOrNull()
|
||||
}
|
||||
|
||||
fun trashDirFor(context: Context, path: String): File? {
|
||||
val filesDir = appExternalFilesDirFor(context, path)
|
||||
if (filesDir == null) {
|
||||
val externalFilesDir = appExternalFilesDirFor(context, path)
|
||||
if (externalFilesDir == null) {
|
||||
Log.e(LOG_TAG, "failed to find external files dir for path=$path")
|
||||
return null
|
||||
}
|
||||
val trashDir = File(filesDir, "trash")
|
||||
val trashDir = File(externalFilesDir, "trash")
|
||||
if (!trashDir.exists() && !trashDir.mkdirs()) {
|
||||
Log.e(LOG_TAG, "failed to create directories at path=$trashDir")
|
||||
return null
|
||||
|
@ -68,6 +69,10 @@ object StorageUtils {
|
|||
return trashDir
|
||||
}
|
||||
|
||||
fun getVaultRoot(context: Context) = ensureTrailingSeparator(File(context.filesDir, "vault").path)
|
||||
|
||||
fun isInVault(context: Context, path: String) = path.startsWith(getVaultRoot(context))
|
||||
|
||||
/**
|
||||
* Volume paths
|
||||
*/
|
||||
|
@ -259,6 +264,7 @@ object StorageUtils {
|
|||
// e.g.
|
||||
// /storage/emulated/0/ -> primary
|
||||
// /storage/10F9-3F13/Pictures/ -> 10F9-3F13
|
||||
// /storage/extSdCard/ -> 1234-5678 [Android 5.1.1, Samsung Galaxy Core Prime]
|
||||
private fun getVolumeUuidForDocumentUri(context: Context, anyPath: String): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
|
||||
|
@ -278,7 +284,22 @@ object StorageUtils {
|
|||
return EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
|
||||
}
|
||||
volumePath.split(File.separator).lastOrNull { it.isNotEmpty() }?.let { uuid ->
|
||||
return uuid.uppercase(Locale.ROOT)
|
||||
if (uuid.matches(UUID_PATTERN)) {
|
||||
return uuid.uppercase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback when UUID does not appear in the SD card volume path
|
||||
context.contentResolver.persistedUriPermissions.firstOrNull { uriPermission ->
|
||||
convertTreeDocumentUriToDirPath(context, uriPermission.uri)?.let {
|
||||
getVolumePath(context, it)?.let { grantedVolumePath ->
|
||||
grantedVolumePath == volumePath
|
||||
}
|
||||
} ?: false
|
||||
}?.let { uriPermission ->
|
||||
splitTreeDocumentUri(uriPermission.uri)?.let { (uuid, _) ->
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,6 +310,7 @@ object StorageUtils {
|
|||
// e.g.
|
||||
// primary -> /storage/emulated/0/
|
||||
// 10F9-3F13 -> /storage/10F9-3F13/
|
||||
// 1234-5678 -> /storage/extSdCard/ [Android 5.1.1, Samsung Galaxy Core Prime]
|
||||
private fun getVolumePathFromTreeDocumentUriUuid(context: Context, uuid: String): String? {
|
||||
if (uuid == EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID) {
|
||||
return getPrimaryVolumePath(context)
|
||||
|
@ -318,6 +340,10 @@ object StorageUtils {
|
|||
}
|
||||
}
|
||||
|
||||
// fallback when UUID does not appear in the SD card volume path
|
||||
val primaryVolumePath = getPrimaryVolumePath(context)
|
||||
getVolumePaths(context).firstOrNull { it != primaryVolumePath }?.let { return it }
|
||||
|
||||
Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid")
|
||||
return null
|
||||
}
|
||||
|
@ -350,9 +376,9 @@ object StorageUtils {
|
|||
}
|
||||
|
||||
// e.g.
|
||||
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
|
||||
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
|
||||
fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? {
|
||||
// content://com.android.externalstorage.documents/tree/primary%3A -> ("primary", "")
|
||||
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> ("10F9-3F13", "Pictures")
|
||||
private fun splitTreeDocumentUri(treeDocumentUri: Uri): Pair<String, String>? {
|
||||
val treeDocumentUriString = treeDocumentUri.toString()
|
||||
if (treeDocumentUriString.length <= TREE_URI_ROOT.length) return null
|
||||
val encoded = treeDocumentUriString.substring(TREE_URI_ROOT.length)
|
||||
|
@ -362,13 +388,24 @@ object StorageUtils {
|
|||
val uuid = group(1)
|
||||
val relativePath = group(2)
|
||||
if (uuid != null && relativePath != null) {
|
||||
val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid)
|
||||
if (volumePath != null) {
|
||||
return ensureTrailingSeparator(volumePath + relativePath)
|
||||
}
|
||||
return Pair(uuid, relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.e(LOG_TAG, "failed to split treeDocumentUri=$treeDocumentUri to UUID and relative path")
|
||||
return null
|
||||
}
|
||||
|
||||
// e.g.
|
||||
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
|
||||
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
|
||||
fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? {
|
||||
splitTreeDocumentUri(treeDocumentUri)?.let { (uuid, relativePath) ->
|
||||
val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid)
|
||||
if (volumePath != null) {
|
||||
return ensureTrailingSeparator(volumePath + relativePath)
|
||||
}
|
||||
}
|
||||
Log.e(LOG_TAG, "failed to convert treeDocumentUri=$treeDocumentUri to path")
|
||||
return null
|
||||
}
|
||||
|
@ -512,7 +549,7 @@ object StorageUtils {
|
|||
}
|
||||
|
||||
// As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used
|
||||
// to work around a bug from Android 10 where metadata redaction corrupts HEIC images.
|
||||
// to work around a bug from Android 10 (API 29) where metadata redaction corrupts HEIC images.
|
||||
// This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException`
|
||||
// for some non image/video content URIs (e.g. `downloads`, `file`)
|
||||
fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long? = null): Uri {
|
||||
|
|
12
android/app/src/main/res/values-eu/strings.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="search_shortcut_short_label">Bilatu</string>
|
||||
<string name="videos_shortcut_short_label">Bideoak</string>
|
||||
<string name="app_widget_label">Argazki-markoa</string>
|
||||
<string name="analysis_service_description">Irudiak eta bideoak eskaneatu</string>
|
||||
<string name="wallpaper">Horma-papera</string>
|
||||
<string name="analysis_channel_name">Media eskaneatu</string>
|
||||
<string name="analysis_notification_action_stop">Gelditu</string>
|
||||
<string name="analysis_notification_default_title">Media eskaneatzen</string>
|
||||
<string name="app_name">Aves</string>
|
||||
</resources>
|
12
android/app/src/main/res/values-sk/strings.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="search_shortcut_short_label">Hľadať</string>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Rám fotky</string>
|
||||
<string name="wallpaper">Tapeta</string>
|
||||
<string name="videos_shortcut_short_label">Videá</string>
|
||||
<string name="analysis_notification_action_stop">Zastaviť</string>
|
||||
<string name="analysis_channel_name">Skenovanie médií</string>
|
||||
<string name="analysis_service_description">Skenovanie obrázkov & videí</string>
|
||||
<string name="analysis_notification_default_title">Skenovanie média</string>
|
||||
</resources>
|
37
android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup disableIfNoEncryptionCapabilities="true">
|
||||
<include
|
||||
domain="file"
|
||||
path="." />
|
||||
<include
|
||||
domain="database"
|
||||
path="." />
|
||||
<include
|
||||
domain="external"
|
||||
path="." />
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
<exclude
|
||||
domain="sharedpref"
|
||||
path="secret_shared_prefs.xml" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<include
|
||||
domain="file"
|
||||
path="." />
|
||||
<include
|
||||
domain="database"
|
||||
path="." />
|
||||
<include
|
||||
domain="external"
|
||||
path="." />
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
<exclude
|
||||
domain="sharedpref"
|
||||
path="secret_shared_prefs.xml" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
18
android/app/src/main/res/xml/full_backup_content.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include
|
||||
domain="file"
|
||||
path="." />
|
||||
<include
|
||||
domain="database"
|
||||
path="." />
|
||||
<include
|
||||
domain="external"
|
||||
path="." />
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
<exclude
|
||||
domain="sharedpref"
|
||||
path="secret_shared_prefs.xml" />
|
||||
</full-backup-content>
|
|
@ -1,7 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<!-- `external-path` only covers files on regular device storage -->
|
||||
<external-path
|
||||
name="external_files"
|
||||
name="external"
|
||||
path="." />
|
||||
|
||||
<!-- `root-path` is necessary to cover files on removable storage -->
|
||||
<!-- cf https://iditect.com/article/fileprovider-for-android-get-content-uri-through-fileprovider.html -->
|
||||
<!--suppress AndroidElementNotAllowed -->
|
||||
<root-path
|
||||
name="root"
|
||||
path="." />
|
||||
|
||||
<!-- embedded images & other media that are exported for viewing and sharing -->
|
||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
“Aves Gallery” is an open-source gallery and metadata explorer app allowing you to access and manage your local photos and videos.
|
||||
|
||||
You must use the app for legal, authorized and acceptable purposes.
|
||||
The app is designed for legal, authorized and acceptable purposes.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
|
5
fastlane/metadata/android/en-US/changelogs/91.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
In v1.8.0:
|
||||
- Android TV support (cont'd)
|
||||
- hide your secrets in vaults
|
||||
- enjoy the app in Basque
|
||||
Full changelog available on GitHub
|
5
fastlane/metadata/android/en-US/changelogs/9101.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
In v1.8.0:
|
||||
- Android TV support (cont'd)
|
||||
- hide your secrets in vaults
|
||||
- enjoy the app in Basque
|
||||
Full changelog available on GitHub
|
5
fastlane/metadata/android/eu/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> aplikazioak mota guztitako irudi eta bideoak, nahiz ohiko zure JPEG eta MP4 fitxategiak eta exotikoagoak diren <b>orri ugaritako TIFF, SVG, AVI zaharrak eta are gehiago</b> maneiatzen ditu! Zure media-bilduma eskaneatzen du <b>mugimendu-argazkiak</b>,<b>panoramikak</b> (argazki esferikoak bezala ere ezagunak), <b>360°-ko bideoak</b>, baita <b>GeoTIFF</b> fitxategiak ere.
|
||||
|
||||
<b>Nabigazioa eta bilaketa</b> <i>Aves</i> aplikazioaren zati garrantzitsu bat da. Helburua, erabiltzaileek albumetatik argazkietara, etiketetara, mapetara, etab. modu errazean mugi ahal izatea da.
|
||||
|
||||
<i>Aves</i> Androidera (KitKatetik Android 13ra, Android TV barne) egiten da ezaugarri ugarirekin: <b>widgetak</b>, <b>aplikazioko lasterbideak</b>, <b>pantaila-babeslea</b> eta <b>bilaketa globala</b>. Baita ere, <b>media-bisore edo -hautagailu</b> bezala ere erabil daiteke.
|
BIN
fastlane/metadata/android/eu/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 285 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 499 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 212 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 342 KiB |
BIN
fastlane/metadata/android/eu/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 340 KiB |
1
fastlane/metadata/android/eu/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Galeria eta metadatuen nabigatzailea
|
5
fastlane/metadata/android/sk/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> aplikácia je schopná si poradiť s veľkým množstvom rôznych formátov obrázkov a videí, ako napríklad typickými JPEG a MP4, ale aj s exotickejšími <b> TIFF, SVG, AVI a mnoho iných</b>! Aplikácia skenuje média v zariadení a identifikje <b>fotky v pohybe</b>, <b>panorámy</b> , <b>360° videá</b>, ako aj <b>GeoTIFF</b> súbory.
|
||||
|
||||
<b>Navigácia a vyhľadávanie</b> je dôležitou súčasťou aplikácie <i>Aves</i>. Jej cieľom je poskytnúť užívateľom jednoduchý prechod z albumov, do fotiek, tagov, máp, atď.
|
||||
|
||||
<i>Aves</i> je schopný pracovať s Android (od KitKat do Android 13, včetne Android TV) a ponúka rozšírenia ako <b>miniaplikácie (widgety)</b>, <b>skratky aplikácie</b>, <b>šetrič obrazovky</b> a <b>globálne vyhľadávanie</b>. Rovnako poskytuje <b>prehľadávnie médií</b>.
|
1
fastlane/metadata/android/sk/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Prehliadač galérie a metadát
|
|
@ -26,6 +26,11 @@ extension ExtraAppMode on AppMode {
|
|||
|
||||
bool get canSelectFilter => this == AppMode.main;
|
||||
|
||||
bool get canCreateFilter => {
|
||||
AppMode.main,
|
||||
AppMode.pickFilterInternal,
|
||||
}.contains(this);
|
||||
|
||||
bool get isPickingMedia => {
|
||||
AppMode.pickSingleMediaExternal,
|
||||
AppMode.pickMultipleMediaExternal,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:aves/geo/topojson.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -48,26 +49,22 @@ class CountryTopology {
|
|||
final topology = await getTopology();
|
||||
if (topology == null) return null;
|
||||
|
||||
return compute<_IsoNumericCodeMapData, Map<int, Set<LatLng>>>(_isoNumericCodeMap, _IsoNumericCodeMapData(topology, positions));
|
||||
}
|
||||
|
||||
static Future<Map<int, Set<LatLng>>> _isoNumericCodeMap(_IsoNumericCodeMapData data) async {
|
||||
try {
|
||||
final topology = data.topology;
|
||||
final countries = (topology.objects['countries'] as GeometryCollection).geometries;
|
||||
final byCode = <int, Set<LatLng>>{};
|
||||
for (final position in data.positions) {
|
||||
final code = _getNumeric(topology, countries, position);
|
||||
if (code != null) {
|
||||
byCode[code] = (byCode[code] ?? {})..add(position);
|
||||
return Isolate.run<Map<int, Set<LatLng>>>(() {
|
||||
final countries = (topology.objects['countries'] as GeometryCollection).geometries;
|
||||
final byCode = <int, Set<LatLng>>{};
|
||||
for (final position in positions) {
|
||||
final code = _getNumeric(topology, countries, position);
|
||||
if (code != null) {
|
||||
byCode[code] = (byCode[code] ?? {})..add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
return byCode;
|
||||
return byCode;
|
||||
});
|
||||
} catch (error, stack) {
|
||||
// an unhandled error in a spawn isolate would make the app crash
|
||||
debugPrint('failed to get country codes with error=$error\n$stack');
|
||||
return null;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static int? _getNumeric(Topology topology, List<Geometry> mruCountries, LatLng position) {
|
||||
|
@ -96,10 +93,3 @@ class CountryTopology {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _IsoNumericCodeMapData {
|
||||
Topology topology;
|
||||
Set<LatLng> positions;
|
||||
|
||||
_IsoNumericCodeMapData(this.topology, this.positions);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
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
|
||||
class TopoJson {
|
||||
Future<Topology?> parse(String data) async {
|
||||
return compute<String, Topology?>(_isoParse, data);
|
||||
}
|
||||
|
||||
static Topology? _isoParse(String jsonData) {
|
||||
Future<Topology?> parse(String jsonData) async {
|
||||
try {
|
||||
final data = jsonDecode(jsonData) as Map<String, dynamic>;
|
||||
return Topology.parse(data);
|
||||
return Isolate.run<Topology>(() {
|
||||
final data = jsonDecode(jsonData) as Map<String, dynamic>;
|
||||
return Topology.parse(data);
|
||||
});
|
||||
} catch (error, stack) {
|
||||
// an unhandled error in a spawn isolate would make the app crash
|
||||
debugPrint('failed to parse TopoJSON with error=$error\n$stack');
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,19 +44,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"timeDays": "{days, plural, =1{1 den} =2..4{{days} dny} other{{days} dnů}}",
|
||||
"timeDays": "{days, plural, =1{1 den} few{{days} dny} other{{days} dnů}}",
|
||||
"@timeDays": {
|
||||
"placeholders": {
|
||||
"days": {}
|
||||
}
|
||||
},
|
||||
"itemCount": "{count, plural, =1{1 položka} =2..4{{count} položky} other{{count} položek}}",
|
||||
"itemCount": "{count, plural, =1{1 položka} few{{count} položky} other{{count} položek}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"columnCount": "{count, plural, =1{1 sloupec} =2..4{{count} sloupce} other{{count} sloupců}}",
|
||||
"columnCount": "{count, plural, =1{1 sloupec} few{{count} sloupce} other{{count} sloupců}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -168,8 +168,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Rychlost přehrávání",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Nastavení",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Nastavení",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Pokračovat",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Zobrazit ve sbírce",
|
||||
|
@ -313,7 +313,7 @@
|
|||
"@videoPlaybackMuted": {},
|
||||
"videoPlaybackWithSound": "Přehrát se zvukem",
|
||||
"@videoPlaybackWithSound": {},
|
||||
"themeBrightnessLight": "Svetlé",
|
||||
"themeBrightnessLight": "Světlé",
|
||||
"@themeBrightnessLight": {},
|
||||
"themeBrightnessDark": "Tmavé",
|
||||
"@themeBrightnessDark": {},
|
||||
|
@ -436,7 +436,7 @@
|
|||
"@addShortcutButtonLabel": {},
|
||||
"noMatchingAppDialogMessage": "Pro tuto operaci není k dispozici žádná aplikace.",
|
||||
"@noMatchingAppDialogMessage": {},
|
||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} =2..4{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}",
|
||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} few{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}",
|
||||
"@deleteEntriesConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -485,13 +485,13 @@
|
|||
"@renameEntrySetPagePreviewSectionTitle": {},
|
||||
"renameProcessorName": "Název",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a tuto položku?} =2..4{Smazat toto album a tyto {count} položky?} other{Smazat toto album a těchto {count} položek?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a v něm obsaženou položku?} few{Smazat toto album a v něm obsažené {count} položky?} other{Smazat toto album a v něm obsažených {count} položek?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a jejich položku?} =2..4{Smazat tato alba a jejich {count} položky?} other{Smazat tato alba a jejich {count} položek?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a v nich obsaženou položku?} few{Smazat tato alba a v nich obsažené {count} položky?} other{Smazat tato alba a v nich obsažených {count} položek?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -955,7 +955,7 @@
|
|||
"@nameConflictStrategySkip": {},
|
||||
"keepScreenOnNever": "Nikdy",
|
||||
"@keepScreenOnNever": {},
|
||||
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} =2..4{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}",
|
||||
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} few{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}",
|
||||
"@binEntriesConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -1121,25 +1121,25 @@
|
|||
"count": {}
|
||||
}
|
||||
},
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} =2..4{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} few{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}",
|
||||
"@collectionCopySuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} =2..4{Přesunuty {count} položky} other{Přesunuto {count} položek}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} few{Přesunuty {count} položky} other{Přesunuto {count} položek}}",
|
||||
"@collectionMoveSuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} =2..4{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} few{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}",
|
||||
"@collectionRenameSuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} =2..4{Upraveny {count} položky} other{Upraveno {count} položek}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} few{Upraveny {count} položky} other{Upraveno {count} položek}}",
|
||||
"@collectionEditSuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -1209,8 +1209,6 @@
|
|||
"@albumGroupNone": {},
|
||||
"albumVideoCaptures": "Snímky videa",
|
||||
"@albumVideoCaptures": {},
|
||||
"createAlbumTooltip": "Vytvořit album",
|
||||
"@createAlbumTooltip": {},
|
||||
"countryPageTitle": "Země",
|
||||
"@countryPageTitle": {},
|
||||
"searchCollectionFieldHint": "Prohledat sbírky",
|
||||
|
@ -1283,13 +1281,13 @@
|
|||
"@settingsThumbnailShowVideoDuration": {},
|
||||
"settingsCollectionQuickActionsTile": "Rychlé akce",
|
||||
"@settingsCollectionQuickActionsTile": {},
|
||||
"timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}",
|
||||
"timeMinutes": "{minutes, plural, =1{1 minuta} few{{minutes} minuty} other{{minutes} minut}}",
|
||||
"@timeMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}",
|
||||
"timeSeconds": "{seconds, plural, =1{1 sekunda} few{{seconds} sekundy} other{{seconds} sekund}}",
|
||||
"@timeSeconds": {
|
||||
"placeholders": {
|
||||
"seconds": {}
|
||||
|
@ -1331,7 +1329,7 @@
|
|||
"@settingsStorageAccessBanner": {},
|
||||
"settingsUnitSystemTile": "Jednotky",
|
||||
"@settingsUnitSystemTile": {},
|
||||
"statsWithGps": "{count, plural, =1{1 položka s polohou} =2..4{{count} položky s polohou} other{{count} položek s polohou}}",
|
||||
"statsWithGps": "{count, plural, =1{1 položka s polohou} few{{count} položky s polohou} other{{count} položek s polohou}}",
|
||||
"@statsWithGps": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -1354,5 +1352,19 @@
|
|||
"mapAttributionStamen": "Mapová data © [OpenStreetMap](https://www.openstreetmap.org/copyright) přispěvatelé • Dlaždice z [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)",
|
||||
"@mapAttributionStamen": {},
|
||||
"panoramaDisableSensorControl": "Zakázat ovládání senzorem",
|
||||
"@panoramaDisableSensorControl": {}
|
||||
"@panoramaDisableSensorControl": {},
|
||||
"filterLocatedLabel": "S polohou",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Se štítky",
|
||||
"@filterTaggedLabel": {},
|
||||
"settingsDisplayUseTvInterface": "Rozhraní Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"tooManyItemsErrorDialogMessage": "Zkuste znovu s několika dalšími položkami.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsModificationWarningDialogMessage": "Ostatní nastavení budou upravena.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsViewerShowDescription": "Zobrazit popis",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Wiedergabegeschwindigkeit",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Einstellungen",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Einstellungen",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Wiedergabe",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "In Sammlung anzeigen",
|
||||
|
@ -389,9 +389,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Name",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Album und der Inhalt gelöscht werden soll?} other{Sicher, dass dieses Album und deren {count} Elemente gelöscht werden sollen?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Das Album und das darin enthaltene Element löschen?} other{Das Album und die {count} darin enthaltenen Elemente löschen?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass diese Alben und deren Inhalt gelöscht werden sollen?} other{Sicher, dass diese Alben und deren {count} Elemente gelöscht werden sollen?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Diese Alben und die darin enthaltenen Elemente löschen?} other{Diese Alben und die darin enthaltenen {count} Objekte löschen?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -595,7 +595,7 @@
|
|||
"@collectionCopySuccessFeedback": {},
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
|
||||
"@collectionMoveSuccessFeedback": {},
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element unmebannt} other{{count} Elemente umbenannt}}",
|
||||
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element umbenannt} other{{count} Elemente umbenannt}}",
|
||||
"@collectionRenameSuccessFeedback": {},
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
|
||||
"@collectionEditSuccessFeedback": {},
|
||||
|
@ -699,8 +699,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Keine Alben",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Album erstellen",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "ERSTELLE",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "Neu",
|
||||
|
@ -1188,5 +1186,27 @@
|
|||
"entryInfoActionRemoveLocation": "Standort entfernen",
|
||||
"@entryInfoActionRemoveLocation": {},
|
||||
"entryActionShareVideoOnly": "Nur das Video teilen",
|
||||
"@entryActionShareVideoOnly": {}
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"filterLocatedLabel": "Mit Standort",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Getaggt",
|
||||
"@filterTaggedLabel": {},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Alternativen für Multi-Touch-Gesten anzeigen",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsDisplayUseTvInterface": "Android-TV Oberfläche",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"columnCount": "{count, plural, =1{1 Spalte} other{{count} Spalten}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Nach oben oder unten wischen, um die Helligkeit/Lautstärke einzustellen",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"tooManyItemsErrorDialogMessage": "Noch einmal mit weniger Elementen versuchen.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsViewerShowDescription": "Beschreibung anzeigen",
|
||||
"@settingsViewerShowDescription": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Ταχύτητα αναπαραγωγής",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Ρυθμίσεις",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Ρυθμίσεις",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Συνέχιση",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Εμφάνιση στη Συλλογή",
|
||||
|
@ -699,8 +699,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Δεν υπάρχουν άλμπουμ",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Δημιουργία άλμπουμ",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "ΔΗΜΙΟΥΡΓΙΑ",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "Νέα",
|
||||
|
@ -1206,5 +1204,49 @@
|
|||
"settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"settingsViewerShowDescription": "Εμφάνιση περιγραφής",
|
||||
"@settingsViewerShowDescription": {}
|
||||
"@settingsViewerShowDescription": {},
|
||||
"chipActionLock": "Κλείδωμα",
|
||||
"@chipActionLock": {},
|
||||
"chipActionCreateVault": "Δημιουργήστε θησαυροφυλάκιο",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "Διαμορφώστε το θησαυροφυλάκιο",
|
||||
"@chipActionConfigureVault": {},
|
||||
"albumTierVaults": "Θησαυροφυλακια",
|
||||
"@albumTierVaults": {},
|
||||
"vaultLockTypePassword": "Κωδικός πρόσβασης",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultDialogTitle": "Νεο Θησαυροφυλακιο",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Διαμορφωστε το θησαυροφυλακιο",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Κλείδωμα όταν σβήνει η οθόνη",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Τύπος κλειδώματος",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogConfirm": "Επιβεβαιώστε το PIN",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Εισάγετε τον κωδικό πρόσβασης",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Επιβεβαιώστε τον κωδικό πρόσβασης",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToUnlockVault": "Πιστοποίηση ταυτότητας για να ξεκλειδώσετε το θησαυροφυλάκιο",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsConfirmationVaultDataLoss": "Εμφάνιση προειδοποίησης απώλειας δεδομένων θησαυροφυλακίου",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"authenticateToConfigureVault": "Πιστοποίηση ταυτότητας για τη διαμόρφωση του θησαυροφυλακίου",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"vaultLockTypePin": "PIN",
|
||||
"@vaultLockTypePin": {},
|
||||
"newVaultWarningDialogMessage": "Τα αρχεία στα θησαυροφυλάκια είναι διαθέσιμα μόνο σε αυτή την εφαρμογή και σε καμία άλλη.\n\nΑν απεγκαταστήσετε την εφαρμογή ή έστω διαγράψετε τα δεδομένα της εφαρμογής, θα χάσετε όλα σας τα κρυφά αρχεία.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"pinDialogEnter": "Εισάγετε το PIN",
|
||||
"@pinDialogEnter": {},
|
||||
"vaultBinUsageDialogMessage": "Ορισμένα θησαυροφυλάκια χρησιμοποιούν τον κάδο ανακύκλωσης.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Τα αρχεία στον κάδο ανακύκλωσης θα διαγραφούν για πάντα.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"tooManyItemsErrorDialogMessage": "Δοκιμάστε ξανά με λιγότερα αρχεία.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Σύρετε προς τα πάνω ή προς τα κάτω για να ρυθμίσετε τη φωτεινότητα/την ένταση του ήχου",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
}
|
||||
|
|
|
@ -78,11 +78,14 @@
|
|||
"chipActionFilterOut": "Filter out",
|
||||
"chipActionFilterIn": "Filter in",
|
||||
"chipActionHide": "Hide",
|
||||
"chipActionLock": "Lock",
|
||||
"chipActionPin": "Pin to top",
|
||||
"chipActionUnpin": "Unpin from top",
|
||||
"chipActionRename": "Rename",
|
||||
"chipActionSetCover": "Set cover",
|
||||
"chipActionCreateAlbum": "Create album",
|
||||
"chipActionCreateVault": "Create vault",
|
||||
"chipActionConfigureVault": "Configure vault",
|
||||
|
||||
"entryActionCopyToClipboard": "Copy to clipboard",
|
||||
"entryActionDelete": "Delete",
|
||||
|
@ -119,7 +122,8 @@
|
|||
"videoActionSkip10": "Seek forward 10 seconds",
|
||||
"videoActionSelectStreams": "Select tracks",
|
||||
"videoActionSetSpeed": "Playback speed",
|
||||
"videoActionSettings": "Settings",
|
||||
|
||||
"viewerActionSettings": "Settings",
|
||||
|
||||
"slideshowActionResume": "Resume",
|
||||
"slideshowActionShowInCollection": "Show in Collection",
|
||||
|
@ -157,6 +161,16 @@
|
|||
"filterMimeImageLabel": "Image",
|
||||
"filterMimeVideoLabel": "Video",
|
||||
|
||||
"accessibilityAnimationsRemove": "Prevent screen effects",
|
||||
"accessibilityAnimationsKeep": "Keep screen effects",
|
||||
|
||||
"albumTierNew": "New",
|
||||
"albumTierPinned": "Pinned",
|
||||
"albumTierSpecial": "Common",
|
||||
"albumTierApps": "Apps",
|
||||
"albumTierVaults": "Vaults",
|
||||
"albumTierRegular": "Others",
|
||||
|
||||
"coordinateFormatDms": "DMS",
|
||||
"coordinateFormatDecimal": "Decimal degrees",
|
||||
"coordinateDms": "{coordinate} {direction}",
|
||||
|
@ -177,17 +191,13 @@
|
|||
"coordinateDmsEast": "E",
|
||||
"coordinateDmsWest": "W",
|
||||
|
||||
"unitSystemMetric": "Metric",
|
||||
"unitSystemImperial": "Imperial",
|
||||
"displayRefreshRatePreferHighest": "Highest rate",
|
||||
"displayRefreshRatePreferLowest": "Lowest rate",
|
||||
|
||||
"videoLoopModeNever": "Never",
|
||||
"videoLoopModeShortOnly": "Short videos only",
|
||||
"videoLoopModeAlways": "Always",
|
||||
|
||||
"videoControlsPlay": "Play",
|
||||
"videoControlsPlaySeek": "Play & seek backward/forward",
|
||||
"videoControlsPlayOutside": "Open with other player",
|
||||
"videoControlsNone": "None",
|
||||
"keepScreenOnNever": "Never",
|
||||
"keepScreenOnVideoPlayback": "During video playback",
|
||||
"keepScreenOnViewerOnly": "Viewer page only",
|
||||
"keepScreenOnAlways": "Always",
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
|
@ -202,28 +212,32 @@
|
|||
"nameConflictStrategyReplace": "Replace",
|
||||
"nameConflictStrategySkip": "Skip",
|
||||
|
||||
"keepScreenOnNever": "Never",
|
||||
"keepScreenOnVideoPlayback": "During video playback",
|
||||
"keepScreenOnViewerOnly": "Viewer page only",
|
||||
"keepScreenOnAlways": "Always",
|
||||
|
||||
"accessibilityAnimationsRemove": "Prevent screen effects",
|
||||
"accessibilityAnimationsKeep": "Keep screen effects",
|
||||
|
||||
"displayRefreshRatePreferHighest": "Highest rate",
|
||||
"displayRefreshRatePreferLowest": "Lowest rate",
|
||||
|
||||
"subtitlePositionTop": "Top",
|
||||
"subtitlePositionBottom": "Bottom",
|
||||
|
||||
"videoPlaybackSkip": "Skip",
|
||||
"videoPlaybackMuted": "Play muted",
|
||||
"videoPlaybackWithSound": "Play with sound",
|
||||
|
||||
"themeBrightnessLight": "Light",
|
||||
"themeBrightnessDark": "Dark",
|
||||
"themeBrightnessBlack": "Black",
|
||||
|
||||
"unitSystemMetric": "Metric",
|
||||
"unitSystemImperial": "Imperial",
|
||||
|
||||
"vaultLockTypePin": "Pin",
|
||||
"vaultLockTypePassword": "Password",
|
||||
|
||||
"videoControlsPlay": "Play",
|
||||
"videoControlsPlaySeek": "Play & seek backward/forward",
|
||||
"videoControlsPlayOutside": "Open with other player",
|
||||
"videoControlsNone": "None",
|
||||
|
||||
"videoLoopModeNever": "Never",
|
||||
"videoLoopModeShortOnly": "Short videos only",
|
||||
"videoLoopModeAlways": "Always",
|
||||
|
||||
"videoPlaybackSkip": "Skip",
|
||||
"videoPlaybackMuted": "Play muted",
|
||||
"videoPlaybackWithSound": "Play with sound",
|
||||
|
||||
"viewerTransitionSlide": "Slide",
|
||||
"viewerTransitionParallax": "Parallax",
|
||||
"viewerTransitionFade": "Fade",
|
||||
|
@ -241,12 +255,6 @@
|
|||
"widgetOpenPageCollection": "Open collection",
|
||||
"widgetOpenPageViewer": "Open viewer",
|
||||
|
||||
"albumTierNew": "New",
|
||||
"albumTierPinned": "Pinned",
|
||||
"albumTierSpecial": "Common",
|
||||
"albumTierApps": "Apps",
|
||||
"albumTierRegular": "Others",
|
||||
|
||||
"storageVolumeDescriptionFallbackPrimary": "Internal storage",
|
||||
"storageVolumeDescriptionFallbackNonPrimary": "SD card",
|
||||
"rootDirectoryDescription": "root directory",
|
||||
|
@ -366,6 +374,23 @@
|
|||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
|
||||
"newAlbumDialogStorageLabel": "Storage:",
|
||||
|
||||
"newVaultWarningDialogMessage": "Items in vaults are only available to this app and no others.\n\nIf you uninstall this app, or clear this app data, you will lose all these items.",
|
||||
"newVaultDialogTitle": "New Vault",
|
||||
"configureVaultDialogTitle": "Configure Vault",
|
||||
"vaultDialogLockModeWhenScreenOff": "Lock when screen turns off",
|
||||
"vaultDialogLockTypeLabel": "Lock type",
|
||||
|
||||
"pinDialogEnter": "Enter pin",
|
||||
"pinDialogConfirm": "Confirm pin",
|
||||
|
||||
"passwordDialogEnter": "Enter password",
|
||||
"passwordDialogConfirm": "Confirm password",
|
||||
|
||||
"authenticateToConfigureVault": "Authenticate to configure vault",
|
||||
"authenticateToUnlockVault": "Authenticate to unlock vault",
|
||||
|
||||
"vaultBinUsageDialogMessage": "Some vaults are using the recycle bin.",
|
||||
|
||||
"renameAlbumDialogLabel": "New name",
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
|
||||
|
||||
|
@ -634,7 +659,6 @@
|
|||
|
||||
"albumPageTitle": "Albums",
|
||||
"albumEmpty": "No albums",
|
||||
"createAlbumTooltip": "Create album",
|
||||
"createAlbumButtonLabel": "CREATE",
|
||||
"newFilterBanner": "new",
|
||||
|
||||
|
@ -687,6 +711,7 @@
|
|||
"settingsConfirmationBeforeMoveToBinItems": "Ask before moving items to the recycle bin",
|
||||
"settingsConfirmationBeforeMoveUndatedItems": "Ask before moving undated items",
|
||||
"settingsConfirmationAfterMoveToBinItems": "Show message after moving items to the recycle bin",
|
||||
"settingsConfirmationVaultDataLoss": "Show vault data loss warning",
|
||||
|
||||
"settingsNavigationDrawerTile": "Navigation menu",
|
||||
"settingsNavigationDrawerEditorPageTitle": "Navigation Menu",
|
||||
|
@ -790,6 +815,7 @@
|
|||
"settingsSaveSearchHistory": "Save search history",
|
||||
"settingsEnableBin": "Use recycle bin",
|
||||
"settingsEnableBinSubtitle": "Keep deleted items for 30 days",
|
||||
"settingsDisablingBinWarningDialogMessage": "Items in the recycle bin will be deleted forever.",
|
||||
"settingsAllowMediaManagement": "Allow media management",
|
||||
|
||||
"settingsHiddenItemsTile": "Hidden items",
|
||||
|
|
|
@ -147,8 +147,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Velocidad de reproducción",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Ajustes",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Ajustes",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Reanudar",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Mostrar en Colección",
|
||||
|
@ -657,8 +657,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Sin álbumes",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Crear álbum",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "CREAR",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "nuevo",
|
||||
|
@ -1210,5 +1208,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "Vuelva a intentarlo con menos elementos.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Deslice hacia arriba o hacia abajo para ajustar el brillo o el volumen",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"chipActionLock": "Bloquear",
|
||||
"@chipActionLock": {},
|
||||
"chipActionConfigureVault": "Configurar la caja fuerte",
|
||||
"@chipActionConfigureVault": {},
|
||||
"albumTierVaults": "Caja fuerte",
|
||||
"@albumTierVaults": {},
|
||||
"vaultLockTypePin": "Pin",
|
||||
"@vaultLockTypePin": {},
|
||||
"vaultLockTypePassword": "Contraseña",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultDialogTitle": "Nueva caja fuerte",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Configurar la caja fuerte",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Bloquear al apagar la pantalla",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Tipo de bloqueo",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogEnter": "Introducir el código pin",
|
||||
"@pinDialogEnter": {},
|
||||
"pinDialogConfirm": "Confirmar el código pin",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Introducir la contraseña",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Confirmar la contraseña",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToConfigureVault": "Autenticarse para configurar la caja fuerte",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"vaultBinUsageDialogMessage": "Algunas cajas fuertes utilizan la papelera de reciclaje.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Los elementos de la papelera de reciclaje se borrarán para siempre.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"chipActionCreateVault": "Crear una caja fuerte",
|
||||
"@chipActionCreateVault": {},
|
||||
"newVaultWarningDialogMessage": "Los elementos de la caja fuerte sólo están disponibles para esta aplicación y no para otras.\n\nSi desinstalas esta aplicación o borras sus datos, perderás todos estos elementos.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"authenticateToUnlockVault": "Autentificarse para desbloquear la caja fuerte",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsConfirmationVaultDataLoss": "Mostrar un aviso de pérdida de datos de la caja fuerte",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
}
|
||||
|
|
1370
lib/l10n/app_eu.arb
Normal file
|
@ -161,8 +161,8 @@
|
|||
"@videoActionUnmute": {},
|
||||
"videoActionSkip10": "جلو رفتن 10 ثانیه",
|
||||
"@videoActionSkip10": {},
|
||||
"videoActionSettings": "تنظیمات",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "تنظیمات",
|
||||
"@viewerActionSettings": {},
|
||||
"entryInfoActionEditRating": "ویرایش رتبه",
|
||||
"@entryInfoActionEditRating": {},
|
||||
"entryInfoActionEditTags": "ویرایش برچسب ها",
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Vitesse de lecture",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Préférences",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Préférences",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Reprendre",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Afficher dans Collection",
|
||||
|
@ -703,8 +703,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Aucun album",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Créer un album",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "CRÉER",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "nouveau",
|
||||
|
@ -1210,5 +1208,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "Réessayez avec moins d’éléments.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Balayer verticalement pour ajuster la luminosité et le volume",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"chipActionLock": "Verrouiller",
|
||||
"@chipActionLock": {},
|
||||
"chipActionCreateVault": "Créer un coffre-fort",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "Configurer le coffre-fort",
|
||||
"@chipActionConfigureVault": {},
|
||||
"albumTierVaults": "Coffres-forts",
|
||||
"@albumTierVaults": {},
|
||||
"vaultLockTypePin": "Code PIN",
|
||||
"@vaultLockTypePin": {},
|
||||
"vaultLockTypePassword": "Mot de passe",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultWarningDialogMessage": "Les éléments dans les coffres-forts ne sont visibles que dans cette app et nulle autre.\n\nSi vous désinstallez cette app, ou que vous supprimez ses données, vous perdrez tous ces éléments.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"newVaultDialogTitle": "Nouveau coffre-fort",
|
||||
"@newVaultDialogTitle": {},
|
||||
"vaultDialogLockTypeLabel": "Verrouillage",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogEnter": "Entrez votre code PIN",
|
||||
"@pinDialogEnter": {},
|
||||
"pinDialogConfirm": "Confirmez votre code PIN",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogConfirm": "Confirmez votre mot de passe",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToConfigureVault": "Authentification pour configurer le coffre-fort",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"vaultBinUsageDialogMessage": "Des coffres-forts utilisent la corbeille.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Verrouiller quand l’écran s’éteint",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"settingsConfirmationVaultDataLoss": "Afficher l’avertissement sur la perte de données avec les coffres-forts",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"configureVaultDialogTitle": "Configuration du coffre-fort",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"passwordDialogEnter": "Entrez votre mot de passe",
|
||||
"@passwordDialogEnter": {},
|
||||
"authenticateToUnlockVault": "Authentification pour déverrouiller le coffre-fort",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Les éléments dans la corbeille seront supprimés définitivement.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {}
|
||||
}
|
||||
|
|
|
@ -119,8 +119,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Velocidade de reprodución",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Configuración",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Configuración",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Resumo",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Mostrar na colección",
|
||||
|
|
|
@ -147,8 +147,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Kecepatan pemutaran",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Pengaturan",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Pengaturan",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Lanjutkan",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Tampilkan di Koleksi",
|
||||
|
@ -683,8 +683,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Tidak ada album",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Buat album",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "BUAT",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "baru",
|
||||
|
@ -1210,5 +1208,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Usap ke atas atau bawah untuk mengatur kecerahan/volume",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"chipActionConfigureVault": "Atur brankas",
|
||||
"@chipActionConfigureVault": {},
|
||||
"albumTierVaults": "Brankas",
|
||||
"@albumTierVaults": {},
|
||||
"newVaultDialogTitle": "Brankas Baru",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Atur Brankas",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Kunci ketika layar mati",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Jenis penguncian",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogEnter": "Masukkan pin",
|
||||
"@pinDialogEnter": {},
|
||||
"pinDialogConfirm": "Konfirmasi pin",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Masukkan kata sandi",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Konfirmasi kata sandi",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToConfigureVault": "Autentikasi untuk mengatur brankas",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"authenticateToUnlockVault": "Autentikasi untuk membuka brankas",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"vaultBinUsageDialogMessage": "Beberapa brankas menggunakan tong sampah.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Item dalam tong sampah akan hilang selamanya.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"chipActionLock": "Kunci",
|
||||
"@chipActionLock": {},
|
||||
"vaultLockTypePassword": "Kata sandi",
|
||||
"@vaultLockTypePassword": {},
|
||||
"chipActionCreateVault": "Buat brankas",
|
||||
"@chipActionCreateVault": {},
|
||||
"vaultLockTypePin": "Pin",
|
||||
"@vaultLockTypePin": {},
|
||||
"newVaultWarningDialogMessage": "Item dalam brankas hanya tersedia untuk aplikasi ini dan bukan yang lain.\n\nJika Anda menghapus aplikasi ini, atau menghapus data aplikasi ini, Anda akan kehilangan semua item tersebut.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"settingsConfirmationVaultDataLoss": "Tampilkan peringatan kehilangan data brankas",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Velocità di riproduzione",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Impostazioni",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Impostazioni",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Riprendi",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Mostra nella Collezione",
|
||||
|
@ -699,8 +699,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Nessun album",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Crea album",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "CREA",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "nuovo",
|
||||
|
@ -1208,5 +1206,7 @@
|
|||
"settingsViewerShowDescription": "Mostra la descrizione",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"tooManyItemsErrorDialogMessage": "Riprova con meno elementi.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Trascina su o giù per aggiustare luminosità/volume",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
}
|
||||
|
|
|
@ -147,8 +147,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "再生速度",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "設定",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "設定",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "再開",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "コレクションで表示",
|
||||
|
@ -657,8 +657,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "アルバムはありません",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "アルバムを作成",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "作成",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "新規",
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "재생 배속",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "설정",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "설정",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "이어서",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "미디어 페이지에서 보기",
|
||||
|
@ -703,8 +703,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "앨범이 없습니다",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "앨범 만들기",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "추가",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "신규",
|
||||
|
@ -1210,5 +1208,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"albumTierVaults": "금고",
|
||||
"@albumTierVaults": {},
|
||||
"chipActionLock": "잠금",
|
||||
"@chipActionLock": {},
|
||||
"vaultLockTypePin": "PIN",
|
||||
"@vaultLockTypePin": {},
|
||||
"vaultLockTypePassword": "비밀번호",
|
||||
"@vaultLockTypePassword": {},
|
||||
"configureVaultDialogTitle": "금고 설정",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"pinDialogEnter": "PIN을 입력하세요",
|
||||
"@pinDialogEnter": {},
|
||||
"passwordDialogEnter": "비밀번호를 입력하세요",
|
||||
"@passwordDialogEnter": {},
|
||||
"pinDialogConfirm": "PIN을 확인하세요",
|
||||
"@pinDialogConfirm": {},
|
||||
"vaultDialogLockTypeLabel": "잠금 방식",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "화면이 꺼진 후 자동으로 잠김",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"authenticateToConfigureVault": "금고 설정을 위한 인증",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"authenticateToUnlockVault": "금고 잠금 해제를 위한 인증",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "휴지통에 있는 항목들이 완전히 삭제될 것입니다.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"newVaultWarningDialogMessage": "금고에 있는 항목들은 이 앱에서만 볼 수 있습니다.\n\n이 앱을 삭제 시, 또한 이 앱의 데이터를 삭제 시, 항목을 완전히 삭제될 것입니다.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"chipActionCreateVault": "금고 만들기",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "금고 설정",
|
||||
"@chipActionConfigureVault": {},
|
||||
"newVaultDialogTitle": "새 금고 만들기",
|
||||
"@newVaultDialogTitle": {},
|
||||
"passwordDialogConfirm": "비밀번호를 확인하세요",
|
||||
"@passwordDialogConfirm": {},
|
||||
"vaultBinUsageDialogMessage": "휴지통을 사용하는 금고가 있습니다.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsConfirmationVaultDataLoss": "금고에 관한 데이터 손실 경고",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
}
|
||||
|
|
|
@ -97,8 +97,8 @@
|
|||
"@videoActionSkip10": {},
|
||||
"videoActionSetSpeed": "Atkūrimo greitis",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Nustatymai",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Nustatymai",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Tęsti",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Rodyti kolekcijoje",
|
||||
|
@ -1171,8 +1171,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Nėra albumų",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Sukurti albumą",
|
||||
"@createAlbumTooltip": {},
|
||||
"newFilterBanner": "nauja",
|
||||
"@newFilterBanner": {},
|
||||
"binPageTitle": "Šiukšlinė",
|
||||
|
|
|
@ -76,8 +76,8 @@
|
|||
"@videoActionSkip10": {},
|
||||
"videoActionSetSpeed": "Avspillingshastighet",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Innstillinger",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Innstillinger",
|
||||
"@viewerActionSettings": {},
|
||||
"entryInfoActionEditTitleDescription": "Rediger navn og beskrivelse",
|
||||
"@entryInfoActionEditTitleDescription": {},
|
||||
"filterNoDateLabel": "Udatert",
|
||||
|
@ -709,8 +709,6 @@
|
|||
"@albumVideoCaptures": {},
|
||||
"albumEmpty": "Ingen album",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Opprett album",
|
||||
"@createAlbumTooltip": {},
|
||||
"binPageTitle": "Papirkurv",
|
||||
"@binPageTitle": {},
|
||||
"countryPageTitle": "Land",
|
||||
|
@ -1366,5 +1364,7 @@
|
|||
"settingsModificationWarningDialogMessage": "Andre innstillinger vil bli endret.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Afspeelsnelheid",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Instellingen",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Instellingen",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Hervatten",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Tonen in Collectie",
|
||||
|
@ -693,8 +693,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Geen albums",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Album aanmaken",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "AANMAKEN",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "nieuw",
|
||||
|
|
|
@ -170,8 +170,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Avspelingssnøggleik",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Innstillingar",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Innstillingar",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Hald fram",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Vis i Samling",
|
||||
|
|
|
@ -85,8 +85,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Prędkość odtwarzania",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Ustawienia",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Ustawienia",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Wznów",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Pokaż w Kolekcji",
|
||||
|
@ -235,31 +235,31 @@
|
|||
"@displayRefreshRatePreferLowest": {},
|
||||
"videoPlaybackMuted": "Odtwarzaj bez dźwięku",
|
||||
"@videoPlaybackMuted": {},
|
||||
"itemCount": "{count, plural, =1{1 element} =2..4{{count} elementy} other{{count} elelmentów}}",
|
||||
"itemCount": "{count, plural, =1{1 element} few{{count} elementy} other{{count} elelmentów}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"columnCount": "{count, plural, =1{1 rząd} =2..4{{count} rzędy} other{{count} rzędów}}",
|
||||
"columnCount": "{count, plural, =1{1 rząd} few{{count} rzędy} other{{count} rzędów}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}",
|
||||
"timeSeconds": "{seconds, plural, =1{1 sekunda} few{{seconds} sekundy} other{{seconds} sekund}}",
|
||||
"@timeSeconds": {
|
||||
"placeholders": {
|
||||
"seconds": {}
|
||||
}
|
||||
},
|
||||
"timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}",
|
||||
"timeMinutes": "{minutes, plural, =1{1 minuta} few{{minutes} minuty} other{{minutes} minut}}",
|
||||
"@timeMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"timeDays": "{days, plural, =1{1 dzień} =2..4{{days} dni} other{{days} dni}}",
|
||||
"timeDays": "{days, plural, =1{1 dzień} few{{days} dni} other{{days} dni}}",
|
||||
"@timeDays": {
|
||||
"placeholders": {
|
||||
"days": {}
|
||||
|
@ -547,7 +547,7 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Nazwa",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} =2..4{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} few{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -603,13 +603,13 @@
|
|||
"@sectionUnknown": {},
|
||||
"dateToday": "Dzisiaj",
|
||||
"@dateToday": {},
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} =2..4{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}",
|
||||
"collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} few{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}",
|
||||
"@collectionCopySuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} =2..4{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}",
|
||||
"collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} few{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}",
|
||||
"@collectionEditSuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -705,8 +705,6 @@
|
|||
"@sortByItemCount": {},
|
||||
"sortBySize": "Według rozmiaru",
|
||||
"@sortBySize": {},
|
||||
"createAlbumTooltip": "Utwórz album",
|
||||
"@createAlbumTooltip": {},
|
||||
"albumEmpty": "Brak albumów",
|
||||
"@albumEmpty": {},
|
||||
"renameEntrySetPageTitle": "Zmień nazwę",
|
||||
|
@ -815,7 +813,7 @@
|
|||
"@albumGroupType": {},
|
||||
"renameEntrySetPagePreviewSectionTitle": "Podgląd",
|
||||
"@renameEntrySetPagePreviewSectionTitle": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} =2..4{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} few{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -903,7 +901,7 @@
|
|||
"count": {}
|
||||
}
|
||||
},
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} =2..4{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}",
|
||||
"collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} few{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}",
|
||||
"@collectionMoveSuccessFeedback": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -1319,7 +1317,7 @@
|
|||
"@settingsStorageAccessPageTitle": {},
|
||||
"settingsStorageAccessBanner": "Niektóre katalogi wymagają jawnego udzielenia dostępu, aby modyfikować znajdujące się w nich pliki. Możesz przejrzeć tutaj katalogi, do których wcześniej udzielono dostępu.",
|
||||
"@settingsStorageAccessBanner": {},
|
||||
"statsWithGps": "{count, plural, =1{1 element z położeniem} =2..4{{count} elementy z położeniem} other{{count} elementów z położeniem}}",
|
||||
"statsWithGps": "{count, plural, =1{1 element z położeniem} few{{count} elementy z położeniem} other{{count} elementów z położeniem}}",
|
||||
"@statsWithGps": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -1368,5 +1366,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "Spróbuj ponownie z mniejszą ilością elementów.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Przesuń palcem w górę lub w dół, aby dostosować jasność/głośność",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"chipActionLock": "Zablokuj",
|
||||
"@chipActionLock": {},
|
||||
"albumTierVaults": "Skarbce",
|
||||
"@albumTierVaults": {},
|
||||
"chipActionConfigureVault": "Konfiguruj Skarbiec",
|
||||
"@chipActionConfigureVault": {},
|
||||
"vaultLockTypePassword": "Hasło",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultDialogTitle": "Nowy Skarbiec",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Konfiguruj Skarbiec",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Blokada po wyłączeniu ekranu",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Rodzaj blokady",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogConfirm": "Potwierdź kod PIN",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Wpisz hasło",
|
||||
"@passwordDialogEnter": {},
|
||||
"pinDialogEnter": "Wpisz kod PIN",
|
||||
"@pinDialogEnter": {},
|
||||
"passwordDialogConfirm": "Potwierdź hasło",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToUnlockVault": "Uwierzytelnij się, aby odblokować skarbiec",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"authenticateToConfigureVault": "Uwierzytelnij się, aby skonfigurować Skarbiec",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"settingsConfirmationVaultDataLoss": "Pokaż ostrzeżenie o utracie danych skarbca",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Elementy znajdujące się w koszu zostaną usunięte na zawsze.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"chipActionCreateVault": "Utwórz skarbiec",
|
||||
"@chipActionCreateVault": {},
|
||||
"newVaultWarningDialogMessage": "Elementy w skarbcach są dostępne tylko dla tej aplikacji i żadnej innej.\n\nJeśli odinstalujesz tę aplikację lub wyczyścisz dane tej aplikacji, stracisz wszystkie te elementy.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"vaultBinUsageDialogMessage": "Niektóre skarbce korzystają z kosza.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"vaultLockTypePin": "PIN",
|
||||
"@vaultLockTypePin": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Velocidade de reprodução",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Configurações",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Configurações",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Retomar",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Mostrar na Coleção",
|
||||
|
@ -699,8 +699,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Nenhum álbum",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Criar álbum",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "CRIA",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "novo",
|
||||
|
|
|
@ -152,8 +152,8 @@
|
|||
"@videoActionSkip10": {},
|
||||
"videoActionSelectStreams": "Selectați piese",
|
||||
"@videoActionSelectStreams": {},
|
||||
"videoActionSettings": "Setări",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Setări",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Reluare",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Afișați în colecție",
|
||||
|
@ -743,8 +743,6 @@
|
|||
"@sortByRating": {},
|
||||
"viewerInfoLabelUri": "URI",
|
||||
"@viewerInfoLabelUri": {},
|
||||
"createAlbumTooltip": "Creați un album",
|
||||
"@createAlbumTooltip": {},
|
||||
"viewerInfoLabelDescription": "Descriere",
|
||||
"@viewerInfoLabelDescription": {},
|
||||
"settingsThumbnailOverlayPageTitle": "Suprapunere",
|
||||
|
@ -1366,5 +1364,47 @@
|
|||
"settingsDisplayUseTvInterface": "Interfață Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"tooManyItemsErrorDialogMessage": "Încearcă din nou cu mai puține elemente.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Glisați în sus sau în jos pentru a regla luminozitatea/volumul",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"chipActionLock": "Blocare",
|
||||
"@chipActionLock": {},
|
||||
"chipActionCreateVault": "Creare seif",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "Configurare seif",
|
||||
"@chipActionConfigureVault": {},
|
||||
"albumTierVaults": "Seifuri",
|
||||
"@albumTierVaults": {},
|
||||
"newVaultDialogTitle": "Seif nou",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Configurare seif",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Blocare atunci când ecranul se oprește",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Tip de blocare",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogConfirm": "Confirmă codul PIN",
|
||||
"@pinDialogConfirm": {},
|
||||
"pinDialogEnter": "Introdu codul PIN",
|
||||
"@pinDialogEnter": {},
|
||||
"passwordDialogEnter": "Introdu parola",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Confirmă parola",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToConfigureVault": "Autentifică-te pentru a configura seiful",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"authenticateToUnlockVault": "Autentifică-te pentru a debloca seiful",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"vaultBinUsageDialogMessage": "Unele seifuri folosesc coșul de reciclare.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Articolele din coșul de reciclare vor fi șterse pentru totdeauna.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"vaultLockTypePin": "Pin",
|
||||
"@vaultLockTypePin": {},
|
||||
"vaultLockTypePassword": "Parolă",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultWarningDialogMessage": "Elementele din seifuri sunt disponibile doar pentru această aplicație și nu pentru altele.\n\nDacă dezinstalezi această aplicație sau ștergi datele acestei aplicații, vei pierde toate aceste elemente.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"settingsConfirmationVaultDataLoss": "Afișare avertisment privind pierderile de date din seif",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Скорость вопспроизведения",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Настройки",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Настройки",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Продолжить",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Показать в Коллекции",
|
||||
|
@ -699,8 +699,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Нет альбомов",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Создать альбом",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "СОЗДАТЬ",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "новый",
|
||||
|
|
456
lib/l10n/app_sk.arb
Normal file
|
@ -0,0 +1,456 @@
|
|||
{
|
||||
"deleteButtonLabel": "ODSTRÁNIŤ",
|
||||
"@deleteButtonLabel": {},
|
||||
"appName": "Aves",
|
||||
"@appName": {},
|
||||
"welcomeMessage": "Vitajte v Aves",
|
||||
"@welcomeMessage": {},
|
||||
"cancelTooltip": "ZRUŠIŤ",
|
||||
"@cancelTooltip": {},
|
||||
"changeTooltip": "ZMENIŤ",
|
||||
"@changeTooltip": {},
|
||||
"clearTooltip": "VYČISTIŤ",
|
||||
"@clearTooltip": {},
|
||||
"previousTooltip": "PREDCHÁDZAJÚCE",
|
||||
"@previousTooltip": {},
|
||||
"nextTooltip": "Ďalej",
|
||||
"@nextTooltip": {},
|
||||
"showTooltip": "Zobraziť",
|
||||
"@showTooltip": {},
|
||||
"resetTooltip": "Znovu",
|
||||
"@resetTooltip": {},
|
||||
"saveTooltip": "Uložiť",
|
||||
"@saveTooltip": {},
|
||||
"pickTooltip": "Vybrať",
|
||||
"@pickTooltip": {},
|
||||
"chipActionDelete": "Odstrániť",
|
||||
"@chipActionDelete": {},
|
||||
"welcomeTermsToggle": "Súhlasím s pravidlami a podmienkami",
|
||||
"@welcomeTermsToggle": {},
|
||||
"timeDays": "{days, plural, other{{days} dni}}",
|
||||
"@timeDays": {
|
||||
"placeholders": {
|
||||
"days": {}
|
||||
}
|
||||
},
|
||||
"focalLength": "{length} mm",
|
||||
"@focalLength": {
|
||||
"placeholders": {
|
||||
"length": {
|
||||
"type": "String",
|
||||
"example": "5.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"applyButtonLabel": "POUŽIŤ",
|
||||
"@applyButtonLabel": {},
|
||||
"continueButtonLabel": "POKRAČOVAŤ",
|
||||
"@continueButtonLabel": {},
|
||||
"doubleBackExitMessage": "Stlač znovu \"späť\" pre ukončenie.",
|
||||
"@doubleBackExitMessage": {},
|
||||
"welcomeOptional": "Voliteľné",
|
||||
"@welcomeOptional": {},
|
||||
"timeMinutes": "{minutes, plural, other{{minutes} minúty}}",
|
||||
"@timeMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"nextButtonLabel": "ĎALEJ",
|
||||
"@nextButtonLabel": {},
|
||||
"showButtonLabel": "ZOBRAZIŤ",
|
||||
"@showButtonLabel": {},
|
||||
"hideButtonLabel": "SCHOVAŤ",
|
||||
"@hideButtonLabel": {},
|
||||
"hideTooltip": "Schovať",
|
||||
"@hideTooltip": {},
|
||||
"actionRemove": "Odstrániť",
|
||||
"@actionRemove": {},
|
||||
"sourceStateLoading": "Načítavanie",
|
||||
"@sourceStateLoading": {},
|
||||
"sourceStateCataloguing": "Indexovanie",
|
||||
"@sourceStateCataloguing": {},
|
||||
"doNotAskAgain": "Nepýtať sa znovu",
|
||||
"@doNotAskAgain": {},
|
||||
"chipActionGoToAlbumPage": "Zobraziť v albumoch",
|
||||
"@chipActionGoToAlbumPage": {},
|
||||
"sourceStateLocatingCountries": "Hľadanie krajín",
|
||||
"@sourceStateLocatingCountries": {},
|
||||
"sourceStateLocatingPlaces": "Hľadanie miest",
|
||||
"@sourceStateLocatingPlaces": {},
|
||||
"chipActionGoToCountryPage": "Zobraziť v krajinách",
|
||||
"@chipActionGoToCountryPage": {},
|
||||
"chipActionGoToTagPage": "Zobraziť v označeniach",
|
||||
"@chipActionGoToTagPage": {},
|
||||
"chipActionFilterOut": "Filtrovať",
|
||||
"@chipActionFilterOut": {},
|
||||
"chipActionFilterIn": "Prefiltrovať",
|
||||
"@chipActionFilterIn": {},
|
||||
"chipActionHide": "Skryť",
|
||||
"@chipActionHide": {},
|
||||
"chipActionPin": "Pripnúť na začiatok",
|
||||
"@chipActionPin": {},
|
||||
"chipActionUnpin": "Odstrániť z pripnutia",
|
||||
"@chipActionUnpin": {},
|
||||
"chipActionRename": "Premenovať",
|
||||
"@chipActionRename": {},
|
||||
"chipActionSetCover": "Nastaviť pozadie",
|
||||
"@chipActionSetCover": {},
|
||||
"chipActionCreateAlbum": "Vytvoriť album",
|
||||
"@chipActionCreateAlbum": {},
|
||||
"entryActionCopyToClipboard": "Skopírovať",
|
||||
"@entryActionCopyToClipboard": {},
|
||||
"entryActionDelete": "Odstrániť",
|
||||
"@entryActionDelete": {},
|
||||
"entryActionConvert": "Zmeniť",
|
||||
"@entryActionConvert": {},
|
||||
"entryActionExport": "Exportovať",
|
||||
"@entryActionExport": {},
|
||||
"entryActionRename": "Premenovať",
|
||||
"@entryActionRename": {},
|
||||
"entryActionRestore": "Obnoviť",
|
||||
"@entryActionRestore": {},
|
||||
"entryActionRotateCCW": "Otočiť proti smeru hodinových ručičiek",
|
||||
"@entryActionRotateCCW": {},
|
||||
"entryActionFlip": "Prevrátiť horizontálne",
|
||||
"@entryActionFlip": {},
|
||||
"entryActionPrint": "Vytlačiť",
|
||||
"@entryActionPrint": {},
|
||||
"entryActionShareImageOnly": "Zdieľať iba obrázok",
|
||||
"@entryActionShareImageOnly": {},
|
||||
"entryActionShareVideoOnly": "Zdieľať iba video",
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"entryActionViewSource": "Zobraziť zdroj",
|
||||
"@entryActionViewSource": {},
|
||||
"entryActionShowGeoTiffOnMap": "Zobraziť na mape",
|
||||
"@entryActionShowGeoTiffOnMap": {},
|
||||
"entryActionConvertMotionPhotoToStillImage": "Konvertovať na statický obrázok",
|
||||
"@entryActionConvertMotionPhotoToStillImage": {},
|
||||
"entryActionViewMotionPhotoVideo": "Otvoriť video",
|
||||
"@entryActionViewMotionPhotoVideo": {},
|
||||
"entryActionOpen": "Otvoriť v",
|
||||
"@entryActionOpen": {},
|
||||
"entryActionSetAs": "Nastaviť ako",
|
||||
"@entryActionSetAs": {},
|
||||
"entryActionInfo": "Informácie",
|
||||
"@entryActionInfo": {},
|
||||
"entryActionRotateCW": "Otočiť po smere hodinových ručičiek",
|
||||
"@entryActionRotateCW": {},
|
||||
"entryActionShare": "Zdieľať",
|
||||
"@entryActionShare": {},
|
||||
"entryActionEdit": "Upraviť",
|
||||
"@entryActionEdit": {},
|
||||
"nameConflictStrategySkip": "Preskočiť",
|
||||
"@nameConflictStrategySkip": {},
|
||||
"filterTypeAnimatedLabel": "Animované",
|
||||
"@filterTypeAnimatedLabel": {},
|
||||
"filterRatingRejectedLabel": "Zamietnuté",
|
||||
"@filterRatingRejectedLabel": {},
|
||||
"coordinateDmsWest": "W",
|
||||
"@coordinateDmsWest": {},
|
||||
"videoLoopModeNever": "Nikdy",
|
||||
"@videoLoopModeNever": {},
|
||||
"mapStyleGoogleTerrain": "Google mapy (terén)",
|
||||
"@mapStyleGoogleTerrain": {},
|
||||
"accessibilityAnimationsKeep": "Zachovanie efektov na obrazovke",
|
||||
"@accessibilityAnimationsKeep": {},
|
||||
"displayRefreshRatePreferLowest": "Najnižšie možné",
|
||||
"@displayRefreshRatePreferLowest": {},
|
||||
"moveUndatedConfirmationDialogSetDate": "Uložiť dátumy",
|
||||
"@moveUndatedConfirmationDialogSetDate": {},
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Dátum modifikácie súboru",
|
||||
"@editEntryDateDialogSourceFileModifiedDate": {},
|
||||
"widgetDisplayedItemMostRecent": "Najnovšie",
|
||||
"@widgetDisplayedItemMostRecent": {},
|
||||
"missingSystemFilePickerDialogMessage": "Systémový vyberač súborov chýba alebo je vypnutý. Povoľte ho a skúste to znova.",
|
||||
"@missingSystemFilePickerDialogMessage": {},
|
||||
"moveUndatedConfirmationDialogMessage": "Uložiť dátumy pred pokračovaním?",
|
||||
"@moveUndatedConfirmationDialogMessage": {},
|
||||
"hideFilterConfirmationDialogMessage": "Vybrané fotky a videá sa nebudú zobrazovať vo vašich kolekciách. Môžete ich obnoviť v nastaveniach \"Súkromie\".\n\nUrčite chcete schovať tieto súbory?",
|
||||
"@hideFilterConfirmationDialogMessage": {},
|
||||
"entryActionOpenMap": "Ukázať na mape v aplikácií",
|
||||
"@entryActionOpenMap": {},
|
||||
"entryActionRotateScreen": "Otočiť obrazovku",
|
||||
"@entryActionRotateScreen": {},
|
||||
"entryActionAddFavourite": "Pridať do obľúbených",
|
||||
"@entryActionAddFavourite": {},
|
||||
"entryActionRemoveFavourite": "Odstrániť z obľúbených",
|
||||
"@entryActionRemoveFavourite": {},
|
||||
"videoActionCaptureFrame": "Zachytiť obraz",
|
||||
"@videoActionCaptureFrame": {},
|
||||
"videoActionMute": "Stlmiť zvuk",
|
||||
"@videoActionMute": {},
|
||||
"videoActionUnmute": "Zapnúť zvuk",
|
||||
"@videoActionUnmute": {},
|
||||
"videoActionPause": "Pozastaviť",
|
||||
"@videoActionPause": {},
|
||||
"videoActionPlay": "Spustiť",
|
||||
"@videoActionPlay": {},
|
||||
"videoActionReplay10": "Pretočiť späť o 10 sekúnd",
|
||||
"@videoActionReplay10": {},
|
||||
"videoActionSkip10": "Pretočiť dopredu o 10 sekúnd",
|
||||
"@videoActionSkip10": {},
|
||||
"videoActionSelectStreams": "Výber stopy",
|
||||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Rýchlosť prehrávania",
|
||||
"@videoActionSetSpeed": {},
|
||||
"slideshowActionResume": "Pokračovať",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Zobraziť v kolekcií",
|
||||
"@slideshowActionShowInCollection": {},
|
||||
"entryInfoActionEditDate": "Upraviť dátum a čas",
|
||||
"@entryInfoActionEditDate": {},
|
||||
"entryInfoActionEditLocation": "Upraviť polohu",
|
||||
"@entryInfoActionEditLocation": {},
|
||||
"entryInfoActionEditTitleDescription": "Upraviť nadpis & popis",
|
||||
"@entryInfoActionEditTitleDescription": {},
|
||||
"entryInfoActionEditRating": "Upraviť hodnotenie",
|
||||
"@entryInfoActionEditRating": {},
|
||||
"entryInfoActionEditTags": "Upraviť značky",
|
||||
"@entryInfoActionEditTags": {},
|
||||
"entryInfoActionRemoveMetadata": "Odstrániť metadáta",
|
||||
"@entryInfoActionRemoveMetadata": {},
|
||||
"entryInfoActionExportMetadata": "Exportovať metadáta",
|
||||
"@entryInfoActionExportMetadata": {},
|
||||
"entryInfoActionRemoveLocation": "Odstrániť polohu",
|
||||
"@entryInfoActionRemoveLocation": {},
|
||||
"filterAspectRatioLandscapeLabel": "Horizontálne",
|
||||
"@filterAspectRatioLandscapeLabel": {},
|
||||
"filterAspectRatioPortraitLabel": "Vetrikálne",
|
||||
"@filterAspectRatioPortraitLabel": {},
|
||||
"filterBinLabel": "Kôš",
|
||||
"@filterBinLabel": {},
|
||||
"filterFavouriteLabel": "Obľúbené",
|
||||
"@filterFavouriteLabel": {},
|
||||
"filterNoDateLabel": "Bez dátumu",
|
||||
"@filterNoDateLabel": {},
|
||||
"filterNoAddressLabel": "Bez adresy",
|
||||
"@filterNoAddressLabel": {},
|
||||
"filterNoRatingLabel": "Nehodnotené",
|
||||
"@filterNoRatingLabel": {},
|
||||
"filterTaggedLabel": "Označené",
|
||||
"@filterTaggedLabel": {},
|
||||
"filterNoTagLabel": "Neoznačené",
|
||||
"@filterNoTagLabel": {},
|
||||
"filterNoTitleLabel": "Bez nadpisu",
|
||||
"@filterNoTitleLabel": {},
|
||||
"filterOnThisDayLabel": "Tento deň",
|
||||
"@filterOnThisDayLabel": {},
|
||||
"filterRecentlyAddedLabel": "Nedávno pridané",
|
||||
"@filterRecentlyAddedLabel": {},
|
||||
"filterTypeMotionPhotoLabel": "Fotka v pohybe",
|
||||
"@filterTypeMotionPhotoLabel": {},
|
||||
"filterTypePanoramaLabel": "Panoráma",
|
||||
"@filterTypePanoramaLabel": {},
|
||||
"filterTypeRawLabel": "Raw",
|
||||
"@filterTypeRawLabel": {},
|
||||
"filterTypeSphericalVideoLabel": "360° Video",
|
||||
"@filterTypeSphericalVideoLabel": {},
|
||||
"filterTypeGeotiffLabel": "GeoTIFF",
|
||||
"@filterTypeGeotiffLabel": {},
|
||||
"filterMimeImageLabel": "Obrázok",
|
||||
"@filterMimeImageLabel": {},
|
||||
"filterMimeVideoLabel": "Video",
|
||||
"@filterMimeVideoLabel": {},
|
||||
"coordinateFormatDms": "DMS",
|
||||
"@coordinateFormatDms": {},
|
||||
"coordinateFormatDecimal": "Desatinné stupne",
|
||||
"@coordinateFormatDecimal": {},
|
||||
"coordinateDmsNorth": "N",
|
||||
"@coordinateDmsNorth": {},
|
||||
"coordinateDmsSouth": "S",
|
||||
"@coordinateDmsSouth": {},
|
||||
"coordinateDmsEast": "E",
|
||||
"@coordinateDmsEast": {},
|
||||
"unitSystemMetric": "Metrické",
|
||||
"@unitSystemMetric": {},
|
||||
"unitSystemImperial": "Imperiálne",
|
||||
"@unitSystemImperial": {},
|
||||
"videoLoopModeShortOnly": "Iba krátke videá",
|
||||
"@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",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google mapy (Hybridne)",
|
||||
"@mapStyleGoogleHybrid": {},
|
||||
"mapStyleHuaweiNormal": "Mapy Petal",
|
||||
"@mapStyleHuaweiNormal": {},
|
||||
"mapStyleHuaweiTerrain": "Mapy Petal (terén)",
|
||||
"@mapStyleHuaweiTerrain": {},
|
||||
"mapStyleOsmHot": "Humanitarian OSM",
|
||||
"@mapStyleOsmHot": {},
|
||||
"mapStyleStamenToner": "Stamen Toner",
|
||||
"@mapStyleStamenToner": {},
|
||||
"mapStyleStamenWatercolor": "Stamen Watercolor",
|
||||
"@mapStyleStamenWatercolor": {},
|
||||
"nameConflictStrategyRename": "Premenovať",
|
||||
"@nameConflictStrategyRename": {},
|
||||
"videoControlsNone": "Žiadne",
|
||||
"@videoControlsNone": {},
|
||||
"nameConflictStrategyReplace": "Nahradiť",
|
||||
"@nameConflictStrategyReplace": {},
|
||||
"keepScreenOnNever": "Nikdy",
|
||||
"@keepScreenOnNever": {},
|
||||
"keepScreenOnVideoPlayback": "Počas prehrávania",
|
||||
"@keepScreenOnVideoPlayback": {},
|
||||
"keepScreenOnViewerOnly": "Iba stránka prehliadača",
|
||||
"@keepScreenOnViewerOnly": {},
|
||||
"keepScreenOnAlways": "Vždy",
|
||||
"@keepScreenOnAlways": {},
|
||||
"accessibilityAnimationsRemove": "Predchádzanie efektom obrazovky",
|
||||
"@accessibilityAnimationsRemove": {},
|
||||
"displayRefreshRatePreferHighest": "Najvyššie možné",
|
||||
"@displayRefreshRatePreferHighest": {},
|
||||
"subtitlePositionTop": "Navrchu",
|
||||
"@subtitlePositionTop": {},
|
||||
"subtitlePositionBottom": "Naspodku",
|
||||
"@subtitlePositionBottom": {},
|
||||
"videoPlaybackSkip": "Preskočiť",
|
||||
"@videoPlaybackSkip": {},
|
||||
"videoPlaybackMuted": "Spustiť stlmené",
|
||||
"@videoPlaybackMuted": {},
|
||||
"videoPlaybackWithSound": "Spustiť so zvukom",
|
||||
"@videoPlaybackWithSound": {},
|
||||
"themeBrightnessLight": "Svetlá",
|
||||
"@themeBrightnessLight": {},
|
||||
"themeBrightnessDark": "Tmavá",
|
||||
"@themeBrightnessDark": {},
|
||||
"themeBrightnessBlack": "Čierna",
|
||||
"@themeBrightnessBlack": {},
|
||||
"viewerTransitionSlide": "Snímka",
|
||||
"@viewerTransitionSlide": {},
|
||||
"viewerTransitionParallax": "Parallax",
|
||||
"@viewerTransitionParallax": {},
|
||||
"viewerTransitionFade": "Vyblednúť",
|
||||
"@viewerTransitionFade": {},
|
||||
"viewerTransitionZoomIn": "Priblížiť",
|
||||
"@viewerTransitionZoomIn": {},
|
||||
"viewerTransitionNone": "Žiadne",
|
||||
"@viewerTransitionNone": {},
|
||||
"wallpaperTargetHome": "Domáca obrazovka",
|
||||
"@wallpaperTargetHome": {},
|
||||
"wallpaperTargetLock": "Zamknutá obrazovka",
|
||||
"@wallpaperTargetLock": {},
|
||||
"wallpaperTargetHomeLock": "Domáca a zamknutá obrazovka",
|
||||
"@wallpaperTargetHomeLock": {},
|
||||
"widgetDisplayedItemRandom": "Náhodné",
|
||||
"@widgetDisplayedItemRandom": {},
|
||||
"widgetOpenPageHome": "Isť domov",
|
||||
"@widgetOpenPageHome": {},
|
||||
"widgetOpenPageCollection": "Otvoriť kolekciu",
|
||||
"@widgetOpenPageCollection": {},
|
||||
"widgetOpenPageViewer": "Otvoriť prehliadač",
|
||||
"@widgetOpenPageViewer": {},
|
||||
"albumTierNew": "Nový",
|
||||
"@albumTierNew": {},
|
||||
"albumTierPinned": "Pripnuté",
|
||||
"@albumTierPinned": {},
|
||||
"albumTierSpecial": "Spoločné",
|
||||
"@albumTierSpecial": {},
|
||||
"albumTierApps": "Aplikácie",
|
||||
"@albumTierApps": {},
|
||||
"albumTierRegular": "Ostatné",
|
||||
"@albumTierRegular": {},
|
||||
"storageVolumeDescriptionFallbackPrimary": "Vnútorný ukladací priestor",
|
||||
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||
"storageVolumeDescriptionFallbackNonPrimary": "SD karta",
|
||||
"@storageVolumeDescriptionFallbackNonPrimary": {},
|
||||
"rootDirectoryDescription": "Koreňový adresár",
|
||||
"@rootDirectoryDescription": {},
|
||||
"nameConflictDialogSingleSourceMessage": "Niektoré súbory v cieľovej destinácií majú rovnaké názvy.",
|
||||
"@nameConflictDialogSingleSourceMessage": {},
|
||||
"nameConflictDialogMultipleSourceMessage": "Niektoré súbory majú rovnaký názov.",
|
||||
"@nameConflictDialogMultipleSourceMessage": {},
|
||||
"addShortcutDialogLabel": "Názov skratky",
|
||||
"@addShortcutDialogLabel": {},
|
||||
"addShortcutButtonLabel": "PRIDAŤ",
|
||||
"@addShortcutButtonLabel": {},
|
||||
"noMatchingAppDialogMessage": "Nie je podporované žiadnou aplikáciou.",
|
||||
"@noMatchingAppDialogMessage": {},
|
||||
"videoResumeDialogMessage": "Pokračovať v prehrávaní od {time}?",
|
||||
"@videoResumeDialogMessage": {
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"type": "String",
|
||||
"example": "13:37"
|
||||
}
|
||||
}
|
||||
},
|
||||
"videoStartOverButtonLabel": "ODZNOVA",
|
||||
"@videoStartOverButtonLabel": {},
|
||||
"videoResumeButtonLabel": "POKRAČOVAŤ",
|
||||
"@videoResumeButtonLabel": {},
|
||||
"setCoverDialogLatest": "Posledná položka",
|
||||
"@setCoverDialogLatest": {},
|
||||
"setCoverDialogAuto": "Automaticky",
|
||||
"@setCoverDialogAuto": {},
|
||||
"setCoverDialogCustom": "Vlastné",
|
||||
"@setCoverDialogCustom": {},
|
||||
"newAlbumDialogTitle": "Nový album",
|
||||
"@newAlbumDialogTitle": {},
|
||||
"newAlbumDialogNameLabel": "Názov albumu",
|
||||
"@newAlbumDialogNameLabel": {},
|
||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Priečinok už existuje",
|
||||
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||
"newAlbumDialogStorageLabel": "Úložisko:",
|
||||
"@newAlbumDialogStorageLabel": {},
|
||||
"renameAlbumDialogLabel": "Nový názov",
|
||||
"@renameAlbumDialogLabel": {},
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "Priečinok už existuje",
|
||||
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||
"renameEntrySetPageTitle": "Premenovať",
|
||||
"@renameEntrySetPageTitle": {},
|
||||
"renameEntrySetPagePatternFieldLabel": "Formát",
|
||||
"@renameEntrySetPagePatternFieldLabel": {},
|
||||
"renameEntrySetPageInsertTooltip": "Vložiť položku",
|
||||
"@renameEntrySetPageInsertTooltip": {},
|
||||
"renameEntrySetPagePreviewSectionTitle": "Náhľad",
|
||||
"@renameEntrySetPagePreviewSectionTitle": {},
|
||||
"renameProcessorCounter": "Počítadlo",
|
||||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Názov",
|
||||
"@renameProcessorName": {},
|
||||
"exportEntryDialogFormat": "Formát:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
"exportEntryDialogWidth": "Šírka",
|
||||
"@exportEntryDialogWidth": {},
|
||||
"exportEntryDialogHeight": "Výška",
|
||||
"@exportEntryDialogHeight": {},
|
||||
"renameEntryDialogLabel": "Nový názov",
|
||||
"@renameEntryDialogLabel": {},
|
||||
"editEntryDialogCopyFromItem": "Kopírovať z inej položky",
|
||||
"@editEntryDialogCopyFromItem": {},
|
||||
"editEntryDialogTargetFieldsHeader": "Polia k úprave",
|
||||
"@editEntryDialogTargetFieldsHeader": {},
|
||||
"editEntryDateDialogTitle": "Dátum & Čas",
|
||||
"@editEntryDateDialogTitle": {},
|
||||
"editEntryDateDialogSetCustom": "Nastaviť vlastný dátum",
|
||||
"@editEntryDateDialogSetCustom": {},
|
||||
"editEntryDateDialogCopyField": "Kopírovať z iného dátumu",
|
||||
"@editEntryDateDialogCopyField": {},
|
||||
"editEntryDateDialogExtractFromTitle": "Extrahovať z nadpisu",
|
||||
"@editEntryDateDialogExtractFromTitle": {},
|
||||
"editEntryDateDialogShift": "Posun",
|
||||
"@editEntryDateDialogShift": {},
|
||||
"durationDialogHours": "Hodiny",
|
||||
"@durationDialogHours": {},
|
||||
"durationDialogMinutes": "Minúty",
|
||||
"@durationDialogMinutes": {},
|
||||
"durationDialogSeconds": "Sekundy",
|
||||
"@durationDialogSeconds": {},
|
||||
"editEntryLocationDialogTitle": "Poloha",
|
||||
"@editEntryLocationDialogTitle": {},
|
||||
"editEntryLocationDialogSetCustom": "Nastaviť vlastnú polohu",
|
||||
"@editEntryLocationDialogSetCustom": {},
|
||||
"editEntryLocationDialogChooseOnMap": "Vybrať z mapy",
|
||||
"@editEntryLocationDialogChooseOnMap": {},
|
||||
"viewerActionSettings": "Nastavenia",
|
||||
"@viewerActionSettings": {}
|
||||
}
|
|
@ -121,8 +121,8 @@
|
|||
"@videoActionSkip10": {},
|
||||
"slideshowActionShowInCollection": "แสดงคอลเลกชัน",
|
||||
"@slideshowActionShowInCollection": {},
|
||||
"videoActionSettings": "ตั้งค่า",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "ตั้งค่า",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "เล่นต่อ",
|
||||
"@slideshowActionResume": {},
|
||||
"entryInfoActionEditTitleDescription": "แก้ไขชื่อและคำบรรยาย",
|
||||
|
|
|
@ -147,8 +147,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Oynatma hızı",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Ayarlar",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Ayarlar",
|
||||
"@viewerActionSettings": {},
|
||||
"entryInfoActionEditDate": "Tarih ve saati düzenle",
|
||||
"@entryInfoActionEditDate": {},
|
||||
"entryInfoActionEditLocation": "Konumu düzenle",
|
||||
|
@ -629,8 +629,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Albüm yok",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Albüm oluştur",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "OLUŞTUR",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "yeni",
|
||||
|
|
|
@ -126,8 +126,8 @@
|
|||
"@videoActionSkip10": {},
|
||||
"videoActionSelectStreams": "Вибрати доріжку",
|
||||
"@videoActionSelectStreams": {},
|
||||
"videoActionSettings": "Налаштування",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "Налаштування",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "Продовжити",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "Показати у Колекції",
|
||||
|
@ -885,8 +885,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "Немає альбомів",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Створити альбом",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "СТВОРИТИ",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"countryPageTitle": "Країни",
|
||||
|
@ -1368,5 +1366,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"chipActionConfigureVault": "Налаштувати сховище",
|
||||
"@chipActionConfigureVault": {},
|
||||
"vaultLockTypePassword": "Пароль",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultDialogTitle": "Нове сховище",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Налаштування сховища",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Заблокувати, коли екран вимикається",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Тип блокування",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogConfirm": "Підтвердити пін-код",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Введіть пароль",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Підтвердити пароль",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToUnlockVault": "Пройдіть автентифікацію, щоб розблокувати сховище",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsConfirmationVaultDataLoss": "Показувати попередження про втрату даних сховища",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"chipActionLock": "Заблокувати",
|
||||
"@chipActionLock": {},
|
||||
"chipActionCreateVault": "Створити сховище",
|
||||
"@chipActionCreateVault": {},
|
||||
"newVaultWarningDialogMessage": "Елементи у сховищах доступні лише для цього додатка і ні для кого іншого.\n\nЯкщо ви видалите цю програму або очистите дані програми, ви втратите всі ці елементи.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"vaultLockTypePin": "Пін-код",
|
||||
"@vaultLockTypePin": {},
|
||||
"albumTierVaults": "Сховища",
|
||||
"@albumTierVaults": {},
|
||||
"authenticateToConfigureVault": "Пройдіть автентифікацію, щоб налаштувати сховище",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"vaultBinUsageDialogMessage": "У деяких сховищах використовується кошик.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Елементи в кошику буде видалено назавжди.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"pinDialogEnter": "Введіть пін-код",
|
||||
"@pinDialogEnter": {}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "播放速度",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "设置",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "设置",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "继续",
|
||||
"@slideshowActionResume": {},
|
||||
"slideshowActionShowInCollection": "在媒体集中显示",
|
||||
|
@ -389,9 +389,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "名称",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{删除此相册及其内容?} other{删除此相册及其 {count} 项内容?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{删除此相册及其中的一个项目?} other{删除此相册及其中的 {count} 个项目?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{删除这些相册及其内容?} other{删除这些相册及其 {count} 项内容?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{删除这些相册及其中的一个项目?} other{删除这些相册及其中的 {count} 个项目?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "格式:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -691,8 +691,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "无相册",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "创建相册",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "创建",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "新的",
|
||||
|
|
|
@ -124,8 +124,8 @@
|
|||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "播放速度",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "設定",
|
||||
"@videoActionSettings": {},
|
||||
"viewerActionSettings": "設定",
|
||||
"@viewerActionSettings": {},
|
||||
"slideshowActionResume": "繼續",
|
||||
"@slideshowActionResume": {},
|
||||
"entryInfoActionEditLocation": "編輯位置",
|
||||
|
@ -615,8 +615,6 @@
|
|||
"@albumPageTitle": {},
|
||||
"albumEmpty": "沒有相簿",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "建立相簿",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "建立",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "新的",
|
||||
|
|
|
@ -8,6 +8,7 @@ enum ChipAction {
|
|||
goToTagPage,
|
||||
reverse,
|
||||
hide,
|
||||
lockVault,
|
||||
}
|
||||
|
||||
extension ExtraChipAction on ChipAction {
|
||||
|
@ -24,6 +25,8 @@ extension ExtraChipAction on ChipAction {
|
|||
return context.l10n.chipActionFilterOut;
|
||||
case ChipAction.hide:
|
||||
return context.l10n.chipActionHide;
|
||||
case ChipAction.lockVault:
|
||||
return context.l10n.chipActionLock;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +44,8 @@ extension ExtraChipAction on ChipAction {
|
|||
return AIcons.reverse;
|
||||
case ChipAction.hide:
|
||||
return AIcons.hide;
|
||||
case ChipAction.lockVault:
|
||||
return AIcons.vaultLock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ enum ChipSetAction {
|
|||
search,
|
||||
toggleTitleSearch,
|
||||
createAlbum,
|
||||
createVault,
|
||||
// browsing or selecting
|
||||
map,
|
||||
slideshow,
|
||||
|
@ -21,9 +22,11 @@ enum ChipSetAction {
|
|||
hide,
|
||||
pin,
|
||||
unpin,
|
||||
lockVault,
|
||||
// selecting (single filter)
|
||||
rename,
|
||||
setCover,
|
||||
configureVault,
|
||||
}
|
||||
|
||||
class ChipSetActions {
|
||||
|
@ -34,15 +37,20 @@ class ChipSetActions {
|
|||
ChipSetAction.selectNone,
|
||||
];
|
||||
|
||||
// `null` items are converted to dividers
|
||||
static const browsing = [
|
||||
ChipSetAction.search,
|
||||
ChipSetAction.toggleTitleSearch,
|
||||
ChipSetAction.createAlbum,
|
||||
null,
|
||||
ChipSetAction.map,
|
||||
ChipSetAction.slideshow,
|
||||
ChipSetAction.stats,
|
||||
null,
|
||||
ChipSetAction.createAlbum,
|
||||
ChipSetAction.createVault,
|
||||
];
|
||||
|
||||
// `null` items are converted to dividers
|
||||
static const selection = [
|
||||
ChipSetAction.setCover,
|
||||
ChipSetAction.pin,
|
||||
|
@ -50,9 +58,13 @@ class ChipSetActions {
|
|||
ChipSetAction.delete,
|
||||
ChipSetAction.rename,
|
||||
ChipSetAction.hide,
|
||||
null,
|
||||
ChipSetAction.map,
|
||||
ChipSetAction.slideshow,
|
||||
ChipSetAction.stats,
|
||||
null,
|
||||
ChipSetAction.configureVault,
|
||||
ChipSetAction.lockVault,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -76,6 +88,8 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
return context.l10n.collectionActionShowTitleSearch;
|
||||
case ChipSetAction.createAlbum:
|
||||
return context.l10n.chipActionCreateAlbum;
|
||||
case ChipSetAction.createVault:
|
||||
return context.l10n.chipActionCreateVault;
|
||||
// browsing or selecting
|
||||
case ChipSetAction.map:
|
||||
return context.l10n.menuActionMap;
|
||||
|
@ -92,11 +106,15 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
return context.l10n.chipActionPin;
|
||||
case ChipSetAction.unpin:
|
||||
return context.l10n.chipActionUnpin;
|
||||
case ChipSetAction.lockVault:
|
||||
return context.l10n.chipActionLock;
|
||||
// selecting (single filter)
|
||||
case ChipSetAction.rename:
|
||||
return context.l10n.chipActionRename;
|
||||
case ChipSetAction.setCover:
|
||||
return context.l10n.chipActionSetCover;
|
||||
case ChipSetAction.configureVault:
|
||||
return context.l10n.chipActionConfigureVault;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,6 +139,8 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
return AIcons.filter;
|
||||
case ChipSetAction.createAlbum:
|
||||
return AIcons.add;
|
||||
case ChipSetAction.createVault:
|
||||
return AIcons.vaultAdd;
|
||||
// browsing or selecting
|
||||
case ChipSetAction.map:
|
||||
return AIcons.map;
|
||||
|
@ -137,11 +157,15 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
return AIcons.pin;
|
||||
case ChipSetAction.unpin:
|
||||
return AIcons.unpin;
|
||||
case ChipSetAction.lockVault:
|
||||
return AIcons.vaultLock;
|
||||
// selecting (single filter)
|
||||
case ChipSetAction.rename:
|
||||
return AIcons.name;
|
||||
case ChipSetAction.setCover:
|
||||
return AIcons.setCover;
|
||||
case ChipSetAction.configureVault:
|
||||
return AIcons.vaultConfigure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,13 +71,15 @@ class EntryActions {
|
|||
];
|
||||
|
||||
static const export = [
|
||||
...exportInternal,
|
||||
...exportExternal,
|
||||
];
|
||||
|
||||
static const exportInternal = [
|
||||
EntryAction.convert,
|
||||
EntryAction.addShortcut,
|
||||
EntryAction.copyToClipboard,
|
||||
EntryAction.print,
|
||||
EntryAction.open,
|
||||
EntryAction.openMap,
|
||||
EntryAction.setAs,
|
||||
];
|
||||
|
||||
static const exportExternal = [
|
||||
|
@ -186,7 +188,7 @@ extension ExtraEntryAction on EntryAction {
|
|||
case EntryAction.videoSetSpeed:
|
||||
return context.l10n.videoActionSetSpeed;
|
||||
case EntryAction.videoSettings:
|
||||
return context.l10n.videoActionSettings;
|
||||
return context.l10n.viewerActionSettings;
|
||||
case EntryAction.videoTogglePlay:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.videoActionPlay;
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
enum SlideshowAction {
|
||||
resume,
|
||||
showInCollection,
|
||||
settings,
|
||||
}
|
||||
|
||||
extension ExtraSlideshowAction on SlideshowAction {
|
||||
|
@ -14,6 +15,8 @@ extension ExtraSlideshowAction on SlideshowAction {
|
|||
return context.l10n.slideshowActionResume;
|
||||
case SlideshowAction.showInCollection:
|
||||
return context.l10n.slideshowActionShowInCollection;
|
||||
case SlideshowAction.settings:
|
||||
return context.l10n.viewerActionSettings;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +28,8 @@ extension ExtraSlideshowAction on SlideshowAction {
|
|||
return AIcons.play;
|
||||
case SlideshowAction.showInCollection:
|
||||
return AIcons.allCollection;
|
||||
case SlideshowAction.settings:
|
||||
return AIcons.settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
|
@ -38,6 +39,8 @@ class Covers {
|
|||
Set<CoverRow> get all => Set.unmodifiable(_rows);
|
||||
|
||||
Tuple3<int?, String?, Color?>? of(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter && vaults.isLocked(filter.album)) return null;
|
||||
|
||||
final row = _rows.firstWhereOrNull((row) => row.filter == filter);
|
||||
return row != null ? Tuple3(row.entryId, row.packageName, row.color) : null;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/metadata/address.dart';
|
||||
import 'package:aves/model/metadata/catalog.dart';
|
||||
import 'package:aves/model/metadata/trash.dart';
|
||||
import 'package:aves/model/vaults/details.dart';
|
||||
import 'package:aves/model/video_playback.dart';
|
||||
|
||||
abstract class MetadataDb {
|
||||
|
@ -16,17 +17,17 @@ abstract class MetadataDb {
|
|||
|
||||
Future<void> reset();
|
||||
|
||||
Future<void> removeIds(Iterable<int> ids, {Set<EntryDataType>? dataTypes});
|
||||
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes});
|
||||
|
||||
// entries
|
||||
|
||||
Future<void> clearEntries();
|
||||
|
||||
Future<Set<AvesEntry>> loadEntries({String? directory});
|
||||
Future<Set<AvesEntry>> loadEntries({int? origin, String? directory});
|
||||
|
||||
Future<Set<AvesEntry>> loadEntriesById(Iterable<int> ids);
|
||||
Future<Set<AvesEntry>> loadEntriesById(Set<int> ids);
|
||||
|
||||
Future<void> saveEntries(Iterable<AvesEntry> entries);
|
||||
Future<void> saveEntries(Set<AvesEntry> entries);
|
||||
|
||||
Future<void> updateEntry(int id, AvesEntry entry);
|
||||
|
||||
|
@ -44,7 +45,7 @@ abstract class MetadataDb {
|
|||
|
||||
Future<Set<CatalogMetadata>> loadCatalogMetadata();
|
||||
|
||||
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Iterable<int> ids);
|
||||
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Set<int> ids);
|
||||
|
||||
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries);
|
||||
|
||||
|
@ -56,12 +57,24 @@ abstract class MetadataDb {
|
|||
|
||||
Future<Set<AddressDetails>> loadAddresses();
|
||||
|
||||
Future<Set<AddressDetails>> loadAddressesById(Iterable<int> ids);
|
||||
Future<Set<AddressDetails>> loadAddressesById(Set<int> ids);
|
||||
|
||||
Future<void> saveAddresses(Set<AddressDetails> addresses);
|
||||
|
||||
Future<void> updateAddress(int id, AddressDetails? address);
|
||||
|
||||
// vaults
|
||||
|
||||
Future<void> clearVaults();
|
||||
|
||||
Future<Set<VaultDetails>> loadAllVaults();
|
||||
|
||||
Future<void> addVaults(Set<VaultDetails> rows);
|
||||
|
||||
Future<void> updateVault(String oldName, VaultDetails row);
|
||||
|
||||
Future<void> removeVaults(Set<VaultDetails> rows);
|
||||
|
||||
// trash
|
||||
|
||||
Future<void> clearTrashDetails();
|
||||
|
@ -76,11 +89,11 @@ abstract class MetadataDb {
|
|||
|
||||
Future<Set<FavouriteRow>> loadAllFavourites();
|
||||
|
||||
Future<void> addFavourites(Iterable<FavouriteRow> rows);
|
||||
Future<void> addFavourites(Set<FavouriteRow> rows);
|
||||
|
||||
Future<void> updateFavouriteId(int id, FavouriteRow row);
|
||||
|
||||
Future<void> removeFavourites(Iterable<FavouriteRow> rows);
|
||||
Future<void> removeFavourites(Set<FavouriteRow> rows);
|
||||
|
||||
// covers
|
||||
|
||||
|
@ -88,7 +101,7 @@ abstract class MetadataDb {
|
|||
|
||||
Future<Set<CoverRow>> loadAllCovers();
|
||||
|
||||
Future<void> addCovers(Iterable<CoverRow> rows);
|
||||
Future<void> addCovers(Set<CoverRow> rows);
|
||||
|
||||
Future<void> updateCoverEntryId(int id, CoverRow row);
|
||||
|
||||
|
@ -104,5 +117,5 @@ abstract class MetadataDb {
|
|||
|
||||
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows);
|
||||
|
||||
Future<void> removeVideoPlayback(Iterable<int> ids);
|
||||
Future<void> removeVideoPlayback(Set<int> ids);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/metadata/address.dart';
|
||||
import 'package:aves/model/metadata/catalog.dart';
|
||||
import 'package:aves/model/metadata/trash.dart';
|
||||
import 'package:aves/model/vaults/details.dart';
|
||||
import 'package:aves/model/video_playback.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -26,6 +27,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
static const addressTable = 'address';
|
||||
static const favouriteTable = 'favourites';
|
||||
static const coverTable = 'covers';
|
||||
static const vaultTable = 'vaults';
|
||||
static const trashTable = 'trash';
|
||||
static const videoPlaybackTable = 'videoPlayback';
|
||||
|
||||
|
@ -55,6 +57,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
', sourceDateTakenMillis INTEGER'
|
||||
', durationMillis INTEGER'
|
||||
', trashed INTEGER DEFAULT 0'
|
||||
', origin INTEGER DEFAULT 0'
|
||||
')');
|
||||
await db.execute('CREATE TABLE $dateTakenTable('
|
||||
'id INTEGER PRIMARY KEY'
|
||||
|
@ -89,6 +92,12 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
', packageName TEXT'
|
||||
', color INTEGER'
|
||||
')');
|
||||
await db.execute('CREATE TABLE $vaultTable('
|
||||
'name TEXT PRIMARY KEY'
|
||||
', autoLock INTEGER'
|
||||
', useBin INTEGER'
|
||||
', lockType TEXT'
|
||||
')');
|
||||
await db.execute('CREATE TABLE $trashTable('
|
||||
'id INTEGER PRIMARY KEY'
|
||||
', path TEXT'
|
||||
|
@ -100,7 +109,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
')');
|
||||
},
|
||||
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
||||
version: 10,
|
||||
version: 11,
|
||||
);
|
||||
|
||||
final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable');
|
||||
|
@ -122,7 +131,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> removeIds(Iterable<int> ids, {Set<EntryDataType>? dataTypes}) async {
|
||||
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes}) async {
|
||||
if (ids.isEmpty) return;
|
||||
|
||||
final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
|
||||
|
@ -162,15 +171,23 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Set<AvesEntry>> loadEntries({String? directory}) async {
|
||||
Future<Set<AvesEntry>> loadEntries({int? origin, String? directory}) async {
|
||||
String? where;
|
||||
final whereArgs = <Object?>[];
|
||||
|
||||
if (origin != null) {
|
||||
where = 'origin = ?';
|
||||
whereArgs.add(origin);
|
||||
}
|
||||
|
||||
if (directory != null) {
|
||||
final separator = pContext.separator;
|
||||
if (!directory.endsWith(separator)) {
|
||||
directory = '$directory$separator';
|
||||
}
|
||||
|
||||
const where = 'path LIKE ?';
|
||||
final whereArgs = ['$directory%'];
|
||||
where = '${where != null ? '$where AND ' : ''}path LIKE ?';
|
||||
whereArgs.add('$directory%');
|
||||
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
|
||||
|
||||
final dirLength = directory.length;
|
||||
|
@ -184,15 +201,15 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
.toSet();
|
||||
}
|
||||
|
||||
final rows = await _db.query(entryTable);
|
||||
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
|
||||
return rows.map(AvesEntry.fromMap).toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<AvesEntry>> loadEntriesById(Iterable<int> ids) => _getByIds(ids, entryTable, AvesEntry.fromMap);
|
||||
Future<Set<AvesEntry>> loadEntriesById(Set<int> ids) => _getByIds(ids, entryTable, AvesEntry.fromMap);
|
||||
|
||||
@override
|
||||
Future<void> saveEntries(Iterable<AvesEntry> entries) async {
|
||||
Future<void> saveEntries(Set<AvesEntry> entries) async {
|
||||
if (entries.isEmpty) return;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final batch = _db.batch();
|
||||
|
@ -258,7 +275,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Iterable<int> ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap);
|
||||
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Set<int> ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap);
|
||||
|
||||
@override
|
||||
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries) async {
|
||||
|
@ -317,7 +334,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Set<AddressDetails>> loadAddressesById(Iterable<int> ids) => _getByIds(ids, addressTable, AddressDetails.fromMap);
|
||||
Future<Set<AddressDetails>> loadAddressesById(Set<int> ids) => _getByIds(ids, addressTable, AddressDetails.fromMap);
|
||||
|
||||
@override
|
||||
Future<void> saveAddresses(Set<AddressDetails> addresses) async {
|
||||
|
@ -346,6 +363,54 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
);
|
||||
}
|
||||
|
||||
// vaults
|
||||
|
||||
@override
|
||||
Future<void> clearVaults() async {
|
||||
final count = await _db.delete(vaultTable, where: '1');
|
||||
debugPrint('$runtimeType clearVaults deleted $count rows');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<VaultDetails>> loadAllVaults() async {
|
||||
final rows = await _db.query(vaultTable);
|
||||
return rows.map(VaultDetails.fromMap).toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addVaults(Set<VaultDetails> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
final batch = _db.batch();
|
||||
rows.forEach((row) => _batchInsertVault(batch, row));
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateVault(String oldName, VaultDetails row) async {
|
||||
final batch = _db.batch();
|
||||
batch.delete(vaultTable, where: 'name = ?', whereArgs: [oldName]);
|
||||
_batchInsertVault(batch, row);
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
void _batchInsertVault(Batch batch, VaultDetails row) {
|
||||
batch.insert(
|
||||
vaultTable,
|
||||
row.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeVaults(Set<VaultDetails> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
|
||||
// using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead
|
||||
final batch = _db.batch();
|
||||
rows.map((v) => v.name).forEach((name) => batch.delete(vaultTable, where: 'name = ?', whereArgs: [name]));
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
// trash
|
||||
|
||||
@override
|
||||
|
@ -392,7 +457,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> addFavourites(Iterable<FavouriteRow> rows) async {
|
||||
Future<void> addFavourites(Set<FavouriteRow> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
final batch = _db.batch();
|
||||
rows.forEach((row) => _batchInsertFavourite(batch, row));
|
||||
|
@ -416,7 +481,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> removeFavourites(Iterable<FavouriteRow> rows) async {
|
||||
Future<void> removeFavourites(Set<FavouriteRow> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
final ids = rows.map((row) => row.entryId);
|
||||
if (ids.isEmpty) return;
|
||||
|
@ -442,7 +507,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> addCovers(Iterable<CoverRow> rows) async {
|
||||
Future<void> addCovers(Set<CoverRow> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
|
||||
final batch = _db.batch();
|
||||
|
@ -532,7 +597,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> removeVideoPlayback(Iterable<int> ids) async {
|
||||
Future<void> removeVideoPlayback(Set<int> ids) async {
|
||||
if (ids.isEmpty) return;
|
||||
|
||||
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead
|
||||
|
@ -543,7 +608,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
|
||||
// convenience methods
|
||||
|
||||
Future<Set<T>> _getByIds<T>(Iterable<int> ids, String table, T Function(Map<String, Object?> row) mapRow) async {
|
||||
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,
|
||||
|
|
|
@ -10,6 +10,7 @@ class MetadataDbUpgrader {
|
|||
static const addressTable = SqfliteMetadataDb.addressTable;
|
||||
static const favouriteTable = SqfliteMetadataDb.favouriteTable;
|
||||
static const coverTable = SqfliteMetadataDb.coverTable;
|
||||
static const vaultTable = SqfliteMetadataDb.vaultTable;
|
||||
static const trashTable = SqfliteMetadataDb.trashTable;
|
||||
static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable;
|
||||
|
||||
|
@ -45,6 +46,9 @@ class MetadataDbUpgrader {
|
|||
case 9:
|
||||
await _upgradeFrom9(db);
|
||||
break;
|
||||
case 10:
|
||||
await _upgradeFrom10(db);
|
||||
break;
|
||||
}
|
||||
oldVersion++;
|
||||
}
|
||||
|
@ -370,4 +374,17 @@ class MetadataDbUpgrader {
|
|||
});
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
static Future<void> _upgradeFrom10(Database db) async {
|
||||
debugPrint('upgrading DB from v10');
|
||||
|
||||
await db.execute('ALTER TABLE $entryTable ADD COLUMN origin INTEGER DEFAULT 0;');
|
||||
|
||||
await db.execute('CREATE TABLE $vaultTable('
|
||||
'name TEXT PRIMARY KEY'
|
||||
', autoLock INTEGER'
|
||||
', useBin INTEGER'
|
||||
', lockType TEXT'
|
||||
')');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
final Device device = Device._private();
|
||||
|
||||
class Device {
|
||||
late final String _userAgent;
|
||||
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper;
|
||||
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
|
||||
late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
|
||||
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
|
||||
|
||||
String get userAgent => _userAgent;
|
||||
|
||||
bool get canAuthenticateUser => _canAuthenticateUser;
|
||||
|
||||
bool get canGrantDirectoryAccess => _canGrantDirectoryAccess;
|
||||
|
||||
bool get canPinShortcut => _canPinShortcut;
|
||||
|
@ -23,6 +27,10 @@ class Device {
|
|||
|
||||
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
|
||||
|
||||
bool get canUseCrypto => _canUseCrypto;
|
||||
|
||||
bool get canUseVaults => canAuthenticateUser || canUseCrypto;
|
||||
|
||||
bool get hasGeocoder => _hasGeocoder;
|
||||
|
||||
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
|
||||
|
@ -42,6 +50,9 @@ class Device {
|
|||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
_isTelevision = androidInfo.systemFeatures.contains('android.software.leanback');
|
||||
|
||||
final auth = LocalAuthentication();
|
||||
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
|
||||
|
||||
final capabilities = await deviceService.getCapabilities();
|
||||
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
||||
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
||||
|
@ -49,6 +60,7 @@ class Device {
|
|||
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
|
||||
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
|
||||
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
|
||||
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
|
||||
_hasGeocoder = capabilities['hasGeocoder'] ?? false;
|
||||
_isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false;
|
||||
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'package:aves/services/common/services.dart';
|
|||
import 'package:aves/services/geocoding_service.dart';
|
||||
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||
import 'package:aves/theme/format.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/utils/time_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -29,6 +30,13 @@ import 'package:latlong2/latlong.dart';
|
|||
|
||||
enum EntryDataType { basic, aspectRatio, catalog, address, references }
|
||||
|
||||
class EntryOrigins {
|
||||
static const int mediaStoreContent = 0;
|
||||
static const int unknownContent = 1;
|
||||
static const int file = 2;
|
||||
static const int vault = 3;
|
||||
}
|
||||
|
||||
class AvesEntry {
|
||||
// `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode
|
||||
int id;
|
||||
|
@ -40,6 +48,7 @@ class AvesEntry {
|
|||
int width, height, sourceRotationDegrees;
|
||||
int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
|
||||
bool trashed;
|
||||
int origin;
|
||||
|
||||
int? _catalogDateMillis;
|
||||
CatalogMetadata? _catalogMetadata;
|
||||
|
@ -67,6 +76,7 @@ class AvesEntry {
|
|||
required this.sourceDateTakenMillis,
|
||||
required int? durationMillis,
|
||||
required this.trashed,
|
||||
required this.origin,
|
||||
this.burstEntries,
|
||||
}) : id = id ?? 0 {
|
||||
this.path = path;
|
||||
|
@ -87,6 +97,7 @@ class AvesEntry {
|
|||
String? title,
|
||||
int? dateAddedSecs,
|
||||
int? dateModifiedSecs,
|
||||
int? origin,
|
||||
List<AvesEntry>? burstEntries,
|
||||
}) {
|
||||
final copyEntryId = id ?? this.id;
|
||||
|
@ -107,6 +118,7 @@ class AvesEntry {
|
|||
sourceDateTakenMillis: sourceDateTakenMillis,
|
||||
durationMillis: durationMillis,
|
||||
trashed: trashed,
|
||||
origin: origin ?? this.origin,
|
||||
burstEntries: burstEntries ?? this.burstEntries,
|
||||
)
|
||||
..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
|
||||
|
@ -135,6 +147,7 @@ class AvesEntry {
|
|||
sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?,
|
||||
durationMillis: map['durationMillis'] as int?,
|
||||
trashed: (map['trashed'] as int? ?? 0) != 0,
|
||||
origin: map['origin'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -156,6 +169,7 @@ class AvesEntry {
|
|||
'sourceDateTakenMillis': sourceDateTakenMillis,
|
||||
'durationMillis': durationMillis,
|
||||
'trashed': trashed ? 1 : 0,
|
||||
'origin': origin,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -173,6 +187,7 @@ class AvesEntry {
|
|||
'sizeBytes': sizeBytes,
|
||||
'trashed': trashed,
|
||||
'trashPath': trashDetails?.path,
|
||||
'origin': origin,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -281,7 +296,9 @@ class AvesEntry {
|
|||
|
||||
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
|
||||
|
||||
bool get canEdit => !settings.isReadOnly && path != null && !trashed && isMediaStoreContent;
|
||||
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
|
||||
|
||||
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
|
||||
|
||||
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class Favourites with ChangeNotifier {
|
|||
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(entryId: entry.id);
|
||||
|
||||
Future<void> add(Set<AvesEntry> entries) async {
|
||||
final newRows = entries.map(_entryToRow);
|
||||
final newRows = entries.map(_entryToRow).toSet();
|
||||
|
||||
await metadataDb.addFavourites(newRows);
|
||||
_rows.addAll(newRows);
|
||||
|
|
|
@ -73,6 +73,7 @@ class AlbumFilter extends CoveredCollectionFilter {
|
|||
final albumType = covers.effectiveAlbumType(album);
|
||||
switch (albumType) {
|
||||
case AlbumType.regular:
|
||||
case AlbumType.vault:
|
||||
break;
|
||||
case AlbumType.app:
|
||||
final appColor = colors.appColor(album);
|
||||
|
|
|
@ -107,6 +107,7 @@ class MultiPageInfo {
|
|||
sourceDateTakenMillis: mainEntry.sourceDateTakenMillis,
|
||||
durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis,
|
||||
trashed: trashed,
|
||||
origin: mainEntry.origin,
|
||||
)
|
||||
..catalogMetadata = mainEntry.catalogMetadata?.copyWith(
|
||||
mimeType: pageInfo.mimeType,
|
||||
|
|
|
@ -31,10 +31,7 @@ class SettingsDefaults {
|
|||
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
||||
static const homePage = HomePageSetting.collection;
|
||||
static const enableBottomNavigationBar = true;
|
||||
static const confirmDeleteForever = true;
|
||||
static const confirmMoveToBin = true;
|
||||
static const confirmMoveUndatedItems = true;
|
||||
static const confirmAfterMoveToBin = true;
|
||||
static const confirm = true;
|
||||
static const setMetadataDateBeforeFileOp = false;
|
||||
static final drawerTypeBookmarks = [
|
||||
null,
|
||||
|
|
|
@ -6,7 +6,7 @@ enum AvesThemeBrightness { system, light, dark, black }
|
|||
|
||||
enum AvesThemeColorMode { monochrome, polychrome }
|
||||
|
||||
enum ConfirmationDialog { deleteForever, moveToBin, moveUndatedItems }
|
||||
enum ConfirmationDialog { createVault, deleteForever, moveToBin, moveUndatedItems }
|
||||
|
||||
enum CoordinateFormat { dms, decimal }
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import 'package:latlong2/latlong.dart';
|
|||
final Settings settings = Settings._private();
|
||||
|
||||
class Settings extends ChangeNotifier {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
|
||||
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
||||
|
||||
|
@ -40,7 +41,7 @@ class Settings extends ChangeNotifier {
|
|||
static const int _recentFilterHistoryMax = 10;
|
||||
static const Set<String> _internalKeys = {
|
||||
hasAcceptedTermsKey,
|
||||
catalogTimeZoneKey,
|
||||
catalogTimeZoneRawOffsetMillisKey,
|
||||
searchHistoryKey,
|
||||
platformAccelerometerRotationKey,
|
||||
platformTransitionAnimationScaleKey,
|
||||
|
@ -56,7 +57,7 @@ class Settings extends ChangeNotifier {
|
|||
static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed';
|
||||
static const isErrorReportingAllowedKey = 'is_crashlytics_enabled';
|
||||
static const localeKey = 'locale';
|
||||
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||
static const catalogTimeZoneRawOffsetMillisKey = 'catalog_time_zone_raw_offset_millis';
|
||||
static const tileExtentPrefixKey = 'tile_extent_';
|
||||
static const tileLayoutPrefixKey = 'tile_layout_';
|
||||
static const entryRenamingPatternKey = 'entry_renaming_pattern';
|
||||
|
@ -77,6 +78,7 @@ class Settings extends ChangeNotifier {
|
|||
static const keepScreenOnKey = 'keep_screen_on';
|
||||
static const homePageKey = 'home_page';
|
||||
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
|
||||
static const confirmCreateVaultKey = 'confirm_create_vault';
|
||||
static const confirmDeleteForeverKey = 'confirm_delete_forever';
|
||||
static const confirmMoveToBinKey = 'confirm_move_to_bin';
|
||||
static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items';
|
||||
|
@ -151,6 +153,7 @@ class Settings extends ChangeNotifier {
|
|||
// tag editor
|
||||
|
||||
static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded';
|
||||
static const tagEditorExpandedSectionKey = 'tag_editor_expanded_section';
|
||||
|
||||
// map
|
||||
static const mapStyleKey = 'info_map_style';
|
||||
|
@ -209,7 +212,10 @@ class Settings extends ChangeNotifier {
|
|||
await settingsStore.init();
|
||||
_appliedLocale = null;
|
||||
if (monitorPlatformSettings) {
|
||||
_platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChanged(event as Map?));
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
_subscriptions.add(_platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChanged(event as Map?)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,10 +283,10 @@ class Settings extends ChangeNotifier {
|
|||
}
|
||||
|
||||
Future<void> sanitize() async {
|
||||
if (timeToTakeAction == AccessibilityTimeout.system && !(await AccessibilityService.hasRecommendedTimeouts())) {
|
||||
if (timeToTakeAction == AccessibilityTimeout.system && !await AccessibilityService.hasRecommendedTimeouts()) {
|
||||
_set(timeToTakeActionKey, null);
|
||||
}
|
||||
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !(await windowService.isCutoutAware())) {
|
||||
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
|
||||
_set(viewerUseCutoutKey, null);
|
||||
}
|
||||
}
|
||||
|
@ -356,9 +362,9 @@ class Settings extends ChangeNotifier {
|
|||
return _appliedLocale!;
|
||||
}
|
||||
|
||||
String get catalogTimeZone => getString(catalogTimeZoneKey) ?? '';
|
||||
int get catalogTimeZoneRawOffsetMillis => getInt(catalogTimeZoneRawOffsetMillisKey) ?? 0;
|
||||
|
||||
set catalogTimeZone(String newValue) => _set(catalogTimeZoneKey, newValue);
|
||||
set catalogTimeZoneRawOffsetMillis(int newValue) => _set(catalogTimeZoneRawOffsetMillisKey, newValue);
|
||||
|
||||
double getTileExtent(String routeName) => getDouble(tileExtentPrefixKey + routeName) ?? 0;
|
||||
|
||||
|
@ -432,19 +438,23 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set enableBottomNavigationBar(bool newValue) => _set(enableBottomNavigationBarKey, newValue);
|
||||
|
||||
bool get confirmDeleteForever => getBool(confirmDeleteForeverKey) ?? SettingsDefaults.confirmDeleteForever;
|
||||
bool get confirmCreateVault => getBool(confirmCreateVaultKey) ?? SettingsDefaults.confirm;
|
||||
|
||||
set confirmCreateVault(bool newValue) => _set(confirmCreateVaultKey, newValue);
|
||||
|
||||
bool get confirmDeleteForever => getBool(confirmDeleteForeverKey) ?? SettingsDefaults.confirm;
|
||||
|
||||
set confirmDeleteForever(bool newValue) => _set(confirmDeleteForeverKey, newValue);
|
||||
|
||||
bool get confirmMoveToBin => getBool(confirmMoveToBinKey) ?? SettingsDefaults.confirmMoveToBin;
|
||||
bool get confirmMoveToBin => getBool(confirmMoveToBinKey) ?? SettingsDefaults.confirm;
|
||||
|
||||
set confirmMoveToBin(bool newValue) => _set(confirmMoveToBinKey, newValue);
|
||||
|
||||
bool get confirmMoveUndatedItems => getBool(confirmMoveUndatedItemsKey) ?? SettingsDefaults.confirmMoveUndatedItems;
|
||||
bool get confirmMoveUndatedItems => getBool(confirmMoveUndatedItemsKey) ?? SettingsDefaults.confirm;
|
||||
|
||||
set confirmMoveUndatedItems(bool newValue) => _set(confirmMoveUndatedItemsKey, newValue);
|
||||
|
||||
bool get confirmAfterMoveToBin => getBool(confirmAfterMoveToBinKey) ?? SettingsDefaults.confirmAfterMoveToBin;
|
||||
bool get confirmAfterMoveToBin => getBool(confirmAfterMoveToBinKey) ?? SettingsDefaults.confirm;
|
||||
|
||||
set confirmAfterMoveToBin(bool newValue) => _set(confirmAfterMoveToBinKey, newValue);
|
||||
|
||||
|
@ -698,6 +708,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set tagEditorCurrentFilterSectionExpanded(bool newValue) => _set(tagEditorCurrentFilterSectionExpandedKey, newValue);
|
||||
|
||||
String? get tagEditorExpandedSection => getString(tagEditorExpandedSectionKey);
|
||||
|
||||
set tagEditorExpandedSection(String? newValue) => _set(tagEditorExpandedSectionKey, newValue);
|
||||
|
||||
// map
|
||||
|
||||
EntryMapStyle? get mapStyle {
|
||||
|
@ -1010,6 +1024,7 @@ class Settings extends ChangeNotifier {
|
|||
case enableBlurEffectKey:
|
||||
case enableBottomNavigationBarKey:
|
||||
case mustBackTwiceToExitKey:
|
||||
case confirmCreateVaultKey:
|
||||
case confirmDeleteForeverKey:
|
||||
case confirmMoveToBinKey:
|
||||
case confirmMoveUndatedItemsKey:
|
||||
|
@ -1076,6 +1091,7 @@ class Settings extends ChangeNotifier {
|
|||
case videoControlsKey:
|
||||
case subtitleTextAlignmentKey:
|
||||
case subtitleTextPositionKey:
|
||||
case tagEditorExpandedSectionKey:
|
||||
case mapStyleKey:
|
||||
case mapDefaultCenterKey:
|
||||
case coordinateFormatKey:
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
|
@ -61,8 +62,10 @@ mixin AlbumMixin on SourceBase {
|
|||
}
|
||||
|
||||
void updateDirectories() {
|
||||
final visibleDirectories = visibleEntries.map((entry) => entry.directory).toSet();
|
||||
addDirectories(albums: visibleDirectories);
|
||||
addDirectories(albums: {
|
||||
...visibleEntries.map((entry) => entry.directory),
|
||||
...vaults.all.map((v) => v.path),
|
||||
});
|
||||
cleanEmptyAlbums();
|
||||
}
|
||||
|
||||
|
@ -73,25 +76,24 @@ mixin AlbumMixin on SourceBase {
|
|||
}
|
||||
}
|
||||
|
||||
void cleanEmptyAlbums([Set<String?>? albums]) {
|
||||
final emptyAlbums = (albums ?? _directories).where((v) => _isEmptyAlbum(v) && !_newAlbums.contains(v)).toSet();
|
||||
if (emptyAlbums.isNotEmpty) {
|
||||
_directories.removeAll(emptyAlbums);
|
||||
void cleanEmptyAlbums([Set<String>? albums]) {
|
||||
final removableAlbums = (albums ?? _directories).where(_isRemovable).toSet();
|
||||
if (removableAlbums.isNotEmpty) {
|
||||
_directories.removeAll(removableAlbums);
|
||||
_onAlbumChanged();
|
||||
invalidateAlbumFilterSummary(directories: emptyAlbums);
|
||||
invalidateAlbumFilterSummary(directories: removableAlbums);
|
||||
|
||||
final bookmarks = settings.drawerAlbumBookmarks;
|
||||
final pinnedFilters = settings.pinnedFilters;
|
||||
emptyAlbums.forEach((album) {
|
||||
removableAlbums.forEach((album) {
|
||||
bookmarks?.remove(album);
|
||||
pinnedFilters.removeWhere((filter) => filter is AlbumFilter && filter.album == album);
|
||||
});
|
||||
settings.drawerAlbumBookmarks = bookmarks;
|
||||
settings.pinnedFilters = pinnedFilters;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isEmptyAlbum(String? album) => !visibleEntries.any((entry) => entry.directory == album);
|
||||
bool _isRemovable(String album) {
|
||||
return !(visibleEntries.any((entry) => entry.directory == album) || _newAlbums.contains(album) || vaults.isVault(album));
|
||||
}
|
||||
|
||||
// filter summary
|
||||
|
||||
|
@ -169,8 +171,8 @@ mixin AlbumMixin on SourceBase {
|
|||
final separator = pContext.separator;
|
||||
assert(!dirPath.endsWith(separator));
|
||||
|
||||
final type = androidFileUtils.getAlbumType(dirPath);
|
||||
if (context != null) {
|
||||
final type = androidFileUtils.getAlbumType(dirPath);
|
||||
switch (type) {
|
||||
case AlbumType.camera:
|
||||
return context.l10n.albumCamera;
|
||||
|
@ -183,11 +185,14 @@ mixin AlbumMixin on SourceBase {
|
|||
case AlbumType.videoCaptures:
|
||||
return context.l10n.albumVideoCaptures;
|
||||
case AlbumType.regular:
|
||||
case AlbumType.vault:
|
||||
case AlbumType.app:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == AlbumType.vault) return pContext.basename(dirPath);
|
||||
|
||||
final dir = VolumeRelativeDirectory.fromPath(dirPath);
|
||||
if (dir == null) return dirPath;
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:aves/model/source/events.dart';
|
|||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/model/source/trash.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/analysis_service.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
@ -60,9 +61,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
final oldValue = event.oldValue;
|
||||
if (oldValue is List<String>?) {
|
||||
final oldHiddenFilters = (oldValue ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
|
||||
_onFilterVisibilityChanged(oldHiddenFilters, settings.hiddenFilters);
|
||||
final newlyVisibleFilters = oldHiddenFilters.whereNot(settings.hiddenFilters.contains).toSet();
|
||||
_onFilterVisibilityChanged(newlyVisibleFilters);
|
||||
}
|
||||
});
|
||||
vaults.addListener(() {
|
||||
final newlyVisibleFilters = vaults.vaultDirectories.whereNot(vaults.isLocked).map((v) => AlbumFilter(v, null)).toSet();
|
||||
_onFilterVisibilityChanged(newlyVisibleFilters);
|
||||
});
|
||||
}
|
||||
|
||||
final EventBus _eventBus = EventBus();
|
||||
|
@ -108,16 +114,22 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
_savedDates = Map.unmodifiable(await metadataDb.loadDates());
|
||||
}
|
||||
|
||||
Set<CollectionFilter> _getAppHiddenFilters() => {
|
||||
...settings.hiddenFilters,
|
||||
...vaults.vaultDirectories.where(vaults.isLocked).map((v) => AlbumFilter(v, null)),
|
||||
};
|
||||
|
||||
Iterable<AvesEntry> _applyHiddenFilters(Iterable<AvesEntry> entries) {
|
||||
final hiddenFilters = {
|
||||
TrashFilter.instance,
|
||||
...settings.hiddenFilters,
|
||||
..._getAppHiddenFilters(),
|
||||
};
|
||||
return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry)));
|
||||
}
|
||||
|
||||
Iterable<AvesEntry> _applyTrashFilter(Iterable<AvesEntry> entries) {
|
||||
return entries.where(TrashFilter.instance.test);
|
||||
final hiddenFilters = _getAppHiddenFilters();
|
||||
return entries.where(TrashFilter.instance.test).where((entry) => !hiddenFilters.any((filter) => filter.test(entry)));
|
||||
}
|
||||
|
||||
void _invalidate({Set<AvesEntry>? entries, bool notify = true}) {
|
||||
|
@ -198,23 +210,24 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
|
||||
Future<void> _moveEntry(AvesEntry entry, Map newFields, {required bool persist}) async {
|
||||
newFields.keys.forEach((key) {
|
||||
final newValue = newFields[key];
|
||||
switch (key) {
|
||||
case 'contentId':
|
||||
entry.contentId = newFields['contentId'] as int?;
|
||||
entry.contentId = newValue as int?;
|
||||
break;
|
||||
case 'dateModifiedSecs':
|
||||
// `dateModifiedSecs` changes when moving entries to another directory,
|
||||
// but it does not change when renaming the containing directory
|
||||
entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int?;
|
||||
entry.dateModifiedSecs = newValue as int?;
|
||||
break;
|
||||
case 'path':
|
||||
entry.path = newFields['path'] as String?;
|
||||
entry.path = newValue as String?;
|
||||
break;
|
||||
case 'title':
|
||||
entry.sourceTitle = newFields['title'] as String?;
|
||||
entry.sourceTitle = newValue as String?;
|
||||
break;
|
||||
case 'trashed':
|
||||
final trashed = newFields['trashed'] as bool;
|
||||
final trashed = newValue as bool;
|
||||
entry.trashed = trashed;
|
||||
entry.trashDetails = trashed
|
||||
? TrashDetails(
|
||||
|
@ -225,7 +238,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
: null;
|
||||
break;
|
||||
case 'uri':
|
||||
entry.uri = newFields['uri'] as String;
|
||||
entry.uri = newValue as String;
|
||||
break;
|
||||
case 'origin':
|
||||
entry.origin = newValue as int;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -251,6 +267,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum);
|
||||
final pinned = settings.pinnedFilters.contains(oldFilter);
|
||||
|
||||
if (vaults.isVault(sourceAlbum)) {
|
||||
await vaults.rename(sourceAlbum, destinationAlbum);
|
||||
}
|
||||
|
||||
final existingCover = covers.of(oldFilter);
|
||||
await covers.set(
|
||||
filter: newFilter,
|
||||
|
@ -266,6 +286,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
destinationAlbums: {destinationAlbum},
|
||||
movedOps: movedOps,
|
||||
);
|
||||
|
||||
// restore bookmark and pin, as the obsolete album got removed and its associated state cleaned
|
||||
if (bookmark != null && bookmark != -1) {
|
||||
settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum);
|
||||
|
@ -312,6 +333,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
title: newFields['title'] as String?,
|
||||
dateAddedSecs: newFields['dateAddedSecs'] as int?,
|
||||
dateModifiedSecs: newFields['dateModifiedSecs'] as int?,
|
||||
origin: newFields['origin'] as int?,
|
||||
));
|
||||
} else {
|
||||
debugPrint('failed to find source entry with uri=$sourceUri');
|
||||
|
@ -345,7 +367,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
break;
|
||||
case MoveType.move:
|
||||
case MoveType.export:
|
||||
cleanEmptyAlbums(fromAlbums);
|
||||
cleanEmptyAlbums(fromAlbums.whereNotNull().toSet());
|
||||
addDirectories(albums: destinationAlbums);
|
||||
break;
|
||||
case MoveType.toBin:
|
||||
|
@ -507,11 +529,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
return recentEntry(filter);
|
||||
}
|
||||
|
||||
void _onFilterVisibilityChanged(Set<CollectionFilter> oldHiddenFilters, Set<CollectionFilter> currentHiddenFilters) {
|
||||
void _onFilterVisibilityChanged(Set<CollectionFilter> newlyVisibleFilters) {
|
||||
updateDerivedFilters();
|
||||
eventBus.fire(const FilterVisibilityChangedEvent());
|
||||
|
||||
final newlyVisibleFilters = oldHiddenFilters.whereNot(currentHiddenFilters.contains).toSet();
|
||||
if (newlyVisibleFilters.isNotEmpty) {
|
||||
final candidateEntries = visibleEntries.where((entry) => newlyVisibleFilters.any((f) => f.test(entry))).toSet();
|
||||
analyze(null, entries: candidateEntries);
|
||||
|
|