Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-07-17 22:39:17 +02:00
commit d813a61b9b
100 changed files with 906 additions and 485 deletions

View file

@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.6"></a>[v1.11.6] - 2024-07-17
### Added
- Explorer: set custom path as home
- Explorer: create shortcut to custom path
- predictive back support (inter-app)
### Changed
- target Android 15 (API 35)
### Fixed
- crash when cataloguing some PNG files
## <a id="v1.11.5"></a>[v1.11.5] - 2024-07-11
### Added

View file

@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace 'deckers.thibault.aves'
compileSdk 34
compileSdk 35
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
ndkVersion '26.1.10909125'
@ -66,7 +66,7 @@ android {
defaultConfig {
applicationId packageName
minSdk flutter.minSdkVersion
targetSdk 34
targetSdk 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
@ -211,6 +211,7 @@ dependencies {
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c'
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c'
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'

View file

@ -14,10 +14,6 @@
android:name="android.software.leanback"
android:required="false" />
<!--
TODO TLAD [Android 14 (API 34)] request/handle READ_MEDIA_VISUAL_USER_SELECTED permission
cf https://developer.android.com/about/versions/14/changes/partial-photo-video-access
-->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
@ -35,10 +31,13 @@
<!-- to access media with original metadata with scoped storage (API >=29) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- to provide a foreground service type, as required by Android 14 (API 34) -->
<!-- TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_MEDIA_PROCESSING` -->
<!-- to provide a foreground service type, as required from Android 14 (API 34) -->
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
android:maxSdkVersion="34"
tools:ignore="SystemPermissionTypo" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
tools:ignore="SystemPermissionTypo" />
<!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode -->
<uses-permission android:name="android.permission.INTERNET" />
@ -103,17 +102,12 @@
</intent>
</queries>
<!--
as of Flutter v3.16.0, predictive back gesture does not work
as expected when extending `FlutterFragmentActivity`
so we disable `enableOnBackInvokedCallback`
-->
<application
android:allowBackup="true"
android:appCategory="image"
android:banner="@drawable/banner"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="false"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/full_backup_content"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
@ -261,11 +255,14 @@
</intent-filter>
</receiver>
<!-- anonymous service for analysis worker is specified here to provide service type -->
<!-- TODO TLAD [Android 15 (API 35)] use `mediaProcessing` -->
<!--
anonymous service for analysis worker is specified here to provide service type:
- `dataSync` for Android 14 (API 34)
- `mediaProcessing` from Android 15 (API 35)
-->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:foregroundServiceType="dataSync|mediaProcessing"
tools:node="merge" />
<service

View file

@ -70,7 +70,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
private fun onStart() {
Log.i(LOG_TAG, "Start analysis worker $id")
runBlocking {
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) {
flutterEngine = it
}
}
@ -78,14 +78,15 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
try {
initChannels(applicationContext)
val preferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
val entryIdStrings = preferences.getStringSet(PREF_ENTRY_IDS_KEY, null)
runBlocking {
FlutterUtils.runOnUiThread {
backgroundChannel?.invokeMethod(
"start", hashMapOf(
"entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(),
"entryIds" to entryIdStrings?.map { Integer.parseUnsignedInt(it) }?.toList(),
"force" to inputData.getBoolean(KEY_FORCE, false),
"progressTotal" to inputData.getInt(KEY_PROGRESS_TOTAL, 0),
"progressOffset" to inputData.getInt(KEY_PROGRESS_OFFSET, 0),
)
)
}
@ -179,13 +180,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
.setContentIntent(openAppIntent)
.addAction(stopAction)
.build()
return if (Build.VERSION.SDK_INT >= 34) {
// from Android 14 (API 34), foreground service type is mandatory
// despite the sample code omitting it at:
return if (Build.VERSION.SDK_INT == 34) {
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
// https://developer.android.com/guide/background/persistent/how-to/long-running
// TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING`
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
ForegroundInfo(NOTIFICATION_ID, notification, type)
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else if (Build.VERSION.SDK_INT >= 35) {
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
} else {
ForegroundInfo(NOTIFICATION_ID, notification)
}
@ -195,14 +195,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
const val SHARED_PREFERENCES_KEY = "analysis_service"
const val CALLBACK_HANDLE_KEY = "callback_handle"
const val PREF_CALLBACK_HANDLE_KEY = "callback_handle"
const val PREF_ENTRY_IDS_KEY = "entry_ids"
const val NOTIFICATION_CHANNEL = "analysis"
const val NOTIFICATION_ID = 1
const val KEY_ENTRY_IDS = "entry_ids"
const val KEY_FORCE = "force"
const val KEY_PROGRESS_TOTAL = "progress_total"
const val KEY_PROGRESS_OFFSET = "progress_offset"
}
}

View file

@ -42,7 +42,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
val pendingResult = goAsync()
defaultScope.launch() {
defaultScope.launch {
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)

View file

@ -54,7 +54,7 @@ import deckers.thibault.aves.channel.streams.SettingsChangeStreamHandler
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
open class MainActivity : FlutterFragmentActivity() {
open class MainActivity : FlutterActivity() {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
@ -294,11 +294,9 @@ open class MainActivity : FlutterFragmentActivity() {
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
fields[INTENT_DATA_KEY_SAFE_MODE] = true
}
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
val filters = extractFiltersFromIntent(intent)
fields[INTENT_DATA_KEY_PAGE] = page
fields[INTENT_DATA_KEY_FILTERS] = filters
}
fields[INTENT_DATA_KEY_PAGE] = intent.getStringExtra(EXTRA_KEY_PAGE)
fields[INTENT_DATA_KEY_FILTERS] = extractFiltersFromIntent(intent)
fields[INTENT_DATA_KEY_EXPLORER_PATH] = intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH)
return fields
}
@ -527,6 +525,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness"
const val INTENT_DATA_KEY_EXPLORER_PATH = "explorerPath"
const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page"
@ -537,6 +536,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_EXPLORER_PATH = "explorerPath"
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_SAFE_MODE = "safeMode"

View file

@ -1,15 +1,14 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.workDataOf
import deckers.thibault.aves.AnalysisWorker
import deckers.thibault.aves.utils.FlutterUtils
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
@ -19,7 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
class AnalysisHandler(private val activity: FlutterActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -37,10 +36,11 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
return
}
activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.edit()
.putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle)
.apply()
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
with(preferences.edit()) {
putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
apply()
}
result.success(true)
}
@ -53,33 +53,24 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
// can be null or empty
val allEntryIds = call.argument<List<Int>>("entryIds")
val progressTotal = allEntryIds?.size ?: 0
var progressOffset = 0
// work `Data` cannot occupy more than 10240 bytes when serialized
// so we split it when we have a long list of entry IDs
val chunked = allEntryIds?.chunked(WORK_DATA_CHUNK_SIZE) ?: listOf(null)
fun buildRequest(entryIds: List<Int>?, progressOffset: Int): OneTimeWorkRequest {
val workData = workDataOf(
AnalysisWorker.KEY_ENTRY_IDS to entryIds?.toIntArray(),
AnalysisWorker.KEY_FORCE to force,
AnalysisWorker.KEY_PROGRESS_TOTAL to progressTotal,
AnalysisWorker.KEY_PROGRESS_OFFSET to progressOffset,
)
return OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build()
// so we save the possibly long list of entry IDs to shared preferences
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
with(preferences.edit()) {
putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet())
apply()
}
var work = WorkManager.getInstance(activity).beginUniqueWork(
val workData = workDataOf(
AnalysisWorker.KEY_FORCE to force,
)
WorkManager.getInstance(activity).beginUniqueWork(
ANALYSIS_WORK_NAME,
ExistingWorkPolicy.KEEP,
buildRequest(chunked.first(), progressOffset),
)
chunked.drop(1).forEach { entryIds ->
progressOffset += WORK_DATA_CHUNK_SIZE
work = work.then(buildRequest(entryIds, progressOffset))
}
work.enqueue()
OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build(),
).enqueue()
attachToActivity()
result.success(null)
@ -105,6 +96,5 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
companion object {
const val CHANNEL = "deckers.thibault/aves/analysis"
private const val ANALYSIS_WORK_NAME = "analysis_work"
private const val WORK_DATA_CHUNK_SIZE = 1000
}
}

View file

@ -19,6 +19,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
@ -351,8 +352,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters")
val explorerPath = call.argument<String>("explorerPath")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (label == null || (filters == null && uri == null)) {
if (label == null) {
result.error("pin-args", "missing arguments", null)
return
}
@ -380,7 +382,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
val intent = when {
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
@ -388,6 +389,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/explorer")
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
else -> {
result.error("pin-intent", "failed to build intent", null)
return

View file

@ -12,7 +12,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper

View file

@ -28,10 +28,11 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
return
}
context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.edit()
.putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
.apply()
val preferences = context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
with(preferences.edit()) {
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
apply()
}
result.success(true)
}

View file

@ -6,7 +6,7 @@ import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory

View file

@ -44,7 +44,8 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
return
}
with(getStore().edit()) {
val preferences = getStore()
with(preferences.edit()) {
when (value) {
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)

View file

@ -1,10 +1,10 @@
package deckers.thibault.aves.channel.streams
import android.app.Activity
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.fragment.app.FragmentActivity
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap
@ -21,9 +21,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
class ImageOpStreamHandler(private val activity: FragmentActivity, private val arguments: Any?) : EventChannel.StreamHandler {
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var eventSink: EventSink
private lateinit var handler: Handler

View file

@ -21,11 +21,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private var knownEntries: Map<Long?, Int?>? = null
private var directory: String? = null
private var safe: Boolean = false
init {
if (arguments is Map<*, *>) {
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
directory = arguments["directory"] as String?
safe = arguments.getOrDefault("safe", false) as Boolean
}
}
@ -59,7 +61,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
}
private fun fetchAll() {
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) }
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) }
endOfStream()
}

View file

@ -152,12 +152,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
@RequiresApi(Build.VERSION_CODES.P)
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
preferredConfig = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
// for wide-gamut and HDR content which does not require alpha blending
setPreferredConfig(Bitmap.Config.RGBA_1010102)
Bitmap.Config.RGBA_1010102
} else {
setPreferredConfig(Bitmap.Config.ARGB_8888)
Bitmap.Config.ARGB_8888
}
}

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.metadata
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.lang.Rational
import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase

View file

@ -2,7 +2,7 @@ package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils

View file

@ -9,7 +9,7 @@ import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.metadata.exif.ExifDirectoryBase

View file

@ -29,7 +29,6 @@ import deckers.thibault.aves.metadata.GeoTiffKeys
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MemoryUtils
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InputStream

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory
@ -116,8 +116,8 @@ class SourceEntry {
// metadata retrieval
// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
fun fillPreCatalogMetadata(context: Context): SourceEntry {
if (isSvg) return this
fun fillPreCatalogMetadata(context: Context, safe: Boolean): SourceEntry {
if (isSvg || safe) return this
if (isVideo) {
fillVideoByMediaMetadataRetriever(context)
if (isSized && hasDuration) return this

View file

@ -52,7 +52,7 @@ internal class FileImageProvider : ImageProvider() {
callback.onFailure(e)
}
}
entry.fillPreCatalogMetadata(context)
entry.fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())

View file

@ -11,8 +11,7 @@ import android.net.Uri
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.FragmentActivity
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -196,7 +195,7 @@ abstract class ImageProvider {
}
suspend fun convertMultiple(
activity: FragmentActivity,
activity: Activity,
imageExportMimeType: String,
targetDir: String,
entries: List<AvesEntry>,
@ -255,7 +254,7 @@ abstract class ImageProvider {
}
private suspend fun convertSingle(
activity: FragmentActivity,
activity: Activity,
sourceEntry: AvesEntry,
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
@ -334,7 +333,7 @@ abstract class ImageProvider {
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
target = Glide.with(activity)
target = Glide.with(activity.applicationContext)
.asBitmap()
.apply(glideOptions)
.load(model)
@ -396,7 +395,7 @@ abstract class ImageProvider {
return newFields
} finally {
// clearing Glide target should happen after effectively writing the bitmap
Glide.with(activity).clear(target)
Glide.with(activity.applicationContext).clear(target)
resolution.replacementFile?.delete()
}

View file

@ -51,8 +51,10 @@ class MediaStoreImageProvider : ImageProvider() {
context: Context,
knownEntries: Map<Long?, Int?>,
directory: String?,
safe: Boolean,
handleNewEntry: NewEntryHandler,
) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory safe=$safe")
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
@ -82,8 +84,8 @@ class MediaStoreImageProvider : ImageProvider() {
} else {
handleNew = handleNewEntry
}
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs)
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs, safe = safe)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs, safe = safe)
}
// the provided URI can point to the wrong media collection,
@ -206,6 +208,7 @@ class MediaStoreImageProvider : ImageProvider() {
selection: String? = null,
selectionArgs: Array<String>? = null,
fileMimeType: String? = null,
safe: Boolean = false,
): Boolean {
var found = false
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
@ -299,7 +302,7 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe)
entryMap = entry.toMap()
}

View file

@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() {
return
}
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())
} else {

View file

@ -5,5 +5,5 @@ import kotlin.math.pow
object MathUtils {
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
}

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.decoder.MultiPageImage
object MimeTypes {
@ -17,8 +17,8 @@ object MimeTypes {
private const val ICO = "image/x-icon"
const val JPEG = "image/jpeg"
const val PNG = "image/png"
const val PSD_VND = "image/vnd.adobe.photoshop"
const val PSD_X = "image/x-photoshop"
private const val PSD_VND = "image/vnd.adobe.photoshop"
private const val PSD_X = "image/x-photoshop"
const val TIFF = "image/tiff"
private const val WBMP = "image/vnd.wap.wbmp"
const val WEBP = "image/webp"

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="app_widget_label">Foto Lijstje</string>
<string name="app_widget_label">Fotolijst</string>
<string name="wallpaper">Achtergrond</string>
<string name="search_shortcut_short_label">Zoeken</string>
<string name="videos_shortcut_short_label">Videos</string>
<string name="analysis_channel_name">Media indexeren</string>
<string name="analysis_notification_default_title">Indexeren van media</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="analysis_notification_action_stop">Stoppen</string>
<string name="safe_mode_shortcut_short_label">Veilige modus</string>
</resources>

View file

@ -16,12 +16,12 @@
package androidx.exifinterface.media;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly;
import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
@ -41,8 +41,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl;
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -84,6 +84,7 @@ import java.util.zip.CRC32;
/*
* Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21
* Named differently to let ExifInterface be loaded as subdependency.
*/
/**
@ -97,7 +98,7 @@ import java.util.zip.CRC32;
* it. This class will search both locations for XMP data, but if XMP data exist both inside and
* outside Exif, will favor the XMP data inside Exif over the one outside.
*/
public class ExifInterface {
public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
@ -3949,7 +3950,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
public ExifInterface(@NonNull File file) throws IOException {
public ExifInterfaceFork(@NonNull File file) throws IOException {
if (file == null) {
throw new NullPointerException("file cannot be null");
}
@ -3964,7 +3965,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
public ExifInterface(@NonNull String filename) throws IOException {
public ExifInterfaceFork(@NonNull String filename) throws IOException {
if (filename == null) {
throw new NullPointerException("filename cannot be null");
}
@ -3980,7 +3981,7 @@ public class ExifInterface {
* @throws NullPointerException if file descriptor is null
* @throws IOException if an error occurs while duplicating the file descriptor.
*/
public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException {
public ExifInterfaceFork(@NonNull FileDescriptor fileDescriptor) throws IOException {
if (fileDescriptor == null) {
throw new NullPointerException("fileDescriptor cannot be null");
}
@ -4023,7 +4024,7 @@ public class ExifInterface {
* @param inputStream the input stream that contains the image data
* @throws NullPointerException if the input stream is null
*/
public ExifInterface(@NonNull InputStream inputStream) throws IOException {
public ExifInterfaceFork(@NonNull InputStream inputStream) throws IOException {
this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA);
}
@ -4039,7 +4040,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType)
public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType)
throws IOException {
if (inputStream == null) {
throw new NullPointerException("inputStream cannot be null");
@ -5071,7 +5072,7 @@ public class ExifInterface {
if (location == null) {
return;
}
setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setLatLong(location.getLatitude(), location.getLongitude());
setAltitude(location.getAltitude());
// Location objects store speeds in m/sec. Translates it to km/hr here.
@ -5080,8 +5081,8 @@ public class ExifInterface {
* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterfaceFork.TAG_GPS_TIMESTAMP, dateTime[1]);
}
/**
@ -5158,11 +5159,11 @@ public class ExifInterface {
}
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME} value as number of milliseconds since
* Jan. 1, 1970, midnight local time.
*
* <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME}.
*
* @return null if date time information is unavailable or invalid.
*/
@ -5175,11 +5176,11 @@ public class ExifInterface {
}
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_DIGITIZED} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
*
* <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_DIGITIZED}.
*
* @return null if digitized date time information is unavailable or invalid.
*/
@ -5192,11 +5193,11 @@ public class ExifInterface {
}
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_ORIGINAL} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
*
* <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_ORIGINAL}.
*
* @return null if original date time information is unavailable or invalid.
*/
@ -5910,18 +5911,18 @@ public class ExifInterface {
}
if (rotation != null) {
int orientation = ExifInterface.ORIENTATION_NORMAL;
int orientation = ExifInterfaceFork.ORIENTATION_NORMAL;
// all rotation angles in CW
switch (Integer.parseInt(rotation)) {
case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90;
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90;
break;
case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180;
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180;
break;
case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270;
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270;
break;
}
@ -6175,7 +6176,11 @@ public class ExifInterface {
// IEND marks the end of the image.
break;
} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
// TODO: Need to handle potential OutOfMemoryError
// TLAD start
if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous exif chunk size=" + length);
}
// TLAD end
byte[] data = new byte[length];
in.readFully(data);
@ -6976,9 +6981,11 @@ public class ExifInterface {
}
final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
// TLAD start
if (byteCount > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous attribute size=" + byteCount);
}
// TLAD end
final byte[] bytes = new byte[(int) byteCount];
dataInputStream.readFully(bytes);
ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,

View file

@ -32,10 +32,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class ExifInterfaceUtils {
class ExifInterfaceUtilsFork {
private static final String TAG = "ExifInterfaceUtils";
private ExifInterfaceUtils() {
private ExifInterfaceUtilsFork() {
// Prevent instantiation
}
/**

View file

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

View file

@ -0,0 +1,4 @@
In v1.11.6:
- explore your collection with the... explorer
- convert your motion photos to stills in bulk
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.11.6:
- explore your collection with the... explorer
- convert your motion photos to stills in bulk
Full changelog available on GitHub

View file

@ -1,4 +1,4 @@
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
<i>Aves</i> आपके ठेठ JPEGs और MP4s सम्मिलित करते हुए, लगभग सभी प्रकार के Photos और Videos को सम्भाल सकता है, साथ के साथ यह <b>multi-page TIFFs, SVGs, old AVIs और भी बहुत कुछ संभालता है </b>! यह आपके Media संग्रह की जाँच करता है, ताकि यह <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, और <b>GeoTIFF</b> files की पहचान कर सके ।
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.

View file

@ -1,5 +1,5 @@
<i>Aves</i> kan allerlei soorten afbeeldingen en video's aan, waaronder de typische JPEG's en MP4's, maar ook minder gangbare formaten zoals <b>multi-pagina TIFF's, SVG's, oude AVI's en meer</b>! Het scant uw media collectie om <b>bewegende foto's</b>, <b>panorama's</b>, <b>360° video's</b>, evenals <b>GeoTIFF</b> bestanden te herkennen.
<i>Aves</i> kan allerlei soorten afbeeldingen en video's aan, waaronder de veelgebruikte JPEG's en MP4's, maar ook minder gangbare formaten zoals <b>multi-pagina TIFF's, SVG's, oude AVI's en meer</b>! Het scant jouw mediacollectie om <b>bewegende foto's</b>, <b>panorama's</b>, <b>360° video's</b>, evenals <b>GeoTIFF</b>-bestanden te herkennen.
<b>Navigatie en zoeken</b> is een belangrijk onderdeel van <i>Aves</i>. Het doel is dat gebruikers gemakkelijk van albums naar foto's naar tags naar kaarten enz. kunnen gaan.
<b>Navigatie en zoeken</b> is een belangrijk onderdeel van <i>Aves</i>. Het doel is dat gebruikers eenvoudig kunnen wisselen van albums naar foto's naar labels naar kaarten enz.
<i>Aves</i> integrates with Android (from KitKat to Android 14, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
<i>Aves</i> integreert met Android (van KitKat t/m Android 14, inclusief Android TV) met functies zoals <b>widgets</b>, <b>app-snelkoppelingen</b>, <b>screensaver</b> en <b>algemene zoekopdrachten</b>. Het werkt ook als een <b>mediaviewer en -kiezer</b>.

View file

@ -1519,8 +1519,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "تعيين كخلفية",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "مجموعة مخصصة",
"@setHomeCustomCollection": {},
"videoActionABRepeat": "تكرار A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",

View file

@ -1517,8 +1517,6 @@
},
"collectionActionSetHome": "Усталяваць як галоўную",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Уласная калекцыя",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
"@settingsThumbnailShowHdrIcon": {},
"videoRepeatActionSetEnd": "Усталяваць канец",

View file

@ -1467,8 +1467,6 @@
"@tagPlaceholderState": {},
"tagPlaceholderPlace": "Lloc",
"@tagPlaceholderPlace": {},
"setHomeCustomCollection": "Coŀlecció personalitzada",
"@setHomeCustomCollection": {},
"settingsConfirmationBeforeMoveToBinItems": "Pregunta abans de moure elements a la paperera de reciclatge",
"@settingsConfirmationBeforeMoveToBinItems": {},
"settingsNavigationDrawerBanner": "Mantén premut per moure i reordenar els elements del menú.",

View file

@ -1515,8 +1515,6 @@
"@entryActionCast": {},
"castDialogTitle": "Zařízení pro promítání",
"@castDialogTitle": {},
"setHomeCustomCollection": "Vlastní sbírka",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Zobrazit ikonu HDR",
"@settingsThumbnailShowHdrIcon": {},
"settingsForceWesternArabicNumeralsTile": "Vynutit arabské číslice",

View file

@ -1355,8 +1355,6 @@
"@overlayHistogramNone": {},
"collectionActionSetHome": "Als Startseite setzen",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Benutzerdefinierte Sammlung",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "HDR-Symbol anzeigen",
"@settingsThumbnailShowHdrIcon": {},
"entryActionCast": "Übertragen",

View file

@ -771,6 +771,9 @@
"binPageTitle": "Recycle Bin",
"explorerPageTitle": "Explorer",
"explorerActionSelectStorageVolume": "Select storage",
"selectStorageVolumeDialogTitle": "Select Storage",
"searchCollectionFieldHint": "Search collection",
"searchRecentSectionTitle": "Recent",
@ -804,7 +807,7 @@
"settingsNavigationSectionTitle": "Navigation",
"settingsHomeTile": "Home",
"settingsHomeDialogTitle": "Home",
"setHomeCustomCollection": "Custom collection",
"setHomeCustom": "Custom",
"settingsShowBottomNavigationBar": "Show bottom navigation bar",
"settingsKeepScreenOnTile": "Keep screen on",
"settingsKeepScreenOnDialogTitle": "Keep Screen On",

View file

@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Fijar como inicio",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Colección personalizada",
"@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "Fijar el inicio",
"@videoRepeatActionSetStart": {},
"stopTooltip": "Parar",
@ -1380,5 +1378,11 @@
"explorerPageTitle": "Explorar",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Mostrar en el explorador",
"@chipActionGoToExplorerPage": {}
"@chipActionGoToExplorerPage": {},
"selectStorageVolumeDialogTitle": "Seleccionar almacenamiento",
"@selectStorageVolumeDialogTitle": {},
"setHomeCustom": "Personalizado",
"@setHomeCustom": {},
"explorerActionSelectStorageVolume": "Seleccionar almacenamiento",
"@explorerActionSelectStorageVolume": {}
}

View file

@ -1519,8 +1519,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Ezarri hasiera gisa",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Bilduma pertsonalizatua",
"@setHomeCustomCollection": {},
"renameProcessorHash": "Hash-a",
"@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak",

View file

@ -1099,8 +1099,6 @@
"@settingsSystemDefault": {},
"settingsConfirmationTile": "درخواست های تایید",
"@settingsConfirmationTile": {},
"setHomeCustomCollection": "مجموعه سفارشی",
"@setHomeCustomCollection": {},
"settingsKeepScreenOnDialogTitle": "صفحه را روشن نگه دار",
"@settingsKeepScreenOnDialogTitle": {},
"settingsShowBottomNavigationBar": "نمایش گزینه‌گان پیمایش پایین",

View file

@ -1359,8 +1359,6 @@
"@castDialogTitle": {},
"collectionActionSetHome": "Définir comme page daccueil",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Collection personnalisée",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afficher licône HDR",
"@settingsThumbnailShowHdrIcon": {},
"videoRepeatActionSetEnd": "Définir la fin",
@ -1380,5 +1378,11 @@
"explorerPageTitle": "Explorateur",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Afficher dans Explorateur",
"@chipActionGoToExplorerPage": {}
"@chipActionGoToExplorerPage": {},
"setHomeCustom": "Personnalisé",
"@setHomeCustom": {},
"explorerActionSelectStorageVolume": "Choisir le stockage",
"@explorerActionSelectStorageVolume": {},
"selectStorageVolumeDialogTitle": "Volumes de stockage",
"@selectStorageVolumeDialogTitle": {}
}

View file

@ -41,7 +41,7 @@
"count": {}
}
},
"deleteButtonLabel": "डिलीट",
"deleteButtonLabel": "मिटाए",
"@deleteButtonLabel": {},
"timeMinutes": "{count, plural, other{{count} मिनट}}",
"@timeMinutes": {
@ -88,7 +88,7 @@
"@chipActionGoToTagPage": {},
"resetTooltip": "रिसेट",
"@resetTooltip": {},
"saveTooltip": "सेव करें",
"saveTooltip": "सहेजें",
"@saveTooltip": {},
"pickTooltip": "चुनें",
"@pickTooltip": {},

View file

@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "HDR ikon megjelenítése",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Egyéni gyűjtemény",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Kezdőlapnak beállít",
"@collectionActionSetHome": {},
"stopTooltip": "Állj",

View file

@ -1355,8 +1355,6 @@
"@aboutDataUsageClearCache": {},
"entryActionCast": "Siarkan",
"@entryActionCast": {},
"setHomeCustomCollection": "Koleksi kustom",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Tetapkan sebagai beranda",
"@collectionActionSetHome": {},
"settingsThumbnailShowHdrIcon": "Tampilkan ikon HDR",

View file

@ -1519,8 +1519,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Setja sem upphafsskjá",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Sérsniðið safn",
"@setHomeCustomCollection": {},
"renameProcessorHash": "Tætigildi",
"@renameProcessorHash": {},
"videoRepeatActionSetStart": "Stilla byrjun",

View file

@ -1369,8 +1369,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Imposta come pagina iniziale",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Collezione personalizzata",
"@setHomeCustomCollection": {},
"chipActionShowCollection": "Mostra nella Collezione",
"@chipActionShowCollection": {},
"renameProcessorHash": "Hash",

View file

@ -1357,8 +1357,6 @@
"@overlayHistogramLuminance": {},
"settingsModificationWarningDialogMessage": "他の設定は変更されます。",
"@settingsModificationWarningDialogMessage": {},
"setHomeCustomCollection": "カスタムコレクション",
"@setHomeCustomCollection": {},
"settingsAccessibilityShowPinchGestureAlternatives": "マルチタッチジェスチャーの選択肢を表示する",
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"chipActionCreateVault": "保管庫を作成",

View file

@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "홈으로 설정",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "지정 미디어",
"@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "시작 지점 설정",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "종료 지점 설정",
@ -1380,5 +1378,11 @@
"explorerPageTitle": "탐색기",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "탐색기 페이지에서 보기",
"@chipActionGoToExplorerPage": {}
"@chipActionGoToExplorerPage": {},
"setHomeCustom": "직접 설정",
"@setHomeCustom": {},
"explorerActionSelectStorageVolume": "저장공간 선택",
"@explorerActionSelectStorageVolume": {},
"selectStorageVolumeDialogTitle": "저장공간",
"@selectStorageVolumeDialogTitle": {}
}

View file

@ -101,9 +101,9 @@
"@entryActionRename": {},
"entryActionRestore": "Herstellen",
"@entryActionRestore": {},
"entryActionRotateCCW": "Roteren tegen de klok in",
"entryActionRotateCCW": "Linksom roteren",
"@entryActionRotateCCW": {},
"entryActionRotateCW": "Roteren met de klok mee",
"entryActionRotateCW": "Rechtsom roteren",
"@entryActionRotateCW": {},
"entryActionFlip": "Horizontaal omdraaien",
"@entryActionFlip": {},
@ -163,25 +163,25 @@
"@entryInfoActionEditLocation": {},
"entryInfoActionEditTitleDescription": "Wijzig titel & omschrijving",
"@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Bewerk waardering",
"entryInfoActionEditRating": "Waardering bewerken",
"@entryInfoActionEditRating": {},
"entryInfoActionEditTags": "Bewerk labels",
"entryInfoActionEditTags": "Labels bewerken",
"@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Verwijder metadata",
"@entryInfoActionRemoveMetadata": {},
"filterBinLabel": "Prullenbak",
"@filterBinLabel": {},
"filterFavouriteLabel": "Favorieten",
"filterFavouriteLabel": "Favoriet",
"@filterFavouriteLabel": {},
"filterNoDateLabel": "Geen datum",
"filterNoDateLabel": "Zonder datum",
"@filterNoDateLabel": {},
"filterNoLocationLabel": "Geen locatie",
"filterNoLocationLabel": "Zonder plaats",
"@filterNoLocationLabel": {},
"filterNoRatingLabel": "Geen rating",
"filterNoRatingLabel": "Zonder waardering",
"@filterNoRatingLabel": {},
"filterNoTagLabel": "Geen label",
"filterNoTagLabel": "Zonder label",
"@filterNoTagLabel": {},
"filterNoTitleLabel": "Geen titel",
"filterNoTitleLabel": "Zonder titel",
"@filterNoTitleLabel": {},
"filterOnThisDayLabel": "Op deze dag",
"@filterOnThisDayLabel": {},
@ -347,7 +347,7 @@
"@videoResumeDialogMessage": {},
"videoStartOverButtonLabel": "OPNIEUW BEGINNEN",
"@videoStartOverButtonLabel": {},
"videoResumeButtonLabel": "HERVAT",
"videoResumeButtonLabel": "HERVATTEN",
"@videoResumeButtonLabel": {},
"setCoverDialogLatest": "Laatste item",
"@setCoverDialogLatest": {},
@ -355,7 +355,7 @@
"@setCoverDialogAuto": {},
"setCoverDialogCustom": "Aangepast",
"@setCoverDialogCustom": {},
"hideFilterConfirmationDialogMessage": "Overeenkomende fotos en videos worden verborgen binnen uw verzameling. Je kunt ze opnieuw weergeven via de “Privacy”-instellingen.\n\nWeet je zeker dat je ze wilt verbergen?",
"hideFilterConfirmationDialogMessage": "Overeenkomende fotos en videos worden verborgen binnen jouw verzameling. Je kunt ze opnieuw weergeven via de “Privacy”-instellingen.\n\nWeet je zeker dat je ze wilt verbergen?",
"@hideFilterConfirmationDialogMessage": {},
"newAlbumDialogTitle": "Nieuw Album",
"@newAlbumDialogTitle": {},
@ -423,7 +423,7 @@
"@editEntryLocationDialogLongitude": {},
"locationPickerUseThisLocationButton": "Gebruik deze locatie",
"@locationPickerUseThisLocationButton": {},
"editEntryRatingDialogTitle": "Beoordeling",
"editEntryRatingDialogTitle": "Waardering",
"@editEntryRatingDialogTitle": {},
"removeEntryMetadataDialogTitle": "Verwijderen metadata",
"@removeEntryMetadataDialogTitle": {},
@ -505,11 +505,11 @@
"@aboutBugReportInstruction": {},
"aboutBugReportButton": "Reporteer",
"@aboutBugReportButton": {},
"aboutCreditsSectionTitle": "Credits",
"aboutCreditsSectionTitle": "Dankbetuiging",
"@aboutCreditsSectionTitle": {},
"aboutCreditsWorldAtlas1": "Deze applicatie gebruikt een TopoJSON-bestand van",
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
"aboutCreditsWorldAtlas2": "onder ISC-licentie.",
"@aboutCreditsWorldAtlas2": {},
"aboutTranslatorsSectionTitle": "Vertalers",
"@aboutTranslatorsSectionTitle": {},
@ -525,7 +525,7 @@
"@aboutLicensesFlutterPackagesSectionTitle": {},
"aboutLicensesDartPackagesSectionTitle": "Dart Packages",
"@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "Laat alle licenties zien",
"aboutLicensesShowAllButtonLabel": "Alle licenties tonen",
"@aboutLicensesShowAllButtonLabel": {},
"collectionPageTitle": "Verzameling",
"@collectionPageTitle": {},
@ -615,7 +615,7 @@
"@drawerCollectionAnimated": {},
"drawerCollectionMotionPhotos": "Bewegende fotos",
"@drawerCollectionMotionPhotos": {},
"drawerCollectionPanoramas": "Panoramas",
"drawerCollectionPanoramas": "Panorama's",
"@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Raw fotos",
"@drawerCollectionRaws": {},
@ -637,7 +637,7 @@
"@sortBySize": {},
"sortByAlbumFileName": "Op album- en bestandsnaam",
"@sortByAlbumFileName": {},
"sortByRating": "Op rating",
"sortByRating": "Op waardering",
"@sortByRating": {},
"sortOrderNewestFirst": "Nieuwste eerst",
"@sortOrderNewestFirst": {},
@ -667,7 +667,7 @@
"@albumMimeTypeMixed": {},
"albumPickPageTitleCopy": "Kopieer naar Album",
"@albumPickPageTitleCopy": {},
"albumPickPageTitleExport": "Exporteer naar Album",
"albumPickPageTitleExport": "Exporteren naar Album",
"@albumPickPageTitleExport": {},
"albumPickPageTitleMove": "Verplaats naar Album",
"@albumPickPageTitleMove": {},
@ -715,7 +715,7 @@
"@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Labels",
"@searchTagsSectionTitle": {},
"searchRatingSectionTitle": "Beoordeling",
"searchRatingSectionTitle": "Waarderingen",
"@searchRatingSectionTitle": {},
"searchMetadataSectionTitle": "Metadata",
"@searchMetadataSectionTitle": {},
@ -731,13 +731,13 @@
"@settingsSearchFieldLabel": {},
"settingsSearchEmpty": "Geen instellingen gevonden",
"@settingsSearchEmpty": {},
"settingsActionExport": "Exporteer",
"settingsActionExport": "Exporteren",
"@settingsActionExport": {},
"settingsActionExportDialogTitle": "Exporteer",
"settingsActionExportDialogTitle": "Exporteren",
"@settingsActionExportDialogTitle": {},
"settingsActionImport": "Importeer",
"settingsActionImport": "Importeren",
"@settingsActionImport": {},
"settingsActionImportDialogTitle": "Importeer",
"settingsActionImportDialogTitle": "Importeren",
"@settingsActionImportDialogTitle": {},
"appExportCovers": "Omslagen",
"@appExportCovers": {},
@ -793,13 +793,13 @@
"@settingsThumbnailOverlayPageTitle": {},
"settingsThumbnailShowFavouriteIcon": "Favorieten icoon zichtbaar",
"@settingsThumbnailShowFavouriteIcon": {},
"settingsThumbnailShowTagIcon": "Label icoon zichtbaar",
"settingsThumbnailShowTagIcon": "Label-pictogram tonen",
"@settingsThumbnailShowTagIcon": {},
"settingsThumbnailShowLocationIcon": "Locatie icoon zichtbaar",
"@settingsThumbnailShowLocationIcon": {},
"settingsThumbnailShowMotionPhotoIcon": "Bewegende foto icoon zichtbaar",
"@settingsThumbnailShowMotionPhotoIcon": {},
"settingsThumbnailShowRating": "Rating zichtbaar",
"settingsThumbnailShowRating": "Waardering tonen",
"@settingsThumbnailShowRating": {},
"settingsThumbnailShowRawIcon": "RAW icoon zichtbaar",
"@settingsThumbnailShowRawIcon": {},
@ -865,7 +865,7 @@
"@settingsViewerSlideshowPageTitle": {},
"settingsSlideshowRepeat": "Herhalen",
"@settingsSlideshowRepeat": {},
"settingsSlideshowShuffle": "Shuffle",
"settingsSlideshowShuffle": "Willekeurige volgorde",
"@settingsSlideshowShuffle": {},
"settingsSlideshowFillScreen": "Volledig scherm",
"@settingsSlideshowFillScreen": {},
@ -951,13 +951,13 @@
"@settingsHiddenItemsPageTitle": {},
"settingsHiddenItemsTabFilters": "Verborgen Filters",
"@settingsHiddenItemsTabFilters": {},
"settingsHiddenFiltersBanner": "Fotos en videos die overeenkomen met verborgen filters, worden niet weergegeven in uw verzameling.",
"settingsHiddenFiltersBanner": "Fotos en videos die overeenkomen met verborgen filters, worden niet weergegeven in je verzameling.",
"@settingsHiddenFiltersBanner": {},
"settingsHiddenFiltersEmpty": "Geen verborgen filters",
"@settingsHiddenFiltersEmpty": {},
"settingsHiddenItemsTabPaths": "Verborgen paden",
"@settingsHiddenItemsTabPaths": {},
"settingsHiddenPathsBanner": "Fotos en videos in deze mappen, of een van hun submappen, verschijnen niet in uw verzameling.",
"settingsHiddenPathsBanner": "Fotos en videos in deze mappen, of een van hun submappen, verschijnen niet in je verzameling.",
"@settingsHiddenPathsBanner": {},
"addPathTooltip": "Pad toevoegen",
"@addPathTooltip": {},
@ -965,7 +965,7 @@
"@settingsStorageAccessTile": {},
"settingsStorageAccessPageTitle": "Toegang tot opslag",
"@settingsStorageAccessPageTitle": {},
"settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. U kunt hier directorys bekijken waartoe u eerder toegang heeft verleend.",
"settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. Je kunt hier directorys bekijken waartoe je eerder toegang hebt verleend.",
"@settingsStorageAccessBanner": {},
"settingsStorageAccessEmpty": "Geen toegang verleend",
"@settingsStorageAccessEmpty": {},
@ -1029,7 +1029,7 @@
"@statsTopTagsSectionTitle": {},
"statsTopAlbumsSectionTitle": "Top Albums",
"@statsTopAlbumsSectionTitle": {},
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
"viewerOpenPanoramaButtonLabel": "PANORAMA OPENEN",
"@viewerOpenPanoramaButtonLabel": {},
"viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN",
"@viewerSetWallpaperButtonLabel": {},
@ -1089,7 +1089,7 @@
"@viewerInfoOpenLinkText": {},
"viewerInfoViewXmlLinkText": "Bekijk XML",
"@viewerInfoViewXmlLinkText": {},
"viewerInfoSearchFieldLabel": "Doorzoek metadata",
"viewerInfoSearchFieldLabel": "Metadata doorzoeken",
"@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "Geen overeenkomstige zoeksleutels",
"@viewerInfoSearchEmpty": {},
@ -1105,7 +1105,7 @@
"@viewerInfoSearchSuggestionRights": {},
"wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm",
"@wallpaperUseScrollEffect": {},
"tagEditorPageTitle": "Wijzig Labels",
"tagEditorPageTitle": "Labels bewerken",
"@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Nieuw label",
"@tagEditorPageNewTagFieldLabel": {},
@ -1155,17 +1155,17 @@
"@lengthUnitPercent": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"filterAspectRatioLandscapeLabel": "Landschap",
"filterAspectRatioLandscapeLabel": "Liggend",
"@filterAspectRatioLandscapeLabel": {},
"chipActionCreateVault": "Creëer kluis",
"chipActionCreateVault": "Kluis aanmaken",
"@chipActionCreateVault": {},
"entryInfoActionRemoveLocation": "Verwijder locatie",
"@entryInfoActionRemoveLocation": {},
"chipActionConfigureVault": "Configureer kluis",
"chipActionConfigureVault": "Kluis configureren",
"@chipActionConfigureVault": {},
"filterNoAddressLabel": "Geen adres",
"filterNoAddressLabel": "Zonder adres",
"@filterNoAddressLabel": {},
"filterAspectRatioPortraitLabel": "Portret",
"filterAspectRatioPortraitLabel": "Staand",
"@filterAspectRatioPortraitLabel": {},
"widgetDisplayedItemRandom": "Willekeurige",
"@widgetDisplayedItemRandom": {},
@ -1175,7 +1175,7 @@
"@keepScreenOnVideoPlayback": {},
"settingsVideoEnablePip": "Beeld-in-beeld",
"@settingsVideoEnablePip": {},
"filterTaggedLabel": "Getagd",
"filterTaggedLabel": "Met label",
"@filterTaggedLabel": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
@ -1191,9 +1191,9 @@
"@stopTooltip": {},
"chipActionLock": "Vergrendel",
"@chipActionLock": {},
"chipActionShowCountryStates": "Status laten xien",
"chipActionShowCountryStates": "Status tonen",
"@chipActionShowCountryStates": {},
"chipActionGoToPlacePage": "Laat zien in plaatsen",
"chipActionGoToPlacePage": "In Plaatsen tonen",
"@chipActionGoToPlacePage": {},
"subtitlePositionTop": "Boven",
"@subtitlePositionTop": {},
@ -1227,7 +1227,7 @@
"@aboutDataUsageMisc": {},
"settingsModificationWarningDialogMessage": "Andere instellingen zullen worden aangepast.",
"@settingsModificationWarningDialogMessage": {},
"vaultDialogLockModeWhenScreenOff": "Vergrendel als scherm uitgaat",
"vaultDialogLockModeWhenScreenOff": "Vergrendelen wanneer het scherm wordt uitgeschakeld",
"@vaultDialogLockModeWhenScreenOff": {},
"aboutDataUsageData": "Data",
"@aboutDataUsageData": {},
@ -1269,8 +1269,122 @@
"@maxBrightnessNever": {},
"videoResumptionModeAlways": "Altijd",
"@videoResumptionModeAlways": {},
"exportEntryDialogWriteMetadata": "Schrijf metadata",
"exportEntryDialogWriteMetadata": "Metadata schrijven",
"@exportEntryDialogWriteMetadata": {},
"chipActionShowCollection": "Tonen in Collectie",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"entryActionCast": "Casten",
"@entryActionCast": {},
"videoRepeatActionSetStart": "Start instellen",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Einde instellen",
"@videoRepeatActionSetEnd": {},
"viewerActionUnlock": "Weergave ontgrendelen",
"@viewerActionUnlock": {},
"filterLocatedLabel": "Met Plaats",
"@filterLocatedLabel": {},
"overlayHistogramNone": "Geen",
"@overlayHistogramNone": {},
"authenticateToUnlockVault": "Verifieer om de kluis te ontgrendelen",
"@authenticateToUnlockVault": {},
"vaultBinUsageDialogMessage": "Sommige kluizen gebruiken de prullenbak.",
"@vaultBinUsageDialogMessage": {},
"settingsDisablingBinWarningDialogMessage": "Items in de Prullenbak worden voor altijd verwijderd.",
"@settingsDisablingBinWarningDialogMessage": {},
"statsTopStatesSectionTitle": "Top Staten",
"@statsTopStatesSectionTitle": {},
"editorTransformRotate": "Roteren",
"@editorTransformRotate": {},
"editorActionTransform": "Transformeren",
"@editorActionTransform": {},
"stateEmpty": "Zonder Staten",
"@stateEmpty": {},
"settingsViewerShowRatingTags": "Waardering & labels tonen",
"@settingsViewerShowRatingTags": {},
"drawerPlacePage": "Plaatsen",
"@drawerPlacePage": {},
"newVaultWarningDialogMessage": "Items in kluizen zijn alleen beschikbaar voor deze app en niet voor andere.\n\nAls je deze app verwijdert of deze app-gegevens wist, verlies je al deze items.",
"@newVaultWarningDialogMessage": {},
"vaultDialogLockTypeLabel": "Vergrendelingstype",
"@vaultDialogLockTypeLabel": {},
"tagEditorDiscardDialogMessage": "Wijzigingen ongedaan maken?",
"@tagEditorDiscardDialogMessage": {},
"renameProcessorHash": "Controlenummer",
"@renameProcessorHash": {},
"castDialogTitle": "Cast-apparaten",
"@castDialogTitle": {},
"aboutDataUsageSectionTitle": "Gegevensgebruik",
"@aboutDataUsageSectionTitle": {},
"statePageTitle": "Staten",
"@statePageTitle": {},
"searchStatesSectionTitle": "Staten",
"@searchStatesSectionTitle": {},
"settingsVideoPlaybackTile": "Afspelen",
"@settingsVideoPlaybackTile": {},
"settingsVideoResumptionModeTile": "Afspelen hervatten",
"@settingsVideoResumptionModeTile": {},
"settingsVideoResumptionModeDialogTitle": "Afspelen hervatten",
"@settingsVideoResumptionModeDialogTitle": {},
"settingsVideoBackgroundMode": "Achtergrond-modus",
"@settingsVideoBackgroundMode": {},
"configureVaultDialogTitle": "Kluis configureren",
"@configureVaultDialogTitle": {},
"settingsWidgetDisplayedItem": "Getoond item",
"@settingsWidgetDisplayedItem": {},
"albumTierVaults": "Kluizen",
"@albumTierVaults": {},
"aboutDataUsageClearCache": "Cache wissen",
"@aboutDataUsageClearCache": {},
"placePageTitle": "Plaatsen",
"@placePageTitle": {},
"placeEmpty": "Zonder plaatsen",
"@placeEmpty": {},
"settingsCollectionBurstPatternsNone": "Geen",
"@settingsCollectionBurstPatternsNone": {},
"settingsVideoPlaybackPageTitle": "Afspelen",
"@settingsVideoPlaybackPageTitle": {},
"settingsVideoBackgroundModeDialogTitle": "Achtergrond-modus",
"@settingsVideoBackgroundModeDialogTitle": {},
"settingsCollectionBurstPatternsTile": "Burst-patronen",
"@settingsCollectionBurstPatternsTile": {},
"settingsAccessibilityShowPinchGestureAlternatives": "Alternatieven voor multi-touch-gebaren weergeven",
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"settingsDisplayUseTvInterface": "Android TV-interface",
"@settingsDisplayUseTvInterface": {},
"settingsForceWesternArabicNumeralsTile": "Arabische cijfers forceren",
"@settingsForceWesternArabicNumeralsTile": {},
"explorerPageTitle": "Bestanden",
"@explorerPageTitle": {},
"columnCount": "{count, plural, =1{{count} kolom} other{{count} kolommen}}",
"@columnCount": {
"placeholders": {
"count": {
"format": "decimalPattern"
}
}
},
"widgetTapUpdateWidget": "Widget bijwerken",
"@widgetTapUpdateWidget": {},
"authenticateToConfigureVault": "Verifieer om de kluis te configureren",
"@authenticateToConfigureVault": {},
"settingsConfirmationVaultDataLoss": "Waarschuwing voor verlies van kluisgegevens weergeven",
"@settingsConfirmationVaultDataLoss": {},
"newVaultDialogTitle": "Nieuwe kluis",
"@newVaultDialogTitle": {},
"chipActionGoToExplorerPage": "In Bestanden tonen",
"@chipActionGoToExplorerPage": {},
"cropAspectRatioFree": "Vrij",
"@cropAspectRatioFree": {},
"videoActionABRepeat": "A-B herhalen",
"@videoActionABRepeat": {},
"viewerActionLock": "Weergave vergrendelen",
"@viewerActionLock": {},
"collectionActionSetHome": "Als startpagina instellen",
"@collectionActionSetHome": {},
"setHomeCustom": "Aangepast",
"@setHomeCustom": {},
"explorerActionSelectStorageVolume": "Selecteer opslag",
"@explorerActionSelectStorageVolume": {},
"selectStorageVolumeDialogTitle": "Selecteer opslag",
"@selectStorageVolumeDialogTitle": {}
}

View file

@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Własna kolekcja",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Ustaw jako stronę główną",
"@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Ustaw początek",

View file

@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Definir como início",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Coleção personalizada",
"@setHomeCustomCollection": {},
"videoActionABRepeat": "Repetição A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Definir fim",
@ -1372,5 +1370,13 @@
"videoRepeatActionSetStart": "Definir início",
"@videoRepeatActionSetStart": {},
"chipActionShowCollection": "Mostrar na Coleção",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"renameProcessorHash": "Hash",
"@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "Forçar numerais arábicos",
"@settingsForceWesternArabicNumeralsTile": {},
"chipActionGoToExplorerPage": "Mostrar no Explorador",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Explorador",
"@explorerPageTitle": {}
}

View file

@ -1491,8 +1491,6 @@
"@collectionActionSetHome": {},
"aboutDataUsageClearCache": "Golește memoria cache",
"@aboutDataUsageClearCache": {},
"setHomeCustomCollection": "Colecție personalizată",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR",
"@settingsThumbnailShowHdrIcon": {},
"settingsViewerShowHistogram": "Afișare histogramă",

View file

@ -1359,8 +1359,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Показать значок HDR",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Собственная коллекция",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Установить как главную",
"@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Установить начало",
@ -1380,5 +1378,7 @@
"chipActionGoToExplorerPage": "Показать в проводнике",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Проводник",
"@explorerPageTitle": {}
"@explorerPageTitle": {},
"explorerActionSelectStorageVolume": "Выбрать хранилище",
"@explorerActionSelectStorageVolume": {}
}

View file

@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"collectionActionSetHome": "Nastaviť ako doma",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Kolekcia na mieru",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR",
"@settingsThumbnailShowHdrIcon": {},
"chipActionShowCollection": "Zobraziť v kolekcií",

View file

@ -1321,8 +1321,6 @@
"@passwordDialogConfirm": {},
"collectionActionSetHome": "Ana ekran olarak ayarla",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "Kişisel koleksiyon",
"@setHomeCustomCollection": {},
"statsTopStatesSectionTitle": "Baş Eyaletler",
"@statsTopStatesSectionTitle": {},
"pinDialogEnter": "PIN girin",

View file

@ -1517,8 +1517,6 @@
"@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Показати іконку HDR",
"@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Власна колекція",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Встановити як головну",
"@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Змінити початок",
@ -1538,5 +1536,11 @@
"chipActionGoToExplorerPage": "Показати в провіднику",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Провідник",
"@explorerPageTitle": {}
"@explorerPageTitle": {},
"setHomeCustom": "Власне",
"@setHomeCustom": {},
"explorerActionSelectStorageVolume": "Обрати сховище",
"@explorerActionSelectStorageVolume": {},
"selectStorageVolumeDialogTitle": "Оберіть сховище",
"@selectStorageVolumeDialogTitle": {}
}

View file

@ -1515,8 +1515,6 @@
"@entryActionCast": {},
"castDialogTitle": "Thiết bị truyền",
"@castDialogTitle": {},
"setHomeCustomCollection": "Bộ sưu tập tùy chỉnh",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR",
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Đặt làm nhà",
@ -1534,5 +1532,13 @@
"settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập",
"@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Hiển thị trong Bộ sưu tập",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"selectStorageVolumeDialogTitle": "Chọn dung lượng",
"@selectStorageVolumeDialogTitle": {},
"explorerActionSelectStorageVolume": "Chọn dung lượng",
"@explorerActionSelectStorageVolume": {},
"chipActionGoToExplorerPage": "Hiển thị ở Explorer",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Explorer",
"@explorerPageTitle": {}
}

View file

@ -519,7 +519,7 @@
"@aboutTranslatorsSectionTitle": {},
"aboutLicensesSectionTitle": "开源许可协议",
"@aboutLicensesSectionTitle": {},
"aboutLicensesBanner": "本应用使用以下开源软件包和库",
"aboutLicensesBanner": "本应用使用以下开源软件包和库",
"@aboutLicensesBanner": {},
"aboutLicensesShowAllButtonLabel": "显示所有许可协议",
"@aboutLicensesShowAllButtonLabel": {},
@ -1161,9 +1161,9 @@
"@settingsSubtitleThemeTextPositionTile": {},
"settingsSubtitleThemeTextPositionDialogTitle": "文本位置",
"@settingsSubtitleThemeTextPositionDialogTitle": {},
"aboutLicensesDartPackagesSectionTitle": "Dart Packages",
"aboutLicensesDartPackagesSectionTitle": "Dart 软件包",
"@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesFlutterPackagesSectionTitle": "Flutter Packages",
"aboutLicensesFlutterPackagesSectionTitle": "Flutter 软件包",
"@aboutLicensesFlutterPackagesSectionTitle": {},
"keepScreenOnVideoPlayback": "视频播放期间",
"@keepScreenOnVideoPlayback": {},
@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "设置为首页",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "自定义媒体集",
"@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "设置起点",
"@videoRepeatActionSetStart": {},
"stopTooltip": "停止",

View file

@ -1511,8 +1511,6 @@
"@overlayHistogramLuminance": {},
"collectionActionSetHome": "設為首頁",
"@collectionActionSetHome": {},
"setHomeCustomCollection": "自訂收藏品",
"@setHomeCustomCollection": {},
"aboutDataUsageClearCache": "清除快取",
"@aboutDataUsageClearCache": {},
"settingsViewerShowHistogram": "顯示直方圖",
@ -1534,5 +1532,9 @@
"settingsForceWesternArabicNumeralsTile": "強制使用阿拉伯數字",
"@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "在收藏品中顯示",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"explorerPageTitle": "檔案總管",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "在檔案總管裡顯示",
"@chipActionGoToExplorerPage": {}
}

View file

@ -93,6 +93,8 @@ class Contributors {
Contributor('Maxi', 'maxitendo01@proton.me'),
Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'),
Contributor('elfriob', 'elfriob@ya.ru'),
Contributor('Stephan Paternotte', 'stephan@paternottes.net'),
Contributor('Tung Anh', 'buihuutunganh2007@gmail.com'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
@ -102,6 +104,7 @@ class Contributors {
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
// Contributor('Sartaj', 'ssaarrttaajj111@gmail.com'), // Hindi
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)

View file

@ -14,11 +14,19 @@ mixin NavigationSettings on SettingsAccess {
HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
set homePage(HomePageSetting newValue) => set(SettingKeys.homePageKey, newValue.toString());
Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set homeCustomCollection(Set<CollectionFilter> newValue) => set(SettingKeys.homeCustomCollectionKey, newValue.map((filter) => filter.toJson()).toList());
String? get homeCustomExplorerPath => getString(SettingKeys.homeCustomExplorerPathKey);
void setHome(
HomePageSetting homePage, {
Set<CollectionFilter> customCollection = const {},
String? customExplorerPath,
}) {
set(SettingKeys.homePageKey, homePage.toString());
set(SettingKeys.homeCustomCollectionKey, customCollection.map((filter) => filter.toJson()).toList());
set(SettingKeys.homeCustomExplorerPathKey, customExplorerPath);
}
bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar;

View file

@ -440,6 +440,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
case SettingKeys.maxBrightnessKey:
case SettingKeys.keepScreenOnKey:
case SettingKeys.homePageKey:
case SettingKeys.homeCustomExplorerPathKey:
case SettingKeys.collectionGroupFactorKey:
case SettingKeys.collectionSortFactorKey:
case SettingKeys.thumbnailLocationIconKey:

View file

@ -93,6 +93,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
_rawEntries.forEach((v) => v.dispose());
}
set safeMode(bool enabled);
final EventBus _eventBus = EventBus();
@override

View file

@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource {
final Set<String> _changedUris = {};
int? _lastGeneration;
SourceInitializationState _initState = SourceInitializationState.none;
bool _safeMode = false;
@override
set safeMode(bool enabled) => _safeMode = enabled;
@override
SourceInitializationState get initState => _initState;
@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
analysisController: analysisController,
directory: directory,
loadTopEntriesFirst: loadTopEntriesFirst,
canAnalyze: canAnalyze,
canAnalyze: canAnalyze && !_safeMode,
));
}
@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource {
pendingNewEntries.clear();
}
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
(entry) {
// when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc.

View file

@ -127,21 +127,16 @@ class Analyzer with WidgetsBindingObserver {
Future<void> start(dynamic args) async {
List<int>? entryIds;
var force = false;
var progressTotal = 0, progressOffset = 0;
if (args is Map) {
entryIds = (args['entryIds'] as List?)?.cast<int>();
force = args['force'] ?? false;
progressTotal = args['progressTotal'];
progressOffset = args['progressOffset'];
}
await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries');
_controller?.dispose();
_controller = AnalysisController(
canStartService: false,
entryIds: entryIds,
force: force,
progressTotal: progressTotal,
progressOffset: progressOffset,
);
settings.systemLocalesFallback = await deviceService.getLocales();

View file

@ -30,7 +30,7 @@ abstract class AppService {
Future<bool> shareSingle(String uri, String mimeType);
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri});
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri});
}
class PlatformAppService implements AppService {
@ -203,7 +203,7 @@ class PlatformAppService implements AppService {
// app shortcuts
@override
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async {
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri}) async {
Uint8List? iconBytes;
if (coverEntry != null) {
final size = coverEntry.isVideo ? 0.0 : 256.0;
@ -222,6 +222,7 @@ class PlatformAppService implements AppService {
'label': label,
'iconBytes': iconBytes,
'filters': filters?.map((filter) => filter.toJson()).toList(),
'explorerPath': explorerPath,
'uri': uri,
});
} on PlatformException catch (e, stack) {

View file

@ -15,7 +15,7 @@ abstract class MediaStoreService {
Future<int?> getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory});
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
@ -75,12 +75,13 @@ class PlatformMediaStoreService implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) {
try {
return _stream
.receiveBroadcastStream(<String, dynamic>{
'knownEntries': knownEntries,
'directory': directory,
'safe': safe,
})
.where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map));

View file

@ -106,8 +106,8 @@ class AndroidFileUtils {
if (isScreenshotsPath(dirPath)) return AlbumType.screenshots;
if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures;
final dir = pContext.split(dirPath).last;
if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
final dir = pContext.split(dirPath).lastOrNull;
if (dir != null && dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
return AlbumType.regular;
}

View file

@ -0,0 +1,23 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
extension ExtraExplorerActionView on ExplorerAction {
String getText(BuildContext context) {
final l10n = context.l10n;
return switch (this) {
ExplorerAction.addShortcut => l10n.collectionActionAddShortcut,
ExplorerAction.setHome => l10n.collectionActionSetHome,
};
}
Widget getIcon() => Icon(_getIconData());
IconData _getIconData() {
return switch (this) {
ExplorerAction.addShortcut => AIcons.addShortcut,
ExplorerAction.setHome => AIcons.home,
};
}
}

View file

@ -21,7 +21,7 @@ class AboutTvPage extends StatelessWidget {
Widget build(BuildContext context) {
return AvesScaffold(
body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop],
handlers: [tvNavigationPopHandler],
child: Row(
children: [
TvRail(

View file

@ -174,7 +174,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// Flutter has various page transition implementations for Android:
// - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
// - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back
static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
static ScreenBrightness? _screenBrightness;

View file

@ -55,7 +55,6 @@ class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = [];
late CollectionLens _collection;
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
@ -80,7 +79,6 @@ class _CollectionPageState extends State<CollectionPage> {
..forEach((sub) => sub.cancel())
..clear();
_collection.dispose();
_doubleBackPopHandler.dispose();
super.dispose();
}
@ -98,16 +96,12 @@ class _CollectionPageState extends State<CollectionPage> {
builder: (context) {
return AvesPopScope(
handlers: [
(context) {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
selection.browse();
return false;
}
return true;
},
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
APopHandler(
canPop: (context) => context.select<Selection<AvesEntry>, bool>((v) => !v.isSelecting),
onPopBlocked: (context) => context.read<Selection<AvesEntry>>().browse(),
),
tvNavigationPopHandler,
doubleBackPopHandler,
],
child: GestureAreaProtectorStack(
child: DirectionalSafeArea(

View file

@ -753,8 +753,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
}
void _setHome(BuildContext context) async {
settings.homeCustomCollection = context.read<CollectionLens>().filters;
settings.homePage = HomePageSetting.collection;
settings.setHome(HomePageSetting.collection, customCollection: context.read<CollectionLens>().filters);
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}

View file

@ -1,48 +1,49 @@
import 'dart:async';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart';
class DoubleBackPopHandler {
final DoubleBackPopHandler doubleBackPopHandler = DoubleBackPopHandler._private();
class DoubleBackPopHandler extends PopHandler {
bool _backOnce = false;
Timer? _backTimer;
DoubleBackPopHandler() {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$DoubleBackPopHandler',
object: this,
);
}
DoubleBackPopHandler._private();
@override
bool canPop(BuildContext context) {
if (context.select<Settings, bool>((s) => !s.mustBackTwiceToExit)) return true;
if (Navigator.canPop(context)) return true;
return false;
}
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
@override
void onPopBlocked(BuildContext context) {
if (_backOnce) {
if (Navigator.canPop(context)) {
Navigator.maybeOf(context)?.pop();
} else {
// exit
reportService.log('Exit by pop');
PopExitNotification().dispatch(context);
SystemNavigator.pop();
}
_stopBackTimer();
}
bool pop(BuildContext context) {
if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) {
} else {
_backOnce = true;
_stopBackTimer();
_backTimer?.cancel();
_backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false);
toast(
context.l10n.doubleBackExitMessage,
duration: ADurations.doubleBackTimerDelay,
);
return false;
}
return true;
}
void _stopBackTimer() {
_backTimer?.cancel();
}
}
}

View file

@ -1,11 +1,9 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random
// so this widget combines multiple handlers with a guaranteed order
// this widget combines multiple pop handlers with a guaranteed order
class AvesPopScope extends StatelessWidget {
final List<bool Function(BuildContext context)> handlers;
final List<PopHandler> handlers;
final Widget child;
const AvesPopScope({
@ -16,21 +14,12 @@ class AvesPopScope extends StatelessWidget {
@override
Widget build(BuildContext context) {
final blocker = handlers.firstWhereOrNull((v) => !v.canPop(context));
return PopScope(
canPop: false,
canPop: blocker == null,
onPopInvoked: (didPop) {
if (didPop) return;
final shouldPop = handlers.fold(true, (prev, v) => prev ? v(context) : false);
if (shouldPop) {
if (Navigator.canPop(context)) {
Navigator.maybeOf(context)?.pop();
} else {
// exit
reportService.log('Exit by pop');
PopExitNotification().dispatch(context);
SystemNavigator.pop();
}
if (!didPop) {
blocker?.onPopBlocked(context);
}
},
child: child,
@ -38,5 +27,28 @@ class AvesPopScope extends StatelessWidget {
}
}
abstract class PopHandler {
bool canPop(BuildContext context);
void onPopBlocked(BuildContext context);
}
class APopHandler implements PopHandler {
final bool Function(BuildContext context) _canPop;
final void Function(BuildContext context) _onPopBlocked;
APopHandler({
required bool Function(BuildContext context) canPop,
required void Function(BuildContext context) onPopBlocked,
}) : _canPop = canPop,
_onPopBlocked = onPopBlocked;
@override
bool canPop(BuildContext context) => _canPop(context);
@override
void onPopBlocked(BuildContext context) => _onPopBlocked(context);
}
@immutable
class PopExitNotification extends Notification {}

View file

@ -3,6 +3,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
@ -11,18 +12,25 @@ import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
final TvNavigationPopHandler tvNavigationPopHandler = TvNavigationPopHandler._private();
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
class TvNavigationPopHandler {
static bool pop(BuildContext context) {
if (!settings.useTvLayout || _isHome(context)) {
return true;
class TvNavigationPopHandler implements PopHandler {
TvNavigationPopHandler._private();
@override
bool canPop(BuildContext context) {
if (context.select<Settings, bool>((s) => !s.useTvLayout)) return true;
if (_isHome(context)) return true;
return false;
}
@override
void onPopBlocked(BuildContext context) {
Navigator.maybeOf(context)?.pushAndRemoveUntil(
_getHomeRoute(),
(route) => false,
);
return false;
}
static bool _isHome(BuildContext context) {

View file

@ -1,4 +1,3 @@
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/debouncer.dart';
@ -31,7 +30,6 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> {
final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay);
final FocusNode _searchFieldFocusNode = FocusNode();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
@ -55,7 +53,6 @@ class _SearchPageState extends State<SearchPage> {
_unregisterWidget(widget);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
_searchFieldFocusNode.dispose();
_doubleBackPopHandler.dispose();
widget.delegate.dispose();
super.dispose();
}
@ -151,8 +148,8 @@ class _SearchPageState extends State<SearchPage> {
),
body: AvesPopScope(
handlers: [
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
tvNavigationPopHandler,
doubleBackPopHandler,
],
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),

View file

@ -67,7 +67,7 @@ class AppDebugPage extends StatelessWidget {
],
),
body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop],
handlers: [tvNavigationPopHandler],
child: SafeArea(
child: ListView(
padding: const EdgeInsets.all(8),

View file

@ -0,0 +1,75 @@
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class SelectStorageDialog extends StatefulWidget {
static const routeName = '/dialog/select_storage';
final StorageVolume? initialVolume;
const SelectStorageDialog({super.key, this.initialVolume});
@override
State<SelectStorageDialog> createState() => _SelectStorageDialogState();
}
class _SelectStorageDialogState extends State<SelectStorageDialog> {
late Set<StorageVolume> _allVolumes;
late StorageVolume? _primaryVolume, _selectedVolume;
@override
void initState() {
super.initState();
_allVolumes = androidFileUtils.storageVolumes;
_primaryVolume = _allVolumes.firstWhereOrNull((volume) => volume.isPrimary) ?? _allVolumes.firstOrNull;
_selectedVolume = widget.initialVolume ?? _primaryVolume;
}
@override
Widget build(BuildContext context) {
final byPrimary = groupBy<StorageVolume, bool>(_allVolumes, (volume) => volume.isPrimary);
int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCaseNatural(a.path, b.path);
final primaryVolumes = (byPrimary[true] ?? [])..sort(compare);
final otherVolumes = (byPrimary[false] ?? [])..sort(compare);
return AvesDialog(
title: context.l10n.selectStorageVolumeDialogTitle,
scrollableContent: [
...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)),
...otherVolumes.map((volume) => _buildVolumeTile(context, volume)),
],
actions: [
const CancelButton(),
TextButton(
onPressed: () => Navigator.maybeOf(context)?.pop(_selectedVolume),
child: Text(context.l10n.applyButtonLabel),
),
],
);
}
Widget _buildVolumeTile(BuildContext context, StorageVolume volume) => RadioListTile<StorageVolume>(
value: volume,
groupValue: _selectedVolume,
onChanged: (volume) {
_selectedVolume = volume!;
setState(() {});
},
title: Text(
volume.getDescription(context),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
subtitle: Text(
volume.path,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}

View file

@ -20,4 +20,4 @@ Future<void> showSelectionDialog<T>({
}
}
typedef TextBuilder<T> = String Function(T value);
typedef TextBuilder<T> = String? Function(T value);

View file

@ -7,6 +7,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/src/actions/explorer.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
@ -15,9 +16,12 @@ import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/dialogs/select_storage_dialog.dart';
import 'package:aves/widgets/explorer/explorer_action_delegate.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
@ -108,11 +112,50 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
onPressed: () => _goToSearch(context),
tooltip: MaterialLocalizations.of(context).searchFieldLabel,
),
if (_volumes.length > 1)
FontSizeIconTheme(
child: PopupMenuButton<StorageVolume>(
if (_volumes.length > 1) _buildVolumeSelector(context),
PopupMenuButton<ExplorerAction>(
itemBuilder: (context) {
return _volumes.map((v) {
return [
ExplorerAction.addShortcut,
ExplorerAction.setHome,
].map((v) {
return PopupMenuItem(
value: v,
child: MenuRow(text: v.getText(context), icon: v.getIcon()),
);
}).toList();
},
onSelected: (action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
final directory = widget.directoryNotifier.value;
ExplorerActionDelegate(directory: directory).onActionSelected(context, action);
},
popUpAnimationStyle: animations.popUpAnimationStyle,
),
].map((v) => FontSizeIconTheme(child: v)).toList();
}
Widget _buildVolumeSelector(BuildContext context) {
if (_volumes.length == 2) {
return ValueListenableBuilder<VolumeRelativeDirectory>(
valueListenable: widget.directoryNotifier,
builder: (context, directory, child) {
final currentVolume = directory.volumePath;
final otherVolume = _volumes.firstWhere((volume) => volume.path != currentVolume);
final icon = otherVolume.isRemovable ? AIcons.storageCard : AIcons.storageMain;
return IconButton(
icon: Icon(icon),
onPressed: () => widget.goTo(otherVolume.path),
tooltip: otherVolume.getDescription(context),
);
},
);
} else {
return IconButton(
icon: const Icon(AIcons.storageCard),
onPressed: () async {
_volumes.map((v) {
final selected = widget.directoryNotifier.value.volumePath == v.path;
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
return PopupMenuItem(
@ -124,16 +167,20 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
),
);
}).toList();
},
onSelected: (volume) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
final volumePath = widget.directoryNotifier.value.volumePath;
final initialVolume = _volumes.firstWhereOrNull((v) => v.path == volumePath);
final volume = await showDialog<StorageVolume?>(
context: context,
builder: (context) => SelectStorageDialog(initialVolume: initialVolume),
routeSettings: const RouteSettings(name: SelectStorageDialog.routeName),
);
if (volume != null) {
widget.goTo(volume.path);
}
},
popUpAnimationStyle: animations.popUpAnimationStyle,
),
),
];
tooltip: context.l10n.explorerActionSelectStorageVolume,
);
}
}
double get appBarContentHeight {

View file

@ -0,0 +1,85 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/path.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ExplorerActionDelegate with FeedbackMixin {
final VolumeRelativeDirectory directory;
ExplorerActionDelegate({required this.directory});
bool isVisible(
ExplorerAction action, {
required AppMode appMode,
}) {
final isMain = appMode == AppMode.main;
final useTvLayout = settings.useTvLayout;
switch (action) {
case ExplorerAction.addShortcut:
return isMain && device.canPinShortcut;
case ExplorerAction.setHome:
return isMain && !useTvLayout;
}
}
bool canApply(ExplorerAction action) {
switch (action) {
case ExplorerAction.addShortcut:
case ExplorerAction.setHome:
return true;
}
}
void onActionSelected(BuildContext context, ExplorerAction action) {
reportService.log('$action');
switch (action) {
case ExplorerAction.addShortcut:
_addShortcut(context);
case ExplorerAction.setHome:
_setHome(context);
}
}
Future<void> _addShortcut(BuildContext context) async {
final path = directory.dirPath;
final filter = PathFilter(path);
final defaultName = filter.getLabel(context);
final collection = CollectionLens(
source: context.read<CollectionSource>(),
filters: {filter},
);
final result = await showDialog<(AvesEntry?, String)>(
context: context,
builder: (context) => AddShortcutDialog(
defaultName: defaultName,
collection: collection,
),
routeSettings: const RouteSettings(name: AddShortcutDialog.routeName),
);
if (result == null) return;
final (coverEntry, name) = result;
if (name.isEmpty) return;
await appService.pinToHomeScreen(name, coverEntry, explorerPath: path);
if (!device.showPinShortcutFeedback) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}
void _setHome(BuildContext context) async {
settings.setHome(HomePageSetting.explorer, customExplorerPath: directory.dirPath);
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
}
}

View file

@ -43,7 +43,6 @@ class _ExplorerPageState extends State<ExplorerPage> {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<VolumeRelativeDirectory> _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: ''));
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
Set<StorageVolume> get _volumes => androidFileUtils.storageVolumes;
@ -78,24 +77,23 @@ class _ExplorerPageState extends State<ExplorerPage> {
..clear();
_directory.dispose();
_contents.dispose();
_doubleBackPopHandler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<VolumeRelativeDirectory>(
valueListenable: _directory,
builder: (context, directory, child) {
final atRoot = directory.relativeDir.isEmpty;
return AvesPopScope(
handlers: [
(context) {
if (_directory.value.relativeDir.isNotEmpty) {
final parent = pContext.dirname(_currentDirectoryPath);
_goTo(parent);
return false;
}
return true;
},
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
APopHandler(
canPop: (context) => atRoot,
onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)),
),
tvNavigationPopHandler,
doubleBackPopHandler,
],
child: AvesScaffold(
drawer: const AppDrawer(),
@ -154,16 +152,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
bottom: true,
child: Padding(
padding: const EdgeInsets.all(8),
child: ValueListenableBuilder<VolumeRelativeDirectory>(
valueListenable: _directory,
builder: (context, directory, child) {
return AvesFilterChip(
child: AvesFilterChip(
filter: PathFilter(_currentDirectoryPath),
maxWidth: double.infinity,
onTap: (filter) => _goToCollectionPage(context, filter),
onLongPress: null,
);
},
),
),
),
@ -172,6 +165,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
),
),
);
},
);
}
Widget _buildEmptyContent() {

View file

@ -191,12 +191,10 @@ class _FilterGrid<T extends CollectionFilter> extends StatefulWidget {
class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>> {
TileExtentController? _tileExtentController;
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void dispose() {
_tileExtentController?.dispose();
_doubleBackPopHandler.dispose();
super.dispose();
}
@ -212,16 +210,12 @@ class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>>
);
return AvesPopScope(
handlers: [
(context) {
final selection = context.read<Selection<FilterGridItem<T>>>();
if (selection.isSelecting) {
selection.browse();
return false;
}
return true;
},
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
APopHandler(
canPop: (context) => context.select<Selection<FilterGridItem<T>>, bool>((v) => !v.isSelecting),
onPopBlocked: (context) => context.read<Selection<FilterGridItem<T>>>().browse(),
),
tvNavigationPopHandler,
doubleBackPopHandler,
],
child: TileExtentControllerProvider(
controller: _tileExtentController!,

View file

@ -61,11 +61,13 @@ class _HomePageState extends State<HomePage> {
int? _widgetId;
String? _initialRouteName, _initialSearchQuery;
Set<CollectionFilter>? _initialFilters;
String? _initialExplorerPath;
List<String>? _secureUris;
static const allowedShortcutRoutes = [
CollectionPage.routeName,
AlbumListPage.routeName,
CollectionPage.routeName,
ExplorerPage.routeName,
SearchPage.routeName,
];
@ -92,6 +94,7 @@ class _HomePageState extends State<HomePage> {
final safeMode = intentData[IntentDataKeys.safeMode] ?? false;
final intentAction = intentData[IntentDataKeys.action];
_initialFilters = null;
_initialExplorerPath = null;
_secureUris = null;
await androidFileUtils.init();
@ -186,6 +189,7 @@ class _HomePageState extends State<HomePage> {
final extraFilters = intentData[IntentDataKeys.filters];
_initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
}
_initialExplorerPath = intentData[IntentDataKeys.explorerPath];
}
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
@ -199,10 +203,10 @@ class _HomePageState extends State<HomePage> {
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
source.safeMode = safeMode;
if (source.initState != SourceInitializationState.full) {
await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
canAnalyze: !safeMode,
);
}
case AppMode.screenSaver:
@ -351,7 +355,8 @@ class _HomePageState extends State<HomePage> {
case TagListPage.routeName:
return buildRoute((context) => const TagListPage());
case ExplorerPage.routeName:
return buildRoute((context) => const ExplorerPage());
final path = _initialExplorerPath ?? settings.homeCustomExplorerPath;
return buildRoute((context) => ExplorerPage(path: path));
case HomeWidgetSettingsPage.routeName:
return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!));
case ScreenSaverPage.routeName:

View file

@ -15,6 +15,7 @@ class IntentDataKeys {
static const action = 'action';
static const allowMultiple = 'allowMultiple';
static const brightness = 'brightness';
static const explorerPath = 'explorerPath';
static const filters = 'filters';
static const mimeType = 'mimeType';
static const page = 'page';

View file

@ -2,8 +2,10 @@ import 'dart:async';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/text.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tile_leading.dart';
@ -43,24 +45,44 @@ class NavigationSection extends SettingsSection {
class _HomeOption {
final HomePageSetting page;
final Set<CollectionFilter> customCollection;
final String? customExplorerPath;
const _HomeOption(
this.page, {
this.customCollection = const {},
this.customExplorerPath,
});
String getName(BuildContext context) {
if (page == HomePageSetting.collection && customCollection.isNotEmpty) {
return context.l10n.setHomeCustomCollection;
final pageName = page.getName(context);
switch (page) {
case HomePageSetting.collection:
return customCollection.isNotEmpty ? context.l10n.setHomeCustom : pageName;
case HomePageSetting.explorer:
return customExplorerPath != null ? context.l10n.setHomeCustom : pageName;
default:
return pageName;
}
}
String? getDetails(BuildContext context) {
switch (page) {
case HomePageSetting.collection:
final filters = customCollection;
return filters.isNotEmpty ? [context.l10n.collectionPageTitle, filters.map((v) => v.getLabel(context)).join(', ')].join(AText.separator) : null;
case HomePageSetting.explorer:
final path = customExplorerPath;
return path != null ? [context.l10n.explorerPageTitle, pContext.basename(path)].join(AText.separator) : null;
default:
return null;
}
return page.getName(context);
}
@override
bool operator ==(Object other) => identical(this, other) || other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection);
bool operator ==(Object other) => identical(this, other) || (other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection) && customExplorerPath == other.customExplorerPath);
@override
int get hashCode => page.hashCode ^ customCollection.hashCode;
int get hashCode => page.hashCode ^ customCollection.hashCode ^ customExplorerPath.hashCode;
}
class SettingsTileNavigationHomePage extends SettingsTile {
@ -75,15 +97,18 @@ class SettingsTileNavigationHomePage extends SettingsTile {
const _HomeOption(HomePageSetting.tags),
const _HomeOption(HomePageSetting.explorer),
if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection),
if (settings.homeCustomExplorerPath != null) _HomeOption(HomePageSetting.explorer, customExplorerPath: settings.homeCustomExplorerPath),
],
getName: (context, v) => v.getName(context),
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection),
onSelection: (v) {
settings.homePage = v.page;
settings.homeCustomCollection = v.customCollection;
},
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection, customExplorerPath: s.homeCustomExplorerPath),
onSelection: (v) => settings.setHome(
v.page,
customCollection: v.customCollection,
customExplorerPath: v.customExplorerPath,
),
tileTitle: title(context),
dialogTitle: context.l10n.settingsHomeDialogTitle,
optionSubtitleBuilder: (v) => v.getDetails(context),
);
}

View file

@ -16,7 +16,7 @@ class SettingsTvPage extends StatelessWidget {
Widget build(BuildContext context) {
return AvesScaffold(
body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop],
handlers: [tvNavigationPopHandler],
child: Row(
children: [
TvRail(

View file

@ -1,6 +1,7 @@
library aves_model;
export 'src/actions/chip.dart';
export 'src/actions/explorer.dart';
export 'src/actions/chip_set.dart';
export 'src/actions/entry.dart';
export 'src/actions/entry_set.dart';

View file

@ -0,0 +1,4 @@
enum ExplorerAction {
addShortcut,
setHome,
}

View file

@ -43,6 +43,7 @@ class SettingKeys {
static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page';
static const homeCustomCollectionKey = 'home_custom_collection';
static const homeCustomExplorerPathKey = 'home_custom_explorer_path';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmCreateVaultKey = 'confirm_create_vault';
static const confirmDeleteForeverKey = 'confirm_delete_forever';

View file

@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript {
ext {
kotlin_version = '1.9.24'
agp_version = '8.5.0'
agp_version = '8.5.1'
}
repositories {

View file

@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
version: 1.11.5+124
version: 1.11.6+125
publish_to: none
environment:

View file

@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
static var _lastId = 1;

View file

@ -30,8 +30,7 @@ Future<void> configureAndLaunch() async {
..enableBlurEffect = true
// navigation
..keepScreenOn = KeepScreenOn.always
..homePage = HomePageSetting.collection
..homeCustomCollection = {}
..setHome(HomePageSetting.collection)
..enableBottomNavigationBar = true
..drawerTypeBookmarks = [null, FavouriteFilter.instance]
// collection

View file

@ -26,8 +26,7 @@ Future<void> configureAndLaunch() async {
..enableBlurEffect = true
// navigation
..keepScreenOn = KeepScreenOn.always
..homePage = HomePageSetting.collection
..homeCustomCollection = {}
..setHome(HomePageSetting.collection)
..enableBottomNavigationBar = true
// collection
..collectionSectionFactor = EntryGroupFactor.album

View file

@ -1,4 +1,4 @@
In v1.11.5:
In v1.11.6:
- explore your collection with the... explorer
- convert your motion photos to stills in bulk
Full changelog available on GitHub