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="v1.8.0"></a>[v1.8.0] - 2023-02-20
### Added
- Vaults
- Viewer: overlay details expand/collapse on tap
- Viewer: export actions available as quick actions
- Slideshow: added settings quick action
- TV: improved support for Info
- Basque translation (thanks Aitor Salaberria)
### Changed
- disabling the recycle bin will delete forever items in it
- remember pin status of albums becoming empty
- allow setting dates before 1970/01/01
- upgraded Flutter to stable v3.7.3
### Fixed
- SD card access grant on Android Lollipop
- copying to SD card in some cases
- sharing SD card files referred by `file` URI
## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18
### Added

View file

@ -18,6 +18,9 @@ if (localPropertiesFile.exists()) {
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Keys
@ -181,10 +184,11 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.exifinterface:exifinterface:1.3.5'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha04'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'

View file

@ -19,7 +19,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:required="false" />
<!--
Scoped storage on Android 10 is inconvenient because users need to confirm edition on each individual file.
Scoped storage on Android 10 (API 29) is inconvenient because users need to confirm edition on each individual file.
So we request `WRITE_EXTERNAL_STORAGE` until Android 10 (API 29), and enable `requestLegacyExternalStorage`
-->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@ -32,28 +32,35 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<!-- to access media with original metadata with scoped storage (API >=29) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- to analyze media in a service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- to fetch map tiles -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->
<uses-permission
android:name="android.permission.MANAGE_MEDIA"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- to show foreground service progress via notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- to access media with original metadata with scoped storage (Android >=10) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- to change wallpaper -->
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<!-- to unlock vaults (API >=28) -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- TODO TLAD remove this permission when this is fixed: https://github.com/flutter/flutter/issues/42451 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- for API <26 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission
android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
android:maxSdkVersion="25" />
<!-- allow install on API 19, but Google Maps is from API 20 -->
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps" />
<!--
allow install on API 19, despite the `minSdkVersion` declared in dependencies:
- Google Maps is from API 20
- the Security library is from API 21
-->
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps, androidx.security:security-crypto" />
<!-- from Android 11, we should define <queries> to make other apps visible to this app -->
<queries>
@ -75,12 +82,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:allowBackup="true"
android:appCategory="image"
android:banner="@drawable/banner"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/full_backup_content"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
tools:targetApi="o">
tools:targetApi="s">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View file

@ -37,12 +37,16 @@ class AnalysisService : Service() {
}
}
initChannels(this)
try {
initChannels(this)
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
serviceHandler = ServiceHandler(looper)
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
serviceHandler = ServiceHandler(looper)
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to initialize service", e)
}
}
@ -79,7 +83,10 @@ class AnalysisService : Service() {
}
private fun initChannels(context: Context) {
val messenger = flutterEngine!!.dartExecutor
val engine = flutterEngine
engine ?: throw Exception("Flutter engine is not initialized")
val messenger = engine.dartExecutor
// channels for analysis

View file

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

View file

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

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.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
open class MainActivity : FlutterActivity() {
open class MainActivity : FlutterFragmentActivity() {
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
private lateinit var intentStreamHandler: IntentStreamHandler
@ -68,8 +69,12 @@ open class MainActivity : FlutterActivity() {
// .build()
// )
super.onCreate(savedInstanceState)
}
val messenger = flutterEngine!!.dartExecutor
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
analysisStreamHandler = AnalysisStreamHandler().apply {
@ -99,6 +104,7 @@ open class MainActivity : FlutterActivity() {
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, SecurityHandler.CHANNEL).setMethodCallHandler(SecurityHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
@ -172,7 +178,18 @@ open class MainActivity : FlutterActivity() {
mediaSessionHandler.dispose()
mediaStoreChangeStreamHandler.dispose()
settingsChangeStreamHandler.dispose()
super.onDestroy()
try {
super.onDestroy()
} catch (e: Exception) {
// on Android 11, app may crash as follows:
// `Fatal Exception:`
// `java.lang.RuntimeException: Unable to destroy activity {deckers.thibault.aves/deckers.thibault.aves.MainActivity}:`
// `java.lang.IllegalArgumentException: NetworkCallback was not registered`
// related to this error:
// `Package android does not belong to 10162`
// cf https://issuetracker.google.com/issues/175055271
Log.e(LOG_TAG, "failed while destroying activity", e)
}
}
override fun onNewIntent(intent: Intent) {
@ -182,6 +199,7 @@ open class MainActivity : FlutterActivity() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
DELETE_SINGLE_PERMISSION_REQUEST,
@ -244,7 +262,7 @@ open class MainActivity : FlutterActivity() {
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to type,
@ -314,7 +332,7 @@ open class MainActivity : FlutterActivity() {
private fun submitPickedItems(call: MethodCall) {
val pickedUris = call.argument<List<String>>("uris")
if (pickedUris != null && pickedUris.isNotEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) }
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {

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 {
setMethodCallHandler { call, result ->
when (call.method) {

View file

@ -1,6 +1,5 @@
package deckers.thibault.aves
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -18,11 +17,12 @@ import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterActivity() {
class WallpaperActivity : FlutterFragmentActivity() {
private lateinit var intentDataMap: MutableMap<String, Any?>
override fun onCreate(savedInstanceState: Bundle?) {
@ -36,8 +36,33 @@ class WallpaperActivity : FlutterActivity() {
Log.i(LOG_TAG, "onCreate intent extras=$it")
}
intentDataMap = extractIntentData(intent)
}
initChannels(this)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
// intent handling
// detail fetch: dart -> platform
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
}
override fun onStart() {
@ -54,32 +79,6 @@ class WallpaperActivity : FlutterActivity() {
}
}
private fun initChannels(activity: Activity) {
val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(activity))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(activity))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(activity))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(activity))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(activity))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(activity))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(activity))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(activity))
// - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(activity))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(activity, args) }
// intent handling
// detail fetch: dart -> platform
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
}
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getIntentData" -> {
@ -94,7 +93,7 @@ class WallpaperActivity : FlutterActivity() {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,

View file

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

View file

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

View file

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

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

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

View file

@ -109,7 +109,7 @@ class ThumbnailFetcher internal constructor(
} else {
@Suppress("deprecation")
var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null)
// from Android 10, returned thumbnail is already rotated according to EXIF orientation
// from Android 10 (API 29), returned thumbnail is already rotated according to EXIF orientation
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}

View file

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

View file

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

View file

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

View file

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

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.metadataextractor.SafeMp4UuidBoxHandler
import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader
import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
@ -130,7 +130,7 @@ object XMP {
) {
if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val xmpBytes = context.queryContentResolverProp(uri, mimeType, MediaStore.MediaColumns.XMP)
val xmpBytes = context.queryContentPropValue(uri, mimeType, MediaStore.MediaColumns.XMP)
if (xmpBytes is ByteArray && xmpBytes.size > 0) {
val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS)
processXmp(xmpMeta)
@ -170,6 +170,11 @@ object XMP {
}
}
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
// TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent,
// because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`,
// so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes.
IsoFile(channel, boxParser).use { isoFile ->
isoFile.processBoxes(UserBox::class.java, true) { box, _ ->
val boxSize = box.size

View file

@ -33,6 +33,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException
class SourceEntry {
private val origin: Int
val uri: Uri // content or file URI
var path: String? = null // best effort to get local path
private val sourceMimeType: String
@ -48,12 +49,14 @@ class SourceEntry {
private var foundExif: Boolean = false
constructor(uri: Uri, sourceMimeType: String) {
constructor(origin: Int, uri: Uri, sourceMimeType: String) {
this.origin = origin
this.uri = uri
this.sourceMimeType = sourceMimeType
}
constructor(map: FieldMap) {
origin = map["origin"] as Int
uri = Uri.parse(map["uri"] as String)
path = map["path"] as String?
sourceMimeType = map["sourceMimeType"] as String
@ -77,6 +80,7 @@ class SourceEntry {
fun toMap(): FieldMap {
return hashMapOf(
"origin" to origin,
"uri" to uri.toString(),
"path" to path,
"sourceMimeType" to sourceMimeType,
@ -249,13 +253,15 @@ class SourceEntry {
private fun fillByTiffDecode(context: Context) {
try {
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() ?: return
val options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = true
context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
val fd = pfd.detachFd()
val options = TiffBitmapFactory.Options().apply {
inJustDecodeBounds = true
}
TiffBitmapFactory.decodeFileDescriptor(fd, options)
width = options.outWidth
height = options.outHeight
}
TiffBitmapFactory.decodeFileDescriptor(fd, options)
width = options.outWidth
height = options.outHeight
} catch (e: Exception) {
// ignore
}
@ -267,5 +273,11 @@ class SourceEntry {
is Int -> o.toLong()
else -> o as? Long
}
// should match `EntryOrigins` on the Dart side
const val ORIGIN_MEDIA_STORE_CONTENT = 0
const val ORIGIN_UNKNOWN_CONTENT = 1
const val ORIGIN_FILE = 2
const val ORIGIN_VAULT = 3
}
}

View file

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

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
import java.io.File
@ -15,7 +16,7 @@ internal class FileImageProvider : ImageProvider() {
return
}
val entry = SourceEntry(uri, sourceMimeType)
val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType)
val path = uri.path
if (path != null) {
@ -52,6 +53,19 @@ internal class FileImageProvider : ImageProvider() {
throw Exception("failed to delete entry with uri=$uri path=$path")
}
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
try {
val file = File(path)
if (file.exists()) {
newFields["dateModifiedSecs"] = file.lastModified() / 1000
newFields["sizeBytes"] = file.length()
}
callback.onSuccess(newFields)
} catch (e: SecurityException) {
callback.onFailure(e)
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>()
}

View file

@ -811,11 +811,6 @@ abstract class ImageProvider {
fields: List<String>,
callback: ImageOpCallback,
) {
if (dateMillis != null && dateMillis < 0) {
callback.onFailure(Exception("dateMillis=$dateMillis cannot be negative"))
return
}
val success = editExif(context, path, uri, mimeType, callback) { exif ->
when {
dateMillis != null -> {

View file

@ -9,6 +9,7 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import androidx.core.net.toUri
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.OutputStream
import java.io.SyncFailedException
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation
@ -219,6 +221,7 @@ class MediaStoreImageProvider : ImageProvider() {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else {
var entryMap: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"uri" to itemUri.toString(),
"path" to cursor.getString(pathColumn),
"sourceMimeType" to mimeType,
@ -349,7 +352,7 @@ class MediaStoreImageProvider : ImageProvider() {
}
} catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory,
// the delete request may yield a `RecoverableSecurityException` on Android >=10
// the delete request may yield a `RecoverableSecurityException` on API >=29
// when the underlying file no longer exists and this is an orphaned entry in the Media Store
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) {
Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException)
@ -386,10 +389,12 @@ class MediaStoreImageProvider : ImageProvider() {
val entries = kv.value
val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER
val toVault = StorageUtils.isInVault(activity, targetDir)
val toAppDir = toBin || toVault
var effectiveTargetDir: String? = null
var targetDirDocFile: DocumentFileCompat? = null
if (!toBin) {
if (!toAppDir) {
effectiveTargetDir = targetDir
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
if (!File(targetDir).exists()) {
@ -437,13 +442,20 @@ class MediaStoreImageProvider : ImageProvider() {
// - there is no documentation regarding support for usage with removable storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try {
if (toBin) {
val trashDir = StorageUtils.trashDirFor(activity, sourcePath)
if (trashDir != null) {
effectiveTargetDir = ensureTrailingSeparator(trashDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(trashDir)
val appDir = when {
toBin -> StorageUtils.trashDirFor(activity, sourcePath)
toVault -> File(targetDir)
else -> null
}
if (appDir != null) {
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
if (toVault) {
appDir.mkdirs()
}
}
if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = File(sourcePath)
@ -462,6 +474,7 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType = mimeType,
copy = copy,
toBin = toBin,
toVault = toVault,
)
}
}
@ -488,6 +501,7 @@ class MediaStoreImageProvider : ImageProvider() {
mimeType: String,
copy: Boolean,
toBin: Boolean,
toVault: Boolean,
): FieldMap {
val sourcePath = sourceFile.path
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
@ -512,7 +526,16 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
) { output: OutputStream -> sourceDocFile.copyTo(output) }
) { output: OutputStream ->
try {
sourceDocFile.copyTo(output)
} catch (e: SyncFailedException) {
// The copied file is synced after writing, but it consistently fails in some cases
// (e.g. copying to SD card on Xiaomi 2201117PG with Android 11).
// It seems this failure can be safely ignored, as the new file is complete.
Log.w(LOG_TAG, "sync failure after copying from uri=$sourceUri, path=$sourcePath to targetDir=$targetDir", e)
}
}
if (!copy) {
// delete original entry
@ -522,13 +545,21 @@ class MediaStoreImageProvider : ImageProvider() {
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
}
}
if (toBin) {
return hashMapOf(
return if (toBin) {
hashMapOf(
"trashed" to true,
"trashPath" to targetPath,
)
} else if (toVault) {
hashMapOf(
"uri" to File(targetPath).toUri().toString(),
"contentId" to null,
"path" to targetPath,
"origin" to SourceEntry.ORIGIN_VAULT,
)
} else {
scanNewPath(activity, targetPath, mimeType)
}
return scanNewPath(activity, targetPath, mimeType)
}
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
@ -910,7 +941,7 @@ class MediaStoreImageProvider : ImageProvider() {
private val VIDEO_PROJECTION = arrayOf(
*BASE_PROJECTION,
MediaColumns.DURATION,
// `ORIENTATION` was only available for images before Android 10
// `ORIENTATION` was only available for images before Android 10 (API 29)
*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(
MediaStore.MediaColumns.ORIENTATION,
) else emptyArray()

View file

@ -7,6 +7,7 @@ import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.util.Log
import deckers.thibault.aves.utils.UriUtils.tryParseId
@ -30,7 +31,13 @@ object ContextUtils {
return am.getRunningServices(Integer.MAX_VALUE).any { it.service.className == serviceClass.name }
}
fun Context.queryContentResolverProp(uri: Uri, mimeType: String, prop: String): Any? {
// `flag`: `DocumentsContract.Document.FLAG_SUPPORTS_COPY`, etc.
fun Context.queryDocumentProviderFlag(docUri: Uri, flag: Int): Boolean {
val flags = queryContentPropValue(docUri, "", DocumentsContract.Document.COLUMN_FLAGS) as Long?
return if (flags != null) (flags.toInt() and flag) == flag else false
}
fun Context.queryContentPropValue(uri: Uri, mimeType: String, column: String): Any? {
var contentUri: Uri = uri
if (StorageUtils.isMediaStoreContentUri(uri)) {
uri.tryParseId()?.let { id ->
@ -43,26 +50,26 @@ object ContextUtils {
}
}
// throws SQLiteException when the requested prop is not a known column
val cursor = contentResolver.query(contentUri, arrayOf(prop), null, null, null)
if (cursor == null || !cursor.moveToFirst()) {
throw Exception("failed to get cursor for contentUri=$contentUri")
}
var value: Any? = null
try {
value = when (cursor.getType(0)) {
Cursor.FIELD_TYPE_NULL -> null
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
else -> null
val cursor = contentResolver.query(contentUri, arrayOf(column), null, null, null)
if (cursor == null || !cursor.moveToFirst()) {
Log.w(LOG_TAG, "failed to get cursor for contentUri=$contentUri column=$column")
} else {
value = when (cursor.getType(0)) {
Cursor.FIELD_TYPE_NULL -> null
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
else -> null
}
cursor.close()
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri key=$prop", e)
// throws SQLiteException/IllegalArgumentException when the requested prop is not a known column
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri column=$column", e)
}
cursor.close()
return value
}
}

View file

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

View file

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

View file

@ -39,28 +39,29 @@ object StorageUtils {
private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+")
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
const val TRASH_PATH_PLACEHOLDER = "#trash"
private fun isAppFile(context: Context, path: String): Boolean {
val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
return filesDirs.any { path.startsWith(it.path) }
val dirs = context.getExternalFilesDirs(null).filterNotNull()
return dirs.any { path.startsWith(it.path) }
}
private fun appExternalFilesDirFor(context: Context, path: String): File? {
val filesDirs = context.getExternalFilesDirs(null).filterNotNull()
val dirs = context.getExternalFilesDirs(null).filterNotNull()
val volumePath = getVolumePath(context, path)
return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.firstOrNull()
return volumePath?.let { dirs.firstOrNull { it.startsWith(volumePath) } } ?: dirs.firstOrNull()
}
fun trashDirFor(context: Context, path: String): File? {
val filesDir = appExternalFilesDirFor(context, path)
if (filesDir == null) {
val externalFilesDir = appExternalFilesDirFor(context, path)
if (externalFilesDir == null) {
Log.e(LOG_TAG, "failed to find external files dir for path=$path")
return null
}
val trashDir = File(filesDir, "trash")
val trashDir = File(externalFilesDir, "trash")
if (!trashDir.exists() && !trashDir.mkdirs()) {
Log.e(LOG_TAG, "failed to create directories at path=$trashDir")
return null
@ -68,6 +69,10 @@ object StorageUtils {
return trashDir
}
fun getVaultRoot(context: Context) = ensureTrailingSeparator(File(context.filesDir, "vault").path)
fun isInVault(context: Context, path: String) = path.startsWith(getVaultRoot(context))
/**
* Volume paths
*/
@ -259,6 +264,7 @@ object StorageUtils {
// e.g.
// /storage/emulated/0/ -> primary
// /storage/10F9-3F13/Pictures/ -> 10F9-3F13
// /storage/extSdCard/ -> 1234-5678 [Android 5.1.1, Samsung Galaxy Core Prime]
private fun getVolumeUuidForDocumentUri(context: Context, anyPath: String): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
@ -278,7 +284,22 @@ object StorageUtils {
return EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
}
volumePath.split(File.separator).lastOrNull { it.isNotEmpty() }?.let { uuid ->
return uuid.uppercase(Locale.ROOT)
if (uuid.matches(UUID_PATTERN)) {
return uuid.uppercase(Locale.ROOT)
}
}
// fallback when UUID does not appear in the SD card volume path
context.contentResolver.persistedUriPermissions.firstOrNull { uriPermission ->
convertTreeDocumentUriToDirPath(context, uriPermission.uri)?.let {
getVolumePath(context, it)?.let { grantedVolumePath ->
grantedVolumePath == volumePath
}
} ?: false
}?.let { uriPermission ->
splitTreeDocumentUri(uriPermission.uri)?.let { (uuid, _) ->
return uuid
}
}
}
@ -289,6 +310,7 @@ object StorageUtils {
// e.g.
// primary -> /storage/emulated/0/
// 10F9-3F13 -> /storage/10F9-3F13/
// 1234-5678 -> /storage/extSdCard/ [Android 5.1.1, Samsung Galaxy Core Prime]
private fun getVolumePathFromTreeDocumentUriUuid(context: Context, uuid: String): String? {
if (uuid == EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID) {
return getPrimaryVolumePath(context)
@ -318,6 +340,10 @@ object StorageUtils {
}
}
// fallback when UUID does not appear in the SD card volume path
val primaryVolumePath = getPrimaryVolumePath(context)
getVolumePaths(context).firstOrNull { it != primaryVolumePath }?.let { return it }
Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid")
return null
}
@ -350,9 +376,9 @@ object StorageUtils {
}
// e.g.
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? {
// content://com.android.externalstorage.documents/tree/primary%3A -> ("primary", "")
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> ("10F9-3F13", "Pictures")
private fun splitTreeDocumentUri(treeDocumentUri: Uri): Pair<String, String>? {
val treeDocumentUriString = treeDocumentUri.toString()
if (treeDocumentUriString.length <= TREE_URI_ROOT.length) return null
val encoded = treeDocumentUriString.substring(TREE_URI_ROOT.length)
@ -362,13 +388,24 @@ object StorageUtils {
val uuid = group(1)
val relativePath = group(2)
if (uuid != null && relativePath != null) {
val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid)
if (volumePath != null) {
return ensureTrailingSeparator(volumePath + relativePath)
}
return Pair(uuid, relativePath)
}
}
}
Log.e(LOG_TAG, "failed to split treeDocumentUri=$treeDocumentUri to UUID and relative path")
return null
}
// e.g.
// content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/
// content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/
fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? {
splitTreeDocumentUri(treeDocumentUri)?.let { (uuid, relativePath) ->
val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid)
if (volumePath != null) {
return ensureTrailingSeparator(volumePath + relativePath)
}
}
Log.e(LOG_TAG, "failed to convert treeDocumentUri=$treeDocumentUri to path")
return null
}
@ -512,7 +549,7 @@ object StorageUtils {
}
// As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used
// to work around a bug from Android 10 where metadata redaction corrupts HEIC images.
// to work around a bug from Android 10 (API 29) where metadata redaction corrupts HEIC images.
// This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException`
// for some non image/video content URIs (e.g. `downloads`, `file`)
fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long? = null): Uri {

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"?>
<paths>
<!-- `external-path` only covers files on regular device storage -->
<external-path
name="external_files"
name="external"
path="." />
<!-- `root-path` is necessary to cover files on removable storage -->
<!-- cf https://iditect.com/article/fileprovider-for-android-get-content-uri-through-fileprovider.html -->
<!--suppress AndroidElementNotAllowed -->
<root-path
name="root"
path="." />
<!-- embedded images & other media that are exported for viewing and sharing -->

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

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.
You must use the app for legal, authorized and acceptable purposes.
The app is designed for legal, authorized and acceptable purposes.
## 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 canCreateFilter => {
AppMode.main,
AppMode.pickFilterInternal,
}.contains(this);
bool get isPickingMedia => {
AppMode.pickSingleMediaExternal,
AppMode.pickMultipleMediaExternal,

View file

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

View file

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

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": {
"placeholders": {
"days": {}
}
},
"itemCount": "{count, plural, =1{1 položka} =2..4{{count} položky} other{{count} položek}}",
"itemCount": "{count, plural, =1{1 položka} few{{count} položky} other{{count} položek}}",
"@itemCount": {
"placeholders": {
"count": {}
}
},
"columnCount": "{count, plural, =1{1 sloupec} =2..4{{count} sloupce} other{{count} sloupců}}",
"columnCount": "{count, plural, =1{1 sloupec} few{{count} sloupce} other{{count} sloupců}}",
"@columnCount": {
"placeholders": {
"count": {}
@ -168,8 +168,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Rychlost přehrávání",
"@videoActionSetSpeed": {},
"videoActionSettings": "Nastavení",
"@videoActionSettings": {},
"viewerActionSettings": "Nastavení",
"@viewerActionSettings": {},
"slideshowActionResume": "Pokračovat",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Zobrazit ve sbírce",
@ -313,7 +313,7 @@
"@videoPlaybackMuted": {},
"videoPlaybackWithSound": "Přehrát se zvukem",
"@videoPlaybackWithSound": {},
"themeBrightnessLight": "Svetlé",
"themeBrightnessLight": "Světlé",
"@themeBrightnessLight": {},
"themeBrightnessDark": "Tmavé",
"@themeBrightnessDark": {},
@ -436,7 +436,7 @@
"@addShortcutButtonLabel": {},
"noMatchingAppDialogMessage": "Pro tuto operaci není k dispozici žádná aplikace.",
"@noMatchingAppDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} =2..4{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}",
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} few{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}",
"@deleteEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -485,13 +485,13 @@
"@renameEntrySetPagePreviewSectionTitle": {},
"renameProcessorName": "Název",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a tuto položku?} =2..4{Smazat toto album a tyto {count} položky?} other{Smazat toto album a těchto {count} položek?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a v něm obsaženou položku?} few{Smazat toto album a v něm obsažené {count} položky?} other{Smazat toto album a v něm obsažených {count} položek?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a jejich položku?} =2..4{Smazat tato alba a jejich {count} položky?} other{Smazat tato alba a jejich {count} položek?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a v nich obsaženou položku?} few{Smazat tato alba a v nich obsažené {count} položky?} other{Smazat tato alba a v nich obsažených {count} položek?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -955,7 +955,7 @@
"@nameConflictStrategySkip": {},
"keepScreenOnNever": "Nikdy",
"@keepScreenOnNever": {},
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} =2..4{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}",
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} few{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}",
"@binEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -1121,25 +1121,25 @@
"count": {}
}
},
"collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} =2..4{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}",
"collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} few{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}",
"@collectionCopySuccessFeedback": {
"placeholders": {
"count": {}
}
},
"collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} =2..4{Přesunuty {count} položky} other{Přesunuto {count} položek}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} few{Přesunuty {count} položky} other{Přesunuto {count} položek}}",
"@collectionMoveSuccessFeedback": {
"placeholders": {
"count": {}
}
},
"collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} =2..4{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} few{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}",
"@collectionRenameSuccessFeedback": {
"placeholders": {
"count": {}
}
},
"collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} =2..4{Upraveny {count} položky} other{Upraveno {count} položek}}",
"collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} few{Upraveny {count} položky} other{Upraveno {count} položek}}",
"@collectionEditSuccessFeedback": {
"placeholders": {
"count": {}
@ -1209,8 +1209,6 @@
"@albumGroupNone": {},
"albumVideoCaptures": "Snímky videa",
"@albumVideoCaptures": {},
"createAlbumTooltip": "Vytvořit album",
"@createAlbumTooltip": {},
"countryPageTitle": "Země",
"@countryPageTitle": {},
"searchCollectionFieldHint": "Prohledat sbírky",
@ -1283,13 +1281,13 @@
"@settingsThumbnailShowVideoDuration": {},
"settingsCollectionQuickActionsTile": "Rychlé akce",
"@settingsCollectionQuickActionsTile": {},
"timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}",
"timeMinutes": "{minutes, plural, =1{1 minuta} few{{minutes} minuty} other{{minutes} minut}}",
"@timeMinutes": {
"placeholders": {
"minutes": {}
}
},
"timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}",
"timeSeconds": "{seconds, plural, =1{1 sekunda} few{{seconds} sekundy} other{{seconds} sekund}}",
"@timeSeconds": {
"placeholders": {
"seconds": {}
@ -1331,7 +1329,7 @@
"@settingsStorageAccessBanner": {},
"settingsUnitSystemTile": "Jednotky",
"@settingsUnitSystemTile": {},
"statsWithGps": "{count, plural, =1{1 položka s polohou} =2..4{{count} položky s polohou} other{{count} položek s polohou}}",
"statsWithGps": "{count, plural, =1{1 položka s polohou} few{{count} položky s polohou} other{{count} položek s polohou}}",
"@statsWithGps": {
"placeholders": {
"count": {}
@ -1354,5 +1352,19 @@
"mapAttributionStamen": "Mapová data © [OpenStreetMap](https://www.openstreetmap.org/copyright) přispěvatelé • Dlaždice z [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)",
"@mapAttributionStamen": {},
"panoramaDisableSensorControl": "Zakázat ovládání senzorem",
"@panoramaDisableSensorControl": {}
"@panoramaDisableSensorControl": {},
"filterLocatedLabel": "S polohou",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Se štítky",
"@filterTaggedLabel": {},
"settingsDisplayUseTvInterface": "Rozhraní Android TV",
"@settingsDisplayUseTvInterface": {},
"tooManyItemsErrorDialogMessage": "Zkuste znovu s několika dalšími položkami.",
"@tooManyItemsErrorDialogMessage": {},
"settingsModificationWarningDialogMessage": "Ostatní nastavení budou upravena.",
"@settingsModificationWarningDialogMessage": {},
"settingsViewerShowDescription": "Zobrazit popis",
"@settingsViewerShowDescription": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
}

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Wiedergabegeschwindigkeit",
"@videoActionSetSpeed": {},
"videoActionSettings": "Einstellungen",
"@videoActionSettings": {},
"viewerActionSettings": "Einstellungen",
"@viewerActionSettings": {},
"slideshowActionResume": "Wiedergabe",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "In Sammlung anzeigen",
@ -389,9 +389,9 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Name",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Album und der Inhalt gelöscht werden soll?} other{Sicher, dass dieses Album und deren {count} Elemente gelöscht werden sollen?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Das Album und das darin enthaltene Element löschen?} other{Das Album und die {count} darin enthaltenen Elemente löschen?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass diese Alben und deren Inhalt gelöscht werden sollen?} other{Sicher, dass diese Alben und deren {count} Elemente gelöscht werden sollen?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Diese Alben und die darin enthaltenen Elemente löschen?} other{Diese Alben und die darin enthaltenen {count} Objekte löschen?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {},
@ -595,7 +595,7 @@
"@collectionCopySuccessFeedback": {},
"collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
"@collectionMoveSuccessFeedback": {},
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element unmebannt} other{{count} Elemente umbenannt}}",
"collectionRenameSuccessFeedback": "{count, plural, =1{1 Element umbenannt} other{{count} Elemente umbenannt}}",
"@collectionRenameSuccessFeedback": {},
"collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
"@collectionEditSuccessFeedback": {},
@ -699,8 +699,6 @@
"@albumPageTitle": {},
"albumEmpty": "Keine Alben",
"@albumEmpty": {},
"createAlbumTooltip": "Album erstellen",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "ERSTELLE",
"@createAlbumButtonLabel": {},
"newFilterBanner": "Neu",
@ -1188,5 +1186,27 @@
"entryInfoActionRemoveLocation": "Standort entfernen",
"@entryInfoActionRemoveLocation": {},
"entryActionShareVideoOnly": "Nur das Video teilen",
"@entryActionShareVideoOnly": {}
"@entryActionShareVideoOnly": {},
"filterLocatedLabel": "Mit Standort",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Getaggt",
"@filterTaggedLabel": {},
"settingsAccessibilityShowPinchGestureAlternatives": "Alternativen für Multi-Touch-Gesten anzeigen",
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"settingsDisplayUseTvInterface": "Android-TV Oberfläche",
"@settingsDisplayUseTvInterface": {},
"columnCount": "{count, plural, =1{1 Spalte} other{{count} Spalten}}",
"@columnCount": {
"placeholders": {
"count": {}
}
},
"settingsVideoGestureVerticalDragBrightnessVolume": "Nach oben oder unten wischen, um die Helligkeit/Lautstärke einzustellen",
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"tooManyItemsErrorDialogMessage": "Noch einmal mit weniger Elementen versuchen.",
"@tooManyItemsErrorDialogMessage": {},
"settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.",
"@settingsModificationWarningDialogMessage": {},
"settingsViewerShowDescription": "Beschreibung anzeigen",
"@settingsViewerShowDescription": {}
}

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Ταχύτητα αναπαραγωγής",
"@videoActionSetSpeed": {},
"videoActionSettings": "Ρυθμίσεις",
"@videoActionSettings": {},
"viewerActionSettings": "Ρυθμίσεις",
"@viewerActionSettings": {},
"slideshowActionResume": "Συνέχιση",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Εμφάνιση στη Συλλογή",
@ -699,8 +699,6 @@
"@albumPageTitle": {},
"albumEmpty": "Δεν υπάρχουν άλμπουμ",
"@albumEmpty": {},
"createAlbumTooltip": "Δημιουργία άλμπουμ",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "ΔΗΜΙΟΥΡΓΙΑ",
"@createAlbumButtonLabel": {},
"newFilterBanner": "Νέα",
@ -1206,5 +1204,49 @@
"settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον",
"@settingsDisplayUseTvInterface": {},
"settingsViewerShowDescription": "Εμφάνιση περιγραφής",
"@settingsViewerShowDescription": {}
"@settingsViewerShowDescription": {},
"chipActionLock": "Κλείδωμα",
"@chipActionLock": {},
"chipActionCreateVault": "Δημιουργήστε θησαυροφυλάκιο",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Διαμορφώστε το θησαυροφυλάκιο",
"@chipActionConfigureVault": {},
"albumTierVaults": "Θησαυροφυλακια",
"@albumTierVaults": {},
"vaultLockTypePassword": "Κωδικός πρόσβασης",
"@vaultLockTypePassword": {},
"newVaultDialogTitle": "Νεο Θησαυροφυλακιο",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Διαμορφωστε το θησαυροφυλακιο",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Κλείδωμα όταν σβήνει η οθόνη",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Τύπος κλειδώματος",
"@vaultDialogLockTypeLabel": {},
"pinDialogConfirm": "Επιβεβαιώστε το PIN",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Εισάγετε τον κωδικό πρόσβασης",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Επιβεβαιώστε τον κωδικό πρόσβασης",
"@passwordDialogConfirm": {},
"authenticateToUnlockVault": "Πιστοποίηση ταυτότητας για να ξεκλειδώσετε το θησαυροφυλάκιο",
"@authenticateToUnlockVault": {},
"settingsConfirmationVaultDataLoss": "Εμφάνιση προειδοποίησης απώλειας δεδομένων θησαυροφυλακίου",
"@settingsConfirmationVaultDataLoss": {},
"authenticateToConfigureVault": "Πιστοποίηση ταυτότητας για τη διαμόρφωση του θησαυροφυλακίου",
"@authenticateToConfigureVault": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"newVaultWarningDialogMessage": "Τα αρχεία στα θησαυροφυλάκια είναι διαθέσιμα μόνο σε αυτή την εφαρμογή και σε καμία άλλη.\n\nΑν απεγκαταστήσετε την εφαρμογή ή έστω διαγράψετε τα δεδομένα της εφαρμογής, θα χάσετε όλα σας τα κρυφά αρχεία.",
"@newVaultWarningDialogMessage": {},
"pinDialogEnter": "Εισάγετε το PIN",
"@pinDialogEnter": {},
"vaultBinUsageDialogMessage": "Ορισμένα θησαυροφυλάκια χρησιμοποιούν τον κάδο ανακύκλωσης.",
"@vaultBinUsageDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "Τα αρχεία στον κάδο ανακύκλωσης θα διαγραφούν για πάντα.",
"@settingsDisablingBinWarningDialogMessage": {},
"tooManyItemsErrorDialogMessage": "Δοκιμάστε ξανά με λιγότερα αρχεία.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Σύρετε προς τα πάνω ή προς τα κάτω για να ρυθμίσετε τη φωτεινότητα/την ένταση του ήχου",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
}

View file

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

View file

@ -147,8 +147,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Velocidad de reproducción",
"@videoActionSetSpeed": {},
"videoActionSettings": "Ajustes",
"@videoActionSettings": {},
"viewerActionSettings": "Ajustes",
"@viewerActionSettings": {},
"slideshowActionResume": "Reanudar",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Mostrar en Colección",
@ -657,8 +657,6 @@
"@albumPageTitle": {},
"albumEmpty": "Sin álbumes",
"@albumEmpty": {},
"createAlbumTooltip": "Crear álbum",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CREAR",
"@createAlbumButtonLabel": {},
"newFilterBanner": "nuevo",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "Vuelva a intentarlo con menos elementos.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Deslice hacia arriba o hacia abajo para ajustar el brillo o el volumen",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"chipActionLock": "Bloquear",
"@chipActionLock": {},
"chipActionConfigureVault": "Configurar la caja fuerte",
"@chipActionConfigureVault": {},
"albumTierVaults": "Caja fuerte",
"@albumTierVaults": {},
"vaultLockTypePin": "Pin",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "Contraseña",
"@vaultLockTypePassword": {},
"newVaultDialogTitle": "Nueva caja fuerte",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Configurar la caja fuerte",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Bloquear al apagar la pantalla",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Tipo de bloqueo",
"@vaultDialogLockTypeLabel": {},
"pinDialogEnter": "Introducir el código pin",
"@pinDialogEnter": {},
"pinDialogConfirm": "Confirmar el código pin",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Introducir la contraseña",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Confirmar la contraseña",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Autenticarse para configurar la caja fuerte",
"@authenticateToConfigureVault": {},
"vaultBinUsageDialogMessage": "Algunas cajas fuertes utilizan la papelera de reciclaje.",
"@vaultBinUsageDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "Los elementos de la papelera de reciclaje se borrarán para siempre.",
"@settingsDisablingBinWarningDialogMessage": {},
"chipActionCreateVault": "Crear una caja fuerte",
"@chipActionCreateVault": {},
"newVaultWarningDialogMessage": "Los elementos de la caja fuerte sólo están disponibles para esta aplicación y no para otras.\n\nSi desinstalas esta aplicación o borras sus datos, perderás todos estos elementos.",
"@newVaultWarningDialogMessage": {},
"authenticateToUnlockVault": "Autentificarse para desbloquear la caja fuerte",
"@authenticateToUnlockVault": {},
"settingsConfirmationVaultDataLoss": "Mostrar un aviso de pérdida de datos de la caja fuerte",
"@settingsConfirmationVaultDataLoss": {}
}

1370
lib/l10n/app_eu.arb Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Vitesse de lecture",
"@videoActionSetSpeed": {},
"videoActionSettings": "Préférences",
"@videoActionSettings": {},
"viewerActionSettings": "Préférences",
"@viewerActionSettings": {},
"slideshowActionResume": "Reprendre",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Afficher dans Collection",
@ -703,8 +703,6 @@
"@albumPageTitle": {},
"albumEmpty": "Aucun album",
"@albumEmpty": {},
"createAlbumTooltip": "Créer un album",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CRÉER",
"@createAlbumButtonLabel": {},
"newFilterBanner": "nouveau",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "Réessayez avec moins déléments.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Balayer verticalement pour ajuster la luminosité et le volume",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"chipActionLock": "Verrouiller",
"@chipActionLock": {},
"chipActionCreateVault": "Créer un coffre-fort",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Configurer le coffre-fort",
"@chipActionConfigureVault": {},
"albumTierVaults": "Coffres-forts",
"@albumTierVaults": {},
"vaultLockTypePin": "Code PIN",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "Mot de passe",
"@vaultLockTypePassword": {},
"newVaultWarningDialogMessage": "Les éléments dans les coffres-forts ne sont visibles que dans cette app et nulle autre.\n\nSi vous désinstallez cette app, ou que vous supprimez ses données, vous perdrez tous ces éléments.",
"@newVaultWarningDialogMessage": {},
"newVaultDialogTitle": "Nouveau coffre-fort",
"@newVaultDialogTitle": {},
"vaultDialogLockTypeLabel": "Verrouillage",
"@vaultDialogLockTypeLabel": {},
"pinDialogEnter": "Entrez votre code PIN",
"@pinDialogEnter": {},
"pinDialogConfirm": "Confirmez votre code PIN",
"@pinDialogConfirm": {},
"passwordDialogConfirm": "Confirmez votre mot de passe",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Authentification pour configurer le coffre-fort",
"@authenticateToConfigureVault": {},
"vaultBinUsageDialogMessage": "Des coffres-forts utilisent la corbeille.",
"@vaultBinUsageDialogMessage": {},
"vaultDialogLockModeWhenScreenOff": "Verrouiller quand lécran séteint",
"@vaultDialogLockModeWhenScreenOff": {},
"settingsConfirmationVaultDataLoss": "Afficher 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": {},
"videoActionSetSpeed": "Velocidade de reprodución",
"@videoActionSetSpeed": {},
"videoActionSettings": "Configuración",
"@videoActionSettings": {},
"viewerActionSettings": "Configuración",
"@viewerActionSettings": {},
"slideshowActionResume": "Resumo",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Mostrar na colección",

View file

@ -147,8 +147,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Kecepatan pemutaran",
"@videoActionSetSpeed": {},
"videoActionSettings": "Pengaturan",
"@videoActionSettings": {},
"viewerActionSettings": "Pengaturan",
"@viewerActionSettings": {},
"slideshowActionResume": "Lanjutkan",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Tampilkan di Koleksi",
@ -683,8 +683,6 @@
"@albumPageTitle": {},
"albumEmpty": "Tidak ada album",
"@albumEmpty": {},
"createAlbumTooltip": "Buat album",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "BUAT",
"@createAlbumButtonLabel": {},
"newFilterBanner": "baru",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Usap ke atas atau bawah untuk mengatur kecerahan/volume",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"chipActionConfigureVault": "Atur brankas",
"@chipActionConfigureVault": {},
"albumTierVaults": "Brankas",
"@albumTierVaults": {},
"newVaultDialogTitle": "Brankas Baru",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Atur Brankas",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Kunci ketika layar mati",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Jenis penguncian",
"@vaultDialogLockTypeLabel": {},
"pinDialogEnter": "Masukkan pin",
"@pinDialogEnter": {},
"pinDialogConfirm": "Konfirmasi pin",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Masukkan kata sandi",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Konfirmasi kata sandi",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Autentikasi untuk mengatur brankas",
"@authenticateToConfigureVault": {},
"authenticateToUnlockVault": "Autentikasi untuk membuka brankas",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Beberapa brankas menggunakan tong sampah.",
"@vaultBinUsageDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "Item dalam tong sampah akan hilang selamanya.",
"@settingsDisablingBinWarningDialogMessage": {},
"chipActionLock": "Kunci",
"@chipActionLock": {},
"vaultLockTypePassword": "Kata sandi",
"@vaultLockTypePassword": {},
"chipActionCreateVault": "Buat brankas",
"@chipActionCreateVault": {},
"vaultLockTypePin": "Pin",
"@vaultLockTypePin": {},
"newVaultWarningDialogMessage": "Item dalam brankas hanya tersedia untuk aplikasi ini dan bukan yang lain.\n\nJika Anda menghapus aplikasi ini, atau menghapus data aplikasi ini, Anda akan kehilangan semua item tersebut.",
"@newVaultWarningDialogMessage": {},
"settingsConfirmationVaultDataLoss": "Tampilkan peringatan kehilangan data brankas",
"@settingsConfirmationVaultDataLoss": {}
}

View file

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

View file

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

View file

@ -151,8 +151,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "재생 배속",
"@videoActionSetSpeed": {},
"videoActionSettings": "설정",
"@videoActionSettings": {},
"viewerActionSettings": "설정",
"@viewerActionSettings": {},
"slideshowActionResume": "이어서",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "미디어 페이지에서 보기",
@ -703,8 +703,6 @@
"@albumPageTitle": {},
"albumEmpty": "앨범이 없습니다",
"@albumEmpty": {},
"createAlbumTooltip": "앨범 만들기",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "추가",
"@createAlbumButtonLabel": {},
"newFilterBanner": "신규",
@ -1210,5 +1208,45 @@
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"albumTierVaults": "금고",
"@albumTierVaults": {},
"chipActionLock": "잠금",
"@chipActionLock": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "비밀번호",
"@vaultLockTypePassword": {},
"configureVaultDialogTitle": "금고 설정",
"@configureVaultDialogTitle": {},
"pinDialogEnter": "PIN을 입력하세요",
"@pinDialogEnter": {},
"passwordDialogEnter": "비밀번호를 입력하세요",
"@passwordDialogEnter": {},
"pinDialogConfirm": "PIN을 확인하세요",
"@pinDialogConfirm": {},
"vaultDialogLockTypeLabel": "잠금 방식",
"@vaultDialogLockTypeLabel": {},
"vaultDialogLockModeWhenScreenOff": "화면이 꺼진 후 자동으로 잠김",
"@vaultDialogLockModeWhenScreenOff": {},
"authenticateToConfigureVault": "금고 설정을 위한 인증",
"@authenticateToConfigureVault": {},
"authenticateToUnlockVault": "금고 잠금 해제를 위한 인증",
"@authenticateToUnlockVault": {},
"settingsDisablingBinWarningDialogMessage": "휴지통에 있는 항목들이 완전히 삭제될 것입니다.",
"@settingsDisablingBinWarningDialogMessage": {},
"newVaultWarningDialogMessage": "금고에 있는 항목들은 이 앱에서만 볼 수 있습니다.\n\n이 앱을 삭제 시, 또한 이 앱의 데이터를 삭제 시, 항목을 완전히 삭제될 것입니다.",
"@newVaultWarningDialogMessage": {},
"chipActionCreateVault": "금고 만들기",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "금고 설정",
"@chipActionConfigureVault": {},
"newVaultDialogTitle": "새 금고 만들기",
"@newVaultDialogTitle": {},
"passwordDialogConfirm": "비밀번호를 확인하세요",
"@passwordDialogConfirm": {},
"vaultBinUsageDialogMessage": "휴지통을 사용하는 금고가 있습니다.",
"@vaultBinUsageDialogMessage": {},
"settingsConfirmationVaultDataLoss": "금고에 관한 데이터 손실 경고",
"@settingsConfirmationVaultDataLoss": {}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -85,8 +85,8 @@
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Prędkość odtwarzania",
"@videoActionSetSpeed": {},
"videoActionSettings": "Ustawienia",
"@videoActionSettings": {},
"viewerActionSettings": "Ustawienia",
"@viewerActionSettings": {},
"slideshowActionResume": "Wznów",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Pokaż w Kolekcji",
@ -235,31 +235,31 @@
"@displayRefreshRatePreferLowest": {},
"videoPlaybackMuted": "Odtwarzaj bez dźwięku",
"@videoPlaybackMuted": {},
"itemCount": "{count, plural, =1{1 element} =2..4{{count} elementy} other{{count} elelmentów}}",
"itemCount": "{count, plural, =1{1 element} few{{count} elementy} other{{count} elelmentów}}",
"@itemCount": {
"placeholders": {
"count": {}
}
},
"columnCount": "{count, plural, =1{1 rząd} =2..4{{count} rzędy} other{{count} rzędów}}",
"columnCount": "{count, plural, =1{1 rząd} few{{count} rzędy} other{{count} rzędów}}",
"@columnCount": {
"placeholders": {
"count": {}
}
},
"timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}",
"timeSeconds": "{seconds, plural, =1{1 sekunda} few{{seconds} sekundy} other{{seconds} sekund}}",
"@timeSeconds": {
"placeholders": {
"seconds": {}
}
},
"timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}",
"timeMinutes": "{minutes, plural, =1{1 minuta} few{{minutes} minuty} other{{minutes} minut}}",
"@timeMinutes": {
"placeholders": {
"minutes": {}
}
},
"timeDays": "{days, plural, =1{1 dzień} =2..4{{days} dni} other{{days} dni}}",
"timeDays": "{days, plural, =1{1 dzień} few{{days} dni} other{{days} dni}}",
"@timeDays": {
"placeholders": {
"days": {}
@ -547,7 +547,7 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Nazwa",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} =2..4{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} few{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -603,13 +603,13 @@
"@sectionUnknown": {},
"dateToday": "Dzisiaj",
"@dateToday": {},
"collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} =2..4{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}",
"collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} few{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}",
"@collectionCopySuccessFeedback": {
"placeholders": {
"count": {}
}
},
"collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} =2..4{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}",
"collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} few{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}",
"@collectionEditSuccessFeedback": {
"placeholders": {
"count": {}
@ -705,8 +705,6 @@
"@sortByItemCount": {},
"sortBySize": "Według rozmiaru",
"@sortBySize": {},
"createAlbumTooltip": "Utwórz album",
"@createAlbumTooltip": {},
"albumEmpty": "Brak albumów",
"@albumEmpty": {},
"renameEntrySetPageTitle": "Zmień nazwę",
@ -815,7 +813,7 @@
"@albumGroupType": {},
"renameEntrySetPagePreviewSectionTitle": "Podgląd",
"@renameEntrySetPagePreviewSectionTitle": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} =2..4{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} few{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -903,7 +901,7 @@
"count": {}
}
},
"collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} =2..4{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}",
"collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} few{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}",
"@collectionMoveSuccessFeedback": {
"placeholders": {
"count": {}
@ -1319,7 +1317,7 @@
"@settingsStorageAccessPageTitle": {},
"settingsStorageAccessBanner": "Niektóre katalogi wymagają jawnego udzielenia dostępu, aby modyfikować znajdujące się w nich pliki. Możesz przejrzeć tutaj katalogi, do których wcześniej udzielono dostępu.",
"@settingsStorageAccessBanner": {},
"statsWithGps": "{count, plural, =1{1 element z położeniem} =2..4{{count} elementy z położeniem} other{{count} elementów z położeniem}}",
"statsWithGps": "{count, plural, =1{1 element z położeniem} few{{count} elementy z położeniem} other{{count} elementów z położeniem}}",
"@statsWithGps": {
"placeholders": {
"count": {}
@ -1368,5 +1366,45 @@
"tooManyItemsErrorDialogMessage": "Spróbuj ponownie z mniejszą ilością elementów.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Przesuń palcem w górę lub w dół, aby dostosować jasność/głośność",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"chipActionLock": "Zablokuj",
"@chipActionLock": {},
"albumTierVaults": "Skarbce",
"@albumTierVaults": {},
"chipActionConfigureVault": "Konfiguruj Skarbiec",
"@chipActionConfigureVault": {},
"vaultLockTypePassword": "Hasło",
"@vaultLockTypePassword": {},
"newVaultDialogTitle": "Nowy Skarbiec",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Konfiguruj Skarbiec",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Blokada po wyłączeniu ekranu",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Rodzaj blokady",
"@vaultDialogLockTypeLabel": {},
"pinDialogConfirm": "Potwierdź kod PIN",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Wpisz hasło",
"@passwordDialogEnter": {},
"pinDialogEnter": "Wpisz kod PIN",
"@pinDialogEnter": {},
"passwordDialogConfirm": "Potwierdź hasło",
"@passwordDialogConfirm": {},
"authenticateToUnlockVault": "Uwierzytelnij się, aby odblokować skarbiec",
"@authenticateToUnlockVault": {},
"authenticateToConfigureVault": "Uwierzytelnij się, aby skonfigurować Skarbiec",
"@authenticateToConfigureVault": {},
"settingsConfirmationVaultDataLoss": "Pokaż ostrzeżenie o utracie danych skarbca",
"@settingsConfirmationVaultDataLoss": {},
"settingsDisablingBinWarningDialogMessage": "Elementy znajdujące się w koszu zostaną usunięte na zawsze.",
"@settingsDisablingBinWarningDialogMessage": {},
"chipActionCreateVault": "Utwórz skarbiec",
"@chipActionCreateVault": {},
"newVaultWarningDialogMessage": "Elementy w skarbcach są dostępne tylko dla tej aplikacji i żadnej innej.\n\nJeśli odinstalujesz tę aplikację lub wyczyścisz dane tej aplikacji, stracisz wszystkie te elementy.",
"@newVaultWarningDialogMessage": {},
"vaultBinUsageDialogMessage": "Niektóre skarbce korzystają z kosza.",
"@vaultBinUsageDialogMessage": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {}
}

View file

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

View file

@ -152,8 +152,8 @@
"@videoActionSkip10": {},
"videoActionSelectStreams": "Selectați piese",
"@videoActionSelectStreams": {},
"videoActionSettings": "Setări",
"@videoActionSettings": {},
"viewerActionSettings": "Setări",
"@viewerActionSettings": {},
"slideshowActionResume": "Reluare",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Afișați în colecție",
@ -743,8 +743,6 @@
"@sortByRating": {},
"viewerInfoLabelUri": "URI",
"@viewerInfoLabelUri": {},
"createAlbumTooltip": "Creați un album",
"@createAlbumTooltip": {},
"viewerInfoLabelDescription": "Descriere",
"@viewerInfoLabelDescription": {},
"settingsThumbnailOverlayPageTitle": "Suprapunere",
@ -1366,5 +1364,47 @@
"settingsDisplayUseTvInterface": "Interfață Android TV",
"@settingsDisplayUseTvInterface": {},
"tooManyItemsErrorDialogMessage": "Încearcă din nou cu mai puține elemente.",
"@tooManyItemsErrorDialogMessage": {}
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Glisați în sus sau în jos pentru a regla luminozitatea/volumul",
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"chipActionLock": "Blocare",
"@chipActionLock": {},
"chipActionCreateVault": "Creare seif",
"@chipActionCreateVault": {},
"chipActionConfigureVault": "Configurare seif",
"@chipActionConfigureVault": {},
"albumTierVaults": "Seifuri",
"@albumTierVaults": {},
"newVaultDialogTitle": "Seif nou",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Configurare seif",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Blocare atunci când ecranul se oprește",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Tip de blocare",
"@vaultDialogLockTypeLabel": {},
"pinDialogConfirm": "Confirmă codul PIN",
"@pinDialogConfirm": {},
"pinDialogEnter": "Introdu codul PIN",
"@pinDialogEnter": {},
"passwordDialogEnter": "Introdu parola",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Confirmă parola",
"@passwordDialogConfirm": {},
"authenticateToConfigureVault": "Autentifică-te pentru a configura seiful",
"@authenticateToConfigureVault": {},
"authenticateToUnlockVault": "Autentifică-te pentru a debloca seiful",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Unele seifuri folosesc coșul de reciclare.",
"@vaultBinUsageDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "Articolele din coșul de reciclare vor fi șterse pentru totdeauna.",
"@settingsDisablingBinWarningDialogMessage": {},
"vaultLockTypePin": "Pin",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "Parolă",
"@vaultLockTypePassword": {},
"newVaultWarningDialogMessage": "Elementele din seifuri sunt disponibile doar pentru această aplicație și nu pentru altele.\n\nDacă dezinstalezi această aplicație sau ștergi datele acestei aplicații, vei pierde toate aceste elemente.",
"@newVaultWarningDialogMessage": {},
"settingsConfirmationVaultDataLoss": "Afișare avertisment privind pierderile de date din seif",
"@settingsConfirmationVaultDataLoss": {}
}

View file

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

456
lib/l10n/app_sk.arb Normal file
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": {},
"slideshowActionShowInCollection": "แสดงคอลเลกชัน",
"@slideshowActionShowInCollection": {},
"videoActionSettings": "ตั้งค่า",
"@videoActionSettings": {},
"viewerActionSettings": "ตั้งค่า",
"@viewerActionSettings": {},
"slideshowActionResume": "เล่นต่อ",
"@slideshowActionResume": {},
"entryInfoActionEditTitleDescription": "แก้ไขชื่อและคำบรรยาย",

View file

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

View file

@ -126,8 +126,8 @@
"@videoActionSkip10": {},
"videoActionSelectStreams": "Вибрати доріжку",
"@videoActionSelectStreams": {},
"videoActionSettings": "Налаштування",
"@videoActionSettings": {},
"viewerActionSettings": "Налаштування",
"@viewerActionSettings": {},
"slideshowActionResume": "Продовжити",
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Показати у Колекції",
@ -885,8 +885,6 @@
"@albumPageTitle": {},
"albumEmpty": "Немає альбомів",
"@albumEmpty": {},
"createAlbumTooltip": "Створити альбом",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "СТВОРИТИ",
"@createAlbumButtonLabel": {},
"countryPageTitle": "Країни",
@ -1368,5 +1366,45 @@
"tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.",
"@tooManyItemsErrorDialogMessage": {},
"settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність",
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
"chipActionConfigureVault": "Налаштувати сховище",
"@chipActionConfigureVault": {},
"vaultLockTypePassword": "Пароль",
"@vaultLockTypePassword": {},
"newVaultDialogTitle": "Нове сховище",
"@newVaultDialogTitle": {},
"configureVaultDialogTitle": "Налаштування сховища",
"@configureVaultDialogTitle": {},
"vaultDialogLockModeWhenScreenOff": "Заблокувати, коли екран вимикається",
"@vaultDialogLockModeWhenScreenOff": {},
"vaultDialogLockTypeLabel": "Тип блокування",
"@vaultDialogLockTypeLabel": {},
"pinDialogConfirm": "Підтвердити пін-код",
"@pinDialogConfirm": {},
"passwordDialogEnter": "Введіть пароль",
"@passwordDialogEnter": {},
"passwordDialogConfirm": "Підтвердити пароль",
"@passwordDialogConfirm": {},
"authenticateToUnlockVault": "Пройдіть автентифікацію, щоб розблокувати сховище",
"@authenticateToUnlockVault": {},
"settingsConfirmationVaultDataLoss": "Показувати попередження про втрату даних сховища",
"@settingsConfirmationVaultDataLoss": {},
"chipActionLock": "Заблокувати",
"@chipActionLock": {},
"chipActionCreateVault": "Створити сховище",
"@chipActionCreateVault": {},
"newVaultWarningDialogMessage": "Елементи у сховищах доступні лише для цього додатка і ні для кого іншого.\n\nЯкщо ви видалите цю програму або очистите дані програми, ви втратите всі ці елементи.",
"@newVaultWarningDialogMessage": {},
"vaultLockTypePin": "Пін-код",
"@vaultLockTypePin": {},
"albumTierVaults": "Сховища",
"@albumTierVaults": {},
"authenticateToConfigureVault": "Пройдіть автентифікацію, щоб налаштувати сховище",
"@authenticateToConfigureVault": {},
"vaultBinUsageDialogMessage": "У деяких сховищах використовується кошик.",
"@vaultBinUsageDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "Елементи в кошику буде видалено назавжди.",
"@settingsDisablingBinWarningDialogMessage": {},
"pinDialogEnter": "Введіть пін-код",
"@pinDialogEnter": {}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -10,6 +10,7 @@ class MetadataDbUpgrader {
static const addressTable = SqfliteMetadataDb.addressTable;
static const favouriteTable = SqfliteMetadataDb.favouriteTable;
static const coverTable = SqfliteMetadataDb.coverTable;
static const vaultTable = SqfliteMetadataDb.vaultTable;
static const trashTable = SqfliteMetadataDb.trashTable;
static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable;
@ -45,6 +46,9 @@ class MetadataDbUpgrader {
case 9:
await _upgradeFrom9(db);
break;
case 10:
await _upgradeFrom10(db);
break;
}
oldVersion++;
}
@ -370,4 +374,17 @@ class MetadataDbUpgrader {
});
await batch.commit(noResult: true);
}
static Future<void> _upgradeFrom10(Database db) async {
debugPrint('upgrading DB from v10');
await db.execute('ALTER TABLE $entryTable ADD COLUMN origin INTEGER DEFAULT 0;');
await db.execute('CREATE TABLE $vaultTable('
'name TEXT PRIMARY KEY'
', autoLock INTEGER'
', useBin INTEGER'
', lockType TEXT'
')');
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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