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="v1.11.6"></a>[v1.11.6] - 2024-07-17
|
||||
|
||||
### Added
|
||||
|
||||
- Explorer: set custom path as home
|
||||
- Explorer: create shortcut to custom path
|
||||
- predictive back support (inter-app)
|
||||
|
||||
### Changed
|
||||
|
||||
- target Android 15 (API 35)
|
||||
|
||||
### Fixed
|
||||
|
||||
- crash when cataloguing some PNG files
|
||||
|
||||
## <a id="v1.11.5"></a>[v1.11.5] - 2024-07-11
|
||||
|
||||
### Added
|
||||
|
|
|
@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
|
||||
android {
|
||||
namespace 'deckers.thibault.aves'
|
||||
compileSdk 34
|
||||
compileSdk 35
|
||||
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
|
||||
ndkVersion '26.1.10909125'
|
||||
|
||||
|
@ -66,7 +66,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId packageName
|
||||
minSdk flutter.minSdkVersion
|
||||
targetSdk 34
|
||||
targetSdk 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
|
||||
|
@ -211,6 +211,7 @@ dependencies {
|
|||
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c'
|
||||
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c'
|
||||
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
|
||||
implementation project(':exifinterface')
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
||||
|
||||
|
|
|
@ -14,10 +14,6 @@
|
|||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<!--
|
||||
TODO TLAD [Android 14 (API 34)] request/handle READ_MEDIA_VISUAL_USER_SELECTED permission
|
||||
cf https://developer.android.com/about/versions/14/changes/partial-photo-video-access
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
|
@ -35,10 +31,13 @@
|
|||
|
||||
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<!-- to provide a foreground service type, as required by Android 14 (API 34) -->
|
||||
<!-- TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_MEDIA_PROCESSING` -->
|
||||
<!-- to provide a foreground service type, as required from Android 14 (API 34) -->
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
||||
android:maxSdkVersion="34"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
@ -103,17 +102,12 @@
|
|||
</intent>
|
||||
</queries>
|
||||
|
||||
<!--
|
||||
as of Flutter v3.16.0, predictive back gesture does not work
|
||||
as expected when extending `FlutterFragmentActivity`
|
||||
so we disable `enableOnBackInvokedCallback`
|
||||
-->
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:appCategory="image"
|
||||
android:banner="@drawable/banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/full_backup_content"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
@ -261,11 +255,14 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- anonymous service for analysis worker is specified here to provide service type -->
|
||||
<!-- TODO TLAD [Android 15 (API 35)] use `mediaProcessing` -->
|
||||
<!--
|
||||
anonymous service for analysis worker is specified here to provide service type:
|
||||
- `dataSync` for Android 14 (API 34)
|
||||
- `mediaProcessing` from Android 15 (API 35)
|
||||
-->
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:foregroundServiceType="dataSync|mediaProcessing"
|
||||
tools:node="merge" />
|
||||
|
||||
<service
|
||||
|
|
|
@ -70,7 +70,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
private fun onStart() {
|
||||
Log.i(LOG_TAG, "Start analysis worker $id")
|
||||
runBlocking {
|
||||
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
|
||||
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) {
|
||||
flutterEngine = it
|
||||
}
|
||||
}
|
||||
|
@ -78,14 +78,15 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
try {
|
||||
initChannels(applicationContext)
|
||||
|
||||
val preferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
val entryIdStrings = preferences.getStringSet(PREF_ENTRY_IDS_KEY, null)
|
||||
|
||||
runBlocking {
|
||||
FlutterUtils.runOnUiThread {
|
||||
backgroundChannel?.invokeMethod(
|
||||
"start", hashMapOf(
|
||||
"entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(),
|
||||
"entryIds" to entryIdStrings?.map { Integer.parseUnsignedInt(it) }?.toList(),
|
||||
"force" to inputData.getBoolean(KEY_FORCE, false),
|
||||
"progressTotal" to inputData.getInt(KEY_PROGRESS_TOTAL, 0),
|
||||
"progressOffset" to inputData.getInt(KEY_PROGRESS_OFFSET, 0),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -179,13 +180,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
.setContentIntent(openAppIntent)
|
||||
.addAction(stopAction)
|
||||
.build()
|
||||
return if (Build.VERSION.SDK_INT >= 34) {
|
||||
// from Android 14 (API 34), foreground service type is mandatory
|
||||
// despite the sample code omitting it at:
|
||||
return if (Build.VERSION.SDK_INT == 34) {
|
||||
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
|
||||
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
||||
// TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING`
|
||||
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, type)
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else if (Build.VERSION.SDK_INT >= 35) {
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
|
||||
} else {
|
||||
ForegroundInfo(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
@ -195,14 +195,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
|
||||
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
|
||||
const val SHARED_PREFERENCES_KEY = "analysis_service"
|
||||
const val CALLBACK_HANDLE_KEY = "callback_handle"
|
||||
const val PREF_CALLBACK_HANDLE_KEY = "callback_handle"
|
||||
const val PREF_ENTRY_IDS_KEY = "entry_ids"
|
||||
|
||||
const val NOTIFICATION_CHANNEL = "analysis"
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
const val KEY_ENTRY_IDS = "entry_ids"
|
||||
const val KEY_FORCE = "force"
|
||||
const val KEY_PROGRESS_TOTAL = "progress_total"
|
||||
const val KEY_PROGRESS_OFFSET = "progress_offset"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
|
||||
|
||||
val pendingResult = goAsync()
|
||||
defaultScope.launch() {
|
||||
defaultScope.launch {
|
||||
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
|
||||
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ import deckers.thibault.aves.channel.streams.SettingsChangeStreamHandler
|
|||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
|
@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
|
|||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
open class MainActivity : FlutterFragmentActivity() {
|
||||
open class MainActivity : FlutterActivity() {
|
||||
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
||||
|
@ -294,11 +294,9 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
|
||||
fields[INTENT_DATA_KEY_SAFE_MODE] = true
|
||||
}
|
||||
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
||||
val filters = extractFiltersFromIntent(intent)
|
||||
fields[INTENT_DATA_KEY_PAGE] = page
|
||||
fields[INTENT_DATA_KEY_FILTERS] = filters
|
||||
}
|
||||
fields[INTENT_DATA_KEY_PAGE] = intent.getStringExtra(EXTRA_KEY_PAGE)
|
||||
fields[INTENT_DATA_KEY_FILTERS] = extractFiltersFromIntent(intent)
|
||||
fields[INTENT_DATA_KEY_EXPLORER_PATH] = intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH)
|
||||
return fields
|
||||
}
|
||||
|
||||
|
@ -527,6 +525,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
const val INTENT_DATA_KEY_ACTION = "action"
|
||||
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
|
||||
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness"
|
||||
const val INTENT_DATA_KEY_EXPLORER_PATH = "explorerPath"
|
||||
const val INTENT_DATA_KEY_FILTERS = "filters"
|
||||
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
|
||||
const val INTENT_DATA_KEY_PAGE = "page"
|
||||
|
@ -537,6 +536,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
|
||||
|
||||
const val EXTRA_KEY_PAGE = "page"
|
||||
const val EXTRA_KEY_EXPLORER_PATH = "explorerPath"
|
||||
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
|
||||
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
|
||||
const val EXTRA_KEY_SAFE_MODE = "safeMode"
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import deckers.thibault.aves.AnalysisWorker
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -19,7 +18,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
|
||||
class AnalysisHandler(private val activity: FlutterActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
@ -37,10 +36,11 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
|
|||
return
|
||||
}
|
||||
|
||||
activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
.apply()
|
||||
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
with(preferences.edit()) {
|
||||
putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
apply()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
|
@ -53,33 +53,24 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
|
|||
|
||||
// can be null or empty
|
||||
val allEntryIds = call.argument<List<Int>>("entryIds")
|
||||
val progressTotal = allEntryIds?.size ?: 0
|
||||
var progressOffset = 0
|
||||
|
||||
// work `Data` cannot occupy more than 10240 bytes when serialized
|
||||
// so we split it when we have a long list of entry IDs
|
||||
val chunked = allEntryIds?.chunked(WORK_DATA_CHUNK_SIZE) ?: listOf(null)
|
||||
|
||||
fun buildRequest(entryIds: List<Int>?, progressOffset: Int): OneTimeWorkRequest {
|
||||
val workData = workDataOf(
|
||||
AnalysisWorker.KEY_ENTRY_IDS to entryIds?.toIntArray(),
|
||||
AnalysisWorker.KEY_FORCE to force,
|
||||
AnalysisWorker.KEY_PROGRESS_TOTAL to progressTotal,
|
||||
AnalysisWorker.KEY_PROGRESS_OFFSET to progressOffset,
|
||||
)
|
||||
return OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build()
|
||||
// so we save the possibly long list of entry IDs to shared preferences
|
||||
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
with(preferences.edit()) {
|
||||
putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet())
|
||||
apply()
|
||||
}
|
||||
|
||||
var work = WorkManager.getInstance(activity).beginUniqueWork(
|
||||
val workData = workDataOf(
|
||||
AnalysisWorker.KEY_FORCE to force,
|
||||
)
|
||||
|
||||
WorkManager.getInstance(activity).beginUniqueWork(
|
||||
ANALYSIS_WORK_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
buildRequest(chunked.first(), progressOffset),
|
||||
)
|
||||
chunked.drop(1).forEach { entryIds ->
|
||||
progressOffset += WORK_DATA_CHUNK_SIZE
|
||||
work = work.then(buildRequest(entryIds, progressOffset))
|
||||
}
|
||||
work.enqueue()
|
||||
OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build(),
|
||||
).enqueue()
|
||||
|
||||
attachToActivity()
|
||||
result.success(null)
|
||||
|
@ -105,6 +96,5 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
|
|||
companion object {
|
||||
const val CHANNEL = "deckers.thibault/aves/analysis"
|
||||
private const val ANALYSIS_WORK_NAME = "analysis_work"
|
||||
private const val WORK_DATA_CHUNK_SIZE = 1000
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.bumptech.glide.Glide
|
|||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
|
||||
|
@ -351,8 +352,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val label = call.argument<String>("label")
|
||||
val iconBytes = call.argument<ByteArray>("iconBytes")
|
||||
val filters = call.argument<List<String>>("filters")
|
||||
val explorerPath = call.argument<String>("explorerPath")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (label == null || (filters == null && uri == null)) {
|
||||
if (label == null) {
|
||||
result.error("pin-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
@ -380,7 +382,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
|
||||
val intent = when {
|
||||
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
|
||||
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
|
||||
.putExtra(EXTRA_KEY_PAGE, "/collection")
|
||||
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
|
||||
|
@ -388,6 +389,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
// so we use a joined `String` as fallback
|
||||
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
||||
|
||||
explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
|
||||
.putExtra(EXTRA_KEY_PAGE, "/explorer")
|
||||
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
|
||||
|
||||
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
|
||||
else -> {
|
||||
result.error("pin-intent", "failed to build intent", null)
|
||||
return
|
||||
|
|
|
@ -12,7 +12,7 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import com.drew.metadata.file.FileTypeDirectory
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper
|
||||
|
|
|
@ -28,10 +28,11 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
|
|||
return
|
||||
}
|
||||
|
||||
context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
.apply()
|
||||
val preferences = context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||
with(preferences.edit()) {
|
||||
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
apply()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.media.MediaMetadataRetriever
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import com.adobe.internal.xmp.XMPException
|
||||
import com.adobe.internal.xmp.XMPMeta
|
||||
import com.adobe.internal.xmp.XMPMetaFactory
|
||||
|
|
|
@ -44,7 +44,8 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
|
|||
return
|
||||
}
|
||||
|
||||
with(getStore().edit()) {
|
||||
val preferences = getStore()
|
||||
with(preferences.edit()) {
|
||||
when (value) {
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
|
@ -21,9 +21,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class ImageOpStreamHandler(private val activity: FragmentActivity, private val arguments: Any?) : EventChannel.StreamHandler {
|
||||
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private lateinit var eventSink: EventSink
|
||||
private lateinit var handler: Handler
|
||||
|
|
|
@ -21,11 +21,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
|||
|
||||
private var knownEntries: Map<Long?, Int?>? = null
|
||||
private var directory: String? = null
|
||||
private var safe: Boolean = false
|
||||
|
||||
init {
|
||||
if (arguments is Map<*, *>) {
|
||||
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
|
||||
directory = arguments["directory"] as String?
|
||||
safe = arguments.getOrDefault("safe", false) as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +61,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
|||
}
|
||||
|
||||
private fun fetchAll() {
|
||||
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) }
|
||||
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) }
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
|
|
|
@ -152,12 +152,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
preferredConfig = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
|
||||
// for wide-gamut and HDR content which does not require alpha blending
|
||||
setPreferredConfig(Bitmap.Config.RGBA_1010102)
|
||||
Bitmap.Config.RGBA_1010102
|
||||
} else {
|
||||
setPreferredConfig(Bitmap.Config.ARGB_8888)
|
||||
Bitmap.Config.ARGB_8888
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package deckers.thibault.aves.metadata
|
||||
|
||||
import android.util.Log
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import com.drew.lang.Rational
|
||||
import com.drew.metadata.Directory
|
||||
import com.drew.metadata.exif.ExifDirectoryBase
|
||||
|
|
|
@ -2,7 +2,7 @@ package deckers.thibault.aves.metadata
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import com.adobe.internal.xmp.XMPMeta
|
||||
import com.drew.imaging.jpeg.JpegSegmentType
|
||||
import com.drew.metadata.exif.ExifDirectoryBase
|
||||
|
|
|
@ -29,7 +29,6 @@ import deckers.thibault.aves.metadata.GeoTiffKeys
|
|||
import deckers.thibault.aves.metadata.Metadata
|
||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context
|
|||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import com.drew.metadata.avi.AviDirectory
|
||||
import com.drew.metadata.exif.ExifIFD0Directory
|
||||
import com.drew.metadata.jpeg.JpegDirectory
|
||||
|
@ -116,8 +116,8 @@ class SourceEntry {
|
|||
// metadata retrieval
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
fun fillPreCatalogMetadata(context: Context): SourceEntry {
|
||||
if (isSvg) return this
|
||||
fun fillPreCatalogMetadata(context: Context, safe: Boolean): SourceEntry {
|
||||
if (isSvg || safe) return this
|
||||
if (isVideo) {
|
||||
fillVideoByMediaMetadataRetriever(context)
|
||||
if (isSized && hasDuration) return this
|
||||
|
|
|
@ -52,7 +52,7 @@ internal class FileImageProvider : ImageProvider() {
|
|||
callback.onFailure(e)
|
||||
}
|
||||
}
|
||||
entry.fillPreCatalogMetadata(context)
|
||||
entry.fillPreCatalogMetadata(context, safe = false)
|
||||
|
||||
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
|
||||
callback.onSuccess(entry.toMap())
|
||||
|
|
|
@ -11,8 +11,7 @@ import android.net.Uri
|
|||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
|
@ -196,7 +195,7 @@ abstract class ImageProvider {
|
|||
}
|
||||
|
||||
suspend fun convertMultiple(
|
||||
activity: FragmentActivity,
|
||||
activity: Activity,
|
||||
imageExportMimeType: String,
|
||||
targetDir: String,
|
||||
entries: List<AvesEntry>,
|
||||
|
@ -255,7 +254,7 @@ abstract class ImageProvider {
|
|||
}
|
||||
|
||||
private suspend fun convertSingle(
|
||||
activity: FragmentActivity,
|
||||
activity: Activity,
|
||||
sourceEntry: AvesEntry,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
|
@ -334,7 +333,7 @@ abstract class ImageProvider {
|
|||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
|
||||
target = Glide.with(activity)
|
||||
target = Glide.with(activity.applicationContext)
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(model)
|
||||
|
@ -396,7 +395,7 @@ abstract class ImageProvider {
|
|||
return newFields
|
||||
} finally {
|
||||
// clearing Glide target should happen after effectively writing the bitmap
|
||||
Glide.with(activity).clear(target)
|
||||
Glide.with(activity.applicationContext).clear(target)
|
||||
|
||||
resolution.replacementFile?.delete()
|
||||
}
|
||||
|
|
|
@ -51,8 +51,10 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
context: Context,
|
||||
knownEntries: Map<Long?, Int?>,
|
||||
directory: String?,
|
||||
safe: Boolean,
|
||||
handleNewEntry: NewEntryHandler,
|
||||
) {
|
||||
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory safe=$safe")
|
||||
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
|
||||
val knownDate = knownEntries[contentId]
|
||||
return knownDate == null || knownDate < dateModifiedSecs
|
||||
|
@ -82,8 +84,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
} else {
|
||||
handleNew = handleNewEntry
|
||||
}
|
||||
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs)
|
||||
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs)
|
||||
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs, safe = safe)
|
||||
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs, safe = safe)
|
||||
}
|
||||
|
||||
// the provided URI can point to the wrong media collection,
|
||||
|
@ -206,6 +208,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
selection: String? = null,
|
||||
selectionArgs: Array<String>? = null,
|
||||
fileMimeType: String? = null,
|
||||
safe: Boolean = false,
|
||||
): Boolean {
|
||||
var found = false
|
||||
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
|
||||
|
@ -299,7 +302,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// missing some attributes such as width, height, orientation.
|
||||
// Also, the reported size of raw images is inconsistent across devices
|
||||
// and Android versions (sometimes the raw size, sometimes the decoded size).
|
||||
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
|
||||
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe)
|
||||
entryMap = entry.toMap()
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() {
|
|||
return
|
||||
}
|
||||
|
||||
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
|
||||
val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false)
|
||||
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
|
||||
callback.onSuccess(entry.toMap())
|
||||
} else {
|
||||
|
|
|
@ -5,5 +5,5 @@ import kotlin.math.pow
|
|||
|
||||
object MathUtils {
|
||||
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
|
||||
fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
|
||||
private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||
import deckers.thibault.aves.decoder.MultiPageImage
|
||||
|
||||
object MimeTypes {
|
||||
|
@ -17,8 +17,8 @@ object MimeTypes {
|
|||
private const val ICO = "image/x-icon"
|
||||
const val JPEG = "image/jpeg"
|
||||
const val PNG = "image/png"
|
||||
const val PSD_VND = "image/vnd.adobe.photoshop"
|
||||
const val PSD_X = "image/x-photoshop"
|
||||
private const val PSD_VND = "image/vnd.adobe.photoshop"
|
||||
private const val PSD_X = "image/x-photoshop"
|
||||
const val TIFF = "image/tiff"
|
||||
private const val WBMP = "image/vnd.wap.wbmp"
|
||||
const val WEBP = "image/webp"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Foto Lijstje</string>
|
||||
<string name="app_widget_label">Fotolijst</string>
|
||||
<string name="wallpaper">Achtergrond</string>
|
||||
<string name="search_shortcut_short_label">Zoeken</string>
|
||||
<string name="videos_shortcut_short_label">Video’s</string>
|
||||
<string name="analysis_channel_name">Media indexeren</string>
|
||||
<string name="analysis_notification_default_title">Indexeren van media</string>
|
||||
<string name="analysis_notification_action_stop">Stop</string>
|
||||
<string name="analysis_notification_action_stop">Stoppen</string>
|
||||
<string name="safe_mode_shortcut_short_label">Veilige modus</string>
|
||||
</resources>
|
|
@ -16,12 +16,12 @@
|
|||
|
||||
package androidx.exifinterface.media;
|
||||
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
|
||||
import static java.nio.ByteOrder.BIG_ENDIAN;
|
||||
import static java.nio.ByteOrder.LITTLE_ENDIAN;
|
||||
|
||||
|
@ -41,8 +41,8 @@ import androidx.annotation.IntDef;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl;
|
||||
import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl;
|
||||
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
|
||||
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
|
@ -84,6 +84,7 @@ import java.util.zip.CRC32;
|
|||
|
||||
/*
|
||||
* Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21
|
||||
* Named differently to let ExifInterface be loaded as subdependency.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -97,7 +98,7 @@ import java.util.zip.CRC32;
|
|||
* it. This class will search both locations for XMP data, but if XMP data exist both inside and
|
||||
* outside Exif, will favor the XMP data inside Exif over the one outside.
|
||||
*/
|
||||
public class ExifInterface {
|
||||
public class ExifInterfaceFork {
|
||||
// TLAD threshold for safer Exif attribute parsing
|
||||
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
|
||||
|
||||
|
@ -3949,7 +3950,7 @@ public class ExifInterface {
|
|||
* @throws IOException if an I/O error occurs while retrieving file descriptor via
|
||||
* {@link FileInputStream#getFD()}.
|
||||
*/
|
||||
public ExifInterface(@NonNull File file) throws IOException {
|
||||
public ExifInterfaceFork(@NonNull File file) throws IOException {
|
||||
if (file == null) {
|
||||
throw new NullPointerException("file cannot be null");
|
||||
}
|
||||
|
@ -3964,7 +3965,7 @@ public class ExifInterface {
|
|||
* @throws IOException if an I/O error occurs while retrieving file descriptor via
|
||||
* {@link FileInputStream#getFD()}.
|
||||
*/
|
||||
public ExifInterface(@NonNull String filename) throws IOException {
|
||||
public ExifInterfaceFork(@NonNull String filename) throws IOException {
|
||||
if (filename == null) {
|
||||
throw new NullPointerException("filename cannot be null");
|
||||
}
|
||||
|
@ -3980,7 +3981,7 @@ public class ExifInterface {
|
|||
* @throws NullPointerException if file descriptor is null
|
||||
* @throws IOException if an error occurs while duplicating the file descriptor.
|
||||
*/
|
||||
public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException {
|
||||
public ExifInterfaceFork(@NonNull FileDescriptor fileDescriptor) throws IOException {
|
||||
if (fileDescriptor == null) {
|
||||
throw new NullPointerException("fileDescriptor cannot be null");
|
||||
}
|
||||
|
@ -4023,7 +4024,7 @@ public class ExifInterface {
|
|||
* @param inputStream the input stream that contains the image data
|
||||
* @throws NullPointerException if the input stream is null
|
||||
*/
|
||||
public ExifInterface(@NonNull InputStream inputStream) throws IOException {
|
||||
public ExifInterfaceFork(@NonNull InputStream inputStream) throws IOException {
|
||||
this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA);
|
||||
}
|
||||
|
||||
|
@ -4039,7 +4040,7 @@ public class ExifInterface {
|
|||
* @throws IOException if an I/O error occurs while retrieving file descriptor via
|
||||
* {@link FileInputStream#getFD()}.
|
||||
*/
|
||||
public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType)
|
||||
public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType)
|
||||
throws IOException {
|
||||
if (inputStream == null) {
|
||||
throw new NullPointerException("inputStream cannot be null");
|
||||
|
@ -5071,7 +5072,7 @@ public class ExifInterface {
|
|||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
|
||||
setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider());
|
||||
setLatLong(location.getLatitude(), location.getLongitude());
|
||||
setAltitude(location.getAltitude());
|
||||
// Location objects store speeds in m/sec. Translates it to km/hr here.
|
||||
|
@ -5080,8 +5081,8 @@ public class ExifInterface {
|
|||
* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
|
||||
String[] dateTime = sFormatterPrimary.format(
|
||||
new Date(location.getTime())).split("\\s+", -1);
|
||||
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
|
||||
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
|
||||
setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
|
||||
setAttribute(ExifInterfaceFork.TAG_GPS_TIMESTAMP, dateTime[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5158,11 +5159,11 @@ public class ExifInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
|
||||
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME} value as number of milliseconds since
|
||||
* Jan. 1, 1970, midnight local time.
|
||||
*
|
||||
* <p>Note: The return value includes the first three digits (or less depending on the length
|
||||
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
|
||||
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME}.
|
||||
*
|
||||
* @return null if date time information is unavailable or invalid.
|
||||
*/
|
||||
|
@ -5175,11 +5176,11 @@ public class ExifInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
|
||||
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_DIGITIZED} value as number of
|
||||
* milliseconds since Jan. 1, 1970, midnight local time.
|
||||
*
|
||||
* <p>Note: The return value includes the first three digits (or less depending on the length
|
||||
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
|
||||
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_DIGITIZED}.
|
||||
*
|
||||
* @return null if digitized date time information is unavailable or invalid.
|
||||
*/
|
||||
|
@ -5192,11 +5193,11 @@ public class ExifInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
|
||||
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_ORIGINAL} value as number of
|
||||
* milliseconds since Jan. 1, 1970, midnight local time.
|
||||
*
|
||||
* <p>Note: The return value includes the first three digits (or less depending on the length
|
||||
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
|
||||
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_ORIGINAL}.
|
||||
*
|
||||
* @return null if original date time information is unavailable or invalid.
|
||||
*/
|
||||
|
@ -5910,18 +5911,18 @@ public class ExifInterface {
|
|||
}
|
||||
|
||||
if (rotation != null) {
|
||||
int orientation = ExifInterface.ORIENTATION_NORMAL;
|
||||
int orientation = ExifInterfaceFork.ORIENTATION_NORMAL;
|
||||
|
||||
// all rotation angles in CW
|
||||
switch (Integer.parseInt(rotation)) {
|
||||
case 90:
|
||||
orientation = ExifInterface.ORIENTATION_ROTATE_90;
|
||||
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90;
|
||||
break;
|
||||
case 180:
|
||||
orientation = ExifInterface.ORIENTATION_ROTATE_180;
|
||||
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180;
|
||||
break;
|
||||
case 270:
|
||||
orientation = ExifInterface.ORIENTATION_ROTATE_270;
|
||||
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -6175,7 +6176,11 @@ public class ExifInterface {
|
|||
// IEND marks the end of the image.
|
||||
break;
|
||||
} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
|
||||
// TODO: Need to handle potential OutOfMemoryError
|
||||
// TLAD start
|
||||
if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
|
||||
throw new IOException("dangerous exif chunk size=" + length);
|
||||
}
|
||||
// TLAD end
|
||||
byte[] data = new byte[length];
|
||||
in.readFully(data);
|
||||
|
||||
|
@ -6976,9 +6981,11 @@ public class ExifInterface {
|
|||
}
|
||||
|
||||
final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
|
||||
if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
|
||||
// TLAD start
|
||||
if (byteCount > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
|
||||
throw new IOException("dangerous attribute size=" + byteCount);
|
||||
}
|
||||
// TLAD end
|
||||
final byte[] bytes = new byte[(int) byteCount];
|
||||
dataInputStream.readFully(bytes);
|
||||
ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,
|
|
@ -32,10 +32,10 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
class ExifInterfaceUtils {
|
||||
class ExifInterfaceUtilsFork {
|
||||
private static final String TAG = "ExifInterfaceUtils";
|
||||
|
||||
private ExifInterfaceUtils() {
|
||||
private ExifInterfaceUtilsFork() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
/**
|
|
@ -10,7 +10,7 @@ pluginManagement {
|
|||
|
||||
settings.ext.kotlin_version = '1.9.24'
|
||||
settings.ext.ksp_version = "$kotlin_version-1.0.20"
|
||||
settings.ext.agp_version = '8.5.0'
|
||||
settings.ext.agp_version = '8.5.1'
|
||||
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
|
|
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.
|
||||
|
||||
|
|
|
@ -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": {},
|
||||
"collectionActionSetHome": "تعيين كخلفية",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "مجموعة مخصصة",
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoActionABRepeat": "تكرار A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",
|
||||
|
|
|
@ -1517,8 +1517,6 @@
|
|||
},
|
||||
"collectionActionSetHome": "Усталяваць як галоўную",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Уласная калекцыя",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"videoRepeatActionSetEnd": "Усталяваць канец",
|
||||
|
|
|
@ -1467,8 +1467,6 @@
|
|||
"@tagPlaceholderState": {},
|
||||
"tagPlaceholderPlace": "Lloc",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"setHomeCustomCollection": "Coŀlecció personalitzada",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsConfirmationBeforeMoveToBinItems": "Pregunta abans de moure elements a la paperera de reciclatge",
|
||||
"@settingsConfirmationBeforeMoveToBinItems": {},
|
||||
"settingsNavigationDrawerBanner": "Mantén premut per moure i reordenar els elements del menú.",
|
||||
|
|
|
@ -1515,8 +1515,6 @@
|
|||
"@entryActionCast": {},
|
||||
"castDialogTitle": "Zařízení pro promítání",
|
||||
"@castDialogTitle": {},
|
||||
"setHomeCustomCollection": "Vlastní sbírka",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Zobrazit ikonu HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"settingsForceWesternArabicNumeralsTile": "Vynutit arabské číslice",
|
||||
|
|
|
@ -1355,8 +1355,6 @@
|
|||
"@overlayHistogramNone": {},
|
||||
"collectionActionSetHome": "Als Startseite setzen",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Benutzerdefinierte Sammlung",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "HDR-Symbol anzeigen",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"entryActionCast": "Übertragen",
|
||||
|
|
|
@ -771,6 +771,9 @@
|
|||
"binPageTitle": "Recycle Bin",
|
||||
|
||||
"explorerPageTitle": "Explorer",
|
||||
"explorerActionSelectStorageVolume": "Select storage",
|
||||
|
||||
"selectStorageVolumeDialogTitle": "Select Storage",
|
||||
|
||||
"searchCollectionFieldHint": "Search collection",
|
||||
"searchRecentSectionTitle": "Recent",
|
||||
|
@ -804,7 +807,7 @@
|
|||
"settingsNavigationSectionTitle": "Navigation",
|
||||
"settingsHomeTile": "Home",
|
||||
"settingsHomeDialogTitle": "Home",
|
||||
"setHomeCustomCollection": "Custom collection",
|
||||
"setHomeCustom": "Custom",
|
||||
"settingsShowBottomNavigationBar": "Show bottom navigation bar",
|
||||
"settingsKeepScreenOnTile": "Keep screen on",
|
||||
"settingsKeepScreenOnDialogTitle": "Keep Screen On",
|
||||
|
|
|
@ -1361,8 +1361,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Fijar como inicio",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Colección personalizada",
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoRepeatActionSetStart": "Fijar el inicio",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"stopTooltip": "Parar",
|
||||
|
@ -1380,5 +1378,11 @@
|
|||
"explorerPageTitle": "Explorar",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "Mostrar en el explorador",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"selectStorageVolumeDialogTitle": "Seleccionar almacenamiento",
|
||||
"@selectStorageVolumeDialogTitle": {},
|
||||
"setHomeCustom": "Personalizado",
|
||||
"@setHomeCustom": {},
|
||||
"explorerActionSelectStorageVolume": "Seleccionar almacenamiento",
|
||||
"@explorerActionSelectStorageVolume": {}
|
||||
}
|
||||
|
|
|
@ -1519,8 +1519,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Ezarri hasiera gisa",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Bilduma pertsonalizatua",
|
||||
"@setHomeCustomCollection": {},
|
||||
"renameProcessorHash": "Hash-a",
|
||||
"@renameProcessorHash": {},
|
||||
"settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak",
|
||||
|
|
|
@ -1099,8 +1099,6 @@
|
|||
"@settingsSystemDefault": {},
|
||||
"settingsConfirmationTile": "درخواست های تایید",
|
||||
"@settingsConfirmationTile": {},
|
||||
"setHomeCustomCollection": "مجموعه سفارشی",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsKeepScreenOnDialogTitle": "صفحه را روشن نگه دار",
|
||||
"@settingsKeepScreenOnDialogTitle": {},
|
||||
"settingsShowBottomNavigationBar": "نمایش گزینهگان پیمایش پایین",
|
||||
|
|
|
@ -1359,8 +1359,6 @@
|
|||
"@castDialogTitle": {},
|
||||
"collectionActionSetHome": "Définir comme page d’accueil",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Collection personnalisée",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Afficher l’icône HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"videoRepeatActionSetEnd": "Définir la fin",
|
||||
|
@ -1380,5 +1378,11 @@
|
|||
"explorerPageTitle": "Explorateur",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "Afficher dans Explorateur",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"setHomeCustom": "Personnalisé",
|
||||
"@setHomeCustom": {},
|
||||
"explorerActionSelectStorageVolume": "Choisir le stockage",
|
||||
"@explorerActionSelectStorageVolume": {},
|
||||
"selectStorageVolumeDialogTitle": "Volumes de stockage",
|
||||
"@selectStorageVolumeDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteButtonLabel": "डिलीट",
|
||||
"deleteButtonLabel": "मिटाए",
|
||||
"@deleteButtonLabel": {},
|
||||
"timeMinutes": "{count, plural, other{{count} मिनट}}",
|
||||
"@timeMinutes": {
|
||||
|
@ -88,7 +88,7 @@
|
|||
"@chipActionGoToTagPage": {},
|
||||
"resetTooltip": "रिसेट",
|
||||
"@resetTooltip": {},
|
||||
"saveTooltip": "सेव करें",
|
||||
"saveTooltip": "सहेजें",
|
||||
"@saveTooltip": {},
|
||||
"pickTooltip": "चुनें",
|
||||
"@pickTooltip": {},
|
||||
|
|
|
@ -1517,8 +1517,6 @@
|
|||
"@castDialogTitle": {},
|
||||
"settingsThumbnailShowHdrIcon": "HDR ikon megjelenítése",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"setHomeCustomCollection": "Egyéni gyűjtemény",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Kezdőlapnak beállít",
|
||||
"@collectionActionSetHome": {},
|
||||
"stopTooltip": "Állj",
|
||||
|
|
|
@ -1355,8 +1355,6 @@
|
|||
"@aboutDataUsageClearCache": {},
|
||||
"entryActionCast": "Siarkan",
|
||||
"@entryActionCast": {},
|
||||
"setHomeCustomCollection": "Koleksi kustom",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Tetapkan sebagai beranda",
|
||||
"@collectionActionSetHome": {},
|
||||
"settingsThumbnailShowHdrIcon": "Tampilkan ikon HDR",
|
||||
|
|
|
@ -1519,8 +1519,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Setja sem upphafsskjá",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Sérsniðið safn",
|
||||
"@setHomeCustomCollection": {},
|
||||
"renameProcessorHash": "Tætigildi",
|
||||
"@renameProcessorHash": {},
|
||||
"videoRepeatActionSetStart": "Stilla byrjun",
|
||||
|
|
|
@ -1369,8 +1369,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Imposta come pagina iniziale",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Collezione personalizzata",
|
||||
"@setHomeCustomCollection": {},
|
||||
"chipActionShowCollection": "Mostra nella Collezione",
|
||||
"@chipActionShowCollection": {},
|
||||
"renameProcessorHash": "Hash",
|
||||
|
|
|
@ -1357,8 +1357,6 @@
|
|||
"@overlayHistogramLuminance": {},
|
||||
"settingsModificationWarningDialogMessage": "他の設定は変更されます。",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"setHomeCustomCollection": "カスタムコレクション",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "マルチタッチジェスチャーの選択肢を表示する",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"chipActionCreateVault": "保管庫を作成",
|
||||
|
|
|
@ -1361,8 +1361,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "홈으로 설정",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "지정 미디어",
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoRepeatActionSetStart": "시작 지점 설정",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"videoRepeatActionSetEnd": "종료 지점 설정",
|
||||
|
@ -1380,5 +1378,11 @@
|
|||
"explorerPageTitle": "탐색기",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "탐색기 페이지에서 보기",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"setHomeCustom": "직접 설정",
|
||||
"@setHomeCustom": {},
|
||||
"explorerActionSelectStorageVolume": "저장공간 선택",
|
||||
"@explorerActionSelectStorageVolume": {},
|
||||
"selectStorageVolumeDialogTitle": "저장공간",
|
||||
"@selectStorageVolumeDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -101,9 +101,9 @@
|
|||
"@entryActionRename": {},
|
||||
"entryActionRestore": "Herstellen",
|
||||
"@entryActionRestore": {},
|
||||
"entryActionRotateCCW": "Roteren tegen de klok in",
|
||||
"entryActionRotateCCW": "Linksom roteren",
|
||||
"@entryActionRotateCCW": {},
|
||||
"entryActionRotateCW": "Roteren met de klok mee",
|
||||
"entryActionRotateCW": "Rechtsom roteren",
|
||||
"@entryActionRotateCW": {},
|
||||
"entryActionFlip": "Horizontaal omdraaien",
|
||||
"@entryActionFlip": {},
|
||||
|
@ -163,25 +163,25 @@
|
|||
"@entryInfoActionEditLocation": {},
|
||||
"entryInfoActionEditTitleDescription": "Wijzig titel & omschrijving",
|
||||
"@entryInfoActionEditTitleDescription": {},
|
||||
"entryInfoActionEditRating": "Bewerk waardering",
|
||||
"entryInfoActionEditRating": "Waardering bewerken",
|
||||
"@entryInfoActionEditRating": {},
|
||||
"entryInfoActionEditTags": "Bewerk labels",
|
||||
"entryInfoActionEditTags": "Labels bewerken",
|
||||
"@entryInfoActionEditTags": {},
|
||||
"entryInfoActionRemoveMetadata": "Verwijder metadata",
|
||||
"@entryInfoActionRemoveMetadata": {},
|
||||
"filterBinLabel": "Prullenbak",
|
||||
"@filterBinLabel": {},
|
||||
"filterFavouriteLabel": "Favorieten",
|
||||
"filterFavouriteLabel": "Favoriet",
|
||||
"@filterFavouriteLabel": {},
|
||||
"filterNoDateLabel": "Geen datum",
|
||||
"filterNoDateLabel": "Zonder datum",
|
||||
"@filterNoDateLabel": {},
|
||||
"filterNoLocationLabel": "Geen locatie",
|
||||
"filterNoLocationLabel": "Zonder plaats",
|
||||
"@filterNoLocationLabel": {},
|
||||
"filterNoRatingLabel": "Geen rating",
|
||||
"filterNoRatingLabel": "Zonder waardering",
|
||||
"@filterNoRatingLabel": {},
|
||||
"filterNoTagLabel": "Geen label",
|
||||
"filterNoTagLabel": "Zonder label",
|
||||
"@filterNoTagLabel": {},
|
||||
"filterNoTitleLabel": "Geen titel",
|
||||
"filterNoTitleLabel": "Zonder titel",
|
||||
"@filterNoTitleLabel": {},
|
||||
"filterOnThisDayLabel": "Op deze dag",
|
||||
"@filterOnThisDayLabel": {},
|
||||
|
@ -347,7 +347,7 @@
|
|||
"@videoResumeDialogMessage": {},
|
||||
"videoStartOverButtonLabel": "OPNIEUW BEGINNEN",
|
||||
"@videoStartOverButtonLabel": {},
|
||||
"videoResumeButtonLabel": "HERVAT",
|
||||
"videoResumeButtonLabel": "HERVATTEN",
|
||||
"@videoResumeButtonLabel": {},
|
||||
"setCoverDialogLatest": "Laatste item",
|
||||
"@setCoverDialogLatest": {},
|
||||
|
@ -355,7 +355,7 @@
|
|||
"@setCoverDialogAuto": {},
|
||||
"setCoverDialogCustom": "Aangepast",
|
||||
"@setCoverDialogCustom": {},
|
||||
"hideFilterConfirmationDialogMessage": "Overeenkomende 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": {},
|
||||
"newAlbumDialogTitle": "Nieuw Album",
|
||||
"@newAlbumDialogTitle": {},
|
||||
|
@ -423,7 +423,7 @@
|
|||
"@editEntryLocationDialogLongitude": {},
|
||||
"locationPickerUseThisLocationButton": "Gebruik deze locatie",
|
||||
"@locationPickerUseThisLocationButton": {},
|
||||
"editEntryRatingDialogTitle": "Beoordeling",
|
||||
"editEntryRatingDialogTitle": "Waardering",
|
||||
"@editEntryRatingDialogTitle": {},
|
||||
"removeEntryMetadataDialogTitle": "Verwijderen metadata",
|
||||
"@removeEntryMetadataDialogTitle": {},
|
||||
|
@ -505,11 +505,11 @@
|
|||
"@aboutBugReportInstruction": {},
|
||||
"aboutBugReportButton": "Reporteer",
|
||||
"@aboutBugReportButton": {},
|
||||
"aboutCreditsSectionTitle": "Credits",
|
||||
"aboutCreditsSectionTitle": "Dankbetuiging",
|
||||
"@aboutCreditsSectionTitle": {},
|
||||
"aboutCreditsWorldAtlas1": "Deze applicatie gebruikt een TopoJSON-bestand van",
|
||||
"@aboutCreditsWorldAtlas1": {},
|
||||
"aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.",
|
||||
"aboutCreditsWorldAtlas2": "onder ISC-licentie.",
|
||||
"@aboutCreditsWorldAtlas2": {},
|
||||
"aboutTranslatorsSectionTitle": "Vertalers",
|
||||
"@aboutTranslatorsSectionTitle": {},
|
||||
|
@ -525,7 +525,7 @@
|
|||
"@aboutLicensesFlutterPackagesSectionTitle": {},
|
||||
"aboutLicensesDartPackagesSectionTitle": "Dart Packages",
|
||||
"@aboutLicensesDartPackagesSectionTitle": {},
|
||||
"aboutLicensesShowAllButtonLabel": "Laat alle licenties zien",
|
||||
"aboutLicensesShowAllButtonLabel": "Alle licenties tonen",
|
||||
"@aboutLicensesShowAllButtonLabel": {},
|
||||
"collectionPageTitle": "Verzameling",
|
||||
"@collectionPageTitle": {},
|
||||
|
@ -615,7 +615,7 @@
|
|||
"@drawerCollectionAnimated": {},
|
||||
"drawerCollectionMotionPhotos": "Bewegende foto’s",
|
||||
"@drawerCollectionMotionPhotos": {},
|
||||
"drawerCollectionPanoramas": "Panoramas",
|
||||
"drawerCollectionPanoramas": "Panorama's",
|
||||
"@drawerCollectionPanoramas": {},
|
||||
"drawerCollectionRaws": "Raw foto’s",
|
||||
"@drawerCollectionRaws": {},
|
||||
|
@ -637,7 +637,7 @@
|
|||
"@sortBySize": {},
|
||||
"sortByAlbumFileName": "Op album- en bestandsnaam",
|
||||
"@sortByAlbumFileName": {},
|
||||
"sortByRating": "Op rating",
|
||||
"sortByRating": "Op waardering",
|
||||
"@sortByRating": {},
|
||||
"sortOrderNewestFirst": "Nieuwste eerst",
|
||||
"@sortOrderNewestFirst": {},
|
||||
|
@ -667,7 +667,7 @@
|
|||
"@albumMimeTypeMixed": {},
|
||||
"albumPickPageTitleCopy": "Kopieer naar Album",
|
||||
"@albumPickPageTitleCopy": {},
|
||||
"albumPickPageTitleExport": "Exporteer naar Album",
|
||||
"albumPickPageTitleExport": "Exporteren naar Album",
|
||||
"@albumPickPageTitleExport": {},
|
||||
"albumPickPageTitleMove": "Verplaats naar Album",
|
||||
"@albumPickPageTitleMove": {},
|
||||
|
@ -715,7 +715,7 @@
|
|||
"@searchPlacesSectionTitle": {},
|
||||
"searchTagsSectionTitle": "Labels",
|
||||
"@searchTagsSectionTitle": {},
|
||||
"searchRatingSectionTitle": "Beoordeling",
|
||||
"searchRatingSectionTitle": "Waarderingen",
|
||||
"@searchRatingSectionTitle": {},
|
||||
"searchMetadataSectionTitle": "Metadata",
|
||||
"@searchMetadataSectionTitle": {},
|
||||
|
@ -731,13 +731,13 @@
|
|||
"@settingsSearchFieldLabel": {},
|
||||
"settingsSearchEmpty": "Geen instellingen gevonden",
|
||||
"@settingsSearchEmpty": {},
|
||||
"settingsActionExport": "Exporteer",
|
||||
"settingsActionExport": "Exporteren",
|
||||
"@settingsActionExport": {},
|
||||
"settingsActionExportDialogTitle": "Exporteer",
|
||||
"settingsActionExportDialogTitle": "Exporteren",
|
||||
"@settingsActionExportDialogTitle": {},
|
||||
"settingsActionImport": "Importeer",
|
||||
"settingsActionImport": "Importeren",
|
||||
"@settingsActionImport": {},
|
||||
"settingsActionImportDialogTitle": "Importeer",
|
||||
"settingsActionImportDialogTitle": "Importeren",
|
||||
"@settingsActionImportDialogTitle": {},
|
||||
"appExportCovers": "Omslagen",
|
||||
"@appExportCovers": {},
|
||||
|
@ -793,13 +793,13 @@
|
|||
"@settingsThumbnailOverlayPageTitle": {},
|
||||
"settingsThumbnailShowFavouriteIcon": "Favorieten icoon zichtbaar",
|
||||
"@settingsThumbnailShowFavouriteIcon": {},
|
||||
"settingsThumbnailShowTagIcon": "Label icoon zichtbaar",
|
||||
"settingsThumbnailShowTagIcon": "Label-pictogram tonen",
|
||||
"@settingsThumbnailShowTagIcon": {},
|
||||
"settingsThumbnailShowLocationIcon": "Locatie icoon zichtbaar",
|
||||
"@settingsThumbnailShowLocationIcon": {},
|
||||
"settingsThumbnailShowMotionPhotoIcon": "Bewegende foto icoon zichtbaar",
|
||||
"@settingsThumbnailShowMotionPhotoIcon": {},
|
||||
"settingsThumbnailShowRating": "Rating zichtbaar",
|
||||
"settingsThumbnailShowRating": "Waardering tonen",
|
||||
"@settingsThumbnailShowRating": {},
|
||||
"settingsThumbnailShowRawIcon": "RAW icoon zichtbaar",
|
||||
"@settingsThumbnailShowRawIcon": {},
|
||||
|
@ -865,7 +865,7 @@
|
|||
"@settingsViewerSlideshowPageTitle": {},
|
||||
"settingsSlideshowRepeat": "Herhalen",
|
||||
"@settingsSlideshowRepeat": {},
|
||||
"settingsSlideshowShuffle": "Shuffle",
|
||||
"settingsSlideshowShuffle": "Willekeurige volgorde",
|
||||
"@settingsSlideshowShuffle": {},
|
||||
"settingsSlideshowFillScreen": "Volledig scherm",
|
||||
"@settingsSlideshowFillScreen": {},
|
||||
|
@ -951,13 +951,13 @@
|
|||
"@settingsHiddenItemsPageTitle": {},
|
||||
"settingsHiddenItemsTabFilters": "Verborgen Filters",
|
||||
"@settingsHiddenItemsTabFilters": {},
|
||||
"settingsHiddenFiltersBanner": "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": {},
|
||||
"settingsHiddenFiltersEmpty": "Geen verborgen filters",
|
||||
"@settingsHiddenFiltersEmpty": {},
|
||||
"settingsHiddenItemsTabPaths": "Verborgen paden",
|
||||
"@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": {},
|
||||
"addPathTooltip": "Pad toevoegen",
|
||||
"@addPathTooltip": {},
|
||||
|
@ -965,7 +965,7 @@
|
|||
"@settingsStorageAccessTile": {},
|
||||
"settingsStorageAccessPageTitle": "Toegang tot opslag",
|
||||
"@settingsStorageAccessPageTitle": {},
|
||||
"settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. U kunt hier 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": {},
|
||||
"settingsStorageAccessEmpty": "Geen toegang verleend",
|
||||
"@settingsStorageAccessEmpty": {},
|
||||
|
@ -1029,7 +1029,7 @@
|
|||
"@statsTopTagsSectionTitle": {},
|
||||
"statsTopAlbumsSectionTitle": "Top Albums",
|
||||
"@statsTopAlbumsSectionTitle": {},
|
||||
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
|
||||
"viewerOpenPanoramaButtonLabel": "PANORAMA OPENEN",
|
||||
"@viewerOpenPanoramaButtonLabel": {},
|
||||
"viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN",
|
||||
"@viewerSetWallpaperButtonLabel": {},
|
||||
|
@ -1089,7 +1089,7 @@
|
|||
"@viewerInfoOpenLinkText": {},
|
||||
"viewerInfoViewXmlLinkText": "Bekijk XML",
|
||||
"@viewerInfoViewXmlLinkText": {},
|
||||
"viewerInfoSearchFieldLabel": "Doorzoek metadata",
|
||||
"viewerInfoSearchFieldLabel": "Metadata doorzoeken",
|
||||
"@viewerInfoSearchFieldLabel": {},
|
||||
"viewerInfoSearchEmpty": "Geen overeenkomstige zoeksleutels",
|
||||
"@viewerInfoSearchEmpty": {},
|
||||
|
@ -1105,7 +1105,7 @@
|
|||
"@viewerInfoSearchSuggestionRights": {},
|
||||
"wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm",
|
||||
"@wallpaperUseScrollEffect": {},
|
||||
"tagEditorPageTitle": "Wijzig Labels",
|
||||
"tagEditorPageTitle": "Labels bewerken",
|
||||
"@tagEditorPageTitle": {},
|
||||
"tagEditorPageNewTagFieldLabel": "Nieuw label",
|
||||
"@tagEditorPageNewTagFieldLabel": {},
|
||||
|
@ -1155,17 +1155,17 @@
|
|||
"@lengthUnitPercent": {},
|
||||
"vaultLockTypePin": "PIN",
|
||||
"@vaultLockTypePin": {},
|
||||
"filterAspectRatioLandscapeLabel": "Landschap",
|
||||
"filterAspectRatioLandscapeLabel": "Liggend",
|
||||
"@filterAspectRatioLandscapeLabel": {},
|
||||
"chipActionCreateVault": "Creëer kluis",
|
||||
"chipActionCreateVault": "Kluis aanmaken",
|
||||
"@chipActionCreateVault": {},
|
||||
"entryInfoActionRemoveLocation": "Verwijder locatie",
|
||||
"@entryInfoActionRemoveLocation": {},
|
||||
"chipActionConfigureVault": "Configureer kluis",
|
||||
"chipActionConfigureVault": "Kluis configureren",
|
||||
"@chipActionConfigureVault": {},
|
||||
"filterNoAddressLabel": "Geen adres",
|
||||
"filterNoAddressLabel": "Zonder adres",
|
||||
"@filterNoAddressLabel": {},
|
||||
"filterAspectRatioPortraitLabel": "Portret",
|
||||
"filterAspectRatioPortraitLabel": "Staand",
|
||||
"@filterAspectRatioPortraitLabel": {},
|
||||
"widgetDisplayedItemRandom": "Willekeurige",
|
||||
"@widgetDisplayedItemRandom": {},
|
||||
|
@ -1175,7 +1175,7 @@
|
|||
"@keepScreenOnVideoPlayback": {},
|
||||
"settingsVideoEnablePip": "Beeld-in-beeld",
|
||||
"@settingsVideoEnablePip": {},
|
||||
"filterTaggedLabel": "Getagd",
|
||||
"filterTaggedLabel": "Met label",
|
||||
"@filterTaggedLabel": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
|
@ -1191,9 +1191,9 @@
|
|||
"@stopTooltip": {},
|
||||
"chipActionLock": "Vergrendel",
|
||||
"@chipActionLock": {},
|
||||
"chipActionShowCountryStates": "Status laten xien",
|
||||
"chipActionShowCountryStates": "Status tonen",
|
||||
"@chipActionShowCountryStates": {},
|
||||
"chipActionGoToPlacePage": "Laat zien in plaatsen",
|
||||
"chipActionGoToPlacePage": "In Plaatsen tonen",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"subtitlePositionTop": "Boven",
|
||||
"@subtitlePositionTop": {},
|
||||
|
@ -1227,7 +1227,7 @@
|
|||
"@aboutDataUsageMisc": {},
|
||||
"settingsModificationWarningDialogMessage": "Andere instellingen zullen worden aangepast.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Vergrendel als scherm uitgaat",
|
||||
"vaultDialogLockModeWhenScreenOff": "Vergrendelen wanneer het scherm wordt uitgeschakeld",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"aboutDataUsageData": "Data",
|
||||
"@aboutDataUsageData": {},
|
||||
|
@ -1269,8 +1269,122 @@
|
|||
"@maxBrightnessNever": {},
|
||||
"videoResumptionModeAlways": "Altijd",
|
||||
"@videoResumptionModeAlways": {},
|
||||
"exportEntryDialogWriteMetadata": "Schrijf metadata",
|
||||
"exportEntryDialogWriteMetadata": "Metadata schrijven",
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"chipActionShowCollection": "Tonen in Collectie",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"entryActionCast": "Casten",
|
||||
"@entryActionCast": {},
|
||||
"videoRepeatActionSetStart": "Start instellen",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"videoRepeatActionSetEnd": "Einde instellen",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"viewerActionUnlock": "Weergave ontgrendelen",
|
||||
"@viewerActionUnlock": {},
|
||||
"filterLocatedLabel": "Met Plaats",
|
||||
"@filterLocatedLabel": {},
|
||||
"overlayHistogramNone": "Geen",
|
||||
"@overlayHistogramNone": {},
|
||||
"authenticateToUnlockVault": "Verifieer om de kluis te ontgrendelen",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"vaultBinUsageDialogMessage": "Sommige kluizen gebruiken de prullenbak.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Items in de Prullenbak worden voor altijd verwijderd.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"statsTopStatesSectionTitle": "Top Staten",
|
||||
"@statsTopStatesSectionTitle": {},
|
||||
"editorTransformRotate": "Roteren",
|
||||
"@editorTransformRotate": {},
|
||||
"editorActionTransform": "Transformeren",
|
||||
"@editorActionTransform": {},
|
||||
"stateEmpty": "Zonder Staten",
|
||||
"@stateEmpty": {},
|
||||
"settingsViewerShowRatingTags": "Waardering & labels tonen",
|
||||
"@settingsViewerShowRatingTags": {},
|
||||
"drawerPlacePage": "Plaatsen",
|
||||
"@drawerPlacePage": {},
|
||||
"newVaultWarningDialogMessage": "Items in kluizen zijn alleen beschikbaar voor deze app en niet voor andere.\n\nAls je deze app verwijdert of deze app-gegevens wist, verlies je al deze items.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"vaultDialogLockTypeLabel": "Vergrendelingstype",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"tagEditorDiscardDialogMessage": "Wijzigingen ongedaan maken?",
|
||||
"@tagEditorDiscardDialogMessage": {},
|
||||
"renameProcessorHash": "Controlenummer",
|
||||
"@renameProcessorHash": {},
|
||||
"castDialogTitle": "Cast-apparaten",
|
||||
"@castDialogTitle": {},
|
||||
"aboutDataUsageSectionTitle": "Gegevensgebruik",
|
||||
"@aboutDataUsageSectionTitle": {},
|
||||
"statePageTitle": "Staten",
|
||||
"@statePageTitle": {},
|
||||
"searchStatesSectionTitle": "Staten",
|
||||
"@searchStatesSectionTitle": {},
|
||||
"settingsVideoPlaybackTile": "Afspelen",
|
||||
"@settingsVideoPlaybackTile": {},
|
||||
"settingsVideoResumptionModeTile": "Afspelen hervatten",
|
||||
"@settingsVideoResumptionModeTile": {},
|
||||
"settingsVideoResumptionModeDialogTitle": "Afspelen hervatten",
|
||||
"@settingsVideoResumptionModeDialogTitle": {},
|
||||
"settingsVideoBackgroundMode": "Achtergrond-modus",
|
||||
"@settingsVideoBackgroundMode": {},
|
||||
"configureVaultDialogTitle": "Kluis configureren",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"settingsWidgetDisplayedItem": "Getoond item",
|
||||
"@settingsWidgetDisplayedItem": {},
|
||||
"albumTierVaults": "Kluizen",
|
||||
"@albumTierVaults": {},
|
||||
"aboutDataUsageClearCache": "Cache wissen",
|
||||
"@aboutDataUsageClearCache": {},
|
||||
"placePageTitle": "Plaatsen",
|
||||
"@placePageTitle": {},
|
||||
"placeEmpty": "Zonder plaatsen",
|
||||
"@placeEmpty": {},
|
||||
"settingsCollectionBurstPatternsNone": "Geen",
|
||||
"@settingsCollectionBurstPatternsNone": {},
|
||||
"settingsVideoPlaybackPageTitle": "Afspelen",
|
||||
"@settingsVideoPlaybackPageTitle": {},
|
||||
"settingsVideoBackgroundModeDialogTitle": "Achtergrond-modus",
|
||||
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||
"settingsCollectionBurstPatternsTile": "Burst-patronen",
|
||||
"@settingsCollectionBurstPatternsTile": {},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Alternatieven voor multi-touch-gebaren weergeven",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsDisplayUseTvInterface": "Android TV-interface",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"settingsForceWesternArabicNumeralsTile": "Arabische cijfers forceren",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"explorerPageTitle": "Bestanden",
|
||||
"@explorerPageTitle": {},
|
||||
"columnCount": "{count, plural, =1{{count} kolom} other{{count} kolommen}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"format": "decimalPattern"
|
||||
}
|
||||
}
|
||||
},
|
||||
"widgetTapUpdateWidget": "Widget bijwerken",
|
||||
"@widgetTapUpdateWidget": {},
|
||||
"authenticateToConfigureVault": "Verifieer om de kluis te configureren",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"settingsConfirmationVaultDataLoss": "Waarschuwing voor verlies van kluisgegevens weergeven",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"newVaultDialogTitle": "Nieuwe kluis",
|
||||
"@newVaultDialogTitle": {},
|
||||
"chipActionGoToExplorerPage": "In Bestanden tonen",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"cropAspectRatioFree": "Vrij",
|
||||
"@cropAspectRatioFree": {},
|
||||
"videoActionABRepeat": "A-B herhalen",
|
||||
"@videoActionABRepeat": {},
|
||||
"viewerActionLock": "Weergave vergrendelen",
|
||||
"@viewerActionLock": {},
|
||||
"collectionActionSetHome": "Als startpagina instellen",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustom": "Aangepast",
|
||||
"@setHomeCustom": {},
|
||||
"explorerActionSelectStorageVolume": "Selecteer opslag",
|
||||
"@explorerActionSelectStorageVolume": {},
|
||||
"selectStorageVolumeDialogTitle": "Selecteer opslag",
|
||||
"@selectStorageVolumeDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -1517,8 +1517,6 @@
|
|||
"@castDialogTitle": {},
|
||||
"settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"setHomeCustomCollection": "Własna kolekcja",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Ustaw jako stronę główną",
|
||||
"@collectionActionSetHome": {},
|
||||
"videoRepeatActionSetStart": "Ustaw początek",
|
||||
|
|
|
@ -1361,8 +1361,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Definir como início",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Coleção personalizada",
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoActionABRepeat": "Repetição A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetEnd": "Definir fim",
|
||||
|
@ -1372,5 +1370,13 @@
|
|||
"videoRepeatActionSetStart": "Definir início",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"chipActionShowCollection": "Mostrar na Coleção",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"renameProcessorHash": "Hash",
|
||||
"@renameProcessorHash": {},
|
||||
"settingsForceWesternArabicNumeralsTile": "Forçar numerais arábicos",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"chipActionGoToExplorerPage": "Mostrar no Explorador",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"explorerPageTitle": "Explorador",
|
||||
"@explorerPageTitle": {}
|
||||
}
|
||||
|
|
|
@ -1491,8 +1491,6 @@
|
|||
"@collectionActionSetHome": {},
|
||||
"aboutDataUsageClearCache": "Golește memoria cache",
|
||||
"@aboutDataUsageClearCache": {},
|
||||
"setHomeCustomCollection": "Colecție personalizată",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"settingsViewerShowHistogram": "Afișare histogramă",
|
||||
|
|
|
@ -1359,8 +1359,6 @@
|
|||
"@castDialogTitle": {},
|
||||
"settingsThumbnailShowHdrIcon": "Показать значок HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"setHomeCustomCollection": "Собственная коллекция",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Установить как главную",
|
||||
"@collectionActionSetHome": {},
|
||||
"videoRepeatActionSetStart": "Установить начало",
|
||||
|
@ -1380,5 +1378,7 @@
|
|||
"chipActionGoToExplorerPage": "Показать в проводнике",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"explorerPageTitle": "Проводник",
|
||||
"@explorerPageTitle": {}
|
||||
"@explorerPageTitle": {},
|
||||
"explorerActionSelectStorageVolume": "Выбрать хранилище",
|
||||
"@explorerActionSelectStorageVolume": {}
|
||||
}
|
||||
|
|
|
@ -1517,8 +1517,6 @@
|
|||
"@castDialogTitle": {},
|
||||
"collectionActionSetHome": "Nastaviť ako doma",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Kolekcia na mieru",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"chipActionShowCollection": "Zobraziť v kolekcií",
|
||||
|
|
|
@ -1321,8 +1321,6 @@
|
|||
"@passwordDialogConfirm": {},
|
||||
"collectionActionSetHome": "Ana ekran olarak ayarla",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Kişisel koleksiyon",
|
||||
"@setHomeCustomCollection": {},
|
||||
"statsTopStatesSectionTitle": "Baş Eyaletler",
|
||||
"@statsTopStatesSectionTitle": {},
|
||||
"pinDialogEnter": "PIN girin",
|
||||
|
|
|
@ -1517,8 +1517,6 @@
|
|||
"@castDialogTitle": {},
|
||||
"settingsThumbnailShowHdrIcon": "Показати іконку HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"setHomeCustomCollection": "Власна колекція",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Встановити як головну",
|
||||
"@collectionActionSetHome": {},
|
||||
"videoRepeatActionSetStart": "Змінити початок",
|
||||
|
@ -1538,5 +1536,11 @@
|
|||
"chipActionGoToExplorerPage": "Показати в провіднику",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"explorerPageTitle": "Провідник",
|
||||
"@explorerPageTitle": {}
|
||||
"@explorerPageTitle": {},
|
||||
"setHomeCustom": "Власне",
|
||||
"@setHomeCustom": {},
|
||||
"explorerActionSelectStorageVolume": "Обрати сховище",
|
||||
"@explorerActionSelectStorageVolume": {},
|
||||
"selectStorageVolumeDialogTitle": "Оберіть сховище",
|
||||
"@selectStorageVolumeDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -1515,8 +1515,6 @@
|
|||
"@entryActionCast": {},
|
||||
"castDialogTitle": "Thiết bị truyền",
|
||||
"@castDialogTitle": {},
|
||||
"setHomeCustomCollection": "Bộ sưu tập tùy chỉnh",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Đặt làm nhà",
|
||||
|
@ -1534,5 +1532,13 @@
|
|||
"settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"chipActionShowCollection": "Hiển thị trong Bộ sưu tập",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"selectStorageVolumeDialogTitle": "Chọn dung lượng",
|
||||
"@selectStorageVolumeDialogTitle": {},
|
||||
"explorerActionSelectStorageVolume": "Chọn dung lượng",
|
||||
"@explorerActionSelectStorageVolume": {},
|
||||
"chipActionGoToExplorerPage": "Hiển thị ở Explorer",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"explorerPageTitle": "Explorer",
|
||||
"@explorerPageTitle": {}
|
||||
}
|
||||
|
|
|
@ -519,7 +519,7 @@
|
|||
"@aboutTranslatorsSectionTitle": {},
|
||||
"aboutLicensesSectionTitle": "开源许可协议",
|
||||
"@aboutLicensesSectionTitle": {},
|
||||
"aboutLicensesBanner": "本应用使用以下开源软件包和库",
|
||||
"aboutLicensesBanner": "本应用使用以下开源软件包和库。",
|
||||
"@aboutLicensesBanner": {},
|
||||
"aboutLicensesShowAllButtonLabel": "显示所有许可协议",
|
||||
"@aboutLicensesShowAllButtonLabel": {},
|
||||
|
@ -1161,9 +1161,9 @@
|
|||
"@settingsSubtitleThemeTextPositionTile": {},
|
||||
"settingsSubtitleThemeTextPositionDialogTitle": "文本位置",
|
||||
"@settingsSubtitleThemeTextPositionDialogTitle": {},
|
||||
"aboutLicensesDartPackagesSectionTitle": "Dart Packages",
|
||||
"aboutLicensesDartPackagesSectionTitle": "Dart 软件包",
|
||||
"@aboutLicensesDartPackagesSectionTitle": {},
|
||||
"aboutLicensesFlutterPackagesSectionTitle": "Flutter Packages",
|
||||
"aboutLicensesFlutterPackagesSectionTitle": "Flutter 软件包",
|
||||
"@aboutLicensesFlutterPackagesSectionTitle": {},
|
||||
"keepScreenOnVideoPlayback": "视频播放期间",
|
||||
"@keepScreenOnVideoPlayback": {},
|
||||
|
@ -1361,8 +1361,6 @@
|
|||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "设置为首页",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "自定义媒体集",
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoRepeatActionSetStart": "设置起点",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"stopTooltip": "停止",
|
||||
|
|
|
@ -1511,8 +1511,6 @@
|
|||
"@overlayHistogramLuminance": {},
|
||||
"collectionActionSetHome": "設為首頁",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "自訂收藏品",
|
||||
"@setHomeCustomCollection": {},
|
||||
"aboutDataUsageClearCache": "清除快取",
|
||||
"@aboutDataUsageClearCache": {},
|
||||
"settingsViewerShowHistogram": "顯示直方圖",
|
||||
|
@ -1534,5 +1532,9 @@
|
|||
"settingsForceWesternArabicNumeralsTile": "強制使用阿拉伯數字",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"chipActionShowCollection": "在收藏品中顯示",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"explorerPageTitle": "檔案總管",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "在檔案總管裡顯示",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,8 @@ class Contributors {
|
|||
Contributor('Maxi', 'maxitendo01@proton.me'),
|
||||
Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'),
|
||||
Contributor('elfriob', 'elfriob@ya.ru'),
|
||||
Contributor('Stephan Paternotte', 'stephan@paternottes.net'),
|
||||
Contributor('Tung Anh', 'buihuutunganh2007@gmail.com'),
|
||||
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
||||
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
||||
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
||||
|
@ -102,6 +104,7 @@ class Contributors {
|
|||
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
||||
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
||||
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
|
||||
// Contributor('Sartaj', 'ssaarrttaajj111@gmail.com'), // Hindi
|
||||
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
|
||||
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
|
||||
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)
|
||||
|
|
|
@ -14,11 +14,19 @@ mixin NavigationSettings on SettingsAccess {
|
|||
|
||||
HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
|
||||
|
||||
set homePage(HomePageSetting newValue) => set(SettingKeys.homePageKey, newValue.toString());
|
||||
|
||||
Set<CollectionFilter> get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
|
||||
|
||||
set homeCustomCollection(Set<CollectionFilter> newValue) => set(SettingKeys.homeCustomCollectionKey, newValue.map((filter) => filter.toJson()).toList());
|
||||
String? get homeCustomExplorerPath => getString(SettingKeys.homeCustomExplorerPathKey);
|
||||
|
||||
void setHome(
|
||||
HomePageSetting homePage, {
|
||||
Set<CollectionFilter> customCollection = const {},
|
||||
String? customExplorerPath,
|
||||
}) {
|
||||
set(SettingKeys.homePageKey, homePage.toString());
|
||||
set(SettingKeys.homeCustomCollectionKey, customCollection.map((filter) => filter.toJson()).toList());
|
||||
set(SettingKeys.homeCustomExplorerPathKey, customExplorerPath);
|
||||
}
|
||||
|
||||
bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar;
|
||||
|
||||
|
|
|
@ -440,6 +440,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
|
|||
case SettingKeys.maxBrightnessKey:
|
||||
case SettingKeys.keepScreenOnKey:
|
||||
case SettingKeys.homePageKey:
|
||||
case SettingKeys.homeCustomExplorerPathKey:
|
||||
case SettingKeys.collectionGroupFactorKey:
|
||||
case SettingKeys.collectionSortFactorKey:
|
||||
case SettingKeys.thumbnailLocationIconKey:
|
||||
|
|
|
@ -93,6 +93,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
_rawEntries.forEach((v) => v.dispose());
|
||||
}
|
||||
|
||||
set safeMode(bool enabled);
|
||||
|
||||
final EventBus _eventBus = EventBus();
|
||||
|
||||
@override
|
||||
|
|
|
@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource {
|
|||
final Set<String> _changedUris = {};
|
||||
int? _lastGeneration;
|
||||
SourceInitializationState _initState = SourceInitializationState.none;
|
||||
bool _safeMode = false;
|
||||
|
||||
@override
|
||||
set safeMode(bool enabled) => _safeMode = enabled;
|
||||
|
||||
@override
|
||||
SourceInitializationState get initState => _initState;
|
||||
|
@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
analysisController: analysisController,
|
||||
directory: directory,
|
||||
loadTopEntriesFirst: loadTopEntriesFirst,
|
||||
canAnalyze: canAnalyze,
|
||||
canAnalyze: canAnalyze && !_safeMode,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
pendingNewEntries.clear();
|
||||
}
|
||||
|
||||
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
|
||||
mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
|
||||
(entry) {
|
||||
// when discovering modified entry with known content ID,
|
||||
// reuse known entry ID to overwrite it while preserving favourites, etc.
|
||||
|
|
|
@ -127,21 +127,16 @@ class Analyzer with WidgetsBindingObserver {
|
|||
Future<void> start(dynamic args) async {
|
||||
List<int>? entryIds;
|
||||
var force = false;
|
||||
var progressTotal = 0, progressOffset = 0;
|
||||
if (args is Map) {
|
||||
entryIds = (args['entryIds'] as List?)?.cast<int>();
|
||||
force = args['force'] ?? false;
|
||||
progressTotal = args['progressTotal'];
|
||||
progressOffset = args['progressOffset'];
|
||||
}
|
||||
await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
|
||||
await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries');
|
||||
_controller?.dispose();
|
||||
_controller = AnalysisController(
|
||||
canStartService: false,
|
||||
entryIds: entryIds,
|
||||
force: force,
|
||||
progressTotal: progressTotal,
|
||||
progressOffset: progressOffset,
|
||||
);
|
||||
|
||||
settings.systemLocalesFallback = await deviceService.getLocales();
|
||||
|
|
|
@ -30,7 +30,7 @@ abstract class AppService {
|
|||
|
||||
Future<bool> shareSingle(String uri, String mimeType);
|
||||
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri});
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri});
|
||||
}
|
||||
|
||||
class PlatformAppService implements AppService {
|
||||
|
@ -203,7 +203,7 @@ class PlatformAppService implements AppService {
|
|||
// app shortcuts
|
||||
|
||||
@override
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async {
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? explorerPath, String? uri}) async {
|
||||
Uint8List? iconBytes;
|
||||
if (coverEntry != null) {
|
||||
final size = coverEntry.isVideo ? 0.0 : 256.0;
|
||||
|
@ -222,6 +222,7 @@ class PlatformAppService implements AppService {
|
|||
'label': label,
|
||||
'iconBytes': iconBytes,
|
||||
'filters': filters?.map((filter) => filter.toJson()).toList(),
|
||||
'explorerPath': explorerPath,
|
||||
'uri': uri,
|
||||
});
|
||||
} on PlatformException catch (e, stack) {
|
||||
|
|
|
@ -15,7 +15,7 @@ abstract class MediaStoreService {
|
|||
Future<int?> getGeneration();
|
||||
|
||||
// knownEntries: map of contentId -> dateModifiedSecs
|
||||
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
|
||||
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory});
|
||||
|
||||
// returns media URI
|
||||
Future<Uri?> scanFile(String path, String mimeType);
|
||||
|
@ -75,12 +75,13 @@ class PlatformMediaStoreService implements MediaStoreService {
|
|||
}
|
||||
|
||||
@override
|
||||
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
|
||||
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) {
|
||||
try {
|
||||
return _stream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'knownEntries': knownEntries,
|
||||
'directory': directory,
|
||||
'safe': safe,
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => AvesEntry.fromMap(event as Map));
|
||||
|
|
|
@ -106,8 +106,8 @@ class AndroidFileUtils {
|
|||
if (isScreenshotsPath(dirPath)) return AlbumType.screenshots;
|
||||
if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures;
|
||||
|
||||
final dir = pContext.split(dirPath).last;
|
||||
if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
|
||||
final dir = pContext.split(dirPath).lastOrNull;
|
||||
if (dir != null && dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
|
||||
|
||||
return AlbumType.regular;
|
||||
}
|
||||
|
|
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) {
|
||||
return AvesScaffold(
|
||||
body: AvesPopScope(
|
||||
handlers: const [TvNavigationPopHandler.pop],
|
||||
handlers: [tvNavigationPopHandler],
|
||||
child: Row(
|
||||
children: [
|
||||
TvRail(
|
||||
|
|
|
@ -174,7 +174,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
// Flutter has various page transition implementations for Android:
|
||||
// - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below
|
||||
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
||||
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
|
||||
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
|
||||
// - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back
|
||||
static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
|
||||
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||
static ScreenBrightness? _screenBrightness;
|
||||
|
|
|
@ -55,7 +55,6 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
final List<StreamSubscription> _subscriptions = [];
|
||||
late CollectionLens _collection;
|
||||
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -80,7 +79,6 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
_collection.dispose();
|
||||
_doubleBackPopHandler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -98,16 +96,12 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
builder: (context) {
|
||||
return AvesPopScope(
|
||||
handlers: [
|
||||
(context) {
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
TvNavigationPopHandler.pop,
|
||||
_doubleBackPopHandler.pop,
|
||||
APopHandler(
|
||||
canPop: (context) => context.select<Selection<AvesEntry>, bool>((v) => !v.isSelecting),
|
||||
onPopBlocked: (context) => context.read<Selection<AvesEntry>>().browse(),
|
||||
),
|
||||
tvNavigationPopHandler,
|
||||
doubleBackPopHandler,
|
||||
],
|
||||
child: GestureAreaProtectorStack(
|
||||
child: DirectionalSafeArea(
|
||||
|
|
|
@ -753,8 +753,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
}
|
||||
|
||||
void _setHome(BuildContext context) async {
|
||||
settings.homeCustomCollection = context.read<CollectionLens>().filters;
|
||||
settings.homePage = HomePageSetting.collection;
|
||||
settings.setHome(HomePageSetting.collection, customCollection: context.read<CollectionLens>().filters);
|
||||
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,49 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:overlay_support/overlay_support.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DoubleBackPopHandler {
|
||||
final DoubleBackPopHandler doubleBackPopHandler = DoubleBackPopHandler._private();
|
||||
|
||||
class DoubleBackPopHandler extends PopHandler {
|
||||
bool _backOnce = false;
|
||||
Timer? _backTimer;
|
||||
|
||||
DoubleBackPopHandler() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$DoubleBackPopHandler',
|
||||
object: this,
|
||||
);
|
||||
}
|
||||
DoubleBackPopHandler._private();
|
||||
|
||||
@override
|
||||
bool canPop(BuildContext context) {
|
||||
if (context.select<Settings, bool>((s) => !s.mustBackTwiceToExit)) return true;
|
||||
if (Navigator.canPop(context)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_stopBackTimer();
|
||||
}
|
||||
|
||||
bool pop(BuildContext context) {
|
||||
if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) {
|
||||
@override
|
||||
void onPopBlocked(BuildContext context) {
|
||||
if (_backOnce) {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.maybeOf(context)?.pop();
|
||||
} else {
|
||||
// exit
|
||||
reportService.log('Exit by pop');
|
||||
PopExitNotification().dispatch(context);
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
} else {
|
||||
_backOnce = true;
|
||||
_stopBackTimer();
|
||||
_backTimer?.cancel();
|
||||
_backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false);
|
||||
toast(
|
||||
context.l10n.doubleBackExitMessage,
|
||||
duration: ADurations.doubleBackTimerDelay,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _stopBackTimer() {
|
||||
_backTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random
|
||||
// so this widget combines multiple handlers with a guaranteed order
|
||||
// this widget combines multiple pop handlers with a guaranteed order
|
||||
class AvesPopScope extends StatelessWidget {
|
||||
final List<bool Function(BuildContext context)> handlers;
|
||||
final List<PopHandler> handlers;
|
||||
final Widget child;
|
||||
|
||||
const AvesPopScope({
|
||||
|
@ -16,21 +14,12 @@ class AvesPopScope extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final blocker = handlers.firstWhereOrNull((v) => !v.canPop(context));
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
canPop: blocker == null,
|
||||
onPopInvoked: (didPop) {
|
||||
if (didPop) return;
|
||||
|
||||
final shouldPop = handlers.fold(true, (prev, v) => prev ? v(context) : false);
|
||||
if (shouldPop) {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.maybeOf(context)?.pop();
|
||||
} else {
|
||||
// exit
|
||||
reportService.log('Exit by pop');
|
||||
PopExitNotification().dispatch(context);
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
if (!didPop) {
|
||||
blocker?.onPopBlocked(context);
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
|
@ -38,5 +27,28 @@ class AvesPopScope extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class PopHandler {
|
||||
bool canPop(BuildContext context);
|
||||
|
||||
void onPopBlocked(BuildContext context);
|
||||
}
|
||||
|
||||
class APopHandler implements PopHandler {
|
||||
final bool Function(BuildContext context) _canPop;
|
||||
final void Function(BuildContext context) _onPopBlocked;
|
||||
|
||||
APopHandler({
|
||||
required bool Function(BuildContext context) canPop,
|
||||
required void Function(BuildContext context) onPopBlocked,
|
||||
}) : _canPop = canPop,
|
||||
_onPopBlocked = onPopBlocked;
|
||||
|
||||
@override
|
||||
bool canPop(BuildContext context) => _canPop(context);
|
||||
|
||||
@override
|
||||
void onPopBlocked(BuildContext context) => _onPopBlocked(context);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class PopExitNotification extends Notification {}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/explorer/explorer_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
|
@ -11,18 +12,25 @@ import 'package:aves_model/aves_model.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||
class TvNavigationPopHandler {
|
||||
static bool pop(BuildContext context) {
|
||||
if (!settings.useTvLayout || _isHome(context)) {
|
||||
return true;
|
||||
}
|
||||
final TvNavigationPopHandler tvNavigationPopHandler = TvNavigationPopHandler._private();
|
||||
|
||||
// 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(
|
||||
_getHomeRoute(),
|
||||
(route) => false,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _isHome(BuildContext context) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/utils/debouncer.dart';
|
||||
|
@ -31,7 +30,6 @@ class SearchPage extends StatefulWidget {
|
|||
class _SearchPageState extends State<SearchPage> {
|
||||
final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay);
|
||||
final FocusNode _searchFieldFocusNode = FocusNode();
|
||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -55,7 +53,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||
_unregisterWidget(widget);
|
||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||
_searchFieldFocusNode.dispose();
|
||||
_doubleBackPopHandler.dispose();
|
||||
widget.delegate.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -151,8 +148,8 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
body: AvesPopScope(
|
||||
handlers: [
|
||||
TvNavigationPopHandler.pop,
|
||||
_doubleBackPopHandler.pop,
|
||||
tvNavigationPopHandler,
|
||||
doubleBackPopHandler,
|
||||
],
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
|
|
@ -67,7 +67,7 @@ class AppDebugPage extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
body: AvesPopScope(
|
||||
handlers: const [TvNavigationPopHandler.pop],
|
||||
handlers: [tvNavigationPopHandler],
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
|
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/themes.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/view/src/actions/explorer.dart';
|
||||
import 'package:aves/view/view.dart';
|
||||
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
||||
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
||||
|
@ -15,9 +16,12 @@ import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/dialogs/select_storage_dialog.dart';
|
||||
import 'package:aves/widgets/explorer/explorer_action_delegate.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -108,32 +112,75 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
|
|||
onPressed: () => _goToSearch(context),
|
||||
tooltip: MaterialLocalizations.of(context).searchFieldLabel,
|
||||
),
|
||||
if (_volumes.length > 1)
|
||||
FontSizeIconTheme(
|
||||
child: PopupMenuButton<StorageVolume>(
|
||||
itemBuilder: (context) {
|
||||
return _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();
|
||||
},
|
||||
onSelected: (volume) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
||||
widget.goTo(volume.path);
|
||||
},
|
||||
popUpAnimationStyle: animations.popUpAnimationStyle,
|
||||
),
|
||||
),
|
||||
];
|
||||
if (_volumes.length > 1) _buildVolumeSelector(context),
|
||||
PopupMenuButton<ExplorerAction>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
ExplorerAction.addShortcut,
|
||||
ExplorerAction.setHome,
|
||||
].map((v) {
|
||||
return PopupMenuItem(
|
||||
value: v,
|
||||
child: MenuRow(text: v.getText(context), icon: v.getIcon()),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (action) async {
|
||||
// wait for the popup menu to hide before proceeding with the action
|
||||
await Future.delayed(animations.popUpAnimationDelay * timeDilation);
|
||||
final directory = widget.directoryNotifier.value;
|
||||
ExplorerActionDelegate(directory: directory).onActionSelected(context, action);
|
||||
},
|
||||
popUpAnimationStyle: animations.popUpAnimationStyle,
|
||||
),
|
||||
].map((v) => FontSizeIconTheme(child: v)).toList();
|
||||
}
|
||||
|
||||
Widget _buildVolumeSelector(BuildContext context) {
|
||||
if (_volumes.length == 2) {
|
||||
return ValueListenableBuilder<VolumeRelativeDirectory>(
|
||||
valueListenable: widget.directoryNotifier,
|
||||
builder: (context, directory, child) {
|
||||
final currentVolume = directory.volumePath;
|
||||
final otherVolume = _volumes.firstWhere((volume) => volume.path != currentVolume);
|
||||
final icon = otherVolume.isRemovable ? AIcons.storageCard : AIcons.storageMain;
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: () => widget.goTo(otherVolume.path),
|
||||
tooltip: otherVolume.getDescription(context),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
icon: const Icon(AIcons.storageCard),
|
||||
onPressed: () async {
|
||||
_volumes.map((v) {
|
||||
final selected = widget.directoryNotifier.value.volumePath == v.path;
|
||||
final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain;
|
||||
return PopupMenuItem(
|
||||
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 {
|
||||
|
|
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 ValueNotifier<VolumeRelativeDirectory> _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: ''));
|
||||
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
|
||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||
|
||||
Set<StorageVolume> get _volumes => androidFileUtils.storageVolumes;
|
||||
|
||||
|
@ -78,99 +77,95 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
..clear();
|
||||
_directory.dispose();
|
||||
_contents.dispose();
|
||||
_doubleBackPopHandler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesPopScope(
|
||||
handlers: [
|
||||
(context) {
|
||||
if (_directory.value.relativeDir.isNotEmpty) {
|
||||
final parent = pContext.dirname(_currentDirectoryPath);
|
||||
_goTo(parent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
TvNavigationPopHandler.pop,
|
||||
_doubleBackPopHandler.pop,
|
||||
],
|
||||
child: AvesScaffold(
|
||||
drawer: const AppDrawer(),
|
||||
body: GestureAreaProtectorStack(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<List<Directory>>(
|
||||
valueListenable: _contents,
|
||||
builder: (context, contents, child) {
|
||||
final durations = context.watch<DurationsData>();
|
||||
return CustomScrollView(
|
||||
// workaround to prevent scrolling the app bar away
|
||||
// when there is no content and we use `SliverFillRemaining`
|
||||
physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
||||
slivers: [
|
||||
ExplorerAppBar(
|
||||
key: const Key('appbar'),
|
||||
directoryNotifier: _directory,
|
||||
goTo: _goTo,
|
||||
),
|
||||
AnimationLimiter(
|
||||
// animation limiter should not be above the app bar
|
||||
// so that the crumb line can automatically scroll
|
||||
key: ValueKey(_currentDirectoryPath),
|
||||
child: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: durations.staggeredAnimation,
|
||||
delay: durations.staggeredAnimationDelay * timeDilation,
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _buildContentLine(context, contents[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: contents.length,
|
||||
),
|
||||
),
|
||||
contents.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: _buildEmptyContent(),
|
||||
)
|
||||
: const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ValueListenableBuilder<VolumeRelativeDirectory>(
|
||||
valueListenable: _directory,
|
||||
builder: (context, directory, child) {
|
||||
return AvesFilterChip(
|
||||
return ValueListenableBuilder<VolumeRelativeDirectory>(
|
||||
valueListenable: _directory,
|
||||
builder: (context, directory, child) {
|
||||
final atRoot = directory.relativeDir.isEmpty;
|
||||
return AvesPopScope(
|
||||
handlers: [
|
||||
APopHandler(
|
||||
canPop: (context) => atRoot,
|
||||
onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)),
|
||||
),
|
||||
tvNavigationPopHandler,
|
||||
doubleBackPopHandler,
|
||||
],
|
||||
child: AvesScaffold(
|
||||
drawer: const AppDrawer(),
|
||||
body: GestureAreaProtectorStack(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<List<Directory>>(
|
||||
valueListenable: _contents,
|
||||
builder: (context, contents, child) {
|
||||
final durations = context.watch<DurationsData>();
|
||||
return CustomScrollView(
|
||||
// workaround to prevent scrolling the app bar away
|
||||
// when there is no content and we use `SliverFillRemaining`
|
||||
physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
||||
slivers: [
|
||||
ExplorerAppBar(
|
||||
key: const Key('appbar'),
|
||||
directoryNotifier: _directory,
|
||||
goTo: _goTo,
|
||||
),
|
||||
AnimationLimiter(
|
||||
// animation limiter should not be above the app bar
|
||||
// so that the crumb line can automatically scroll
|
||||
key: ValueKey(_currentDirectoryPath),
|
||||
child: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: durations.staggeredAnimation,
|
||||
delay: durations.staggeredAnimationDelay * timeDilation,
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _buildContentLine(context, contents[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: contents.length,
|
||||
),
|
||||
),
|
||||
contents.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: _buildEmptyContent(),
|
||||
)
|
||||
: const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesFilterChip(
|
||||
filter: PathFilter(_currentDirectoryPath),
|
||||
maxWidth: double.infinity,
|
||||
onTap: (filter) => _goToCollectionPage(context, filter),
|
||||
onLongPress: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -191,12 +191,10 @@ class _FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
|||
|
||||
class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>> {
|
||||
TileExtentController? _tileExtentController;
|
||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tileExtentController?.dispose();
|
||||
_doubleBackPopHandler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -212,16 +210,12 @@ class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>>
|
|||
);
|
||||
return AvesPopScope(
|
||||
handlers: [
|
||||
(context) {
|
||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
TvNavigationPopHandler.pop,
|
||||
_doubleBackPopHandler.pop,
|
||||
APopHandler(
|
||||
canPop: (context) => context.select<Selection<FilterGridItem<T>>, bool>((v) => !v.isSelecting),
|
||||
onPopBlocked: (context) => context.read<Selection<FilterGridItem<T>>>().browse(),
|
||||
),
|
||||
tvNavigationPopHandler,
|
||||
doubleBackPopHandler,
|
||||
],
|
||||
child: TileExtentControllerProvider(
|
||||
controller: _tileExtentController!,
|
||||
|
|
|
@ -61,11 +61,13 @@ class _HomePageState extends State<HomePage> {
|
|||
int? _widgetId;
|
||||
String? _initialRouteName, _initialSearchQuery;
|
||||
Set<CollectionFilter>? _initialFilters;
|
||||
String? _initialExplorerPath;
|
||||
List<String>? _secureUris;
|
||||
|
||||
static const allowedShortcutRoutes = [
|
||||
CollectionPage.routeName,
|
||||
AlbumListPage.routeName,
|
||||
CollectionPage.routeName,
|
||||
ExplorerPage.routeName,
|
||||
SearchPage.routeName,
|
||||
];
|
||||
|
||||
|
@ -92,6 +94,7 @@ class _HomePageState extends State<HomePage> {
|
|||
final safeMode = intentData[IntentDataKeys.safeMode] ?? false;
|
||||
final intentAction = intentData[IntentDataKeys.action];
|
||||
_initialFilters = null;
|
||||
_initialExplorerPath = null;
|
||||
_secureUris = null;
|
||||
|
||||
await androidFileUtils.init();
|
||||
|
@ -186,6 +189,7 @@ class _HomePageState extends State<HomePage> {
|
|||
final extraFilters = intentData[IntentDataKeys.filters];
|
||||
_initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
|
||||
}
|
||||
_initialExplorerPath = intentData[IntentDataKeys.explorerPath];
|
||||
}
|
||||
context.read<ValueNotifier<AppMode>>().value = appMode;
|
||||
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
|
||||
|
@ -199,10 +203,10 @@ class _HomePageState extends State<HomePage> {
|
|||
unawaited(GlobalSearch.registerCallback());
|
||||
unawaited(AnalysisService.registerCallback());
|
||||
final source = context.read<CollectionSource>();
|
||||
source.safeMode = safeMode;
|
||||
if (source.initState != SourceInitializationState.full) {
|
||||
await source.init(
|
||||
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
|
||||
canAnalyze: !safeMode,
|
||||
);
|
||||
}
|
||||
case AppMode.screenSaver:
|
||||
|
@ -351,7 +355,8 @@ class _HomePageState extends State<HomePage> {
|
|||
case TagListPage.routeName:
|
||||
return buildRoute((context) => const TagListPage());
|
||||
case ExplorerPage.routeName:
|
||||
return buildRoute((context) => const ExplorerPage());
|
||||
final path = _initialExplorerPath ?? settings.homeCustomExplorerPath;
|
||||
return buildRoute((context) => ExplorerPage(path: path));
|
||||
case HomeWidgetSettingsPage.routeName:
|
||||
return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!));
|
||||
case ScreenSaverPage.routeName:
|
||||
|
|
|
@ -15,6 +15,7 @@ class IntentDataKeys {
|
|||
static const action = 'action';
|
||||
static const allowMultiple = 'allowMultiple';
|
||||
static const brightness = 'brightness';
|
||||
static const explorerPath = 'explorerPath';
|
||||
static const filters = 'filters';
|
||||
static const mimeType = 'mimeType';
|
||||
static const page = 'page';
|
||||
|
|
|
@ -2,8 +2,10 @@ import 'dart:async';
|
|||
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/theme/text.dart';
|
||||
import 'package:aves/view/view.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
|
@ -43,24 +45,44 @@ class NavigationSection extends SettingsSection {
|
|||
class _HomeOption {
|
||||
final HomePageSetting page;
|
||||
final Set<CollectionFilter> customCollection;
|
||||
final String? customExplorerPath;
|
||||
|
||||
const _HomeOption(
|
||||
this.page, {
|
||||
this.customCollection = const {},
|
||||
this.customExplorerPath,
|
||||
});
|
||||
|
||||
String getName(BuildContext context) {
|
||||
if (page == HomePageSetting.collection && customCollection.isNotEmpty) {
|
||||
return context.l10n.setHomeCustomCollection;
|
||||
final pageName = page.getName(context);
|
||||
switch (page) {
|
||||
case HomePageSetting.collection:
|
||||
return customCollection.isNotEmpty ? context.l10n.setHomeCustom : pageName;
|
||||
case HomePageSetting.explorer:
|
||||
return customExplorerPath != null ? context.l10n.setHomeCustom : pageName;
|
||||
default:
|
||||
return pageName;
|
||||
}
|
||||
}
|
||||
|
||||
String? getDetails(BuildContext context) {
|
||||
switch (page) {
|
||||
case HomePageSetting.collection:
|
||||
final filters = customCollection;
|
||||
return filters.isNotEmpty ? [context.l10n.collectionPageTitle, filters.map((v) => v.getLabel(context)).join(', ')].join(AText.separator) : null;
|
||||
case HomePageSetting.explorer:
|
||||
final path = customExplorerPath;
|
||||
return path != null ? [context.l10n.explorerPageTitle, pContext.basename(path)].join(AText.separator) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return page.getName(context);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection);
|
||||
bool operator ==(Object other) => identical(this, other) || (other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection) && customExplorerPath == other.customExplorerPath);
|
||||
|
||||
@override
|
||||
int get hashCode => page.hashCode ^ customCollection.hashCode;
|
||||
int get hashCode => page.hashCode ^ customCollection.hashCode ^ customExplorerPath.hashCode;
|
||||
}
|
||||
|
||||
class SettingsTileNavigationHomePage extends SettingsTile {
|
||||
|
@ -75,15 +97,18 @@ class SettingsTileNavigationHomePage extends SettingsTile {
|
|||
const _HomeOption(HomePageSetting.tags),
|
||||
const _HomeOption(HomePageSetting.explorer),
|
||||
if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection),
|
||||
if (settings.homeCustomExplorerPath != null) _HomeOption(HomePageSetting.explorer, customExplorerPath: settings.homeCustomExplorerPath),
|
||||
],
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection),
|
||||
onSelection: (v) {
|
||||
settings.homePage = v.page;
|
||||
settings.homeCustomCollection = v.customCollection;
|
||||
},
|
||||
selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection, customExplorerPath: s.homeCustomExplorerPath),
|
||||
onSelection: (v) => settings.setHome(
|
||||
v.page,
|
||||
customCollection: v.customCollection,
|
||||
customExplorerPath: v.customExplorerPath,
|
||||
),
|
||||
tileTitle: title(context),
|
||||
dialogTitle: context.l10n.settingsHomeDialogTitle,
|
||||
optionSubtitleBuilder: (v) => v.getDetails(context),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class SettingsTvPage extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return AvesScaffold(
|
||||
body: AvesPopScope(
|
||||
handlers: const [TvNavigationPopHandler.pop],
|
||||
handlers: [tvNavigationPopHandler],
|
||||
child: Row(
|
||||
children: [
|
||||
TvRail(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
library aves_model;
|
||||
|
||||
export 'src/actions/chip.dart';
|
||||
export 'src/actions/explorer.dart';
|
||||
export 'src/actions/chip_set.dart';
|
||||
export 'src/actions/entry.dart';
|
||||
export 'src/actions/entry_set.dart';
|
||||
|
|
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 homePageKey = 'home_page';
|
||||
static const homeCustomCollectionKey = 'home_custom_collection';
|
||||
static const homeCustomExplorerPathKey = 'home_custom_explorer_path';
|
||||
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
|
||||
static const confirmCreateVaultKey = 'confirm_create_vault';
|
||||
static const confirmDeleteForeverKey = 'confirm_delete_forever';
|
||||
|
|
|
@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
|
|||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '1.9.24'
|
||||
agp_version = '8.5.0'
|
||||
agp_version = '8.5.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
|
|
@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
|
|||
# - play changelog: /whatsnew/whatsnew-en-US
|
||||
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
|
||||
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
|
||||
version: 1.11.5+124
|
||||
version: 1.11.6+125
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
|
|
@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
|
|||
}
|
||||
|
||||
@override
|
||||
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
|
||||
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
|
||||
|
||||
static var _lastId = 1;
|
||||
|
||||
|
|
|
@ -30,8 +30,7 @@ Future<void> configureAndLaunch() async {
|
|||
..enableBlurEffect = true
|
||||
// navigation
|
||||
..keepScreenOn = KeepScreenOn.always
|
||||
..homePage = HomePageSetting.collection
|
||||
..homeCustomCollection = {}
|
||||
..setHome(HomePageSetting.collection)
|
||||
..enableBottomNavigationBar = true
|
||||
..drawerTypeBookmarks = [null, FavouriteFilter.instance]
|
||||
// collection
|
||||
|
|
|
@ -26,8 +26,7 @@ Future<void> configureAndLaunch() async {
|
|||
..enableBlurEffect = true
|
||||
// navigation
|
||||
..keepScreenOn = KeepScreenOn.always
|
||||
..homePage = HomePageSetting.collection
|
||||
..homeCustomCollection = {}
|
||||
..setHome(HomePageSetting.collection)
|
||||
..enableBottomNavigationBar = true
|
||||
// collection
|
||||
..collectionSectionFactor = EntryGroupFactor.album
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
In v1.11.5:
|
||||
In v1.11.6:
|
||||
- explore your collection with the... explorer
|
||||
- convert your motion photos to stills in bulk
|
||||
Full changelog available on GitHub
|
Loading…
Reference in a new issue