Merge branch 'develop'
This commit is contained in:
commit
d813a61b9b
100 changed files with 906 additions and 485 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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">Video’s</string>
|
<string name="videos_shortcut_short_label">Video’s</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>
|
|
@ -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,
|
|
@ -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
|
||||||
}
|
}
|
||||||
/**
|
/**
|
|
@ -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")
|
||||||
|
|
||||||
|
|
4
fastlane/metadata/android/en-US/changelogs/125.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/125.txt
Normal 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
|
4
fastlane/metadata/android/en-US/changelogs/12501.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/12501.txt
Normal 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
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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>.
|
||||||
|
|
|
@ -1519,8 +1519,6 @@
|
||||||
"@settingsThumbnailShowHdrIcon": {},
|
"@settingsThumbnailShowHdrIcon": {},
|
||||||
"collectionActionSetHome": "تعيين كخلفية",
|
"collectionActionSetHome": "تعيين كخلفية",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "مجموعة مخصصة",
|
|
||||||
"@setHomeCustomCollection": {},
|
|
||||||
"videoActionABRepeat": "تكرار A-B",
|
"videoActionABRepeat": "تكرار A-B",
|
||||||
"@videoActionABRepeat": {},
|
"@videoActionABRepeat": {},
|
||||||
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",
|
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",
|
||||||
|
|
|
@ -1517,8 +1517,6 @@
|
||||||
},
|
},
|
||||||
"collectionActionSetHome": "Усталяваць як галоўную",
|
"collectionActionSetHome": "Усталяваць як галоўную",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "Уласная калекцыя",
|
|
||||||
"@setHomeCustomCollection": {},
|
|
||||||
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
|
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
|
||||||
"@settingsThumbnailShowHdrIcon": {},
|
"@settingsThumbnailShowHdrIcon": {},
|
||||||
"videoRepeatActionSetEnd": "Усталяваць канец",
|
"videoRepeatActionSetEnd": "Усталяваць канец",
|
||||||
|
|
|
@ -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ú.",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -1099,8 +1099,6 @@
|
||||||
"@settingsSystemDefault": {},
|
"@settingsSystemDefault": {},
|
||||||
"settingsConfirmationTile": "درخواست های تایید",
|
"settingsConfirmationTile": "درخواست های تایید",
|
||||||
"@settingsConfirmationTile": {},
|
"@settingsConfirmationTile": {},
|
||||||
"setHomeCustomCollection": "مجموعه سفارشی",
|
|
||||||
"@setHomeCustomCollection": {},
|
|
||||||
"settingsKeepScreenOnDialogTitle": "صفحه را روشن نگه دار",
|
"settingsKeepScreenOnDialogTitle": "صفحه را روشن نگه دار",
|
||||||
"@settingsKeepScreenOnDialogTitle": {},
|
"@settingsKeepScreenOnDialogTitle": {},
|
||||||
"settingsShowBottomNavigationBar": "نمایش گزینهگان پیمایش پایین",
|
"settingsShowBottomNavigationBar": "نمایش گزینهگان پیمایش پایین",
|
||||||
|
|
|
@ -1359,8 +1359,6 @@
|
||||||
"@castDialogTitle": {},
|
"@castDialogTitle": {},
|
||||||
"collectionActionSetHome": "Définir comme page d’accueil",
|
"collectionActionSetHome": "Définir comme page d’accueil",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "Collection personnalisée",
|
|
||||||
"@setHomeCustomCollection": {},
|
|
||||||
"settingsThumbnailShowHdrIcon": "Afficher l’icône HDR",
|
"settingsThumbnailShowHdrIcon": "Afficher l’icô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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {},
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -1357,8 +1357,6 @@
|
||||||
"@overlayHistogramLuminance": {},
|
"@overlayHistogramLuminance": {},
|
||||||
"settingsModificationWarningDialogMessage": "他の設定は変更されます。",
|
"settingsModificationWarningDialogMessage": "他の設定は変更されます。",
|
||||||
"@settingsModificationWarningDialogMessage": {},
|
"@settingsModificationWarningDialogMessage": {},
|
||||||
"setHomeCustomCollection": "カスタムコレクション",
|
|
||||||
"@setHomeCustomCollection": {},
|
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives": "マルチタッチジェスチャーの選択肢を表示する",
|
"settingsAccessibilityShowPinchGestureAlternatives": "マルチタッチジェスチャーの選択肢を表示する",
|
||||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||||
"chipActionCreateVault": "保管庫を作成",
|
"chipActionCreateVault": "保管庫を作成",
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 foto’s en video’s 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 foto’s en video’s 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 foto’s",
|
"drawerCollectionMotionPhotos": "Bewegende foto’s",
|
||||||
"@drawerCollectionMotionPhotos": {},
|
"@drawerCollectionMotionPhotos": {},
|
||||||
"drawerCollectionPanoramas": "Panoramas",
|
"drawerCollectionPanoramas": "Panorama's",
|
||||||
"@drawerCollectionPanoramas": {},
|
"@drawerCollectionPanoramas": {},
|
||||||
"drawerCollectionRaws": "Raw foto’s",
|
"drawerCollectionRaws": "Raw foto’s",
|
||||||
"@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": "Foto’s en video’s die overeenkomen met verborgen filters, worden niet weergegeven in uw verzameling.",
|
"settingsHiddenFiltersBanner": "Foto’s en video’s 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": "Foto’s en video’s in deze mappen, of een van hun submappen, verschijnen niet in uw verzameling.",
|
"settingsHiddenPathsBanner": "Foto’s en video’s 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 directory’s bekijken waartoe u eerder toegang heeft verleend.",
|
"settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. Je kunt hier directory’s 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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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ă",
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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í",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": "停止",
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
23
lib/view/src/actions/explorer.dart
Normal file
23
lib/view/src/actions/explorer.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
75
lib/widgets/dialogs/select_storage_dialog.dart
Normal file
75
lib/widgets/dialogs/select_storage_dialog.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -20,4 +20,4 @@ Future<void> showSelectionDialog<T>({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef TextBuilder<T> = String Function(T value);
|
typedef TextBuilder<T> = String? Function(T value);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
85
lib/widgets/explorer/explorer_action_delegate.dart
Normal file
85
lib/widgets/explorer/explorer_action_delegate.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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!,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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';
|
||||||
|
|
4
plugins/aves_model/lib/src/actions/explorer.dart
Normal file
4
plugins/aves_model/lib/src/actions/explorer.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
enum ExplorerAction {
|
||||||
|
addShortcut,
|
||||||
|
setHome,
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
Loading…
Reference in a new issue