Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2023-02-20 10:06:42 +01:00
commit 5298416819
308 changed files with 8037 additions and 2167 deletions

@ -1 +1 @@
Subproject commit 135454af32477f815a7525073027a3ff9eff1bfd Subproject commit 9944297138845a94256f1cf37beb88ff9a8e811a

View file

@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="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 ## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18
### Added ### Added

View file

@ -18,6 +18,9 @@ if (localPropertiesFile.exists()) {
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterVersionName = localProperties.getProperty('flutter.versionName')
def flutterRoot = localProperties.getProperty('flutter.sdk') 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" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Keys // Keys
@ -181,10 +184,11 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0' 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.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.media:media:1.6.0' implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1' 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.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0' implementation 'com.commonsware.cwac:document:0.5.0'

View file

@ -19,7 +19,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:required="false" /> 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` So we request `WRITE_EXTERNAL_STORAGE` until Android 10 (API 29), and enable `requestLegacyExternalStorage`
--> -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <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" android:maxSdkVersion="29"
tools:ignore="ScopedStorage" /> 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 --> <!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->
<uses-permission <uses-permission
android:name="android.permission.MANAGE_MEDIA" android:name="android.permission.MANAGE_MEDIA"
tools:ignore="ProtectedPermissions" /> 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 --> <!-- to show foreground service progress via notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- to change wallpaper -->
<!-- to access media with original metadata with scoped storage (Android >=10) --> <uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <!-- 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 --> <!-- TODO TLAD remove this permission when this is fixed: https://github.com/flutter/flutter/issues/42451 -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- for API <26 --> <uses-permission
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> 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 --> <!-- from Android 11, we should define <queries> to make other apps visible to this app -->
<queries> <queries>
@ -75,12 +82,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:allowBackup="true" android:allowBackup="true"
android:appCategory="image" android:appCategory="image"
android:banner="@drawable/banner" android:banner="@drawable/banner"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/full_backup_content"
android:fullBackupOnly="true" android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
tools:targetApi="o"> tools:targetApi="s">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View file

@ -37,12 +37,16 @@ class AnalysisService : Service() {
} }
} }
initChannels(this) try {
initChannels(this)
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply { HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
start() start()
serviceLooper = looper serviceLooper = looper
serviceHandler = ServiceHandler(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) { 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 // channels for analysis

View file

@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class HomeWidgetSettingsActivity : MainActivity() { class HomeWidgetSettingsActivity : MainActivity() {
@ -28,8 +29,12 @@ class HomeWidgetSettingsActivity : MainActivity() {
finish() finish()
return return
} }
}
val messenger = flutterEngine!!.dartExecutor override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
MethodChannel(messenger, CHANNEL).setMethodCallHandler { call, result -> MethodChannel(messenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"configure" -> { "configure" -> {
@ -42,9 +47,9 @@ class HomeWidgetSettingsActivity : MainActivity() {
} }
private fun saveWidget() { private fun saveWidget() {
val appWidgetManager = AppWidgetManager.getInstance(context) val appWidgetManager = AppWidgetManager.getInstance(this)
val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId) 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) val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, intent) setResult(RESULT_OK, intent)

View file

@ -90,7 +90,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
val messenger = flutterEngine!!.dartExecutor val messenger = flutterEngine!!.dartExecutor
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
try { try {
val bytes = suspendCoroutine { cont -> val bytes = suspendCoroutine<Any?> { cont ->
defaultScope.launch { defaultScope.launch {
FlutterUtils.runOnUiThread { FlutterUtils.runOnUiThread {
channel.invokeMethod("drawWidget", hashMapOf( channel.invokeMethod("drawWidget", hashMapOf(
@ -194,7 +194,10 @@ class HomeWidgetProvider : AppWidgetProvider() {
} }
private fun initChannels(context: Context) { 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 // dart -> platform -> dart
// - need Context // - need Context

View file

@ -25,14 +25,15 @@ import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat 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.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
open class MainActivity : FlutterActivity() { open class MainActivity : FlutterFragmentActivity() {
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
private lateinit var intentStreamHandler: IntentStreamHandler private lateinit var intentStreamHandler: IntentStreamHandler
@ -68,8 +69,12 @@ open class MainActivity : FlutterActivity() {
// .build() // .build()
// ) // )
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
}
val messenger = flutterEngine!!.dartExecutor override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// notification: platform -> dart // notification: platform -> dart
analysisStreamHandler = AnalysisStreamHandler().apply { analysisStreamHandler = AnalysisStreamHandler().apply {
@ -99,6 +104,7 @@ open class MainActivity : FlutterActivity() {
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler) MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, SecurityHandler.CHANNEL).setMethodCallHandler(SecurityHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
@ -172,7 +178,18 @@ open class MainActivity : FlutterActivity() {
mediaSessionHandler.dispose() mediaSessionHandler.dispose()
mediaStoreChangeStreamHandler.dispose() mediaStoreChangeStreamHandler.dispose()
settingsChangeStreamHandler.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) { override fun onNewIntent(intent: Intent) {
@ -182,6 +199,7 @@ open class MainActivity : FlutterActivity() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data) DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
DELETE_SINGLE_PERMISSION_REQUEST, DELETE_SINGLE_PERMISSION_REQUEST,
@ -244,7 +262,7 @@ open class MainActivity : FlutterActivity() {
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> { Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional // MIME type is optional
val type = intent.type ?: intent.resolveType(context) val type = intent.type ?: intent.resolveType(this)
return hashMapOf( return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW, INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to type, INTENT_DATA_KEY_MIME_TYPE to type,
@ -314,7 +332,7 @@ open class MainActivity : FlutterActivity() {
private fun submitPickedItems(call: MethodCall) { private fun submitPickedItems(call: MethodCall) {
val pickedUris = call.argument<List<String>>("uris") val pickedUris = call.argument<List<String>>("uris")
if (pickedUris != null && pickedUris.isNotEmpty()) { 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 intent = Intent().apply {
val firstUri = toUri(pickedUris.first()) val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) { if (pickedUris.size == 1) {

View file

@ -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 { val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
setMethodCallHandler { call, result -> setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {

View file

@ -1,6 +1,5 @@
package deckers.thibault.aves package deckers.thibault.aves
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build 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.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat 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.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterActivity() { class WallpaperActivity : FlutterFragmentActivity() {
private lateinit var intentDataMap: MutableMap<String, Any?> private lateinit var intentDataMap: MutableMap<String, Any?>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -36,8 +36,33 @@ class WallpaperActivity : FlutterActivity() {
Log.i(LOG_TAG, "onCreate intent extras=$it") Log.i(LOG_TAG, "onCreate intent extras=$it")
} }
intentDataMap = extractIntentData(intent) 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() { 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) { private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getIntentData" -> { "getIntentData" -> {
@ -94,7 +93,7 @@ class WallpaperActivity : FlutterActivity() {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> { Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional // MIME type is optional
val type = intent.type ?: intent.resolveType(context) val type = intent.type ?: intent.resolveType(this)
return hashMapOf( return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER, MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type, MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,

View file

@ -21,7 +21,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
when (call.method) { when (call.method) {
"canManageMedia" -> safe(call, result, ::canManageMedia) "canManageMedia" -> safe(call, result, ::canManageMedia)
"getCapabilities" -> safe(call, result, ::getCapabilities) "getCapabilities" -> safe(call, result, ::getCapabilities)
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone) "getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
"getLocales" -> safe(call, result, ::getLocales) "getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled) "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
@ -41,9 +41,10 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), "canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT), "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), "canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N), "canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"hasGeocoder" to Geocoder.isPresent(), "hasGeocoder" to Geocoder.isPresent(),
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S), "isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), "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) { private fun getDefaultTimeZoneRawOffsetMillis(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id) result.success(TimeZone.getDefault().rawOffset)
} }
private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {

View file

@ -35,6 +35,15 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
} }
fun dispose() { fun dispose() {
unregisterNoisyAudioReceiver()
}
private fun registerNoisyAudioReceiver() {
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
isNoisyAudioReceiverRegistered = true
}
private fun unregisterNoisyAudioReceiver() {
if (isNoisyAudioReceiverRegistered) { if (isNoisyAudioReceiverRegistered) {
context.unregisterReceiver(noisyAudioReceiver) context.unregisterReceiver(noisyAudioReceiver)
isNoisyAudioReceiverRegistered = false isNoisyAudioReceiverRegistered = false
@ -51,14 +60,17 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) { private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } 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 durationMillis = call.argument<Number>("durationMillis")?.toLong()
val stateString = call.argument<String>("state") val stateString = call.argument<String>("state")
val positionMillis = call.argument<Number>("positionMillis")?.toLong() val positionMillis = call.argument<Number>("positionMillis")?.toLong()
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat() val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) { 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 return
} }
@ -67,7 +79,7 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
STATE_PAUSED -> PlaybackStateCompat.STATE_PAUSED STATE_PAUSED -> PlaybackStateCompat.STATE_PAUSED
STATE_PLAYING -> PlaybackStateCompat.STATE_PLAYING STATE_PLAYING -> PlaybackStateCompat.STATE_PLAYING
else -> { else -> {
result.error("update-state", "unknown state=$stateString", null) result.error("updateSession-state", "unknown state=$stateString", null)
return return
} }
} }
@ -90,39 +102,41 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
.build() .build()
FlutterUtils.runOnUiThread { FlutterUtils.runOnUiThread {
if (session == null) { try {
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE) if (session == null) {
val mbrName = ComponentName(context, MediaButtonReceiver::class.java) val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply { val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
setCallback(mediaCommandHandler) session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
setCallback(mediaCommandHandler)
}
} }
} session!!.apply {
session!!.apply { val metadata = MediaMetadataCompat.Builder()
val metadata = MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis) .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString()) .build()
.build() setMetadata(metadata)
setMetadata(metadata) setPlaybackState(playbackState)
setPlaybackState(playbackState) if (!isActive) {
if (!isActive) { isActive = true
isActive = true }
} }
}
val isPlaying = state == PlaybackStateCompat.STATE_PLAYING val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
if (!wasPlaying && isPlaying) { if (!wasPlaying && isPlaying) {
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) registerNoisyAudioReceiver()
isNoisyAudioReceiverRegistered = true } else if (wasPlaying && !isPlaying) {
} else if (wasPlaying && !isPlaying) { unregisterNoisyAudioReceiver()
context.unregisterReceiver(noisyAudioReceiver) }
isNoisyAudioReceiverRegistered = false 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) { private fun releaseSession(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.FileNotFoundException
class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler { class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -195,6 +196,8 @@ private class MetadataOpCallback(
} else { } else {
"$errorCodeBase-mp4largeother" "$errorCodeBase-mp4largeother"
} }
} else if (throwable is FileNotFoundException) {
"$errorCodeBase-filenotfound"
} else { } else {
"$errorCodeBase-failure" "$errorCodeBase-failure"
} }

View file

@ -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.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
import deckers.thibault.aves.model.FieldMap 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.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN 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) } "getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
"getIptc" -> ioScope.launch { safe(call, result, ::getIptc) } "getIptc" -> ioScope.launch { safe(call, result, ::getIptc) }
"getXmp" -> ioScope.launch { safe(call, result, ::getXmp) } "getXmp" -> ioScope.launch { safe(call, result, ::getXmp) }
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentResolverProp) } "hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentResolverProp) } "getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
"getDate" -> ioScope.launch { safe(call, result, ::getDate) } "getDate" -> ioScope.launch { safe(call, result, ::getDate) }
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) } "getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
else -> result.notImplemented() else -> result.notImplemented()
@ -1047,10 +1047,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(xmpStrings) 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") val prop = call.argument<String>("prop")
if (prop == null) { if (prop == null) {
result.error("hasContentResolverProp-args", "missing arguments", null) result.error("hasContentProp-args", "missing arguments", null)
return return
} }
@ -1058,27 +1058,27 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
when (prop) { when (prop) {
"owner_package_name" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q "owner_package_name" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
else -> { else -> {
result.error("hasContentResolverProp-unknown", "unknown property=$prop", null) result.error("hasContentProp-unknown", "unknown property=$prop", null)
return return
} }
} }
) )
} }
private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) { private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val prop = call.argument<String>("prop") val prop = call.argument<String>("prop")
if (mimeType == null || uri == null || prop == null) { if (mimeType == null || uri == null || prop == null) {
result.error("getContentResolverProp-args", "missing arguments", null) result.error("getContentPropValue-args", "missing arguments", null)
return return
} }
try { try {
val value = context.queryContentResolverProp(uri, mimeType, prop) val value = context.queryContentPropValue(uri, mimeType, prop)
result.success(value?.toString()) result.success(value?.toString())
} catch (e: Exception) { } 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)
} }
} }

View file

@ -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"
}
}

View file

@ -8,6 +8,7 @@ import androidx.core.os.EnvironmentCompat
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.PermissionManager 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.getPrimaryVolumePath
import deckers.thibault.aves.utils.StorageUtils.getVolumePaths import deckers.thibault.aves.utils.StorageUtils.getVolumePaths
import io.flutter.plugin.common.MethodCall 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) } "getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) } "getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) } "getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
"getInaccessibleDirectories" -> ioScope.launch { safe(call, result, ::getInaccessibleDirectories) } "getInaccessibleDirectories" -> ioScope.launch { safe(call, result, ::getInaccessibleDirectories) }
@ -88,6 +90,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(volumes) 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) { private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path") val path = call.argument<String>("path")
if (path == null) { if (path == null) {

View file

@ -109,7 +109,7 @@ class ThumbnailFetcher internal constructor(
} else { } else {
@Suppress("deprecation") @Suppress("deprecation")
var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null) 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) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
} }

View file

@ -12,7 +12,7 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(true) 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") val on = call.argument<Boolean>("on")
if (on == null) { if (on == null) {
result.error("keepOn-args", "missing arguments", 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 window = activity.window
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
val old = (window.attributes.flags and flag) != 0 val old = (window.attributes.flags and flag) != 0
if (old != on) { if (old != on) {
if (on) { if (on) {
@ -33,6 +31,14 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(null) 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) { override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
val orientation = call.argument<Int>("orientation") val orientation = call.argument<Int>("orientation")
if (orientation == null) { if (orientation == null) {

View file

@ -13,6 +13,10 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
result.success(null) result.success(null)
} }
override fun secureScreen(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) { override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
result.success(false) result.success(false)
} }

View file

@ -13,6 +13,7 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
when (call.method) { when (call.method) {
"isActivity" -> Coresult.safe(call, result, ::isActivity) "isActivity" -> Coresult.safe(call, result, ::isActivity)
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn) "keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
"secureScreen" -> Coresult.safe(call, result, ::secureScreen)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked) "isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware) "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 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) { private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var locked = false var locked = false
try { try {

View file

@ -139,8 +139,8 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
var destinationDir = arguments["destinationPath"] as String? var destinationDir = arguments["destinationPath"] as String?
val mimeType = arguments["mimeType"] as String? val mimeType = arguments["mimeType"] as String?
val width = arguments["width"] as Int? val width = (arguments["width"] as Number?)?.toInt()
val height = arguments["height"] as Int? val height = (arguments["height"] as Number?)?.toInt()
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) { if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
error("export-args", "missing arguments", null) error("export-args", "missing arguments", null)

View file

@ -15,7 +15,7 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader 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.LogUtils
import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes 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) { if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try { 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) { if (xmpBytes is ByteArray && xmpBytes.size > 0) {
val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS) val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS)
processXmp(xmpMeta) processXmp(xmpMeta)
@ -170,6 +170,11 @@ object XMP {
} }
} }
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device` // 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(channel, boxParser).use { isoFile ->
isoFile.processBoxes(UserBox::class.java, true) { box, _ -> isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
val boxSize = box.size val boxSize = box.size

View file

@ -33,6 +33,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException import java.io.IOException
class SourceEntry { class SourceEntry {
private val origin: Int
val uri: Uri // content or file URI val uri: Uri // content or file URI
var path: String? = null // best effort to get local path var path: String? = null // best effort to get local path
private val sourceMimeType: String private val sourceMimeType: String
@ -48,12 +49,14 @@ class SourceEntry {
private var foundExif: Boolean = false private var foundExif: Boolean = false
constructor(uri: Uri, sourceMimeType: String) { constructor(origin: Int, uri: Uri, sourceMimeType: String) {
this.origin = origin
this.uri = uri this.uri = uri
this.sourceMimeType = sourceMimeType this.sourceMimeType = sourceMimeType
} }
constructor(map: FieldMap) { constructor(map: FieldMap) {
origin = map["origin"] as Int
uri = Uri.parse(map["uri"] as String) uri = Uri.parse(map["uri"] as String)
path = map["path"] as String? path = map["path"] as String?
sourceMimeType = map["sourceMimeType"] as String sourceMimeType = map["sourceMimeType"] as String
@ -77,6 +80,7 @@ class SourceEntry {
fun toMap(): FieldMap { fun toMap(): FieldMap {
return hashMapOf( return hashMapOf(
"origin" to origin,
"uri" to uri.toString(), "uri" to uri.toString(),
"path" to path, "path" to path,
"sourceMimeType" to sourceMimeType, "sourceMimeType" to sourceMimeType,
@ -249,13 +253,15 @@ class SourceEntry {
private fun fillByTiffDecode(context: Context) { private fun fillByTiffDecode(context: Context) {
try { try {
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() ?: return context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
val options = TiffBitmapFactory.Options().apply { val fd = pfd.detachFd()
inJustDecodeBounds = true 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) { } catch (e: Exception) {
// ignore // ignore
} }
@ -267,5 +273,11 @@ class SourceEntry {
is Int -> o.toLong() is Int -> o.toLong()
else -> o as? Long 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
} }
} }

View file

@ -44,6 +44,7 @@ internal class ContentImageProvider : ImageProvider() {
} }
val fields: FieldMap = hashMapOf( val fields: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
"uri" to uri.toString(), "uri" to uri.toString(),
"sourceMimeType" to mimeType, "sourceMimeType" to mimeType,
) )

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import java.io.File import java.io.File
@ -15,7 +16,7 @@ internal class FileImageProvider : ImageProvider() {
return return
} }
val entry = SourceEntry(uri, sourceMimeType) val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType)
val path = uri.path val path = uri.path
if (path != null) { if (path != null) {
@ -52,6 +53,19 @@ internal class FileImageProvider : ImageProvider() {
throw Exception("failed to delete entry with uri=$uri path=$path") 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 { companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>() private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>()
} }

View file

@ -811,11 +811,6 @@ abstract class ImageProvider {
fields: List<String>, fields: List<String>,
callback: ImageOpCallback, 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 -> val success = editExif(context, path, uri, mimeType, callback) { exif ->
when { when {
dateMillis != null -> { dateMillis != null -> {

View file

@ -9,6 +9,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.core.net.toUri
import com.commonsware.cwac.document.DocumentFileCompat import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
import java.io.SyncFailedException
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation 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") Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else { } else {
var entryMap: FieldMap = hashMapOf( var entryMap: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"uri" to itemUri.toString(), "uri" to itemUri.toString(),
"path" to cursor.getString(pathColumn), "path" to cursor.getString(pathColumn),
"sourceMimeType" to mimeType, "sourceMimeType" to mimeType,
@ -349,7 +352,7 @@ class MediaStoreImageProvider : ImageProvider() {
} }
} catch (securityException: SecurityException) { } catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory, // 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 // 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) { 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) 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 entries = kv.value
val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER
val toVault = StorageUtils.isInVault(activity, targetDir)
val toAppDir = toBin || toVault
var effectiveTargetDir: String? = null var effectiveTargetDir: String? = null
var targetDirDocFile: DocumentFileCompat? = null var targetDirDocFile: DocumentFileCompat? = null
if (!toBin) { if (!toAppDir) {
effectiveTargetDir = targetDir effectiveTargetDir = targetDir
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
if (!File(targetDir).exists()) { if (!File(targetDir).exists()) {
@ -437,13 +442,20 @@ class MediaStoreImageProvider : ImageProvider() {
// - there is no documentation regarding support for usage with removable storage // - 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 // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try { try {
if (toBin) { val appDir = when {
val trashDir = StorageUtils.trashDirFor(activity, sourcePath) toBin -> StorageUtils.trashDirFor(activity, sourcePath)
if (trashDir != null) { toVault -> File(targetDir)
effectiveTargetDir = ensureTrailingSeparator(trashDir.path) else -> null
targetDirDocFile = DocumentFileCompat.fromFile(trashDir) }
if (appDir != null) {
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
if (toVault) {
appDir.mkdirs()
} }
} }
if (effectiveTargetDir != null) { if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else { val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = File(sourcePath) val sourceFile = File(sourcePath)
@ -462,6 +474,7 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType = mimeType, mimeType = mimeType,
copy = copy, copy = copy,
toBin = toBin, toBin = toBin,
toVault = toVault,
) )
} }
} }
@ -488,6 +501,7 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType: String, mimeType: String,
copy: Boolean, copy: Boolean,
toBin: Boolean, toBin: Boolean,
toVault: Boolean,
): FieldMap { ): FieldMap {
val sourcePath = sourceFile.path val sourcePath = sourceFile.path
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
@ -512,7 +526,16 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, 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) { if (!copy) {
// delete original entry // delete original entry
@ -522,13 +545,21 @@ class MediaStoreImageProvider : ImageProvider() {
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
} }
} }
if (toBin) { return if (toBin) {
return hashMapOf( hashMapOf(
"trashed" to true, "trashed" to true,
"trashPath" to targetPath, "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 // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
@ -910,7 +941,7 @@ class MediaStoreImageProvider : ImageProvider() {
private val VIDEO_PROJECTION = arrayOf( private val VIDEO_PROJECTION = arrayOf(
*BASE_PROJECTION, *BASE_PROJECTION,
MediaColumns.DURATION, 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( *if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(
MediaStore.MediaColumns.ORIENTATION, MediaStore.MediaColumns.ORIENTATION,
) else emptyArray() ) else emptyArray()

View file

@ -7,6 +7,7 @@ import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import deckers.thibault.aves.utils.UriUtils.tryParseId 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 } 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 var contentUri: Uri = uri
if (StorageUtils.isMediaStoreContentUri(uri)) { if (StorageUtils.isMediaStoreContentUri(uri)) {
uri.tryParseId()?.let { id -> 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 var value: Any? = null
try { try {
value = when (cursor.getType(0)) { val cursor = contentResolver.query(contentUri, arrayOf(column), null, null, null)
Cursor.FIELD_TYPE_NULL -> null if (cursor == null || !cursor.moveToFirst()) {
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0) Log.w(LOG_TAG, "failed to get cursor for contentUri=$contentUri column=$column")
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0) } else {
Cursor.FIELD_TYPE_STRING -> cursor.getString(0) value = when (cursor.getType(0)) {
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0) Cursor.FIELD_TYPE_NULL -> null
else -> 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) { } 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 return value
} }
} }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
object MimeTypes { object MimeTypes {
@ -153,47 +154,11 @@ object MimeTypes {
// among other refs: // among other refs:
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types // - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
fun extensionFor(mimeType: String): String? = when (mimeType) { fun extensionFor(mimeType: String): String? = when (mimeType) {
ARW -> ".arw"
AVI, AVI_VND -> ".avi" 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" HEIC, HEIF -> ".heif"
ICO -> ".ico"
JPEG -> ".jpg"
K25 -> ".k25"
KDC -> ".kdc"
MKV -> ".mkv"
MOV -> ".mov"
MP2T, MP2TS -> ".m2ts" MP2T, MP2TS -> ".m2ts"
MP4 -> ".mp4"
MRW -> ".mrw"
NEF -> ".nef"
NRW -> ".nrw"
OGV -> ".ogv"
ORF -> ".orf"
PEF -> ".pef"
PNG -> ".png"
PSD_VND, PSD_X -> ".psd" PSD_VND, PSD_X -> ".psd"
RAF -> ".raf" else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
RAW -> ".raw"
RW2 -> ".rw2"
SR2 -> ".sr2"
SRF -> ".srf"
SRW -> ".srw"
SVG -> ".svg"
TIFF -> ".tiff"
WBMP -> ".wbmp"
WEBM -> ".webm"
WEBP -> ".webp"
X3F -> ".x3f"
else -> null
} }
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE) val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)

View file

@ -119,7 +119,7 @@ object PermissionManager {
dirSet.add("") dirSet.add("")
} }
} else { } else {
// request volume root until Android 10 // request volume root until Android 10 (API 29)
dirSet.add("") dirSet.add("")
} }
dirsPerVolume[volumePath] = dirSet dirsPerVolume[volumePath] = dirSet

View file

@ -39,28 +39,29 @@ object StorageUtils {
private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/" 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("(.*?):(.*)") private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
const val TRASH_PATH_PLACEHOLDER = "#trash" const val TRASH_PATH_PLACEHOLDER = "#trash"
private fun isAppFile(context: Context, path: String): Boolean { private fun isAppFile(context: Context, path: String): Boolean {
val filesDirs = context.getExternalFilesDirs(null).filterNotNull() val dirs = context.getExternalFilesDirs(null).filterNotNull()
return filesDirs.any { path.startsWith(it.path) } return dirs.any { path.startsWith(it.path) }
} }
private fun appExternalFilesDirFor(context: Context, path: String): File? { 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) 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? { fun trashDirFor(context: Context, path: String): File? {
val filesDir = appExternalFilesDirFor(context, path) val externalFilesDir = appExternalFilesDirFor(context, path)
if (filesDir == null) { if (externalFilesDir == null) {
Log.e(LOG_TAG, "failed to find external files dir for path=$path") Log.e(LOG_TAG, "failed to find external files dir for path=$path")
return null return null
} }
val trashDir = File(filesDir, "trash") val trashDir = File(externalFilesDir, "trash")
if (!trashDir.exists() && !trashDir.mkdirs()) { if (!trashDir.exists() && !trashDir.mkdirs()) {
Log.e(LOG_TAG, "failed to create directories at path=$trashDir") Log.e(LOG_TAG, "failed to create directories at path=$trashDir")
return null return null
@ -68,6 +69,10 @@ object StorageUtils {
return trashDir return trashDir
} }
fun getVaultRoot(context: Context) = ensureTrailingSeparator(File(context.filesDir, "vault").path)
fun isInVault(context: Context, path: String) = path.startsWith(getVaultRoot(context))
/** /**
* Volume paths * Volume paths
*/ */
@ -259,6 +264,7 @@ object StorageUtils {
// e.g. // e.g.
// /storage/emulated/0/ -> primary // /storage/emulated/0/ -> primary
// /storage/10F9-3F13/Pictures/ -> 10F9-3F13 // /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? { private fun getVolumeUuidForDocumentUri(context: Context, anyPath: String): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
@ -278,7 +284,22 @@ object StorageUtils {
return EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID return EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
} }
volumePath.split(File.separator).lastOrNull { it.isNotEmpty() }?.let { uuid -> 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. // e.g.
// primary -> /storage/emulated/0/ // primary -> /storage/emulated/0/
// 10F9-3F13 -> /storage/10F9-3F13/ // 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? { private fun getVolumePathFromTreeDocumentUriUuid(context: Context, uuid: String): String? {
if (uuid == EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID) { if (uuid == EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID) {
return getPrimaryVolumePath(context) 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") Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid")
return null return null
} }
@ -350,9 +376,9 @@ object StorageUtils {
} }
// e.g. // e.g.
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/ // content://com.android.externalstorage.documents/tree/primary%3A -> ("primary", "")
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/ // content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> ("10F9-3F13", "Pictures")
fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? { private fun splitTreeDocumentUri(treeDocumentUri: Uri): Pair<String, String>? {
val treeDocumentUriString = treeDocumentUri.toString() val treeDocumentUriString = treeDocumentUri.toString()
if (treeDocumentUriString.length <= TREE_URI_ROOT.length) return null if (treeDocumentUriString.length <= TREE_URI_ROOT.length) return null
val encoded = treeDocumentUriString.substring(TREE_URI_ROOT.length) val encoded = treeDocumentUriString.substring(TREE_URI_ROOT.length)
@ -362,13 +388,24 @@ object StorageUtils {
val uuid = group(1) val uuid = group(1)
val relativePath = group(2) val relativePath = group(2)
if (uuid != null && relativePath != null) { if (uuid != null && relativePath != null) {
val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid) return Pair(uuid, relativePath)
if (volumePath != null) {
return ensureTrailingSeparator(volumePath + 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") Log.e(LOG_TAG, "failed to convert treeDocumentUri=$treeDocumentUri to path")
return null return null
} }
@ -512,7 +549,7 @@ object StorageUtils {
} }
// As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used // 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` // This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException`
// for some non image/video content URIs (e.g. `downloads`, `file`) // for some non image/video content URIs (e.g. `downloads`, `file`)
fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long? = null): Uri { fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long? = null): Uri {

View 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>

View 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 &amp; videí</string>
<string name="analysis_notification_default_title">Skenovanie média</string>
</resources>

View 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>

View 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>

View file

@ -1,7 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<!-- `external-path` only covers files on regular device storage -->
<external-path <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="." /> path="." />
<!-- embedded images & other media that are exported for viewing and sharing --> <!-- embedded images & other media that are exported for viewing and sharing -->

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View file

@ -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. “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 ## Disclaimer

View 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

View 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

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

View file

@ -0,0 +1 @@
Galeria eta metadatuen nabigatzailea

View 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>.

View file

@ -0,0 +1 @@
Prehliadač galérie a metadát

View file

@ -26,6 +26,11 @@ extension ExtraAppMode on AppMode {
bool get canSelectFilter => this == AppMode.main; bool get canSelectFilter => this == AppMode.main;
bool get canCreateFilter => {
AppMode.main,
AppMode.pickFilterInternal,
}.contains(this);
bool get isPickingMedia => { bool get isPickingMedia => {
AppMode.pickSingleMediaExternal, AppMode.pickSingleMediaExternal,
AppMode.pickMultipleMediaExternal, AppMode.pickMultipleMediaExternal,

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:isolate';
import 'package:aves/geo/topojson.dart'; import 'package:aves/geo/topojson.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -48,26 +49,22 @@ class CountryTopology {
final topology = await getTopology(); final topology = await getTopology();
if (topology == null) return null; 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 { try {
final topology = data.topology; return Isolate.run<Map<int, Set<LatLng>>>(() {
final countries = (topology.objects['countries'] as GeometryCollection).geometries; final countries = (topology.objects['countries'] as GeometryCollection).geometries;
final byCode = <int, Set<LatLng>>{}; final byCode = <int, Set<LatLng>>{};
for (final position in data.positions) { for (final position in positions) {
final code = _getNumeric(topology, countries, position); final code = _getNumeric(topology, countries, position);
if (code != null) { if (code != null) {
byCode[code] = (byCode[code] ?? {})..add(position); byCode[code] = (byCode[code] ?? {})..add(position);
}
} }
} return byCode;
return byCode; });
} catch (error, stack) { } 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'); debugPrint('failed to get country codes with error=$error\n$stack');
return null;
} }
return {};
} }
static int? _getNumeric(Topology topology, List<Geometry> mruCountries, LatLng position) { static int? _getNumeric(Topology topology, List<Geometry> mruCountries, LatLng position) {
@ -96,10 +93,3 @@ class CountryTopology {
return null; return null;
} }
} }
class _IsoNumericCodeMapData {
Topology topology;
Set<LatLng> positions;
_IsoNumericCodeMapData(this.topology, this.positions);
}

View file

@ -1,24 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
// cf https://github.com/topojson/topojson-specification // cf https://github.com/topojson/topojson-specification
class TopoJson { class TopoJson {
Future<Topology?> parse(String data) async { Future<Topology?> parse(String jsonData) async {
return compute<String, Topology?>(_isoParse, data);
}
static Topology? _isoParse(String jsonData) {
try { try {
final data = jsonDecode(jsonData) as Map<String, dynamic>; return Isolate.run<Topology>(() {
return Topology.parse(data); final data = jsonDecode(jsonData) as Map<String, dynamic>;
return Topology.parse(data);
});
} catch (error, stack) { } 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'); debugPrint('failed to parse TopoJSON with error=$error\n$stack');
return null;
} }
return null;
} }
} }

View file

@ -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": { "@timeDays": {
"placeholders": { "placeholders": {
"days": {} "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": { "@itemCount": {
"placeholders": { "placeholders": {
"count": {} "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": { "@columnCount": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -168,8 +168,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Rychlost přehrávání", "videoActionSetSpeed": "Rychlost přehrávání",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Nastavení", "viewerActionSettings": "Nastavení",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Pokračovat", "slideshowActionResume": "Pokračovat",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Zobrazit ve sbírce", "slideshowActionShowInCollection": "Zobrazit ve sbírce",
@ -313,7 +313,7 @@
"@videoPlaybackMuted": {}, "@videoPlaybackMuted": {},
"videoPlaybackWithSound": "Přehrát se zvukem", "videoPlaybackWithSound": "Přehrát se zvukem",
"@videoPlaybackWithSound": {}, "@videoPlaybackWithSound": {},
"themeBrightnessLight": "Svetlé", "themeBrightnessLight": "Světlé",
"@themeBrightnessLight": {}, "@themeBrightnessLight": {},
"themeBrightnessDark": "Tmavé", "themeBrightnessDark": "Tmavé",
"@themeBrightnessDark": {}, "@themeBrightnessDark": {},
@ -436,7 +436,7 @@
"@addShortcutButtonLabel": {}, "@addShortcutButtonLabel": {},
"noMatchingAppDialogMessage": "Pro tuto operaci není k dispozici žádná aplikace.", "noMatchingAppDialogMessage": "Pro tuto operaci není k dispozici žádná aplikace.",
"@noMatchingAppDialogMessage": {}, "@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": { "@deleteEntriesConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -485,13 +485,13 @@
"@renameEntrySetPagePreviewSectionTitle": {}, "@renameEntrySetPagePreviewSectionTitle": {},
"renameProcessorName": "Název", "renameProcessorName": "Název",
"@renameProcessorName": {}, "@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": { "@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "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": { "@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -955,7 +955,7 @@
"@nameConflictStrategySkip": {}, "@nameConflictStrategySkip": {},
"keepScreenOnNever": "Nikdy", "keepScreenOnNever": "Nikdy",
"@keepScreenOnNever": {}, "@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": { "@binEntriesConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1121,25 +1121,25 @@
"count": {} "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": { "@collectionCopySuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "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": { "@collectionMoveSuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "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": { "@collectionRenameSuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "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": { "@collectionEditSuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1209,8 +1209,6 @@
"@albumGroupNone": {}, "@albumGroupNone": {},
"albumVideoCaptures": "Snímky videa", "albumVideoCaptures": "Snímky videa",
"@albumVideoCaptures": {}, "@albumVideoCaptures": {},
"createAlbumTooltip": "Vytvořit album",
"@createAlbumTooltip": {},
"countryPageTitle": "Země", "countryPageTitle": "Země",
"@countryPageTitle": {}, "@countryPageTitle": {},
"searchCollectionFieldHint": "Prohledat sbírky", "searchCollectionFieldHint": "Prohledat sbírky",
@ -1283,13 +1281,13 @@
"@settingsThumbnailShowVideoDuration": {}, "@settingsThumbnailShowVideoDuration": {},
"settingsCollectionQuickActionsTile": "Rychlé akce", "settingsCollectionQuickActionsTile": "Rychlé akce",
"@settingsCollectionQuickActionsTile": {}, "@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": { "@timeMinutes": {
"placeholders": { "placeholders": {
"minutes": {} "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": { "@timeSeconds": {
"placeholders": { "placeholders": {
"seconds": {} "seconds": {}
@ -1331,7 +1329,7 @@
"@settingsStorageAccessBanner": {}, "@settingsStorageAccessBanner": {},
"settingsUnitSystemTile": "Jednotky", "settingsUnitSystemTile": "Jednotky",
"@settingsUnitSystemTile": {}, "@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": { "@statsWithGps": {
"placeholders": { "placeholders": {
"count": {} "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": "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": {}, "@mapAttributionStamen": {},
"panoramaDisableSensorControl": "Zakázat ovládání senzorem", "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": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Wiedergabegeschwindigkeit", "videoActionSetSpeed": "Wiedergabegeschwindigkeit",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Einstellungen", "viewerActionSettings": "Einstellungen",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Wiedergabe", "slideshowActionResume": "Wiedergabe",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "In Sammlung anzeigen", "slideshowActionShowInCollection": "In Sammlung anzeigen",
@ -389,9 +389,9 @@
"@renameProcessorCounter": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Name", "renameProcessorName": "Name",
"@renameProcessorName": {}, "@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": {}, "@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": {}, "@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Format:", "exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {}, "@exportEntryDialogFormat": {},
@ -595,7 +595,7 @@
"@collectionCopySuccessFeedback": {}, "@collectionCopySuccessFeedback": {},
"collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}", "collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
"@collectionMoveSuccessFeedback": {}, "@collectionMoveSuccessFeedback": {},
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element unmebannt} other{{count} Elemente umbenannt}}", "collectionRenameSuccessFeedback": "{count, plural, =1{1 Element umbenannt} other{{count} Elemente umbenannt}}",
"@collectionRenameSuccessFeedback": {}, "@collectionRenameSuccessFeedback": {},
"collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}", "collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
"@collectionEditSuccessFeedback": {}, "@collectionEditSuccessFeedback": {},
@ -699,8 +699,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Keine Alben", "albumEmpty": "Keine Alben",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Album erstellen",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "ERSTELLE", "createAlbumButtonLabel": "ERSTELLE",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "Neu", "newFilterBanner": "Neu",
@ -1188,5 +1186,27 @@
"entryInfoActionRemoveLocation": "Standort entfernen", "entryInfoActionRemoveLocation": "Standort entfernen",
"@entryInfoActionRemoveLocation": {}, "@entryInfoActionRemoveLocation": {},
"entryActionShareVideoOnly": "Nur das Video teilen", "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": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Ταχύτητα αναπαραγωγής", "videoActionSetSpeed": "Ταχύτητα αναπαραγωγής",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Ρυθμίσεις", "viewerActionSettings": "Ρυθμίσεις",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Συνέχιση", "slideshowActionResume": "Συνέχιση",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Εμφάνιση στη Συλλογή", "slideshowActionShowInCollection": "Εμφάνιση στη Συλλογή",
@ -699,8 +699,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Δεν υπάρχουν άλμπουμ", "albumEmpty": "Δεν υπάρχουν άλμπουμ",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Δημιουργία άλμπουμ",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "ΔΗΜΙΟΥΡΓΙΑ", "createAlbumButtonLabel": "ΔΗΜΙΟΥΡΓΙΑ",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "Νέα", "newFilterBanner": "Νέα",
@ -1206,5 +1204,49 @@
"settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον", "settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον",
"@settingsDisplayUseTvInterface": {}, "@settingsDisplayUseTvInterface": {},
"settingsViewerShowDescription": "Εμφάνιση περιγραφής", "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": {}
} }

View file

@ -78,11 +78,14 @@
"chipActionFilterOut": "Filter out", "chipActionFilterOut": "Filter out",
"chipActionFilterIn": "Filter in", "chipActionFilterIn": "Filter in",
"chipActionHide": "Hide", "chipActionHide": "Hide",
"chipActionLock": "Lock",
"chipActionPin": "Pin to top", "chipActionPin": "Pin to top",
"chipActionUnpin": "Unpin from top", "chipActionUnpin": "Unpin from top",
"chipActionRename": "Rename", "chipActionRename": "Rename",
"chipActionSetCover": "Set cover", "chipActionSetCover": "Set cover",
"chipActionCreateAlbum": "Create album", "chipActionCreateAlbum": "Create album",
"chipActionCreateVault": "Create vault",
"chipActionConfigureVault": "Configure vault",
"entryActionCopyToClipboard": "Copy to clipboard", "entryActionCopyToClipboard": "Copy to clipboard",
"entryActionDelete": "Delete", "entryActionDelete": "Delete",
@ -119,7 +122,8 @@
"videoActionSkip10": "Seek forward 10 seconds", "videoActionSkip10": "Seek forward 10 seconds",
"videoActionSelectStreams": "Select tracks", "videoActionSelectStreams": "Select tracks",
"videoActionSetSpeed": "Playback speed", "videoActionSetSpeed": "Playback speed",
"videoActionSettings": "Settings",
"viewerActionSettings": "Settings",
"slideshowActionResume": "Resume", "slideshowActionResume": "Resume",
"slideshowActionShowInCollection": "Show in Collection", "slideshowActionShowInCollection": "Show in Collection",
@ -157,6 +161,16 @@
"filterMimeImageLabel": "Image", "filterMimeImageLabel": "Image",
"filterMimeVideoLabel": "Video", "filterMimeVideoLabel": "Video",
"accessibilityAnimationsRemove": "Prevent screen effects",
"accessibilityAnimationsKeep": "Keep screen effects",
"albumTierNew": "New",
"albumTierPinned": "Pinned",
"albumTierSpecial": "Common",
"albumTierApps": "Apps",
"albumTierVaults": "Vaults",
"albumTierRegular": "Others",
"coordinateFormatDms": "DMS", "coordinateFormatDms": "DMS",
"coordinateFormatDecimal": "Decimal degrees", "coordinateFormatDecimal": "Decimal degrees",
"coordinateDms": "{coordinate} {direction}", "coordinateDms": "{coordinate} {direction}",
@ -177,17 +191,13 @@
"coordinateDmsEast": "E", "coordinateDmsEast": "E",
"coordinateDmsWest": "W", "coordinateDmsWest": "W",
"unitSystemMetric": "Metric", "displayRefreshRatePreferHighest": "Highest rate",
"unitSystemImperial": "Imperial", "displayRefreshRatePreferLowest": "Lowest rate",
"videoLoopModeNever": "Never", "keepScreenOnNever": "Never",
"videoLoopModeShortOnly": "Short videos only", "keepScreenOnVideoPlayback": "During video playback",
"videoLoopModeAlways": "Always", "keepScreenOnViewerOnly": "Viewer page only",
"keepScreenOnAlways": "Always",
"videoControlsPlay": "Play",
"videoControlsPlaySeek": "Play & seek backward/forward",
"videoControlsPlayOutside": "Open with other player",
"videoControlsNone": "None",
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
@ -202,28 +212,32 @@
"nameConflictStrategyReplace": "Replace", "nameConflictStrategyReplace": "Replace",
"nameConflictStrategySkip": "Skip", "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", "subtitlePositionTop": "Top",
"subtitlePositionBottom": "Bottom", "subtitlePositionBottom": "Bottom",
"videoPlaybackSkip": "Skip",
"videoPlaybackMuted": "Play muted",
"videoPlaybackWithSound": "Play with sound",
"themeBrightnessLight": "Light", "themeBrightnessLight": "Light",
"themeBrightnessDark": "Dark", "themeBrightnessDark": "Dark",
"themeBrightnessBlack": "Black", "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", "viewerTransitionSlide": "Slide",
"viewerTransitionParallax": "Parallax", "viewerTransitionParallax": "Parallax",
"viewerTransitionFade": "Fade", "viewerTransitionFade": "Fade",
@ -241,12 +255,6 @@
"widgetOpenPageCollection": "Open collection", "widgetOpenPageCollection": "Open collection",
"widgetOpenPageViewer": "Open viewer", "widgetOpenPageViewer": "Open viewer",
"albumTierNew": "New",
"albumTierPinned": "Pinned",
"albumTierSpecial": "Common",
"albumTierApps": "Apps",
"albumTierRegular": "Others",
"storageVolumeDescriptionFallbackPrimary": "Internal storage", "storageVolumeDescriptionFallbackPrimary": "Internal storage",
"storageVolumeDescriptionFallbackNonPrimary": "SD card", "storageVolumeDescriptionFallbackNonPrimary": "SD card",
"rootDirectoryDescription": "root directory", "rootDirectoryDescription": "root directory",
@ -366,6 +374,23 @@
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists", "newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
"newAlbumDialogStorageLabel": "Storage:", "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", "renameAlbumDialogLabel": "New name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists", "renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
@ -634,7 +659,6 @@
"albumPageTitle": "Albums", "albumPageTitle": "Albums",
"albumEmpty": "No albums", "albumEmpty": "No albums",
"createAlbumTooltip": "Create album",
"createAlbumButtonLabel": "CREATE", "createAlbumButtonLabel": "CREATE",
"newFilterBanner": "new", "newFilterBanner": "new",
@ -687,6 +711,7 @@
"settingsConfirmationBeforeMoveToBinItems": "Ask before moving items to the recycle bin", "settingsConfirmationBeforeMoveToBinItems": "Ask before moving items to the recycle bin",
"settingsConfirmationBeforeMoveUndatedItems": "Ask before moving undated items", "settingsConfirmationBeforeMoveUndatedItems": "Ask before moving undated items",
"settingsConfirmationAfterMoveToBinItems": "Show message after moving items to the recycle bin", "settingsConfirmationAfterMoveToBinItems": "Show message after moving items to the recycle bin",
"settingsConfirmationVaultDataLoss": "Show vault data loss warning",
"settingsNavigationDrawerTile": "Navigation menu", "settingsNavigationDrawerTile": "Navigation menu",
"settingsNavigationDrawerEditorPageTitle": "Navigation Menu", "settingsNavigationDrawerEditorPageTitle": "Navigation Menu",
@ -790,6 +815,7 @@
"settingsSaveSearchHistory": "Save search history", "settingsSaveSearchHistory": "Save search history",
"settingsEnableBin": "Use recycle bin", "settingsEnableBin": "Use recycle bin",
"settingsEnableBinSubtitle": "Keep deleted items for 30 days", "settingsEnableBinSubtitle": "Keep deleted items for 30 days",
"settingsDisablingBinWarningDialogMessage": "Items in the recycle bin will be deleted forever.",
"settingsAllowMediaManagement": "Allow media management", "settingsAllowMediaManagement": "Allow media management",
"settingsHiddenItemsTile": "Hidden items", "settingsHiddenItemsTile": "Hidden items",

View file

@ -147,8 +147,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Velocidad de reproducción", "videoActionSetSpeed": "Velocidad de reproducción",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Ajustes", "viewerActionSettings": "Ajustes",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Reanudar", "slideshowActionResume": "Reanudar",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Mostrar en Colección", "slideshowActionShowInCollection": "Mostrar en Colección",
@ -657,8 +657,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Sin álbumes", "albumEmpty": "Sin álbumes",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Crear álbum",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CREAR", "createAlbumButtonLabel": "CREAR",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "nuevo", "newFilterBanner": "nuevo",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "Vuelva a intentarlo con menos elementos.", "tooManyItemsErrorDialogMessage": "Vuelva a intentarlo con menos elementos.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Deslice hacia arriba o hacia abajo para ajustar el brillo o el volumen", "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

File diff suppressed because it is too large Load diff

View file

@ -161,8 +161,8 @@
"@videoActionUnmute": {}, "@videoActionUnmute": {},
"videoActionSkip10": "جلو رفتن 10 ثانیه", "videoActionSkip10": "جلو رفتن 10 ثانیه",
"@videoActionSkip10": {}, "@videoActionSkip10": {},
"videoActionSettings": "تنظیمات", "viewerActionSettings": "تنظیمات",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"entryInfoActionEditRating": "ویرایش رتبه", "entryInfoActionEditRating": "ویرایش رتبه",
"@entryInfoActionEditRating": {}, "@entryInfoActionEditRating": {},
"entryInfoActionEditTags": "ویرایش برچسب ها", "entryInfoActionEditTags": "ویرایش برچسب ها",

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Vitesse de lecture", "videoActionSetSpeed": "Vitesse de lecture",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Préférences", "viewerActionSettings": "Préférences",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Reprendre", "slideshowActionResume": "Reprendre",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Afficher dans Collection", "slideshowActionShowInCollection": "Afficher dans Collection",
@ -703,8 +703,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Aucun album", "albumEmpty": "Aucun album",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Créer un album",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CRÉER", "createAlbumButtonLabel": "CRÉER",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "nouveau", "newFilterBanner": "nouveau",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "Réessayez avec moins déléments.", "tooManyItemsErrorDialogMessage": "Réessayez avec moins déléments.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Balayer verticalement pour ajuster la luminosité et le volume", "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 lavertissement 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": {}
} }

View file

@ -119,8 +119,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Velocidade de reprodución", "videoActionSetSpeed": "Velocidade de reprodución",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Configuración", "viewerActionSettings": "Configuración",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Resumo", "slideshowActionResume": "Resumo",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Mostrar na colección", "slideshowActionShowInCollection": "Mostrar na colección",

View file

@ -147,8 +147,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Kecepatan pemutaran", "videoActionSetSpeed": "Kecepatan pemutaran",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Pengaturan", "viewerActionSettings": "Pengaturan",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Lanjutkan", "slideshowActionResume": "Lanjutkan",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Tampilkan di Koleksi", "slideshowActionShowInCollection": "Tampilkan di Koleksi",
@ -683,8 +683,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Tidak ada album", "albumEmpty": "Tidak ada album",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Buat album",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "BUAT", "createAlbumButtonLabel": "BUAT",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "baru", "newFilterBanner": "baru",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.", "tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Usap ke atas atau bawah untuk mengatur kecerahan/volume", "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": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Velocità di riproduzione", "videoActionSetSpeed": "Velocità di riproduzione",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Impostazioni", "viewerActionSettings": "Impostazioni",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Riprendi", "slideshowActionResume": "Riprendi",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Mostra nella Collezione", "slideshowActionShowInCollection": "Mostra nella Collezione",
@ -699,8 +699,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Nessun album", "albumEmpty": "Nessun album",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Crea album",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CREA", "createAlbumButtonLabel": "CREA",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "nuovo", "newFilterBanner": "nuovo",
@ -1208,5 +1206,7 @@
"settingsViewerShowDescription": "Mostra la descrizione", "settingsViewerShowDescription": "Mostra la descrizione",
"@settingsViewerShowDescription": {}, "@settingsViewerShowDescription": {},
"tooManyItemsErrorDialogMessage": "Riprova con meno elementi.", "tooManyItemsErrorDialogMessage": "Riprova con meno elementi.",
"@tooManyItemsErrorDialogMessage": {} "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Trascina su o giù per aggiustare luminosità/volume",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
} }

View file

@ -147,8 +147,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "再生速度", "videoActionSetSpeed": "再生速度",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "設定", "viewerActionSettings": "設定",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "再開", "slideshowActionResume": "再開",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "コレクションで表示", "slideshowActionShowInCollection": "コレクションで表示",
@ -657,8 +657,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "アルバムはありません", "albumEmpty": "アルバムはありません",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "アルバムを作成",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "作成", "createAlbumButtonLabel": "作成",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "新規", "newFilterBanner": "新規",

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "재생 배속", "videoActionSetSpeed": "재생 배속",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "설정", "viewerActionSettings": "설정",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "이어서", "slideshowActionResume": "이어서",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "미디어 페이지에서 보기", "slideshowActionShowInCollection": "미디어 페이지에서 보기",
@ -703,8 +703,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "앨범이 없습니다", "albumEmpty": "앨범이 없습니다",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "앨범 만들기",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "추가", "createAlbumButtonLabel": "추가",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "신규", "newFilterBanner": "신규",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.", "tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기", "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": {}
} }

View file

@ -97,8 +97,8 @@
"@videoActionSkip10": {}, "@videoActionSkip10": {},
"videoActionSetSpeed": "Atkūrimo greitis", "videoActionSetSpeed": "Atkūrimo greitis",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Nustatymai", "viewerActionSettings": "Nustatymai",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Tęsti", "slideshowActionResume": "Tęsti",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Rodyti kolekcijoje", "slideshowActionShowInCollection": "Rodyti kolekcijoje",
@ -1171,8 +1171,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Nėra albumų", "albumEmpty": "Nėra albumų",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Sukurti albumą",
"@createAlbumTooltip": {},
"newFilterBanner": "nauja", "newFilterBanner": "nauja",
"@newFilterBanner": {}, "@newFilterBanner": {},
"binPageTitle": "Šiukšlinė", "binPageTitle": "Šiukšlinė",

View file

@ -76,8 +76,8 @@
"@videoActionSkip10": {}, "@videoActionSkip10": {},
"videoActionSetSpeed": "Avspillingshastighet", "videoActionSetSpeed": "Avspillingshastighet",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Innstillinger", "viewerActionSettings": "Innstillinger",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"entryInfoActionEditTitleDescription": "Rediger navn og beskrivelse", "entryInfoActionEditTitleDescription": "Rediger navn og beskrivelse",
"@entryInfoActionEditTitleDescription": {}, "@entryInfoActionEditTitleDescription": {},
"filterNoDateLabel": "Udatert", "filterNoDateLabel": "Udatert",
@ -709,8 +709,6 @@
"@albumVideoCaptures": {}, "@albumVideoCaptures": {},
"albumEmpty": "Ingen album", "albumEmpty": "Ingen album",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Opprett album",
"@createAlbumTooltip": {},
"binPageTitle": "Papirkurv", "binPageTitle": "Papirkurv",
"@binPageTitle": {}, "@binPageTitle": {},
"countryPageTitle": "Land", "countryPageTitle": "Land",
@ -1366,5 +1364,7 @@
"settingsModificationWarningDialogMessage": "Andre innstillinger vil bli endret.", "settingsModificationWarningDialogMessage": "Andre innstillinger vil bli endret.",
"@settingsModificationWarningDialogMessage": {}, "@settingsModificationWarningDialogMessage": {},
"tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.", "tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.",
"@tooManyItemsErrorDialogMessage": {} "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Afspeelsnelheid", "videoActionSetSpeed": "Afspeelsnelheid",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Instellingen", "viewerActionSettings": "Instellingen",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Hervatten", "slideshowActionResume": "Hervatten",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Tonen in Collectie", "slideshowActionShowInCollection": "Tonen in Collectie",
@ -693,8 +693,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Geen albums", "albumEmpty": "Geen albums",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Album aanmaken",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "AANMAKEN", "createAlbumButtonLabel": "AANMAKEN",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "nieuw", "newFilterBanner": "nieuw",

View file

@ -170,8 +170,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Avspelingssnøggleik", "videoActionSetSpeed": "Avspelingssnøggleik",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Innstillingar", "viewerActionSettings": "Innstillingar",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Hald fram", "slideshowActionResume": "Hald fram",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Vis i Samling", "slideshowActionShowInCollection": "Vis i Samling",

View file

@ -85,8 +85,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Prędkość odtwarzania", "videoActionSetSpeed": "Prędkość odtwarzania",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Ustawienia", "viewerActionSettings": "Ustawienia",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Wznów", "slideshowActionResume": "Wznów",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Pokaż w Kolekcji", "slideshowActionShowInCollection": "Pokaż w Kolekcji",
@ -235,31 +235,31 @@
"@displayRefreshRatePreferLowest": {}, "@displayRefreshRatePreferLowest": {},
"videoPlaybackMuted": "Odtwarzaj bez dźwięku", "videoPlaybackMuted": "Odtwarzaj bez dźwięku",
"@videoPlaybackMuted": {}, "@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": { "@itemCount": {
"placeholders": { "placeholders": {
"count": {} "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": { "@columnCount": {
"placeholders": { "placeholders": {
"count": {} "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": { "@timeSeconds": {
"placeholders": { "placeholders": {
"seconds": {} "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": { "@timeMinutes": {
"placeholders": { "placeholders": {
"minutes": {} "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": { "@timeDays": {
"placeholders": { "placeholders": {
"days": {} "days": {}
@ -547,7 +547,7 @@
"@renameProcessorCounter": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Nazwa", "renameProcessorName": "Nazwa",
"@renameProcessorName": {}, "@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": { "@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -603,13 +603,13 @@
"@sectionUnknown": {}, "@sectionUnknown": {},
"dateToday": "Dzisiaj", "dateToday": "Dzisiaj",
"@dateToday": {}, "@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": { "@collectionCopySuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "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": { "@collectionEditSuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -705,8 +705,6 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Według rozmiaru", "sortBySize": "Według rozmiaru",
"@sortBySize": {}, "@sortBySize": {},
"createAlbumTooltip": "Utwórz album",
"@createAlbumTooltip": {},
"albumEmpty": "Brak albumów", "albumEmpty": "Brak albumów",
"@albumEmpty": {}, "@albumEmpty": {},
"renameEntrySetPageTitle": "Zmień nazwę", "renameEntrySetPageTitle": "Zmień nazwę",
@ -815,7 +813,7 @@
"@albumGroupType": {}, "@albumGroupType": {},
"renameEntrySetPagePreviewSectionTitle": "Podgląd", "renameEntrySetPagePreviewSectionTitle": "Podgląd",
"@renameEntrySetPagePreviewSectionTitle": {}, "@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": { "@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -903,7 +901,7 @@
"count": {} "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": { "@collectionMoveSuccessFeedback": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1319,7 +1317,7 @@
"@settingsStorageAccessPageTitle": {}, "@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": "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": {}, "@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": { "@statsWithGps": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1368,5 +1366,45 @@
"tooManyItemsErrorDialogMessage": "Spróbuj ponownie z mniejszą ilością elementów.", "tooManyItemsErrorDialogMessage": "Spróbuj ponownie z mniejszą ilością elementów.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Przesuń palcem w górę lub w dół, aby dostosować jasność/głośność", "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": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Velocidade de reprodução", "videoActionSetSpeed": "Velocidade de reprodução",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Configurações", "viewerActionSettings": "Configurações",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Retomar", "slideshowActionResume": "Retomar",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Mostrar na Coleção", "slideshowActionShowInCollection": "Mostrar na Coleção",
@ -699,8 +699,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Nenhum álbum", "albumEmpty": "Nenhum álbum",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Criar álbum",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CRIA", "createAlbumButtonLabel": "CRIA",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "novo", "newFilterBanner": "novo",

View file

@ -152,8 +152,8 @@
"@videoActionSkip10": {}, "@videoActionSkip10": {},
"videoActionSelectStreams": "Selectați piese", "videoActionSelectStreams": "Selectați piese",
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSettings": "Setări", "viewerActionSettings": "Setări",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Reluare", "slideshowActionResume": "Reluare",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Afișați în colecție", "slideshowActionShowInCollection": "Afișați în colecție",
@ -743,8 +743,6 @@
"@sortByRating": {}, "@sortByRating": {},
"viewerInfoLabelUri": "URI", "viewerInfoLabelUri": "URI",
"@viewerInfoLabelUri": {}, "@viewerInfoLabelUri": {},
"createAlbumTooltip": "Creați un album",
"@createAlbumTooltip": {},
"viewerInfoLabelDescription": "Descriere", "viewerInfoLabelDescription": "Descriere",
"@viewerInfoLabelDescription": {}, "@viewerInfoLabelDescription": {},
"settingsThumbnailOverlayPageTitle": "Suprapunere", "settingsThumbnailOverlayPageTitle": "Suprapunere",
@ -1366,5 +1364,47 @@
"settingsDisplayUseTvInterface": "Interfață Android TV", "settingsDisplayUseTvInterface": "Interfață Android TV",
"@settingsDisplayUseTvInterface": {}, "@settingsDisplayUseTvInterface": {},
"tooManyItemsErrorDialogMessage": "Încearcă din nou cu mai puține elemente.", "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": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Скорость вопспроизведения", "videoActionSetSpeed": "Скорость вопспроизведения",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Настройки", "viewerActionSettings": "Настройки",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Продолжить", "slideshowActionResume": "Продолжить",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Показать в Коллекции", "slideshowActionShowInCollection": "Показать в Коллекции",
@ -699,8 +699,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Нет альбомов", "albumEmpty": "Нет альбомов",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Создать альбом",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "СОЗДАТЬ", "createAlbumButtonLabel": "СОЗДАТЬ",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "новый", "newFilterBanner": "новый",

456
lib/l10n/app_sk.arb Normal file
View 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": {}
}

View file

@ -121,8 +121,8 @@
"@videoActionSkip10": {}, "@videoActionSkip10": {},
"slideshowActionShowInCollection": "แสดงคอลเลกชัน", "slideshowActionShowInCollection": "แสดงคอลเลกชัน",
"@slideshowActionShowInCollection": {}, "@slideshowActionShowInCollection": {},
"videoActionSettings": "ตั้งค่า", "viewerActionSettings": "ตั้งค่า",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "เล่นต่อ", "slideshowActionResume": "เล่นต่อ",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"entryInfoActionEditTitleDescription": "แก้ไขชื่อและคำบรรยาย", "entryInfoActionEditTitleDescription": "แก้ไขชื่อและคำบรรยาย",

View file

@ -147,8 +147,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "Oynatma hızı", "videoActionSetSpeed": "Oynatma hızı",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "Ayarlar", "viewerActionSettings": "Ayarlar",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"entryInfoActionEditDate": "Tarih ve saati düzenle", "entryInfoActionEditDate": "Tarih ve saati düzenle",
"@entryInfoActionEditDate": {}, "@entryInfoActionEditDate": {},
"entryInfoActionEditLocation": "Konumu düzenle", "entryInfoActionEditLocation": "Konumu düzenle",
@ -629,8 +629,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Albüm yok", "albumEmpty": "Albüm yok",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Albüm oluştur",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "OLUŞTUR", "createAlbumButtonLabel": "OLUŞTUR",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "yeni", "newFilterBanner": "yeni",

View file

@ -126,8 +126,8 @@
"@videoActionSkip10": {}, "@videoActionSkip10": {},
"videoActionSelectStreams": "Вибрати доріжку", "videoActionSelectStreams": "Вибрати доріжку",
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSettings": "Налаштування", "viewerActionSettings": "Налаштування",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "Продовжити", "slideshowActionResume": "Продовжити",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "Показати у Колекції", "slideshowActionShowInCollection": "Показати у Колекції",
@ -885,8 +885,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "Немає альбомів", "albumEmpty": "Немає альбомів",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "Створити альбом",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "СТВОРИТИ", "createAlbumButtonLabel": "СТВОРИТИ",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"countryPageTitle": "Країни", "countryPageTitle": "Країни",
@ -1368,5 +1366,45 @@
"tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.", "tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.",
"@tooManyItemsErrorDialogMessage": {}, "@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність", "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": {}
} }

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "播放速度", "videoActionSetSpeed": "播放速度",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "设置", "viewerActionSettings": "设置",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "继续", "slideshowActionResume": "继续",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"slideshowActionShowInCollection": "在媒体集中显示", "slideshowActionShowInCollection": "在媒体集中显示",
@ -389,9 +389,9 @@
"@renameProcessorCounter": {}, "@renameProcessorCounter": {},
"renameProcessorName": "名称", "renameProcessorName": "名称",
"@renameProcessorName": {}, "@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{删除此相册及其内容?} other{删除此相册及其 {count} 项内容}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{删除此相册及其中的一个项目?} other{删除此相册及其中的 {count} 个项目}}",
"@deleteSingleAlbumConfirmationDialogMessage": {}, "@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{删除这些相册及其内容?} other{删除这些相册及其 {count} 项内容}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{删除这些相册及其中的一个项目?} other{删除这些相册及其中的 {count} 个项目}}",
"@deleteMultiAlbumConfirmationDialogMessage": {}, "@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "格式:", "exportEntryDialogFormat": "格式:",
"@exportEntryDialogFormat": {}, "@exportEntryDialogFormat": {},
@ -691,8 +691,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "无相册", "albumEmpty": "无相册",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "创建相册",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "创建", "createAlbumButtonLabel": "创建",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "新的", "newFilterBanner": "新的",

View file

@ -124,8 +124,8 @@
"@videoActionSelectStreams": {}, "@videoActionSelectStreams": {},
"videoActionSetSpeed": "播放速度", "videoActionSetSpeed": "播放速度",
"@videoActionSetSpeed": {}, "@videoActionSetSpeed": {},
"videoActionSettings": "設定", "viewerActionSettings": "設定",
"@videoActionSettings": {}, "@viewerActionSettings": {},
"slideshowActionResume": "繼續", "slideshowActionResume": "繼續",
"@slideshowActionResume": {}, "@slideshowActionResume": {},
"entryInfoActionEditLocation": "編輯位置", "entryInfoActionEditLocation": "編輯位置",
@ -615,8 +615,6 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"albumEmpty": "沒有相簿", "albumEmpty": "沒有相簿",
"@albumEmpty": {}, "@albumEmpty": {},
"createAlbumTooltip": "建立相簿",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "建立", "createAlbumButtonLabel": "建立",
"@createAlbumButtonLabel": {}, "@createAlbumButtonLabel": {},
"newFilterBanner": "新的", "newFilterBanner": "新的",

View file

@ -8,6 +8,7 @@ enum ChipAction {
goToTagPage, goToTagPage,
reverse, reverse,
hide, hide,
lockVault,
} }
extension ExtraChipAction on ChipAction { extension ExtraChipAction on ChipAction {
@ -24,6 +25,8 @@ extension ExtraChipAction on ChipAction {
return context.l10n.chipActionFilterOut; return context.l10n.chipActionFilterOut;
case ChipAction.hide: case ChipAction.hide:
return context.l10n.chipActionHide; return context.l10n.chipActionHide;
case ChipAction.lockVault:
return context.l10n.chipActionLock;
} }
} }
@ -41,6 +44,8 @@ extension ExtraChipAction on ChipAction {
return AIcons.reverse; return AIcons.reverse;
case ChipAction.hide: case ChipAction.hide:
return AIcons.hide; return AIcons.hide;
case ChipAction.lockVault:
return AIcons.vaultLock;
} }
} }
} }

View file

@ -12,6 +12,7 @@ enum ChipSetAction {
search, search,
toggleTitleSearch, toggleTitleSearch,
createAlbum, createAlbum,
createVault,
// browsing or selecting // browsing or selecting
map, map,
slideshow, slideshow,
@ -21,9 +22,11 @@ enum ChipSetAction {
hide, hide,
pin, pin,
unpin, unpin,
lockVault,
// selecting (single filter) // selecting (single filter)
rename, rename,
setCover, setCover,
configureVault,
} }
class ChipSetActions { class ChipSetActions {
@ -34,15 +37,20 @@ class ChipSetActions {
ChipSetAction.selectNone, ChipSetAction.selectNone,
]; ];
// `null` items are converted to dividers
static const browsing = [ static const browsing = [
ChipSetAction.search, ChipSetAction.search,
ChipSetAction.toggleTitleSearch, ChipSetAction.toggleTitleSearch,
ChipSetAction.createAlbum, null,
ChipSetAction.map, ChipSetAction.map,
ChipSetAction.slideshow, ChipSetAction.slideshow,
ChipSetAction.stats, ChipSetAction.stats,
null,
ChipSetAction.createAlbum,
ChipSetAction.createVault,
]; ];
// `null` items are converted to dividers
static const selection = [ static const selection = [
ChipSetAction.setCover, ChipSetAction.setCover,
ChipSetAction.pin, ChipSetAction.pin,
@ -50,9 +58,13 @@ class ChipSetActions {
ChipSetAction.delete, ChipSetAction.delete,
ChipSetAction.rename, ChipSetAction.rename,
ChipSetAction.hide, ChipSetAction.hide,
null,
ChipSetAction.map, ChipSetAction.map,
ChipSetAction.slideshow, ChipSetAction.slideshow,
ChipSetAction.stats, ChipSetAction.stats,
null,
ChipSetAction.configureVault,
ChipSetAction.lockVault,
]; ];
} }
@ -76,6 +88,8 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.collectionActionShowTitleSearch; return context.l10n.collectionActionShowTitleSearch;
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
return context.l10n.chipActionCreateAlbum; return context.l10n.chipActionCreateAlbum;
case ChipSetAction.createVault:
return context.l10n.chipActionCreateVault;
// browsing or selecting // browsing or selecting
case ChipSetAction.map: case ChipSetAction.map:
return context.l10n.menuActionMap; return context.l10n.menuActionMap;
@ -92,11 +106,15 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.chipActionPin; return context.l10n.chipActionPin;
case ChipSetAction.unpin: case ChipSetAction.unpin:
return context.l10n.chipActionUnpin; return context.l10n.chipActionUnpin;
case ChipSetAction.lockVault:
return context.l10n.chipActionLock;
// selecting (single filter) // selecting (single filter)
case ChipSetAction.rename: case ChipSetAction.rename:
return context.l10n.chipActionRename; return context.l10n.chipActionRename;
case ChipSetAction.setCover: case ChipSetAction.setCover:
return context.l10n.chipActionSetCover; return context.l10n.chipActionSetCover;
case ChipSetAction.configureVault:
return context.l10n.chipActionConfigureVault;
} }
} }
@ -121,6 +139,8 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.filter; return AIcons.filter;
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
return AIcons.add; return AIcons.add;
case ChipSetAction.createVault:
return AIcons.vaultAdd;
// browsing or selecting // browsing or selecting
case ChipSetAction.map: case ChipSetAction.map:
return AIcons.map; return AIcons.map;
@ -137,11 +157,15 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.pin; return AIcons.pin;
case ChipSetAction.unpin: case ChipSetAction.unpin:
return AIcons.unpin; return AIcons.unpin;
case ChipSetAction.lockVault:
return AIcons.vaultLock;
// selecting (single filter) // selecting (single filter)
case ChipSetAction.rename: case ChipSetAction.rename:
return AIcons.name; return AIcons.name;
case ChipSetAction.setCover: case ChipSetAction.setCover:
return AIcons.setCover; return AIcons.setCover;
case ChipSetAction.configureVault:
return AIcons.vaultConfigure;
} }
} }
} }

View file

@ -71,13 +71,15 @@ class EntryActions {
]; ];
static const export = [ static const export = [
...exportInternal,
...exportExternal,
];
static const exportInternal = [
EntryAction.convert, EntryAction.convert,
EntryAction.addShortcut, EntryAction.addShortcut,
EntryAction.copyToClipboard, EntryAction.copyToClipboard,
EntryAction.print, EntryAction.print,
EntryAction.open,
EntryAction.openMap,
EntryAction.setAs,
]; ];
static const exportExternal = [ static const exportExternal = [
@ -186,7 +188,7 @@ extension ExtraEntryAction on EntryAction {
case EntryAction.videoSetSpeed: case EntryAction.videoSetSpeed:
return context.l10n.videoActionSetSpeed; return context.l10n.videoActionSetSpeed;
case EntryAction.videoSettings: case EntryAction.videoSettings:
return context.l10n.videoActionSettings; return context.l10n.viewerActionSettings;
case EntryAction.videoTogglePlay: case EntryAction.videoTogglePlay:
// different data depending on toggle state // different data depending on toggle state
return context.l10n.videoActionPlay; return context.l10n.videoActionPlay;

View file

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
enum SlideshowAction { enum SlideshowAction {
resume, resume,
showInCollection, showInCollection,
settings,
} }
extension ExtraSlideshowAction on SlideshowAction { extension ExtraSlideshowAction on SlideshowAction {
@ -14,6 +15,8 @@ extension ExtraSlideshowAction on SlideshowAction {
return context.l10n.slideshowActionResume; return context.l10n.slideshowActionResume;
case SlideshowAction.showInCollection: case SlideshowAction.showInCollection:
return context.l10n.slideshowActionShowInCollection; return context.l10n.slideshowActionShowInCollection;
case SlideshowAction.settings:
return context.l10n.viewerActionSettings;
} }
} }
@ -25,6 +28,8 @@ extension ExtraSlideshowAction on SlideshowAction {
return AIcons.play; return AIcons.play;
case SlideshowAction.showInCollection: case SlideshowAction.showInCollection:
return AIcons.allCollection; return AIcons.allCollection;
case SlideshowAction.settings:
return AIcons.settings;
} }
} }
} }

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
@ -38,6 +39,8 @@ class Covers {
Set<CoverRow> get all => Set.unmodifiable(_rows); Set<CoverRow> get all => Set.unmodifiable(_rows);
Tuple3<int?, String?, Color?>? of(CollectionFilter filter) { 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); final row = _rows.firstWhereOrNull((row) => row.filter == filter);
return row != null ? Tuple3(row.entryId, row.packageName, row.color) : null; return row != null ? Tuple3(row.entryId, row.packageName, row.color) : null;
} }

View file

@ -5,6 +5,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.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/model/video_playback.dart';
abstract class MetadataDb { abstract class MetadataDb {
@ -16,17 +17,17 @@ abstract class MetadataDb {
Future<void> reset(); Future<void> reset();
Future<void> removeIds(Iterable<int> ids, {Set<EntryDataType>? dataTypes}); Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes});
// entries // entries
Future<void> clearEntries(); 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); Future<void> updateEntry(int id, AvesEntry entry);
@ -44,7 +45,7 @@ abstract class MetadataDb {
Future<Set<CatalogMetadata>> loadCatalogMetadata(); Future<Set<CatalogMetadata>> loadCatalogMetadata();
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Iterable<int> ids); Future<Set<CatalogMetadata>> loadCatalogMetadataById(Set<int> ids);
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries); Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries);
@ -56,12 +57,24 @@ abstract class MetadataDb {
Future<Set<AddressDetails>> loadAddresses(); 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> saveAddresses(Set<AddressDetails> addresses);
Future<void> updateAddress(int id, AddressDetails? address); 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 // trash
Future<void> clearTrashDetails(); Future<void> clearTrashDetails();
@ -76,11 +89,11 @@ abstract class MetadataDb {
Future<Set<FavouriteRow>> loadAllFavourites(); 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> updateFavouriteId(int id, FavouriteRow row);
Future<void> removeFavourites(Iterable<FavouriteRow> rows); Future<void> removeFavourites(Set<FavouriteRow> rows);
// covers // covers
@ -88,7 +101,7 @@ abstract class MetadataDb {
Future<Set<CoverRow>> loadAllCovers(); Future<Set<CoverRow>> loadAllCovers();
Future<void> addCovers(Iterable<CoverRow> rows); Future<void> addCovers(Set<CoverRow> rows);
Future<void> updateCoverEntryId(int id, CoverRow row); Future<void> updateCoverEntryId(int id, CoverRow row);
@ -104,5 +117,5 @@ abstract class MetadataDb {
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows); Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows);
Future<void> removeVideoPlayback(Iterable<int> ids); Future<void> removeVideoPlayback(Set<int> ids);
} }

View file

@ -9,6 +9,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.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/model/video_playback.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -26,6 +27,7 @@ class SqfliteMetadataDb implements MetadataDb {
static const addressTable = 'address'; static const addressTable = 'address';
static const favouriteTable = 'favourites'; static const favouriteTable = 'favourites';
static const coverTable = 'covers'; static const coverTable = 'covers';
static const vaultTable = 'vaults';
static const trashTable = 'trash'; static const trashTable = 'trash';
static const videoPlaybackTable = 'videoPlayback'; static const videoPlaybackTable = 'videoPlayback';
@ -55,6 +57,7 @@ class SqfliteMetadataDb implements MetadataDb {
', sourceDateTakenMillis INTEGER' ', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER' ', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0' ', trashed INTEGER DEFAULT 0'
', origin INTEGER DEFAULT 0'
')'); ')');
await db.execute('CREATE TABLE $dateTakenTable(' await db.execute('CREATE TABLE $dateTakenTable('
'id INTEGER PRIMARY KEY' 'id INTEGER PRIMARY KEY'
@ -89,6 +92,12 @@ class SqfliteMetadataDb implements MetadataDb {
', packageName TEXT' ', packageName TEXT'
', color INTEGER' ', color INTEGER'
')'); ')');
await db.execute('CREATE TABLE $vaultTable('
'name TEXT PRIMARY KEY'
', autoLock INTEGER'
', useBin INTEGER'
', lockType TEXT'
')');
await db.execute('CREATE TABLE $trashTable(' await db.execute('CREATE TABLE $trashTable('
'id INTEGER PRIMARY KEY' 'id INTEGER PRIMARY KEY'
', path TEXT' ', path TEXT'
@ -100,7 +109,7 @@ class SqfliteMetadataDb implements MetadataDb {
')'); ')');
}, },
onUpgrade: MetadataDbUpgrader.upgradeDb, onUpgrade: MetadataDbUpgrader.upgradeDb,
version: 10, version: 11,
); );
final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable'); final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable');
@ -122,7 +131,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @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; if (ids.isEmpty) return;
final _dataTypes = dataTypes ?? EntryDataType.values.toSet(); final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
@ -162,15 +171,23 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @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) { if (directory != null) {
final separator = pContext.separator; final separator = pContext.separator;
if (!directory.endsWith(separator)) { if (!directory.endsWith(separator)) {
directory = '$directory$separator'; directory = '$directory$separator';
} }
const where = 'path LIKE ?'; where = '${where != null ? '$where AND ' : ''}path LIKE ?';
final whereArgs = ['$directory%']; whereArgs.add('$directory%');
final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs); final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
final dirLength = directory.length; final dirLength = directory.length;
@ -184,15 +201,15 @@ class SqfliteMetadataDb implements MetadataDb {
.toSet(); .toSet();
} }
final rows = await _db.query(entryTable); final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs);
return rows.map(AvesEntry.fromMap).toSet(); return rows.map(AvesEntry.fromMap).toSet();
} }
@override @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 @override
Future<void> saveEntries(Iterable<AvesEntry> entries) async { Future<void> saveEntries(Set<AvesEntry> entries) async {
if (entries.isEmpty) return; if (entries.isEmpty) return;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final batch = _db.batch(); final batch = _db.batch();
@ -258,7 +275,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @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 @override
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries) async { Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries) async {
@ -317,7 +334,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @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 @override
Future<void> saveAddresses(Set<AddressDetails> addresses) async { 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 // trash
@override @override
@ -392,7 +457,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @override
Future<void> addFavourites(Iterable<FavouriteRow> rows) async { Future<void> addFavourites(Set<FavouriteRow> rows) async {
if (rows.isEmpty) return; if (rows.isEmpty) return;
final batch = _db.batch(); final batch = _db.batch();
rows.forEach((row) => _batchInsertFavourite(batch, row)); rows.forEach((row) => _batchInsertFavourite(batch, row));
@ -416,7 +481,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @override
Future<void> removeFavourites(Iterable<FavouriteRow> rows) async { Future<void> removeFavourites(Set<FavouriteRow> rows) async {
if (rows.isEmpty) return; if (rows.isEmpty) return;
final ids = rows.map((row) => row.entryId); final ids = rows.map((row) => row.entryId);
if (ids.isEmpty) return; if (ids.isEmpty) return;
@ -442,7 +507,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @override
Future<void> addCovers(Iterable<CoverRow> rows) async { Future<void> addCovers(Set<CoverRow> rows) async {
if (rows.isEmpty) return; if (rows.isEmpty) return;
final batch = _db.batch(); final batch = _db.batch();
@ -532,7 +597,7 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @override
Future<void> removeVideoPlayback(Iterable<int> ids) async { Future<void> removeVideoPlayback(Set<int> ids) async {
if (ids.isEmpty) return; if (ids.isEmpty) return;
// using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead // 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 // 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 {}; if (ids.isEmpty) return {};
final rows = await _db.query( final rows = await _db.query(
table, table,

View file

@ -10,6 +10,7 @@ class MetadataDbUpgrader {
static const addressTable = SqfliteMetadataDb.addressTable; static const addressTable = SqfliteMetadataDb.addressTable;
static const favouriteTable = SqfliteMetadataDb.favouriteTable; static const favouriteTable = SqfliteMetadataDb.favouriteTable;
static const coverTable = SqfliteMetadataDb.coverTable; static const coverTable = SqfliteMetadataDb.coverTable;
static const vaultTable = SqfliteMetadataDb.vaultTable;
static const trashTable = SqfliteMetadataDb.trashTable; static const trashTable = SqfliteMetadataDb.trashTable;
static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable; static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable;
@ -45,6 +46,9 @@ class MetadataDbUpgrader {
case 9: case 9:
await _upgradeFrom9(db); await _upgradeFrom9(db);
break; break;
case 10:
await _upgradeFrom10(db);
break;
} }
oldVersion++; oldVersion++;
} }
@ -370,4 +374,17 @@ class MetadataDbUpgrader {
}); });
await batch.commit(noResult: true); 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'
')');
}
} }

View file

@ -1,16 +1,20 @@
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:device_info_plus/device_info_plus.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'; import 'package:package_info_plus/package_info_plus.dart';
final Device device = Device._private(); final Device device = Device._private();
class Device { class Device {
late final String _userAgent; 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; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
String get userAgent => _userAgent; String get userAgent => _userAgent;
bool get canAuthenticateUser => _canAuthenticateUser;
bool get canGrantDirectoryAccess => _canGrantDirectoryAccess; bool get canGrantDirectoryAccess => _canGrantDirectoryAccess;
bool get canPinShortcut => _canPinShortcut; bool get canPinShortcut => _canPinShortcut;
@ -23,6 +27,10 @@ class Device {
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper; bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
bool get canUseCrypto => _canUseCrypto;
bool get canUseVaults => canAuthenticateUser || canUseCrypto;
bool get hasGeocoder => _hasGeocoder; bool get hasGeocoder => _hasGeocoder;
bool get isDynamicColorAvailable => _isDynamicColorAvailable; bool get isDynamicColorAvailable => _isDynamicColorAvailable;
@ -42,6 +50,9 @@ class Device {
final androidInfo = await DeviceInfoPlugin().androidInfo; final androidInfo = await DeviceInfoPlugin().androidInfo;
_isTelevision = androidInfo.systemFeatures.contains('android.software.leanback'); _isTelevision = androidInfo.systemFeatures.contains('android.software.leanback');
final auth = LocalAuthentication();
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
final capabilities = await deviceService.getCapabilities(); final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPinShortcut = capabilities['canPinShortcut'] ?? false;
@ -49,6 +60,7 @@ class Device {
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false; _canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false; _canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
_hasGeocoder = capabilities['hasGeocoder'] ?? false; _hasGeocoder = capabilities['hasGeocoder'] ?? false;
_isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false; _isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false;
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false; _showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;

View file

@ -20,6 +20,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/services/geocoding_service.dart'; import 'package:aves/services/geocoding_service.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/format.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/change_notifier.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -29,6 +30,13 @@ import 'package:latlong2/latlong.dart';
enum EntryDataType { basic, aspectRatio, catalog, address, references } 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 { class AvesEntry {
// `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode // `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode
int id; int id;
@ -40,6 +48,7 @@ class AvesEntry {
int width, height, sourceRotationDegrees; int width, height, sourceRotationDegrees;
int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
bool trashed; bool trashed;
int origin;
int? _catalogDateMillis; int? _catalogDateMillis;
CatalogMetadata? _catalogMetadata; CatalogMetadata? _catalogMetadata;
@ -67,6 +76,7 @@ class AvesEntry {
required this.sourceDateTakenMillis, required this.sourceDateTakenMillis,
required int? durationMillis, required int? durationMillis,
required this.trashed, required this.trashed,
required this.origin,
this.burstEntries, this.burstEntries,
}) : id = id ?? 0 { }) : id = id ?? 0 {
this.path = path; this.path = path;
@ -87,6 +97,7 @@ class AvesEntry {
String? title, String? title,
int? dateAddedSecs, int? dateAddedSecs,
int? dateModifiedSecs, int? dateModifiedSecs,
int? origin,
List<AvesEntry>? burstEntries, List<AvesEntry>? burstEntries,
}) { }) {
final copyEntryId = id ?? this.id; final copyEntryId = id ?? this.id;
@ -107,6 +118,7 @@ class AvesEntry {
sourceDateTakenMillis: sourceDateTakenMillis, sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis, durationMillis: durationMillis,
trashed: trashed, trashed: trashed,
origin: origin ?? this.origin,
burstEntries: burstEntries ?? this.burstEntries, burstEntries: burstEntries ?? this.burstEntries,
) )
..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId) ..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
@ -135,6 +147,7 @@ class AvesEntry {
sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?,
durationMillis: map['durationMillis'] as int?, durationMillis: map['durationMillis'] as int?,
trashed: (map['trashed'] as int? ?? 0) != 0, trashed: (map['trashed'] as int? ?? 0) != 0,
origin: map['origin'] as int,
); );
} }
@ -156,6 +169,7 @@ class AvesEntry {
'sourceDateTakenMillis': sourceDateTakenMillis, 'sourceDateTakenMillis': sourceDateTakenMillis,
'durationMillis': durationMillis, 'durationMillis': durationMillis,
'trashed': trashed ? 1 : 0, 'trashed': trashed ? 1 : 0,
'origin': origin,
}; };
} }
@ -173,6 +187,7 @@ class AvesEntry {
'sizeBytes': sizeBytes, 'sizeBytes': sizeBytes,
'trashed': trashed, 'trashed': trashed,
'trashPath': trashDetails?.path, 'trashPath': trashDetails?.path,
'origin': origin,
}; };
} }
@ -281,7 +296,9 @@ class AvesEntry {
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); 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); bool get canEditDate => canEdit && (canEditExif || canEditXmp);

View file

@ -26,7 +26,7 @@ class Favourites with ChangeNotifier {
FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(entryId: entry.id); FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(entryId: entry.id);
Future<void> add(Set<AvesEntry> entries) async { Future<void> add(Set<AvesEntry> entries) async {
final newRows = entries.map(_entryToRow); final newRows = entries.map(_entryToRow).toSet();
await metadataDb.addFavourites(newRows); await metadataDb.addFavourites(newRows);
_rows.addAll(newRows); _rows.addAll(newRows);

View file

@ -73,6 +73,7 @@ class AlbumFilter extends CoveredCollectionFilter {
final albumType = covers.effectiveAlbumType(album); final albumType = covers.effectiveAlbumType(album);
switch (albumType) { switch (albumType) {
case AlbumType.regular: case AlbumType.regular:
case AlbumType.vault:
break; break;
case AlbumType.app: case AlbumType.app:
final appColor = colors.appColor(album); final appColor = colors.appColor(album);

View file

@ -107,6 +107,7 @@ class MultiPageInfo {
sourceDateTakenMillis: mainEntry.sourceDateTakenMillis, sourceDateTakenMillis: mainEntry.sourceDateTakenMillis,
durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis, durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis,
trashed: trashed, trashed: trashed,
origin: mainEntry.origin,
) )
..catalogMetadata = mainEntry.catalogMetadata?.copyWith( ..catalogMetadata = mainEntry.catalogMetadata?.copyWith(
mimeType: pageInfo.mimeType, mimeType: pageInfo.mimeType,

View file

@ -31,10 +31,7 @@ class SettingsDefaults {
static const keepScreenOn = KeepScreenOn.viewerOnly; static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection; static const homePage = HomePageSetting.collection;
static const enableBottomNavigationBar = true; static const enableBottomNavigationBar = true;
static const confirmDeleteForever = true; static const confirm = true;
static const confirmMoveToBin = true;
static const confirmMoveUndatedItems = true;
static const confirmAfterMoveToBin = true;
static const setMetadataDateBeforeFileOp = false; static const setMetadataDateBeforeFileOp = false;
static final drawerTypeBookmarks = [ static final drawerTypeBookmarks = [
null, null,

View file

@ -6,7 +6,7 @@ enum AvesThemeBrightness { system, light, dark, black }
enum AvesThemeColorMode { monochrome, polychrome } enum AvesThemeColorMode { monochrome, polychrome }
enum ConfirmationDialog { deleteForever, moveToBin, moveUndatedItems } enum ConfirmationDialog { createVault, deleteForever, moveToBin, moveUndatedItems }
enum CoordinateFormat { dms, decimal } enum CoordinateFormat { dms, decimal }

View file

@ -30,6 +30,7 @@ import 'package:latlong2/latlong.dart';
final Settings settings = Settings._private(); final Settings settings = Settings._private();
class Settings extends ChangeNotifier { class Settings extends ChangeNotifier {
final List<StreamSubscription> _subscriptions = [];
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change'); final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast(); final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
@ -40,7 +41,7 @@ class Settings extends ChangeNotifier {
static const int _recentFilterHistoryMax = 10; static const int _recentFilterHistoryMax = 10;
static const Set<String> _internalKeys = { static const Set<String> _internalKeys = {
hasAcceptedTermsKey, hasAcceptedTermsKey,
catalogTimeZoneKey, catalogTimeZoneRawOffsetMillisKey,
searchHistoryKey, searchHistoryKey,
platformAccelerometerRotationKey, platformAccelerometerRotationKey,
platformTransitionAnimationScaleKey, platformTransitionAnimationScaleKey,
@ -56,7 +57,7 @@ class Settings extends ChangeNotifier {
static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed'; static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed';
static const isErrorReportingAllowedKey = 'is_crashlytics_enabled'; static const isErrorReportingAllowedKey = 'is_crashlytics_enabled';
static const localeKey = 'locale'; 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 tileExtentPrefixKey = 'tile_extent_';
static const tileLayoutPrefixKey = 'tile_layout_'; static const tileLayoutPrefixKey = 'tile_layout_';
static const entryRenamingPatternKey = 'entry_renaming_pattern'; static const entryRenamingPatternKey = 'entry_renaming_pattern';
@ -77,6 +78,7 @@ class Settings extends ChangeNotifier {
static const keepScreenOnKey = 'keep_screen_on'; static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page'; static const homePageKey = 'home_page';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmCreateVaultKey = 'confirm_create_vault';
static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmDeleteForeverKey = 'confirm_delete_forever';
static const confirmMoveToBinKey = 'confirm_move_to_bin'; static const confirmMoveToBinKey = 'confirm_move_to_bin';
static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items'; static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items';
@ -151,6 +153,7 @@ class Settings extends ChangeNotifier {
// tag editor // tag editor
static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded'; static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded';
static const tagEditorExpandedSectionKey = 'tag_editor_expanded_section';
// map // map
static const mapStyleKey = 'info_map_style'; static const mapStyleKey = 'info_map_style';
@ -209,7 +212,10 @@ class Settings extends ChangeNotifier {
await settingsStore.init(); await settingsStore.init();
_appliedLocale = null; _appliedLocale = null;
if (monitorPlatformSettings) { 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 { Future<void> sanitize() async {
if (timeToTakeAction == AccessibilityTimeout.system && !(await AccessibilityService.hasRecommendedTimeouts())) { if (timeToTakeAction == AccessibilityTimeout.system && !await AccessibilityService.hasRecommendedTimeouts()) {
_set(timeToTakeActionKey, null); _set(timeToTakeActionKey, null);
} }
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !(await windowService.isCutoutAware())) { if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
_set(viewerUseCutoutKey, null); _set(viewerUseCutoutKey, null);
} }
} }
@ -356,9 +362,9 @@ class Settings extends ChangeNotifier {
return _appliedLocale!; 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; double getTileExtent(String routeName) => getDouble(tileExtentPrefixKey + routeName) ?? 0;
@ -432,19 +438,23 @@ class Settings extends ChangeNotifier {
set enableBottomNavigationBar(bool newValue) => _set(enableBottomNavigationBarKey, newValue); 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); 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); 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); 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); set confirmAfterMoveToBin(bool newValue) => _set(confirmAfterMoveToBinKey, newValue);
@ -698,6 +708,10 @@ class Settings extends ChangeNotifier {
set tagEditorCurrentFilterSectionExpanded(bool newValue) => _set(tagEditorCurrentFilterSectionExpandedKey, newValue); set tagEditorCurrentFilterSectionExpanded(bool newValue) => _set(tagEditorCurrentFilterSectionExpandedKey, newValue);
String? get tagEditorExpandedSection => getString(tagEditorExpandedSectionKey);
set tagEditorExpandedSection(String? newValue) => _set(tagEditorExpandedSectionKey, newValue);
// map // map
EntryMapStyle? get mapStyle { EntryMapStyle? get mapStyle {
@ -1010,6 +1024,7 @@ class Settings extends ChangeNotifier {
case enableBlurEffectKey: case enableBlurEffectKey:
case enableBottomNavigationBarKey: case enableBottomNavigationBarKey:
case mustBackTwiceToExitKey: case mustBackTwiceToExitKey:
case confirmCreateVaultKey:
case confirmDeleteForeverKey: case confirmDeleteForeverKey:
case confirmMoveToBinKey: case confirmMoveToBinKey:
case confirmMoveUndatedItemsKey: case confirmMoveUndatedItemsKey:
@ -1076,6 +1091,7 @@ class Settings extends ChangeNotifier {
case videoControlsKey: case videoControlsKey:
case subtitleTextAlignmentKey: case subtitleTextAlignmentKey:
case subtitleTextPositionKey: case subtitleTextPositionKey:
case tagEditorExpandedSectionKey:
case mapStyleKey: case mapStyleKey:
case mapDefaultCenterKey: case mapDefaultCenterKey:
case coordinateFormatKey: case coordinateFormatKey:

View file

@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.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/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
@ -61,8 +62,10 @@ mixin AlbumMixin on SourceBase {
} }
void updateDirectories() { void updateDirectories() {
final visibleDirectories = visibleEntries.map((entry) => entry.directory).toSet(); addDirectories(albums: {
addDirectories(albums: visibleDirectories); ...visibleEntries.map((entry) => entry.directory),
...vaults.all.map((v) => v.path),
});
cleanEmptyAlbums(); cleanEmptyAlbums();
} }
@ -73,25 +76,24 @@ mixin AlbumMixin on SourceBase {
} }
} }
void cleanEmptyAlbums([Set<String?>? albums]) { void cleanEmptyAlbums([Set<String>? albums]) {
final emptyAlbums = (albums ?? _directories).where((v) => _isEmptyAlbum(v) && !_newAlbums.contains(v)).toSet(); final removableAlbums = (albums ?? _directories).where(_isRemovable).toSet();
if (emptyAlbums.isNotEmpty) { if (removableAlbums.isNotEmpty) {
_directories.removeAll(emptyAlbums); _directories.removeAll(removableAlbums);
_onAlbumChanged(); _onAlbumChanged();
invalidateAlbumFilterSummary(directories: emptyAlbums); invalidateAlbumFilterSummary(directories: removableAlbums);
final bookmarks = settings.drawerAlbumBookmarks; final bookmarks = settings.drawerAlbumBookmarks;
final pinnedFilters = settings.pinnedFilters; removableAlbums.forEach((album) {
emptyAlbums.forEach((album) {
bookmarks?.remove(album); bookmarks?.remove(album);
pinnedFilters.removeWhere((filter) => filter is AlbumFilter && filter.album == album);
}); });
settings.drawerAlbumBookmarks = bookmarks; 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 // filter summary
@ -169,8 +171,8 @@ mixin AlbumMixin on SourceBase {
final separator = pContext.separator; final separator = pContext.separator;
assert(!dirPath.endsWith(separator)); assert(!dirPath.endsWith(separator));
final type = androidFileUtils.getAlbumType(dirPath);
if (context != null) { if (context != null) {
final type = androidFileUtils.getAlbumType(dirPath);
switch (type) { switch (type) {
case AlbumType.camera: case AlbumType.camera:
return context.l10n.albumCamera; return context.l10n.albumCamera;
@ -183,11 +185,14 @@ mixin AlbumMixin on SourceBase {
case AlbumType.videoCaptures: case AlbumType.videoCaptures:
return context.l10n.albumVideoCaptures; return context.l10n.albumVideoCaptures;
case AlbumType.regular: case AlbumType.regular:
case AlbumType.vault:
case AlbumType.app: case AlbumType.app:
break; break;
} }
} }
if (type == AlbumType.vault) return pContext.basename(dirPath);
final dir = VolumeRelativeDirectory.fromPath(dirPath); final dir = VolumeRelativeDirectory.fromPath(dirPath);
if (dir == null) return dirPath; if (dir == null) return dirPath;

View file

@ -18,6 +18,7 @@ import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/model/source/trash.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/analysis_service.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -60,9 +61,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
final oldValue = event.oldValue; final oldValue = event.oldValue;
if (oldValue is List<String>?) { if (oldValue is List<String>?) {
final oldHiddenFilters = (oldValue ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); 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(); final EventBus _eventBus = EventBus();
@ -108,16 +114,22 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
_savedDates = Map.unmodifiable(await metadataDb.loadDates()); _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) { Iterable<AvesEntry> _applyHiddenFilters(Iterable<AvesEntry> entries) {
final hiddenFilters = { final hiddenFilters = {
TrashFilter.instance, TrashFilter.instance,
...settings.hiddenFilters, ..._getAppHiddenFilters(),
}; };
return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry)));
} }
Iterable<AvesEntry> _applyTrashFilter(Iterable<AvesEntry> entries) { 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}) { 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 { Future<void> _moveEntry(AvesEntry entry, Map newFields, {required bool persist}) async {
newFields.keys.forEach((key) { newFields.keys.forEach((key) {
final newValue = newFields[key];
switch (key) { switch (key) {
case 'contentId': case 'contentId':
entry.contentId = newFields['contentId'] as int?; entry.contentId = newValue as int?;
break; break;
case 'dateModifiedSecs': case 'dateModifiedSecs':
// `dateModifiedSecs` changes when moving entries to another directory, // `dateModifiedSecs` changes when moving entries to another directory,
// but it does not change when renaming the containing directory // but it does not change when renaming the containing directory
entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int?; entry.dateModifiedSecs = newValue as int?;
break; break;
case 'path': case 'path':
entry.path = newFields['path'] as String?; entry.path = newValue as String?;
break; break;
case 'title': case 'title':
entry.sourceTitle = newFields['title'] as String?; entry.sourceTitle = newValue as String?;
break; break;
case 'trashed': case 'trashed':
final trashed = newFields['trashed'] as bool; final trashed = newValue as bool;
entry.trashed = trashed; entry.trashed = trashed;
entry.trashDetails = trashed entry.trashDetails = trashed
? TrashDetails( ? TrashDetails(
@ -225,7 +238,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
: null; : null;
break; break;
case 'uri': case 'uri':
entry.uri = newFields['uri'] as String; entry.uri = newValue as String;
break;
case 'origin':
entry.origin = newValue as int;
break; break;
} }
}); });
@ -251,6 +267,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum); final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum);
final pinned = settings.pinnedFilters.contains(oldFilter); final pinned = settings.pinnedFilters.contains(oldFilter);
if (vaults.isVault(sourceAlbum)) {
await vaults.rename(sourceAlbum, destinationAlbum);
}
final existingCover = covers.of(oldFilter); final existingCover = covers.of(oldFilter);
await covers.set( await covers.set(
filter: newFilter, filter: newFilter,
@ -266,6 +286,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
destinationAlbums: {destinationAlbum}, destinationAlbums: {destinationAlbum},
movedOps: movedOps, movedOps: movedOps,
); );
// restore bookmark and pin, as the obsolete album got removed and its associated state cleaned // restore bookmark and pin, as the obsolete album got removed and its associated state cleaned
if (bookmark != null && bookmark != -1) { if (bookmark != null && bookmark != -1) {
settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum); settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum);
@ -312,6 +333,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
title: newFields['title'] as String?, title: newFields['title'] as String?,
dateAddedSecs: newFields['dateAddedSecs'] as int?, dateAddedSecs: newFields['dateAddedSecs'] as int?,
dateModifiedSecs: newFields['dateModifiedSecs'] as int?, dateModifiedSecs: newFields['dateModifiedSecs'] as int?,
origin: newFields['origin'] as int?,
)); ));
} else { } else {
debugPrint('failed to find source entry with uri=$sourceUri'); debugPrint('failed to find source entry with uri=$sourceUri');
@ -345,7 +367,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
break; break;
case MoveType.move: case MoveType.move:
case MoveType.export: case MoveType.export:
cleanEmptyAlbums(fromAlbums); cleanEmptyAlbums(fromAlbums.whereNotNull().toSet());
addDirectories(albums: destinationAlbums); addDirectories(albums: destinationAlbums);
break; break;
case MoveType.toBin: case MoveType.toBin:
@ -507,11 +529,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
return recentEntry(filter); return recentEntry(filter);
} }
void _onFilterVisibilityChanged(Set<CollectionFilter> oldHiddenFilters, Set<CollectionFilter> currentHiddenFilters) { void _onFilterVisibilityChanged(Set<CollectionFilter> newlyVisibleFilters) {
updateDerivedFilters(); updateDerivedFilters();
eventBus.fire(const FilterVisibilityChangedEvent()); eventBus.fire(const FilterVisibilityChangedEvent());
final newlyVisibleFilters = oldHiddenFilters.whereNot(currentHiddenFilters.contains).toSet();
if (newlyVisibleFilters.isNotEmpty) { if (newlyVisibleFilters.isNotEmpty) {
final candidateEntries = visibleEntries.where((entry) => newlyVisibleFilters.any((f) => f.test(entry))).toSet(); final candidateEntries = visibleEntries.where((entry) => newlyVisibleFilters.any((f) => f.test(entry))).toSet();
analyze(null, entries: candidateEntries); analyze(null, entries: candidateEntries);

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