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="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 ## <a id="v1.11.5"></a>[v1.11.5] - 2024-07-11
### Added ### Added

View file

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

View file

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

View file

@ -70,7 +70,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
private fun onStart() { private fun onStart() {
Log.i(LOG_TAG, "Start analysis worker $id") Log.i(LOG_TAG, "Start analysis worker $id")
runBlocking { runBlocking {
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) {
flutterEngine = it flutterEngine = it
} }
} }
@ -78,14 +78,15 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
try { try {
initChannels(applicationContext) initChannels(applicationContext)
val preferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
val entryIdStrings = preferences.getStringSet(PREF_ENTRY_IDS_KEY, null)
runBlocking { runBlocking {
FlutterUtils.runOnUiThread { FlutterUtils.runOnUiThread {
backgroundChannel?.invokeMethod( backgroundChannel?.invokeMethod(
"start", hashMapOf( "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), "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) .setContentIntent(openAppIntent)
.addAction(stopAction) .addAction(stopAction)
.build() .build()
return if (Build.VERSION.SDK_INT >= 34) { return if (Build.VERSION.SDK_INT == 34) {
// from Android 14 (API 34), foreground service type is mandatory // from Android 14 (API 34), foreground service type is mandatory for long-running workers:
// despite the sample code omitting it at:
// https://developer.android.com/guide/background/persistent/how-to/long-running // https://developer.android.com/guide/background/persistent/how-to/long-running
// TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING` ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC } else if (Build.VERSION.SDK_INT >= 35) {
ForegroundInfo(NOTIFICATION_ID, notification, type) ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
} else { } else {
ForegroundInfo(NOTIFICATION_ID, notification) ForegroundInfo(NOTIFICATION_ID, notification)
} }
@ -195,14 +195,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>() private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background" private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
const val SHARED_PREFERENCES_KEY = "analysis_service" 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_CHANNEL = "analysis"
const val NOTIFICATION_ID = 1 const val NOTIFICATION_ID = 1
const val KEY_ENTRY_IDS = "entry_ids"
const val KEY_FORCE = "force" 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 widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
val pendingResult = goAsync() val pendingResult = goAsync()
defaultScope.launch() { defaultScope.launch {
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false) val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps) 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.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
open class MainActivity : FlutterFragmentActivity() { open class MainActivity : FlutterActivity() {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
@ -294,11 +294,9 @@ open class MainActivity : FlutterFragmentActivity() {
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) { if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
fields[INTENT_DATA_KEY_SAFE_MODE] = true fields[INTENT_DATA_KEY_SAFE_MODE] = true
} }
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page -> fields[INTENT_DATA_KEY_PAGE] = intent.getStringExtra(EXTRA_KEY_PAGE)
val filters = extractFiltersFromIntent(intent) fields[INTENT_DATA_KEY_FILTERS] = extractFiltersFromIntent(intent)
fields[INTENT_DATA_KEY_PAGE] = page fields[INTENT_DATA_KEY_EXPLORER_PATH] = intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH)
fields[INTENT_DATA_KEY_FILTERS] = filters
}
return fields return fields
} }
@ -527,6 +525,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness" 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_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page" 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 INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page" 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_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString" const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_SAFE_MODE = "safeMode" const val EXTRA_KEY_SAFE_MODE = "safeMode"

View file

@ -1,15 +1,14 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import androidx.activity.ComponentActivity
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.workDataOf import androidx.work.workDataOf
import deckers.thibault.aves.AnalysisWorker import deckers.thibault.aves.AnalysisWorker
import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.FlutterUtils
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -19,7 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking 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) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -37,10 +36,11 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
return return
} }
activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.edit() with(preferences.edit()) {
.putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle) putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
.apply() apply()
}
result.success(true) result.success(true)
} }
@ -53,33 +53,24 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
// can be null or empty // can be null or empty
val allEntryIds = call.argument<List<Int>>("entryIds") 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 // work `Data` cannot occupy more than 10240 bytes when serialized
// so we split it when we have a long list of entry IDs // so we save the possibly long list of entry IDs to shared preferences
val chunked = allEntryIds?.chunked(WORK_DATA_CHUNK_SIZE) ?: listOf(null) val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
with(preferences.edit()) {
fun buildRequest(entryIds: List<Int>?, progressOffset: Int): OneTimeWorkRequest { putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet())
val workData = workDataOf( apply()
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()
} }
var work = WorkManager.getInstance(activity).beginUniqueWork( val workData = workDataOf(
AnalysisWorker.KEY_FORCE to force,
)
WorkManager.getInstance(activity).beginUniqueWork(
ANALYSIS_WORK_NAME, ANALYSIS_WORK_NAME,
ExistingWorkPolicy.KEEP, ExistingWorkPolicy.KEEP,
buildRequest(chunked.first(), progressOffset), OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build(),
) ).enqueue()
chunked.drop(1).forEach { entryIds ->
progressOffset += WORK_DATA_CHUNK_SIZE
work = work.then(buildRequest(entryIds, progressOffset))
}
work.enqueue()
attachToActivity() attachToActivity()
result.success(null) result.success(null)
@ -105,6 +96,5 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/analysis" const val CHANNEL = "deckers.thibault/aves/analysis"
private const val ANALYSIS_WORK_NAME = "analysis_work" 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.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity 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_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
@ -351,8 +352,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val label = call.argument<String>("label") val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes") val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters") val filters = call.argument<List<String>>("filters")
val explorerPath = call.argument<String>("explorerPath")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } 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) result.error("pin-args", "missing arguments", null)
return return
} }
@ -380,7 +382,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
val intent = when { 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) filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/collection") .putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) .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 // so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) .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 -> { else -> {
result.error("pin-intent", "failed to build intent", null) result.error("pin-intent", "failed to build intent", null)
return return

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log 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.XMPException
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory import com.adobe.internal.xmp.XMPMetaFactory

View file

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

View file

@ -1,10 +1,10 @@
package deckers.thibault.aves.channel.streams package deckers.thibault.aves.channel.streams
import android.app.Activity
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.fragment.app.FragmentActivity
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
@ -21,9 +21,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.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 val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
private lateinit var handler: Handler 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 knownEntries: Map<Long?, Int?>? = null
private var directory: String? = null private var directory: String? = null
private var safe: Boolean = false
init { init {
if (arguments is Map<*, *>) { if (arguments is Map<*, *>) {
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap() knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
directory = arguments["directory"] as String? 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() { private fun fetchAll() {
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) } MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) }
endOfStream() endOfStream()
} }

View file

@ -152,12 +152,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
@RequiresApi(Build.VERSION_CODES.P) @RequiresApi(Build.VERSION_CODES.P)
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply { 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) // 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 // for wide-gamut and HDR content which does not require alpha blending
setPreferredConfig(Bitmap.Config.RGBA_1010102) Bitmap.Config.RGBA_1010102
} else { } else {
setPreferredConfig(Bitmap.Config.ARGB_8888) Bitmap.Config.ARGB_8888
} }
} }

View file

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

View file

@ -2,7 +2,7 @@ package deckers.thibault.aves.metadata
import android.content.Context import android.content.Context
import android.net.Uri 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.FileUtils.transferFrom
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils

View file

@ -9,7 +9,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.metadata.exif.ExifDirectoryBase 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.Metadata
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MemoryUtils
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,5 +5,5 @@ import kotlin.math.pow
object MathUtils { object MathUtils {
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble()) 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 package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.decoder.MultiPageImage import deckers.thibault.aves.decoder.MultiPageImage
object MimeTypes { object MimeTypes {
@ -17,8 +17,8 @@ object MimeTypes {
private const val ICO = "image/x-icon" private const val ICO = "image/x-icon"
const val JPEG = "image/jpeg" const val JPEG = "image/jpeg"
const val PNG = "image/png" const val PNG = "image/png"
const val PSD_VND = "image/vnd.adobe.photoshop" private const val PSD_VND = "image/vnd.adobe.photoshop"
const val PSD_X = "image/x-photoshop" private const val PSD_X = "image/x-photoshop"
const val TIFF = "image/tiff" const val TIFF = "image/tiff"
private const val WBMP = "image/vnd.wap.wbmp" private const val WBMP = "image/vnd.wap.wbmp"
const val WEBP = "image/webp" const val WEBP = "image/webp"

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <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="wallpaper">Achtergrond</string>
<string name="search_shortcut_short_label">Zoeken</string> <string name="search_shortcut_short_label">Zoeken</string>
<string name="videos_shortcut_short_label">Videos</string> <string name="videos_shortcut_short_label">Videos</string>
<string name="analysis_channel_name">Media indexeren</string> <string name="analysis_channel_name">Media indexeren</string>
<string name="analysis_notification_default_title">Indexeren van media</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> <string name="safe_mode_shortcut_short_label">Veilige modus</string>
</resources> </resources>

View file

@ -16,12 +16,12 @@
package androidx.exifinterface.media; package androidx.exifinterface.media;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly;
import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray;
import static androidx.exifinterface.media.ExifInterfaceUtils.copy; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN;
@ -41,8 +41,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl; import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl; import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; 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 * 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 * 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. * 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 // TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB 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 * @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}. * {@link FileInputStream#getFD()}.
*/ */
public ExifInterface(@NonNull File file) throws IOException { public ExifInterfaceFork(@NonNull File file) throws IOException {
if (file == null) { if (file == null) {
throw new NullPointerException("file cannot be 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 * @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}. * {@link FileInputStream#getFD()}.
*/ */
public ExifInterface(@NonNull String filename) throws IOException { public ExifInterfaceFork(@NonNull String filename) throws IOException {
if (filename == null) { if (filename == null) {
throw new NullPointerException("filename cannot be null"); throw new NullPointerException("filename cannot be null");
} }
@ -3980,7 +3981,7 @@ public class ExifInterface {
* @throws NullPointerException if file descriptor is null * @throws NullPointerException if file descriptor is null
* @throws IOException if an error occurs while duplicating the file descriptor. * @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) { if (fileDescriptor == null) {
throw new NullPointerException("fileDescriptor cannot be 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 * @param inputStream the input stream that contains the image data
* @throws NullPointerException if the input stream is null * @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); 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 * @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}. * {@link FileInputStream#getFD()}.
*/ */
public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType) public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType)
throws IOException { throws IOException {
if (inputStream == null) { if (inputStream == null) {
throw new NullPointerException("inputStream cannot be null"); throw new NullPointerException("inputStream cannot be null");
@ -5071,7 +5072,7 @@ public class ExifInterface {
if (location == null) { if (location == null) {
return; return;
} }
setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()); setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setLatLong(location.getLatitude(), location.getLongitude()); setLatLong(location.getLatitude(), location.getLongitude());
setAltitude(location.getAltitude()); setAltitude(location.getAltitude());
// Location objects store speeds in m/sec. Translates it to km/hr here. // 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()); * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
String[] dateTime = sFormatterPrimary.format( String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1); new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]); setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]); 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. * Jan. 1, 1970, midnight local time.
* *
* <p>Note: The return value includes the first three digits (or less depending on the length * <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. * @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. * milliseconds since Jan. 1, 1970, midnight local time.
* *
* <p>Note: The return value includes the first three digits (or less depending on the length * <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. * @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. * milliseconds since Jan. 1, 1970, midnight local time.
* *
* <p>Note: The return value includes the first three digits (or less depending on the length * <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. * @return null if original date time information is unavailable or invalid.
*/ */
@ -5910,18 +5911,18 @@ public class ExifInterface {
} }
if (rotation != null) { if (rotation != null) {
int orientation = ExifInterface.ORIENTATION_NORMAL; int orientation = ExifInterfaceFork.ORIENTATION_NORMAL;
// all rotation angles in CW // all rotation angles in CW
switch (Integer.parseInt(rotation)) { switch (Integer.parseInt(rotation)) {
case 90: case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90; orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90;
break; break;
case 180: case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180; orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180;
break; break;
case 270: case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270; orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270;
break; break;
} }
@ -6175,7 +6176,11 @@ public class ExifInterface {
// IEND marks the end of the image. // IEND marks the end of the image.
break; break;
} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) { } 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]; byte[] data = new byte[length];
in.readFully(data); in.readFully(data);
@ -6976,9 +6981,11 @@ public class ExifInterface {
} }
final int bytesOffset = dataInputStream.position() + mOffsetToExifData; 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); throw new IOException("dangerous attribute size=" + byteCount);
} }
// TLAD end
final byte[] bytes = new byte[(int) byteCount]; final byte[] bytes = new byte[(int) byteCount];
dataInputStream.readFully(bytes); dataInputStream.readFully(bytes);
ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,

View file

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

View file

@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24' settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20" 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") 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. <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": {}, "@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "تعيين كخلفية", "collectionActionSetHome": "تعيين كخلفية",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "مجموعة مخصصة",
"@setHomeCustomCollection": {},
"videoActionABRepeat": "تكرار A-B", "videoActionABRepeat": "تكرار A-B",
"@videoActionABRepeat": {}, "@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل", "videoRepeatActionSetEnd": "تعيين نهاية التشغيل",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Fijar como inicio", "collectionActionSetHome": "Fijar como inicio",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "Colección personalizada",
"@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "Fijar el inicio", "videoRepeatActionSetStart": "Fijar el inicio",
"@videoRepeatActionSetStart": {}, "@videoRepeatActionSetStart": {},
"stopTooltip": "Parar", "stopTooltip": "Parar",
@ -1380,5 +1378,11 @@
"explorerPageTitle": "Explorar", "explorerPageTitle": "Explorar",
"@explorerPageTitle": {}, "@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Mostrar en el explorador", "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": {}, "@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Ezarri hasiera gisa", "collectionActionSetHome": "Ezarri hasiera gisa",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "Bilduma pertsonalizatua",
"@setHomeCustomCollection": {},
"renameProcessorHash": "Hash-a", "renameProcessorHash": "Hash-a",
"@renameProcessorHash": {}, "@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak", "settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak",

View file

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

View file

@ -1359,8 +1359,6 @@
"@castDialogTitle": {}, "@castDialogTitle": {},
"collectionActionSetHome": "Définir comme page daccueil", "collectionActionSetHome": "Définir comme page daccueil",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "Collection personnalisée",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afficher licône HDR", "settingsThumbnailShowHdrIcon": "Afficher licône HDR",
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"videoRepeatActionSetEnd": "Définir la fin", "videoRepeatActionSetEnd": "Définir la fin",
@ -1380,5 +1378,11 @@
"explorerPageTitle": "Explorateur", "explorerPageTitle": "Explorateur",
"@explorerPageTitle": {}, "@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Afficher dans Explorateur", "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": {} "count": {}
} }
}, },
"deleteButtonLabel": "डिलीट", "deleteButtonLabel": "मिटाए",
"@deleteButtonLabel": {}, "@deleteButtonLabel": {},
"timeMinutes": "{count, plural, other{{count} मिनट}}", "timeMinutes": "{count, plural, other{{count} मिनट}}",
"@timeMinutes": { "@timeMinutes": {
@ -88,7 +88,7 @@
"@chipActionGoToTagPage": {}, "@chipActionGoToTagPage": {},
"resetTooltip": "रिसेट", "resetTooltip": "रिसेट",
"@resetTooltip": {}, "@resetTooltip": {},
"saveTooltip": "सेव करें", "saveTooltip": "सहेजें",
"@saveTooltip": {}, "@saveTooltip": {},
"pickTooltip": "चुनें", "pickTooltip": "चुनें",
"@pickTooltip": {}, "@pickTooltip": {},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -101,9 +101,9 @@
"@entryActionRename": {}, "@entryActionRename": {},
"entryActionRestore": "Herstellen", "entryActionRestore": "Herstellen",
"@entryActionRestore": {}, "@entryActionRestore": {},
"entryActionRotateCCW": "Roteren tegen de klok in", "entryActionRotateCCW": "Linksom roteren",
"@entryActionRotateCCW": {}, "@entryActionRotateCCW": {},
"entryActionRotateCW": "Roteren met de klok mee", "entryActionRotateCW": "Rechtsom roteren",
"@entryActionRotateCW": {}, "@entryActionRotateCW": {},
"entryActionFlip": "Horizontaal omdraaien", "entryActionFlip": "Horizontaal omdraaien",
"@entryActionFlip": {}, "@entryActionFlip": {},
@ -163,25 +163,25 @@
"@entryInfoActionEditLocation": {}, "@entryInfoActionEditLocation": {},
"entryInfoActionEditTitleDescription": "Wijzig titel & omschrijving", "entryInfoActionEditTitleDescription": "Wijzig titel & omschrijving",
"@entryInfoActionEditTitleDescription": {}, "@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Bewerk waardering", "entryInfoActionEditRating": "Waardering bewerken",
"@entryInfoActionEditRating": {}, "@entryInfoActionEditRating": {},
"entryInfoActionEditTags": "Bewerk labels", "entryInfoActionEditTags": "Labels bewerken",
"@entryInfoActionEditTags": {}, "@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Verwijder metadata", "entryInfoActionRemoveMetadata": "Verwijder metadata",
"@entryInfoActionRemoveMetadata": {}, "@entryInfoActionRemoveMetadata": {},
"filterBinLabel": "Prullenbak", "filterBinLabel": "Prullenbak",
"@filterBinLabel": {}, "@filterBinLabel": {},
"filterFavouriteLabel": "Favorieten", "filterFavouriteLabel": "Favoriet",
"@filterFavouriteLabel": {}, "@filterFavouriteLabel": {},
"filterNoDateLabel": "Geen datum", "filterNoDateLabel": "Zonder datum",
"@filterNoDateLabel": {}, "@filterNoDateLabel": {},
"filterNoLocationLabel": "Geen locatie", "filterNoLocationLabel": "Zonder plaats",
"@filterNoLocationLabel": {}, "@filterNoLocationLabel": {},
"filterNoRatingLabel": "Geen rating", "filterNoRatingLabel": "Zonder waardering",
"@filterNoRatingLabel": {}, "@filterNoRatingLabel": {},
"filterNoTagLabel": "Geen label", "filterNoTagLabel": "Zonder label",
"@filterNoTagLabel": {}, "@filterNoTagLabel": {},
"filterNoTitleLabel": "Geen titel", "filterNoTitleLabel": "Zonder titel",
"@filterNoTitleLabel": {}, "@filterNoTitleLabel": {},
"filterOnThisDayLabel": "Op deze dag", "filterOnThisDayLabel": "Op deze dag",
"@filterOnThisDayLabel": {}, "@filterOnThisDayLabel": {},
@ -347,7 +347,7 @@
"@videoResumeDialogMessage": {}, "@videoResumeDialogMessage": {},
"videoStartOverButtonLabel": "OPNIEUW BEGINNEN", "videoStartOverButtonLabel": "OPNIEUW BEGINNEN",
"@videoStartOverButtonLabel": {}, "@videoStartOverButtonLabel": {},
"videoResumeButtonLabel": "HERVAT", "videoResumeButtonLabel": "HERVATTEN",
"@videoResumeButtonLabel": {}, "@videoResumeButtonLabel": {},
"setCoverDialogLatest": "Laatste item", "setCoverDialogLatest": "Laatste item",
"@setCoverDialogLatest": {}, "@setCoverDialogLatest": {},
@ -355,7 +355,7 @@
"@setCoverDialogAuto": {}, "@setCoverDialogAuto": {},
"setCoverDialogCustom": "Aangepast", "setCoverDialogCustom": "Aangepast",
"@setCoverDialogCustom": {}, "@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": {}, "@hideFilterConfirmationDialogMessage": {},
"newAlbumDialogTitle": "Nieuw Album", "newAlbumDialogTitle": "Nieuw Album",
"@newAlbumDialogTitle": {}, "@newAlbumDialogTitle": {},
@ -423,7 +423,7 @@
"@editEntryLocationDialogLongitude": {}, "@editEntryLocationDialogLongitude": {},
"locationPickerUseThisLocationButton": "Gebruik deze locatie", "locationPickerUseThisLocationButton": "Gebruik deze locatie",
"@locationPickerUseThisLocationButton": {}, "@locationPickerUseThisLocationButton": {},
"editEntryRatingDialogTitle": "Beoordeling", "editEntryRatingDialogTitle": "Waardering",
"@editEntryRatingDialogTitle": {}, "@editEntryRatingDialogTitle": {},
"removeEntryMetadataDialogTitle": "Verwijderen metadata", "removeEntryMetadataDialogTitle": "Verwijderen metadata",
"@removeEntryMetadataDialogTitle": {}, "@removeEntryMetadataDialogTitle": {},
@ -505,11 +505,11 @@
"@aboutBugReportInstruction": {}, "@aboutBugReportInstruction": {},
"aboutBugReportButton": "Reporteer", "aboutBugReportButton": "Reporteer",
"@aboutBugReportButton": {}, "@aboutBugReportButton": {},
"aboutCreditsSectionTitle": "Credits", "aboutCreditsSectionTitle": "Dankbetuiging",
"@aboutCreditsSectionTitle": {}, "@aboutCreditsSectionTitle": {},
"aboutCreditsWorldAtlas1": "Deze applicatie gebruikt een TopoJSON-bestand van", "aboutCreditsWorldAtlas1": "Deze applicatie gebruikt een TopoJSON-bestand van",
"@aboutCreditsWorldAtlas1": {}, "@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.", "aboutCreditsWorldAtlas2": "onder ISC-licentie.",
"@aboutCreditsWorldAtlas2": {}, "@aboutCreditsWorldAtlas2": {},
"aboutTranslatorsSectionTitle": "Vertalers", "aboutTranslatorsSectionTitle": "Vertalers",
"@aboutTranslatorsSectionTitle": {}, "@aboutTranslatorsSectionTitle": {},
@ -525,7 +525,7 @@
"@aboutLicensesFlutterPackagesSectionTitle": {}, "@aboutLicensesFlutterPackagesSectionTitle": {},
"aboutLicensesDartPackagesSectionTitle": "Dart Packages", "aboutLicensesDartPackagesSectionTitle": "Dart Packages",
"@aboutLicensesDartPackagesSectionTitle": {}, "@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "Laat alle licenties zien", "aboutLicensesShowAllButtonLabel": "Alle licenties tonen",
"@aboutLicensesShowAllButtonLabel": {}, "@aboutLicensesShowAllButtonLabel": {},
"collectionPageTitle": "Verzameling", "collectionPageTitle": "Verzameling",
"@collectionPageTitle": {}, "@collectionPageTitle": {},
@ -615,7 +615,7 @@
"@drawerCollectionAnimated": {}, "@drawerCollectionAnimated": {},
"drawerCollectionMotionPhotos": "Bewegende fotos", "drawerCollectionMotionPhotos": "Bewegende fotos",
"@drawerCollectionMotionPhotos": {}, "@drawerCollectionMotionPhotos": {},
"drawerCollectionPanoramas": "Panoramas", "drawerCollectionPanoramas": "Panorama's",
"@drawerCollectionPanoramas": {}, "@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Raw fotos", "drawerCollectionRaws": "Raw fotos",
"@drawerCollectionRaws": {}, "@drawerCollectionRaws": {},
@ -637,7 +637,7 @@
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Op album- en bestandsnaam", "sortByAlbumFileName": "Op album- en bestandsnaam",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Op rating", "sortByRating": "Op waardering",
"@sortByRating": {}, "@sortByRating": {},
"sortOrderNewestFirst": "Nieuwste eerst", "sortOrderNewestFirst": "Nieuwste eerst",
"@sortOrderNewestFirst": {}, "@sortOrderNewestFirst": {},
@ -667,7 +667,7 @@
"@albumMimeTypeMixed": {}, "@albumMimeTypeMixed": {},
"albumPickPageTitleCopy": "Kopieer naar Album", "albumPickPageTitleCopy": "Kopieer naar Album",
"@albumPickPageTitleCopy": {}, "@albumPickPageTitleCopy": {},
"albumPickPageTitleExport": "Exporteer naar Album", "albumPickPageTitleExport": "Exporteren naar Album",
"@albumPickPageTitleExport": {}, "@albumPickPageTitleExport": {},
"albumPickPageTitleMove": "Verplaats naar Album", "albumPickPageTitleMove": "Verplaats naar Album",
"@albumPickPageTitleMove": {}, "@albumPickPageTitleMove": {},
@ -715,7 +715,7 @@
"@searchPlacesSectionTitle": {}, "@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Labels", "searchTagsSectionTitle": "Labels",
"@searchTagsSectionTitle": {}, "@searchTagsSectionTitle": {},
"searchRatingSectionTitle": "Beoordeling", "searchRatingSectionTitle": "Waarderingen",
"@searchRatingSectionTitle": {}, "@searchRatingSectionTitle": {},
"searchMetadataSectionTitle": "Metadata", "searchMetadataSectionTitle": "Metadata",
"@searchMetadataSectionTitle": {}, "@searchMetadataSectionTitle": {},
@ -731,13 +731,13 @@
"@settingsSearchFieldLabel": {}, "@settingsSearchFieldLabel": {},
"settingsSearchEmpty": "Geen instellingen gevonden", "settingsSearchEmpty": "Geen instellingen gevonden",
"@settingsSearchEmpty": {}, "@settingsSearchEmpty": {},
"settingsActionExport": "Exporteer", "settingsActionExport": "Exporteren",
"@settingsActionExport": {}, "@settingsActionExport": {},
"settingsActionExportDialogTitle": "Exporteer", "settingsActionExportDialogTitle": "Exporteren",
"@settingsActionExportDialogTitle": {}, "@settingsActionExportDialogTitle": {},
"settingsActionImport": "Importeer", "settingsActionImport": "Importeren",
"@settingsActionImport": {}, "@settingsActionImport": {},
"settingsActionImportDialogTitle": "Importeer", "settingsActionImportDialogTitle": "Importeren",
"@settingsActionImportDialogTitle": {}, "@settingsActionImportDialogTitle": {},
"appExportCovers": "Omslagen", "appExportCovers": "Omslagen",
"@appExportCovers": {}, "@appExportCovers": {},
@ -793,13 +793,13 @@
"@settingsThumbnailOverlayPageTitle": {}, "@settingsThumbnailOverlayPageTitle": {},
"settingsThumbnailShowFavouriteIcon": "Favorieten icoon zichtbaar", "settingsThumbnailShowFavouriteIcon": "Favorieten icoon zichtbaar",
"@settingsThumbnailShowFavouriteIcon": {}, "@settingsThumbnailShowFavouriteIcon": {},
"settingsThumbnailShowTagIcon": "Label icoon zichtbaar", "settingsThumbnailShowTagIcon": "Label-pictogram tonen",
"@settingsThumbnailShowTagIcon": {}, "@settingsThumbnailShowTagIcon": {},
"settingsThumbnailShowLocationIcon": "Locatie icoon zichtbaar", "settingsThumbnailShowLocationIcon": "Locatie icoon zichtbaar",
"@settingsThumbnailShowLocationIcon": {}, "@settingsThumbnailShowLocationIcon": {},
"settingsThumbnailShowMotionPhotoIcon": "Bewegende foto icoon zichtbaar", "settingsThumbnailShowMotionPhotoIcon": "Bewegende foto icoon zichtbaar",
"@settingsThumbnailShowMotionPhotoIcon": {}, "@settingsThumbnailShowMotionPhotoIcon": {},
"settingsThumbnailShowRating": "Rating zichtbaar", "settingsThumbnailShowRating": "Waardering tonen",
"@settingsThumbnailShowRating": {}, "@settingsThumbnailShowRating": {},
"settingsThumbnailShowRawIcon": "RAW icoon zichtbaar", "settingsThumbnailShowRawIcon": "RAW icoon zichtbaar",
"@settingsThumbnailShowRawIcon": {}, "@settingsThumbnailShowRawIcon": {},
@ -865,7 +865,7 @@
"@settingsViewerSlideshowPageTitle": {}, "@settingsViewerSlideshowPageTitle": {},
"settingsSlideshowRepeat": "Herhalen", "settingsSlideshowRepeat": "Herhalen",
"@settingsSlideshowRepeat": {}, "@settingsSlideshowRepeat": {},
"settingsSlideshowShuffle": "Shuffle", "settingsSlideshowShuffle": "Willekeurige volgorde",
"@settingsSlideshowShuffle": {}, "@settingsSlideshowShuffle": {},
"settingsSlideshowFillScreen": "Volledig scherm", "settingsSlideshowFillScreen": "Volledig scherm",
"@settingsSlideshowFillScreen": {}, "@settingsSlideshowFillScreen": {},
@ -951,13 +951,13 @@
"@settingsHiddenItemsPageTitle": {}, "@settingsHiddenItemsPageTitle": {},
"settingsHiddenItemsTabFilters": "Verborgen Filters", "settingsHiddenItemsTabFilters": "Verborgen Filters",
"@settingsHiddenItemsTabFilters": {}, "@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": {}, "@settingsHiddenFiltersBanner": {},
"settingsHiddenFiltersEmpty": "Geen verborgen filters", "settingsHiddenFiltersEmpty": "Geen verborgen filters",
"@settingsHiddenFiltersEmpty": {}, "@settingsHiddenFiltersEmpty": {},
"settingsHiddenItemsTabPaths": "Verborgen paden", "settingsHiddenItemsTabPaths": "Verborgen paden",
"@settingsHiddenItemsTabPaths": {}, "@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": {}, "@settingsHiddenPathsBanner": {},
"addPathTooltip": "Pad toevoegen", "addPathTooltip": "Pad toevoegen",
"@addPathTooltip": {}, "@addPathTooltip": {},
@ -965,7 +965,7 @@
"@settingsStorageAccessTile": {}, "@settingsStorageAccessTile": {},
"settingsStorageAccessPageTitle": "Toegang tot opslag", "settingsStorageAccessPageTitle": "Toegang tot opslag",
"@settingsStorageAccessPageTitle": {}, "@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": {}, "@settingsStorageAccessBanner": {},
"settingsStorageAccessEmpty": "Geen toegang verleend", "settingsStorageAccessEmpty": "Geen toegang verleend",
"@settingsStorageAccessEmpty": {}, "@settingsStorageAccessEmpty": {},
@ -1029,7 +1029,7 @@
"@statsTopTagsSectionTitle": {}, "@statsTopTagsSectionTitle": {},
"statsTopAlbumsSectionTitle": "Top Albums", "statsTopAlbumsSectionTitle": "Top Albums",
"@statsTopAlbumsSectionTitle": {}, "@statsTopAlbumsSectionTitle": {},
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA", "viewerOpenPanoramaButtonLabel": "PANORAMA OPENEN",
"@viewerOpenPanoramaButtonLabel": {}, "@viewerOpenPanoramaButtonLabel": {},
"viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN", "viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN",
"@viewerSetWallpaperButtonLabel": {}, "@viewerSetWallpaperButtonLabel": {},
@ -1089,7 +1089,7 @@
"@viewerInfoOpenLinkText": {}, "@viewerInfoOpenLinkText": {},
"viewerInfoViewXmlLinkText": "Bekijk XML", "viewerInfoViewXmlLinkText": "Bekijk XML",
"@viewerInfoViewXmlLinkText": {}, "@viewerInfoViewXmlLinkText": {},
"viewerInfoSearchFieldLabel": "Doorzoek metadata", "viewerInfoSearchFieldLabel": "Metadata doorzoeken",
"@viewerInfoSearchFieldLabel": {}, "@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "Geen overeenkomstige zoeksleutels", "viewerInfoSearchEmpty": "Geen overeenkomstige zoeksleutels",
"@viewerInfoSearchEmpty": {}, "@viewerInfoSearchEmpty": {},
@ -1105,7 +1105,7 @@
"@viewerInfoSearchSuggestionRights": {}, "@viewerInfoSearchSuggestionRights": {},
"wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm", "wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm",
"@wallpaperUseScrollEffect": {}, "@wallpaperUseScrollEffect": {},
"tagEditorPageTitle": "Wijzig Labels", "tagEditorPageTitle": "Labels bewerken",
"@tagEditorPageTitle": {}, "@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Nieuw label", "tagEditorPageNewTagFieldLabel": "Nieuw label",
"@tagEditorPageNewTagFieldLabel": {}, "@tagEditorPageNewTagFieldLabel": {},
@ -1155,17 +1155,17 @@
"@lengthUnitPercent": {}, "@lengthUnitPercent": {},
"vaultLockTypePin": "PIN", "vaultLockTypePin": "PIN",
"@vaultLockTypePin": {}, "@vaultLockTypePin": {},
"filterAspectRatioLandscapeLabel": "Landschap", "filterAspectRatioLandscapeLabel": "Liggend",
"@filterAspectRatioLandscapeLabel": {}, "@filterAspectRatioLandscapeLabel": {},
"chipActionCreateVault": "Creëer kluis", "chipActionCreateVault": "Kluis aanmaken",
"@chipActionCreateVault": {}, "@chipActionCreateVault": {},
"entryInfoActionRemoveLocation": "Verwijder locatie", "entryInfoActionRemoveLocation": "Verwijder locatie",
"@entryInfoActionRemoveLocation": {}, "@entryInfoActionRemoveLocation": {},
"chipActionConfigureVault": "Configureer kluis", "chipActionConfigureVault": "Kluis configureren",
"@chipActionConfigureVault": {}, "@chipActionConfigureVault": {},
"filterNoAddressLabel": "Geen adres", "filterNoAddressLabel": "Zonder adres",
"@filterNoAddressLabel": {}, "@filterNoAddressLabel": {},
"filterAspectRatioPortraitLabel": "Portret", "filterAspectRatioPortraitLabel": "Staand",
"@filterAspectRatioPortraitLabel": {}, "@filterAspectRatioPortraitLabel": {},
"widgetDisplayedItemRandom": "Willekeurige", "widgetDisplayedItemRandom": "Willekeurige",
"@widgetDisplayedItemRandom": {}, "@widgetDisplayedItemRandom": {},
@ -1175,7 +1175,7 @@
"@keepScreenOnVideoPlayback": {}, "@keepScreenOnVideoPlayback": {},
"settingsVideoEnablePip": "Beeld-in-beeld", "settingsVideoEnablePip": "Beeld-in-beeld",
"@settingsVideoEnablePip": {}, "@settingsVideoEnablePip": {},
"filterTaggedLabel": "Getagd", "filterTaggedLabel": "Met label",
"@filterTaggedLabel": {}, "@filterTaggedLabel": {},
"lengthUnitPixel": "px", "lengthUnitPixel": "px",
"@lengthUnitPixel": {}, "@lengthUnitPixel": {},
@ -1191,9 +1191,9 @@
"@stopTooltip": {}, "@stopTooltip": {},
"chipActionLock": "Vergrendel", "chipActionLock": "Vergrendel",
"@chipActionLock": {}, "@chipActionLock": {},
"chipActionShowCountryStates": "Status laten xien", "chipActionShowCountryStates": "Status tonen",
"@chipActionShowCountryStates": {}, "@chipActionShowCountryStates": {},
"chipActionGoToPlacePage": "Laat zien in plaatsen", "chipActionGoToPlacePage": "In Plaatsen tonen",
"@chipActionGoToPlacePage": {}, "@chipActionGoToPlacePage": {},
"subtitlePositionTop": "Boven", "subtitlePositionTop": "Boven",
"@subtitlePositionTop": {}, "@subtitlePositionTop": {},
@ -1227,7 +1227,7 @@
"@aboutDataUsageMisc": {}, "@aboutDataUsageMisc": {},
"settingsModificationWarningDialogMessage": "Andere instellingen zullen worden aangepast.", "settingsModificationWarningDialogMessage": "Andere instellingen zullen worden aangepast.",
"@settingsModificationWarningDialogMessage": {}, "@settingsModificationWarningDialogMessage": {},
"vaultDialogLockModeWhenScreenOff": "Vergrendel als scherm uitgaat", "vaultDialogLockModeWhenScreenOff": "Vergrendelen wanneer het scherm wordt uitgeschakeld",
"@vaultDialogLockModeWhenScreenOff": {}, "@vaultDialogLockModeWhenScreenOff": {},
"aboutDataUsageData": "Data", "aboutDataUsageData": "Data",
"@aboutDataUsageData": {}, "@aboutDataUsageData": {},
@ -1269,8 +1269,122 @@
"@maxBrightnessNever": {}, "@maxBrightnessNever": {},
"videoResumptionModeAlways": "Altijd", "videoResumptionModeAlways": "Altijd",
"@videoResumptionModeAlways": {}, "@videoResumptionModeAlways": {},
"exportEntryDialogWriteMetadata": "Schrijf metadata", "exportEntryDialogWriteMetadata": "Metadata schrijven",
"@exportEntryDialogWriteMetadata": {}, "@exportEntryDialogWriteMetadata": {},
"chipActionShowCollection": "Tonen in Collectie", "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": {}, "@castDialogTitle": {},
"settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR", "settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR",
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"setHomeCustomCollection": "Własna kolekcja",
"@setHomeCustomCollection": {},
"collectionActionSetHome": "Ustaw jako stronę główną", "collectionActionSetHome": "Ustaw jako stronę główną",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"videoRepeatActionSetStart": "Ustaw początek", "videoRepeatActionSetStart": "Ustaw początek",

View file

@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Definir como início", "collectionActionSetHome": "Definir como início",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "Coleção personalizada",
"@setHomeCustomCollection": {},
"videoActionABRepeat": "Repetição A-B", "videoActionABRepeat": "Repetição A-B",
"@videoActionABRepeat": {}, "@videoActionABRepeat": {},
"videoRepeatActionSetEnd": "Definir fim", "videoRepeatActionSetEnd": "Definir fim",
@ -1372,5 +1370,13 @@
"videoRepeatActionSetStart": "Definir início", "videoRepeatActionSetStart": "Definir início",
"@videoRepeatActionSetStart": {}, "@videoRepeatActionSetStart": {},
"chipActionShowCollection": "Mostrar na Coleção", "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": {}, "@collectionActionSetHome": {},
"aboutDataUsageClearCache": "Golește memoria cache", "aboutDataUsageClearCache": "Golește memoria cache",
"@aboutDataUsageClearCache": {}, "@aboutDataUsageClearCache": {},
"setHomeCustomCollection": "Colecție personalizată",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR", "settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR",
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"settingsViewerShowHistogram": "Afișare histogramă", "settingsViewerShowHistogram": "Afișare histogramă",

View file

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

View file

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

View file

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

View file

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

View file

@ -1515,8 +1515,6 @@
"@entryActionCast": {}, "@entryActionCast": {},
"castDialogTitle": "Thiết bị truyền", "castDialogTitle": "Thiết bị truyền",
"@castDialogTitle": {}, "@castDialogTitle": {},
"setHomeCustomCollection": "Bộ sưu tập tùy chỉnh",
"@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR", "settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR",
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "Đặt làm nhà", "collectionActionSetHome": "Đặt làm nhà",
@ -1534,5 +1532,13 @@
"settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập", "settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Hiển thị trong Bộ sưu tập", "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": {}, "@aboutTranslatorsSectionTitle": {},
"aboutLicensesSectionTitle": "开源许可协议", "aboutLicensesSectionTitle": "开源许可协议",
"@aboutLicensesSectionTitle": {}, "@aboutLicensesSectionTitle": {},
"aboutLicensesBanner": "本应用使用以下开源软件包和库", "aboutLicensesBanner": "本应用使用以下开源软件包和库",
"@aboutLicensesBanner": {}, "@aboutLicensesBanner": {},
"aboutLicensesShowAllButtonLabel": "显示所有许可协议", "aboutLicensesShowAllButtonLabel": "显示所有许可协议",
"@aboutLicensesShowAllButtonLabel": {}, "@aboutLicensesShowAllButtonLabel": {},
@ -1161,9 +1161,9 @@
"@settingsSubtitleThemeTextPositionTile": {}, "@settingsSubtitleThemeTextPositionTile": {},
"settingsSubtitleThemeTextPositionDialogTitle": "文本位置", "settingsSubtitleThemeTextPositionDialogTitle": "文本位置",
"@settingsSubtitleThemeTextPositionDialogTitle": {}, "@settingsSubtitleThemeTextPositionDialogTitle": {},
"aboutLicensesDartPackagesSectionTitle": "Dart Packages", "aboutLicensesDartPackagesSectionTitle": "Dart 软件包",
"@aboutLicensesDartPackagesSectionTitle": {}, "@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesFlutterPackagesSectionTitle": "Flutter Packages", "aboutLicensesFlutterPackagesSectionTitle": "Flutter 软件包",
"@aboutLicensesFlutterPackagesSectionTitle": {}, "@aboutLicensesFlutterPackagesSectionTitle": {},
"keepScreenOnVideoPlayback": "视频播放期间", "keepScreenOnVideoPlayback": "视频播放期间",
"@keepScreenOnVideoPlayback": {}, "@keepScreenOnVideoPlayback": {},
@ -1361,8 +1361,6 @@
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"collectionActionSetHome": "设置为首页", "collectionActionSetHome": "设置为首页",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "自定义媒体集",
"@setHomeCustomCollection": {},
"videoRepeatActionSetStart": "设置起点", "videoRepeatActionSetStart": "设置起点",
"@videoRepeatActionSetStart": {}, "@videoRepeatActionSetStart": {},
"stopTooltip": "停止", "stopTooltip": "停止",

View file

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

View file

@ -93,6 +93,8 @@ class Contributors {
Contributor('Maxi', 'maxitendo01@proton.me'), Contributor('Maxi', 'maxitendo01@proton.me'),
Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'), Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'),
Contributor('elfriob', 'elfriob@ya.ru'), 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('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese // Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese // Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
@ -102,6 +104,7 @@ class Contributors {
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi // Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi // Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
// Contributor('Sartaj', 'ssaarrttaajj111@gmail.com'), // Hindi
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada // Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central) // Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
// Contributor('Rasti K5', 'rasti.khdhr@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); 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<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; 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.maxBrightnessKey:
case SettingKeys.keepScreenOnKey: case SettingKeys.keepScreenOnKey:
case SettingKeys.homePageKey: case SettingKeys.homePageKey:
case SettingKeys.homeCustomExplorerPathKey:
case SettingKeys.collectionGroupFactorKey: case SettingKeys.collectionGroupFactorKey:
case SettingKeys.collectionSortFactorKey: case SettingKeys.collectionSortFactorKey:
case SettingKeys.thumbnailLocationIconKey: case SettingKeys.thumbnailLocationIconKey:

View file

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

View file

@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource {
final Set<String> _changedUris = {}; final Set<String> _changedUris = {};
int? _lastGeneration; int? _lastGeneration;
SourceInitializationState _initState = SourceInitializationState.none; SourceInitializationState _initState = SourceInitializationState.none;
bool _safeMode = false;
@override
set safeMode(bool enabled) => _safeMode = enabled;
@override @override
SourceInitializationState get initState => _initState; SourceInitializationState get initState => _initState;
@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
analysisController: analysisController, analysisController: analysisController,
directory: directory, directory: directory,
loadTopEntriesFirst: loadTopEntriesFirst, loadTopEntriesFirst: loadTopEntriesFirst,
canAnalyze: canAnalyze, canAnalyze: canAnalyze && !_safeMode,
)); ));
} }
@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource {
pendingNewEntries.clear(); pendingNewEntries.clear();
} }
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen( mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
(entry) { (entry) {
// when discovering modified entry with known content ID, // when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc. // 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 { Future<void> start(dynamic args) async {
List<int>? entryIds; List<int>? entryIds;
var force = false; var force = false;
var progressTotal = 0, progressOffset = 0;
if (args is Map) { if (args is Map) {
entryIds = (args['entryIds'] as List?)?.cast<int>(); entryIds = (args['entryIds'] as List?)?.cast<int>();
force = args['force'] ?? false; 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?.dispose();
_controller = AnalysisController( _controller = AnalysisController(
canStartService: false, canStartService: false,
entryIds: entryIds, entryIds: entryIds,
force: force, force: force,
progressTotal: progressTotal,
progressOffset: progressOffset,
); );
settings.systemLocalesFallback = await deviceService.getLocales(); settings.systemLocalesFallback = await deviceService.getLocales();

View file

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

View file

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

View file

@ -106,8 +106,8 @@ class AndroidFileUtils {
if (isScreenshotsPath(dirPath)) return AlbumType.screenshots; if (isScreenshotsPath(dirPath)) return AlbumType.screenshots;
if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures; if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures;
final dir = pContext.split(dirPath).last; final dir = pContext.split(dirPath).lastOrNull;
if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app; if (dir != null && dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
return AlbumType.regular; 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) { Widget build(BuildContext context) {
return AvesScaffold( return AvesScaffold(
body: AvesPopScope( body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop], handlers: [tvNavigationPopHandler],
child: Row( child: Row(
children: [ children: [
TvRail( TvRail(

View file

@ -174,7 +174,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// Flutter has various page transition implementations for Android: // Flutter has various page transition implementations for Android:
// - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 // - `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 const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
static ScreenBrightness? _screenBrightness; static ScreenBrightness? _screenBrightness;

View file

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

View file

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

View file

@ -1,48 +1,49 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.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:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:overlay_support/overlay_support.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; bool _backOnce = false;
Timer? _backTimer; Timer? _backTimer;
DoubleBackPopHandler() { DoubleBackPopHandler._private();
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated( @override
library: 'aves', bool canPop(BuildContext context) {
className: '$DoubleBackPopHandler', if (context.select<Settings, bool>((s) => !s.mustBackTwiceToExit)) return true;
object: this, if (Navigator.canPop(context)) return true;
); return false;
}
} }
void dispose() { @override
if (kFlutterMemoryAllocationsEnabled) { void onPopBlocked(BuildContext context) {
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); if (_backOnce) {
} if (Navigator.canPop(context)) {
_stopBackTimer(); Navigator.maybeOf(context)?.pop();
} } else {
// exit
bool pop(BuildContext context) { reportService.log('Exit by pop');
if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) { PopExitNotification().dispatch(context);
SystemNavigator.pop();
}
} else {
_backOnce = true; _backOnce = true;
_stopBackTimer(); _backTimer?.cancel();
_backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false); _backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false);
toast( toast(
context.l10n.doubleBackExitMessage, context.l10n.doubleBackExitMessage,
duration: ADurations.doubleBackTimerDelay, 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:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random // this widget combines multiple pop handlers with a guaranteed order
// so this widget combines multiple handlers with a guaranteed order
class AvesPopScope extends StatelessWidget { class AvesPopScope extends StatelessWidget {
final List<bool Function(BuildContext context)> handlers; final List<PopHandler> handlers;
final Widget child; final Widget child;
const AvesPopScope({ const AvesPopScope({
@ -16,21 +14,12 @@ class AvesPopScope extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final blocker = handlers.firstWhereOrNull((v) => !v.canPop(context));
return PopScope( return PopScope(
canPop: false, canPop: blocker == null,
onPopInvoked: (didPop) { onPopInvoked: (didPop) {
if (didPop) return; if (!didPop) {
blocker?.onPopBlocked(context);
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();
}
} }
}, },
child: child, 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 @immutable
class PopExitNotification extends Notification {} 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_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/collection/collection_page.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/common/extensions/build_context.dart';
import 'package:aves/widgets/explorer/explorer_page.dart'; import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality final TvNavigationPopHandler tvNavigationPopHandler = TvNavigationPopHandler._private();
class TvNavigationPopHandler {
static bool pop(BuildContext context) {
if (!settings.useTvLayout || _isHome(context)) {
return true;
}
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
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( Navigator.maybeOf(context)?.pushAndRemoveUntil(
_getHomeRoute(), _getHomeRoute(),
(route) => false, (route) => false,
); );
return false;
} }
static bool _isHome(BuildContext context) { static bool _isHome(BuildContext context) {

View file

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

View file

@ -67,7 +67,7 @@ class AppDebugPage extends StatelessWidget {
], ],
), ),
body: AvesPopScope( body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop], handlers: [tvNavigationPopHandler],
child: SafeArea( child: SafeArea(
child: ListView( child: ListView(
padding: const EdgeInsets.all(8), 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/icons.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/utils/android_file_utils.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/view/view.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.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/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/search/route.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/search/search_delegate.dart';
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart'; import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -108,32 +112,75 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
onPressed: () => _goToSearch(context), onPressed: () => _goToSearch(context),
tooltip: MaterialLocalizations.of(context).searchFieldLabel, tooltip: MaterialLocalizations.of(context).searchFieldLabel,
), ),
if (_volumes.length > 1) if (_volumes.length > 1) _buildVolumeSelector(context),
FontSizeIconTheme( PopupMenuButton<ExplorerAction>(
child: PopupMenuButton<StorageVolume>( itemBuilder: (context) {
itemBuilder: (context) { return [
return _volumes.map((v) { ExplorerAction.addShortcut,
final selected = widget.directoryNotifier.value.volumePath == v.path; ExplorerAction.setHome,
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain; ].map((v) {
return PopupMenuItem( return PopupMenuItem(
value: v, value: v,
enabled: !selected, child: MenuRow(text: v.getText(context), icon: v.getIcon()),
child: MenuRow( );
text: v.getDescription(context), }).toList();
icon: Icon(icon), },
), onSelected: (action) async {
); // wait for the popup menu to hide before proceeding with the action
}).toList(); await Future.delayed(animations.popUpAnimationDelay * timeDilation);
}, final directory = widget.directoryNotifier.value;
onSelected: (volume) async { ExplorerActionDelegate(directory: directory).onActionSelected(context, action);
// wait for the popup menu to hide before proceeding with the action },
await Future.delayed(animations.popUpAnimationDelay * timeDilation); popUpAnimationStyle: animations.popUpAnimationStyle,
widget.goTo(volume.path); ),
}, ].map((v) => FontSizeIconTheme(child: v)).toList();
popUpAnimationStyle: animations.popUpAnimationStyle, }
),
), 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(
value: v,
enabled: !selected,
child: MenuRow(
text: v.getDescription(context),
icon: Icon(icon),
),
);
}).toList();
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);
}
},
tooltip: context.l10n.explorerActionSelectStorageVolume,
);
}
} }
double get appBarContentHeight { 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 List<StreamSubscription> _subscriptions = [];
final ValueNotifier<VolumeRelativeDirectory> _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: '')); final ValueNotifier<VolumeRelativeDirectory> _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: ''));
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]); final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
Set<StorageVolume> get _volumes => androidFileUtils.storageVolumes; Set<StorageVolume> get _volumes => androidFileUtils.storageVolumes;
@ -78,99 +77,95 @@ class _ExplorerPageState extends State<ExplorerPage> {
..clear(); ..clear();
_directory.dispose(); _directory.dispose();
_contents.dispose(); _contents.dispose();
_doubleBackPopHandler.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesPopScope( return ValueListenableBuilder<VolumeRelativeDirectory>(
handlers: [ valueListenable: _directory,
(context) { builder: (context, directory, child) {
if (_directory.value.relativeDir.isNotEmpty) { final atRoot = directory.relativeDir.isEmpty;
final parent = pContext.dirname(_currentDirectoryPath); return AvesPopScope(
_goTo(parent); handlers: [
return false; APopHandler(
} canPop: (context) => atRoot,
return true; onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)),
}, ),
TvNavigationPopHandler.pop, tvNavigationPopHandler,
_doubleBackPopHandler.pop, doubleBackPopHandler,
], ],
child: AvesScaffold( child: AvesScaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
body: GestureAreaProtectorStack( body: GestureAreaProtectorStack(
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
child: ValueListenableBuilder<List<Directory>>( child: ValueListenableBuilder<List<Directory>>(
valueListenable: _contents, valueListenable: _contents,
builder: (context, contents, child) { builder: (context, contents, child) {
final durations = context.watch<DurationsData>(); final durations = context.watch<DurationsData>();
return CustomScrollView( return CustomScrollView(
// workaround to prevent scrolling the app bar away // workaround to prevent scrolling the app bar away
// when there is no content and we use `SliverFillRemaining` // when there is no content and we use `SliverFillRemaining`
physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null, physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [ slivers: [
ExplorerAppBar( ExplorerAppBar(
key: const Key('appbar'), key: const Key('appbar'),
directoryNotifier: _directory, directoryNotifier: _directory,
goTo: _goTo, goTo: _goTo,
), ),
AnimationLimiter( AnimationLimiter(
// animation limiter should not be above the app bar // animation limiter should not be above the app bar
// so that the crumb line can automatically scroll // so that the crumb line can automatically scroll
key: ValueKey(_currentDirectoryPath), key: ValueKey(_currentDirectoryPath),
child: SliverList.builder( child: SliverList.builder(
itemBuilder: (context, index) { itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList( return AnimationConfiguration.staggeredList(
position: index, position: index,
duration: durations.staggeredAnimation, duration: durations.staggeredAnimation,
delay: durations.staggeredAnimationDelay * timeDilation, delay: durations.staggeredAnimationDelay * timeDilation,
child: SlideAnimation( child: SlideAnimation(
verticalOffset: 50.0, verticalOffset: 50.0,
child: FadeInAnimation( child: FadeInAnimation(
child: _buildContentLine(context, contents[index]), child: _buildContentLine(context, contents[index]),
), ),
), ),
); );
}, },
itemCount: contents.length, itemCount: contents.length,
), ),
), ),
contents.isEmpty contents.isEmpty
? SliverFillRemaining( ? SliverFillRemaining(
child: _buildEmptyContent(), child: _buildEmptyContent(),
) )
: const SliverPadding(padding: EdgeInsets.only(bottom: 8)), : const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
], ],
); );
}, },
), ),
), ),
const Divider(height: 0), const Divider(height: 0),
SafeArea( SafeArea(
top: false, top: false,
bottom: true, bottom: true,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: ValueListenableBuilder<VolumeRelativeDirectory>( child: AvesFilterChip(
valueListenable: _directory,
builder: (context, directory, child) {
return AvesFilterChip(
filter: PathFilter(_currentDirectoryPath), filter: PathFilter(_currentDirectoryPath),
maxWidth: double.infinity, maxWidth: double.infinity,
onTap: (filter) => _goToCollectionPage(context, filter), onTap: (filter) => _goToCollectionPage(context, filter),
onLongPress: null, onLongPress: null,
); ),
}, ),
), ),
), ],
), ),
], ),
), ),
), );
), },
); );
} }

View file

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

View file

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

View file

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

View file

@ -2,8 +2,10 @@ import 'dart:async';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.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/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/text.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart';
@ -43,24 +45,44 @@ class NavigationSection extends SettingsSection {
class _HomeOption { class _HomeOption {
final HomePageSetting page; final HomePageSetting page;
final Set<CollectionFilter> customCollection; final Set<CollectionFilter> customCollection;
final String? customExplorerPath;
const _HomeOption( const _HomeOption(
this.page, { this.page, {
this.customCollection = const {}, this.customCollection = const {},
this.customExplorerPath,
}); });
String getName(BuildContext context) { String getName(BuildContext context) {
if (page == HomePageSetting.collection && customCollection.isNotEmpty) { final pageName = page.getName(context);
return context.l10n.setHomeCustomCollection; 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 @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 @override
int get hashCode => page.hashCode ^ customCollection.hashCode; int get hashCode => page.hashCode ^ customCollection.hashCode ^ customExplorerPath.hashCode;
} }
class SettingsTileNavigationHomePage extends SettingsTile { class SettingsTileNavigationHomePage extends SettingsTile {
@ -75,15 +97,18 @@ class SettingsTileNavigationHomePage extends SettingsTile {
const _HomeOption(HomePageSetting.tags), const _HomeOption(HomePageSetting.tags),
const _HomeOption(HomePageSetting.explorer), const _HomeOption(HomePageSetting.explorer),
if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection), 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), getName: (context, v) => v.getName(context),
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection), selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection, customExplorerPath: s.homeCustomExplorerPath),
onSelection: (v) { onSelection: (v) => settings.setHome(
settings.homePage = v.page; v.page,
settings.homeCustomCollection = v.customCollection; customCollection: v.customCollection,
}, customExplorerPath: v.customExplorerPath,
),
tileTitle: title(context), tileTitle: title(context),
dialogTitle: context.l10n.settingsHomeDialogTitle, dialogTitle: context.l10n.settingsHomeDialogTitle,
optionSubtitleBuilder: (v) => v.getDetails(context),
); );
} }

View file

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

View file

@ -1,6 +1,7 @@
library aves_model; library aves_model;
export 'src/actions/chip.dart'; export 'src/actions/chip.dart';
export 'src/actions/explorer.dart';
export 'src/actions/chip_set.dart'; export 'src/actions/chip_set.dart';
export 'src/actions/entry.dart'; export 'src/actions/entry.dart';
export 'src/actions/entry_set.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 keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page'; static const homePageKey = 'home_page';
static const homeCustomCollectionKey = 'home_custom_collection'; static const homeCustomCollectionKey = 'home_custom_collection';
static const homeCustomExplorerPathKey = 'home_custom_explorer_path';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmCreateVaultKey = 'confirm_create_vault'; static const confirmCreateVaultKey = 'confirm_create_vault';
static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmDeleteForeverKey = 'confirm_delete_forever';

View file

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

View file

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

View file

@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
} }
@override @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; static var _lastId = 1;

View file

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

View file

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

View file

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